native macOS codings agent orchestrator
5
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge pull request #181 from yaroslavyaroslav/bugfix/focus-release-fix

Fix split-pane zoom focus state

authored by

khoi and committed by
GitHub
fb093ac4 ac59c4b0

+83 -16
+8
supacode/Features/Terminal/Models/SplitTree.swift
··· 74 74 if case .split = root { true } else { false } 75 75 } 76 76 77 + var visibleNode: Node? { 78 + zoomed ?? root 79 + } 80 + 77 81 init() { 78 82 self.init(root: nil, zoomed: nil) 79 83 } ··· 265 269 266 270 func leaves() -> [ViewType] { 267 271 root?.leaves() ?? [] 272 + } 273 + 274 + func visibleLeaves() -> [ViewType] { 275 + visibleNode?.leaves() ?? [] 268 276 } 269 277 270 278 var structuralIdentity: StructuralIdentity {
+35 -12
supacode/Features/Terminal/Models/WorktreeTerminalState.swift
··· 32 32 private var isEnsuringInitialTab = false 33 33 private var lastReportedTaskStatus: WorktreeTaskStatus? 34 34 private var lastEmittedFocusSurfaceId: UUID? 35 + private var lastWindowIsKey: Bool? 36 + private var lastWindowIsVisible: Bool? 35 37 var notifications: [WorktreeTerminalNotification] = [] 36 38 var notificationsEnabled = true 37 39 var hasUnseenNotification: Bool { ··· 233 235 } 234 236 235 237 func syncFocus(windowIsKey: Bool, windowIsVisible: Bool) { 238 + lastWindowIsKey = windowIsKey 239 + lastWindowIsVisible = windowIsVisible 240 + applySurfaceActivity() 241 + } 242 + 243 + private func applySurfaceActivity() { 236 244 let selectedTabId = tabManager.selectedTabId 237 245 var surfaceToFocus: GhosttySurfaceView? 238 246 for (tabId, tree) in trees { 239 247 let focusedId = focusedSurfaceIdByTab[tabId] 240 248 let isSelectedTab = (tabId == selectedTabId) 249 + let visibleSurfaceIDs = Set(tree.visibleLeaves().map(\.id)) 241 250 for surface in tree.leaves() { 242 251 let activity = Self.surfaceActivity( 252 + isSurfaceVisibleInTree: visibleSurfaceIDs.contains(surface.id), 243 253 isSelectedTab: isSelectedTab, 244 - windowIsVisible: windowIsVisible, 245 - windowIsKey: windowIsKey, 254 + windowIsVisible: lastWindowIsVisible == true, 255 + windowIsKey: lastWindowIsKey == true, 246 256 focusedSurfaceID: focusedId, 247 257 surfaceID: surface.id 248 258 ) ··· 259 269 } 260 270 261 271 static func surfaceActivity( 272 + isSurfaceVisibleInTree: Bool = true, 262 273 isSelectedTab: Bool, 263 274 windowIsVisible: Bool, 264 275 windowIsKey: Bool, 265 276 focusedSurfaceID: UUID?, 266 277 surfaceID: UUID 267 278 ) -> SurfaceActivity { 268 - let isVisible = isSelectedTab && windowIsVisible 279 + let isVisible = isSurfaceVisibleInTree && isSelectedTab && windowIsVisible 269 280 let isFocused = isVisible && windowIsKey && focusedSurfaceID == surfaceID 270 281 return SurfaceActivity(isVisible: isVisible, isFocused: isFocused) 271 282 } ··· 413 424 at: targetSurface, 414 425 direction: mapSplitDirection(direction) 415 426 ) 416 - trees[tabId] = newTree 427 + updateTree(newTree, for: tabId) 417 428 focusSurface(newSurface, in: tabId) 418 429 return true 419 430 } catch { ··· 432 443 trees[tabId] = tree 433 444 } 434 445 focusSurface(nextSurface, in: tabId) 446 + syncFocusIfNeeded() 435 447 return true 436 448 437 449 case .resizeSplit(let direction, let amount): ··· 443 455 in: spatialDirection, 444 456 with: CGRect(origin: .zero, size: tree.viewBounds()) 445 457 ) 446 - trees[tabId] = newTree 458 + updateTree(newTree, for: tabId) 447 459 return true 448 460 } catch { 449 461 return false 450 462 } 451 463 452 464 case .equalizeSplits: 453 - trees[tabId] = tree.equalized() 465 + updateTree(tree.equalized(), for: tabId) 454 466 return true 455 467 456 468 case .toggleSplitZoom: 457 469 guard tree.isSplit else { return false } 458 470 let newZoomed = (tree.zoomed == targetNode) ? nil : targetNode 459 - trees[tabId] = tree.settingZoomed(newZoomed) 471 + updateTree(tree.settingZoomed(newZoomed), for: tabId) 472 + focusSurface(targetSurface, in: tabId) 460 473 return true 461 474 } 462 475 } ··· 469 482 let resizedNode = node.resizing(to: ratio) 470 483 do { 471 484 tree = try tree.replacing(node: node, with: resizedNode) 472 - trees[tabId] = tree 485 + updateTree(tree, for: tabId) 473 486 } catch { 474 487 return 475 488 } ··· 487 500 at: destination, 488 501 direction: mapDropZone(zone) 489 502 ) 490 - trees[tabId] = newTree 503 + updateTree(newTree, for: tabId) 491 504 focusSurface(payload, in: tabId) 492 505 } catch { 493 506 return 494 507 } 495 508 496 509 case .equalize: 497 - trees[tabId] = tree.equalized() 510 + updateTree(tree.equalized(), for: tabId) 498 511 } 499 512 } 500 513 ··· 779 792 return 780 793 } 781 794 let tree = splitTree(for: tabId) 782 - if let surface = tree.root?.leftmostLeaf() { 795 + if let surface = tree.visibleLeaves().first { 783 796 focusSurface(surface, in: tabId) 784 797 } 785 798 } ··· 870 883 } 871 884 } 872 885 886 + private func syncFocusIfNeeded() { 887 + guard lastWindowIsKey != nil, lastWindowIsVisible != nil else { return } 888 + applySurfaceActivity() 889 + } 890 + 891 + private func updateTree(_ tree: SplitTree<GhosttySurfaceView>, for tabId: TerminalTabID) { 892 + trees[tabId] = tree 893 + syncFocusIfNeeded() 894 + } 895 + 873 896 private func isRunningProgressState(_ state: ghostty_action_progress_report_state_e?) -> Bool { 874 897 switch state { 875 898 case .some(GHOSTTY_PROGRESS_STATE_SET), ··· 967 990 } 968 991 return 969 992 } 970 - trees[tabId] = newTree 993 + updateTree(newTree, for: tabId) 971 994 updateRunningState(for: tabId) 972 995 if focusedSurfaceIdByTab[tabId] == view.id { 973 996 if let nextSurface {
+2 -4
supacode/Features/Terminal/Views/TerminalSplitTreeView.swift
··· 21 21 } 22 22 23 23 var body: some View { 24 - if let node = tree.zoomed ?? tree.root { 24 + if let node = tree.visibleNode { 25 25 SubtreeView(node: node, isRoot: node == tree.root, action: action) 26 26 .id(node.structuralIdentity) 27 27 } ··· 288 288 } 289 289 290 290 func updateNSView(_ nsView: TerminalSplitAXContainerView, context: Context) { 291 - let visibleNode = tree.zoomed ?? tree.root 292 - let visiblePanes = visibleNode?.leaves() ?? [] 293 291 nsView.update( 294 292 rootView: AnyView(TerminalSplitTreeView(tree: tree, action: action)), 295 - panes: visiblePanes 293 + panes: tree.visibleLeaves() 296 294 ) 297 295 } 298 296 }
+5
supacode/Infrastructure/Ghostty/GhosttySurfaceView.swift
··· 312 312 313 313 override func viewDidMoveToWindow() { 314 314 super.viewDidMoveToWindow() 315 + if window == nil { 316 + // SwiftUI can temporarily detach a pane while rebuilding split/zoom layout. 317 + // If we keep the stale local focus bit, detached panes still intercept bindings. 318 + focusDidChange(false) 319 + } 315 320 updateScreenObservers() 316 321 updateContentScale() 317 322 updateSurfaceSize()
+14
supacodeTests/SplitTreeTests.swift
··· 30 30 let node = try #require(tree.find(id: third.id)) 31 31 #expect(tree.focusTargetAfterClosing(node) === second) 32 32 } 33 + 34 + @Test func visibleLeavesOnlyReturnZoomedPane() throws { 35 + let first = SplitTreeTestView() 36 + let second = SplitTreeTestView() 37 + 38 + let tree = try SplitTree(view: first) 39 + .inserting(view: second, at: first, direction: .right) 40 + 41 + let zoomed = tree.settingZoomed(try #require(tree.find(id: second.id))) 42 + let visibleLeaves = zoomed.visibleLeaves() 43 + 44 + #expect(visibleLeaves.count == 1) 45 + #expect(visibleLeaves.first === second) 46 + } 33 47 } 34 48 35 49 private final class SplitTreeTestView: NSView, Identifiable {
+19
supacodeTests/TerminalRenderingPolicyTests.swift
··· 8 8 @Test func surfaceActivityForSelectedVisibleFocusedSurfaceIsFocused() { 9 9 let focusedID = UUID() 10 10 let activity = WorktreeTerminalState.surfaceActivity( 11 + isSurfaceVisibleInTree: true, 11 12 isSelectedTab: true, 12 13 windowIsVisible: true, 13 14 windowIsKey: true, ··· 20 21 21 22 @Test func surfaceActivityForSelectedVisibleUnfocusedSurfaceIsNotFocused() { 22 23 let activity = WorktreeTerminalState.surfaceActivity( 24 + isSurfaceVisibleInTree: true, 23 25 isSelectedTab: true, 24 26 windowIsVisible: true, 25 27 windowIsKey: true, ··· 33 35 @Test func surfaceActivityForSelectedTabInBackgroundWindowIsVisibleButNotFocused() { 34 36 let surfaceID = UUID() 35 37 let activity = WorktreeTerminalState.surfaceActivity( 38 + isSurfaceVisibleInTree: true, 36 39 isSelectedTab: true, 37 40 windowIsVisible: true, 38 41 windowIsKey: false, ··· 46 49 @Test func surfaceActivityForOccludedWindowIsHiddenAndUnfocused() { 47 50 let surfaceID = UUID() 48 51 let activity = WorktreeTerminalState.surfaceActivity( 52 + isSurfaceVisibleInTree: true, 49 53 isSelectedTab: true, 50 54 windowIsVisible: false, 51 55 windowIsKey: true, ··· 59 63 @Test func surfaceActivityForUnselectedTabIsHiddenAndUnfocused() { 60 64 let surfaceID = UUID() 61 65 let activity = WorktreeTerminalState.surfaceActivity( 66 + isSurfaceVisibleInTree: true, 62 67 isSelectedTab: false, 68 + windowIsVisible: true, 69 + windowIsKey: true, 70 + focusedSurfaceID: surfaceID, 71 + surfaceID: surfaceID 72 + ) 73 + #expect(!activity.isVisible) 74 + #expect(!activity.isFocused) 75 + } 76 + 77 + @Test func surfaceActivityForZoomHiddenSurfaceIsHiddenAndUnfocused() { 78 + let surfaceID = UUID() 79 + let activity = WorktreeTerminalState.surfaceActivity( 80 + isSurfaceVisibleInTree: false, 81 + isSelectedTab: true, 63 82 windowIsVisible: true, 64 83 windowIsKey: true, 65 84 focusedSurfaceID: surfaceID,