Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 65 additions & 11 deletions Sources/Workspace.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10067,12 +10067,18 @@ 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's preferred keyboard target
// is not yet active. That target can be either the surface view or the terminal
// find field, so surface-first-responder alone is too strict here.
let needsFocusFollowUp = trigger == .terminalFirstResponder
|| terminalPanelKeyboardFocusNeedsFollowUp(panelId: panelId, terminalPanel: terminalPanel)
if needsFocusFollowUp {
beginEventDrivenLayoutFollowUp(
reason: "workspace.focusPanel.terminal",
terminalFocusPanelId: panelId
)
}
}
}

Expand Down Expand Up @@ -10589,7 +10595,19 @@ final class Workspace: Identifiable, ObservableObject {
let terminalPanel = terminalPanel(for: panelId) else {
return false
}
return focusedPanelId != panelId || !terminalPanel.hostedView.isSurfaceViewFirstResponder()
return terminalPanelKeyboardFocusNeedsFollowUp(panelId: panelId, terminalPanel: terminalPanel)
}

private func terminalPanelKeyboardFocusNeedsFollowUp(
panelId: UUID,
terminalPanel: TerminalPanel
) -> Bool {
guard focusedPanelId == panelId else { return true }
guard let window = terminalPanel.hostedView.window,
let firstResponder = window.firstResponder else {
return true
}
return !terminalPanel.hostedView.responderMatchesPreferredKeyboardFocus(firstResponder)
}

private func browserPanelNeedsFollowUp() -> Bool {
Expand Down Expand Up @@ -10621,8 +10639,16 @@ final class Workspace: Identifiable, ObservableObject {
if let terminalFocusPanelId = layoutFollowUpTerminalFocusPanelId {
if let terminalPanel = terminalPanel(for: terminalFocusPanelId),
focusedPanelId == terminalFocusPanelId {
terminalPanel.hostedView.ensureFocus(for: id, surfaceId: terminalFocusPanelId)
if terminalPanel.hostedView.isSurfaceViewFirstResponder() {
if terminalPanelKeyboardFocusNeedsFollowUp(
panelId: terminalFocusPanelId,
terminalPanel: terminalPanel
) {
terminalPanel.hostedView.ensureFocus(for: id, surfaceId: terminalFocusPanelId)
}
if !terminalPanelKeyboardFocusNeedsFollowUp(
panelId: terminalFocusPanelId,
terminalPanel: terminalPanel
) {
layoutFollowUpTerminalFocusPanelId = nil
}
} else if terminalPanel(for: terminalFocusPanelId) == nil {
Expand Down Expand Up @@ -11811,7 +11837,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()
}
Expand Down Expand Up @@ -11931,7 +11973,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()
}
Expand Down
Loading