diff --git a/Sources/Workspace.swift b/Sources/Workspace.swift index 87ebe6372..1ac77b3ba 100644 --- a/Sources/Workspace.swift +++ b/Sources/Workspace.swift @@ -10067,12 +10067,19 @@ final class Workspace: Identifiable, ObservableObject { maybeAutoFocusBrowserAddressBarOnPanelFocus(browserPanel, trigger: trigger) } - if trigger == .terminalFirstResponder, - panels[panelId] is TerminalPanel { - beginEventDrivenLayoutFollowUp( - reason: "workspace.focusPanel.terminal", - terminalFocusPanelId: panelId - ) + if let terminalPanel = panels[panelId] as? TerminalPanel { + // Always set up a focus follow-up when the terminal is not yet first responder. + // The .terminalFirstResponder trigger always needs it (reentrant focus assertion). + // The .standard trigger needs it when the surface view isn't ready yet (e.g. + // new workspace via Cmd+T where the portal hasn't attached to a window yet). + let needsFocusFollowUp = trigger == .terminalFirstResponder + || !terminalPanel.hostedView.isSurfaceViewFirstResponder() + if needsFocusFollowUp { + beginEventDrivenLayoutFollowUp( + reason: "workspace.focusPanel.terminal", + terminalFocusPanelId: panelId + ) + } } } @@ -11811,7 +11818,23 @@ extension Workspace: BonsplitDelegate { if bonsplitController.allPaneIds.contains(pane) { normalizePinnedTabs(in: pane) } - scheduleTerminalGeometryReconcile() + // When a pane was auto-closed (e.g. N→1 panes), the SwiftUI split view rebuilds + // asynchronously, transiently detaching the surviving terminal surface. The synchronous + // ensureFocus from applyTabSelection may succeed on the old layout but the first responder + // is lost when SwiftUI tears down the split and recreates a single-pane wrapper. Include + // the surviving panel in the layout follow-up so the retry loop re-applies focus once the + // view is reattached (#2665). + if !isDetaching && !bonsplitController.allPaneIds.contains(pane), + let survivingPanelId = focusedPanelId, + terminalPanel(for: survivingPanelId) != nil { + beginEventDrivenLayoutFollowUp( + reason: "workspace.paneCollapse", + terminalFocusPanelId: survivingPanelId, + includeGeometry: true + ) + } else { + scheduleTerminalGeometryReconcile() + } if !isDetaching { scheduleFocusReconcile() } @@ -11931,7 +11954,19 @@ extension Workspace: BonsplitDelegate { } } - scheduleTerminalGeometryReconcile() + // Same pane-collapse focus fix as didCloseTab (#2665): the SwiftUI split view + // rebuild can transiently detach the surviving terminal, losing first responder. + if shouldScheduleFocusReconcile, + let survivingPanelId = focusedPanelId, + terminalPanel(for: survivingPanelId) != nil { + beginEventDrivenLayoutFollowUp( + reason: "workspace.paneCollapse", + terminalFocusPanelId: survivingPanelId, + includeGeometry: true + ) + } else { + scheduleTerminalGeometryReconcile() + } if shouldScheduleFocusReconcile { scheduleFocusReconcile() }