diff --git a/supacode/Features/Terminal/Models/SplitTree.swift b/supacode/Features/Terminal/Models/SplitTree.swift index 8ae0edeb..f0cbe411 100644 --- a/supacode/Features/Terminal/Models/SplitTree.swift +++ b/supacode/Features/Terminal/Models/SplitTree.swift @@ -164,6 +164,18 @@ struct SplitTree { } } + func focusTargetAfterClosing(_ node: Node) -> ViewType? { + guard let root else { return nil } + + // Match Ghostty's macOS controller: closing the leftmost leaf moves to the next + // surface, otherwise we move to the previous one. + if root.leftmostLeaf() === node.leftmostLeaf() { + return focusTarget(for: .next, from: node) + } else { + return focusTarget(for: .previous, from: node) + } + } + func equalized() -> Self { guard let root else { return self } let newRoot = root.equalize() diff --git a/supacode/Features/Terminal/Models/WorktreeTerminalState.swift b/supacode/Features/Terminal/Models/WorktreeTerminalState.swift index 66fbb893..979c3e96 100644 --- a/supacode/Features/Terminal/Models/WorktreeTerminalState.swift +++ b/supacode/Features/Terminal/Models/WorktreeTerminalState.swift @@ -845,6 +845,10 @@ final class WorktreeTerminalState { surfaces.removeValue(forKey: view.id) return } + let nextSurface = + focusedSurfaceIdByTab[tabId] == view.id + ? tree.focusTargetAfterClosing(node) + : nil let newTree = tree.removing(node) view.closeSurface() surfaces.removeValue(forKey: view.id) @@ -860,7 +864,7 @@ final class WorktreeTerminalState { trees[tabId] = newTree updateRunningState(for: tabId) if focusedSurfaceIdByTab[tabId] == view.id { - if let nextSurface = newTree.root?.leftmostLeaf() { + if let nextSurface { focusSurface(nextSurface, in: tabId) } else { focusedSurfaceIdByTab.removeValue(forKey: tabId) diff --git a/supacodeTests/SplitTreeTests.swift b/supacodeTests/SplitTreeTests.swift new file mode 100644 index 00000000..cdab2f19 --- /dev/null +++ b/supacodeTests/SplitTreeTests.swift @@ -0,0 +1,37 @@ +import AppKit +import Testing + +@testable import supacode + +@MainActor +struct SplitTreeTests { + @Test func focusTargetAfterClosingUsesNextForLeftmostLeaf() throws { + let first = SplitTreeTestView() + let second = SplitTreeTestView() + let third = SplitTreeTestView() + + let tree = try SplitTree(view: first) + .inserting(view: second, at: first, direction: .right) + .inserting(view: third, at: second, direction: .right) + + let node = try #require(tree.find(id: first.id)) + #expect(tree.focusTargetAfterClosing(node) === second) + } + + @Test func focusTargetAfterClosingUsesPreviousForNonLeftmostLeaf() throws { + let first = SplitTreeTestView() + let second = SplitTreeTestView() + let third = SplitTreeTestView() + + let tree = try SplitTree(view: first) + .inserting(view: second, at: first, direction: .right) + .inserting(view: third, at: second, direction: .right) + + let node = try #require(tree.find(id: third.id)) + #expect(tree.focusTargetAfterClosing(node) === second) + } +} + +private final class SplitTreeTestView: NSView, Identifiable { + let id = UUID() +} -- 2.51.2 From 42a2832b153c19c0320207b25b54121c2cd8475d Mon Sep 17 00:00:00 2001 From: khoi <6994441+khoi@users.noreply.github.com> Date: Sat, 14 Mar 2026 22:52:14 +0000 Subject: [PATCH 2/2] bump v0.6.8 --- supacode.xcodeproj/project.pbxproj | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/supacode.xcodeproj/project.pbxproj b/supacode.xcodeproj/project.pbxproj index 6bc9be1d..a28e2702 100644 --- a/supacode.xcodeproj/project.pbxproj +++ b/supacode.xcodeproj/project.pbxproj @@ -436,7 +436,7 @@ CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; COMPILATION_CACHE_ENABLE_CACHING = YES; - CURRENT_PROJECT_VERSION = 112; + CURRENT_PROJECT_VERSION = 113; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = ""; @@ -458,7 +458,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 26.0; - MARKETING_VERSION = 0.6.7; + MARKETING_VERSION = 0.6.8; OTHER_LDFLAGS = ( "$(inherited)", "-lc++", @@ -492,7 +492,7 @@ CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; COMPILATION_CACHE_ENABLE_CACHING = NO; - CURRENT_PROJECT_VERSION = 112; + CURRENT_PROJECT_VERSION = 113; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = ""; @@ -514,7 +514,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 26.0; - MARKETING_VERSION = 0.6.7; + MARKETING_VERSION = 0.6.8; OTHER_LDFLAGS = ( "$(inherited)", "-lc++", @@ -543,12 +543,12 @@ buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 112; + CURRENT_PROJECT_VERSION = 113; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = ""; GENERATE_INFOPLIST_FILE = YES; MACOSX_DEPLOYMENT_TARGET = 26.1; - MARKETING_VERSION = 0.6.7; + MARKETING_VERSION = 0.6.8; PRODUCT_BUNDLE_IDENTIFIER = app.supabit.supacodeTests; PRODUCT_NAME = "$(TARGET_NAME)"; STRING_CATALOG_GENERATE_SYMBOLS = NO; @@ -565,12 +565,12 @@ buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 112; + CURRENT_PROJECT_VERSION = 113; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = ""; GENERATE_INFOPLIST_FILE = YES; MACOSX_DEPLOYMENT_TARGET = 26.1; - MARKETING_VERSION = 0.6.7; + MARKETING_VERSION = 0.6.8; PRODUCT_BUNDLE_IDENTIFIER = app.supabit.supacodeTests; PRODUCT_NAME = "$(TARGET_NAME)"; STRING_CATALOG_GENERATE_SYMBOLS = NO;