Merge pull request #267 from manaflow-ai/hotfix-reopen-browser-stale-focus

Fix intermittent Cmd+Shift+T stale-focus override
This commit is contained in:
Lawrence Chen 2026-02-21 03:31:32 -08:00 committed by GitHub
commit 89953f387b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 139 additions and 4 deletions

View file

@ -1613,16 +1613,18 @@ class TabManager: ObservableObject {
?? tabs.first else {
return false
}
let preReopenFocusedPanelId = focusedPanelId(for: targetWorkspace.id)
if selectedTabId != targetWorkspace.id {
selectedTabId = targetWorkspace.id
}
if let reopenedPanelId = reopenClosedBrowserPanel(snapshot, in: targetWorkspace) {
// Workspace switches defer focus restoration to the next main-queue turn.
// Record the reopened browser immediately so deferred restore doesn't snap
// back to the previously focused terminal in that workspace.
rememberFocusedSurface(tabId: targetWorkspace.id, surfaceId: reopenedPanelId)
enforceReopenedBrowserFocus(
tabId: targetWorkspace.id,
reopenedPanelId: reopenedPanelId,
preReopenFocusedPanelId: preReopenFocusedPanelId
)
return true
}
}
@ -1630,6 +1632,62 @@ class TabManager: ObservableObject {
return false
}
private func enforceReopenedBrowserFocus(
tabId: UUID,
reopenedPanelId: UUID,
preReopenFocusedPanelId: UUID?
) {
// Keep workspace-switch restoration pinned to the reopened browser panel.
rememberFocusedSurface(tabId: tabId, surfaceId: reopenedPanelId)
enforceReopenedBrowserFocusIfNeeded(
tabId: tabId,
reopenedPanelId: reopenedPanelId,
preReopenFocusedPanelId: preReopenFocusedPanelId
)
// Some stale focus callbacks can land one runloop turn later. Re-assert focus in two
// consecutive turns, but only when focus drifted back to the pre-reopen panel.
DispatchQueue.main.async { [weak self] in
guard let self else { return }
self.enforceReopenedBrowserFocusIfNeeded(
tabId: tabId,
reopenedPanelId: reopenedPanelId,
preReopenFocusedPanelId: preReopenFocusedPanelId
)
DispatchQueue.main.async { [weak self] in
self?.enforceReopenedBrowserFocusIfNeeded(
tabId: tabId,
reopenedPanelId: reopenedPanelId,
preReopenFocusedPanelId: preReopenFocusedPanelId
)
}
}
}
private func enforceReopenedBrowserFocusIfNeeded(
tabId: UUID,
reopenedPanelId: UUID,
preReopenFocusedPanelId: UUID?
) {
guard selectedTabId == tabId,
let tab = tabs.first(where: { $0.id == tabId }),
tab.panels[reopenedPanelId] != nil else {
return
}
rememberFocusedSurface(tabId: tabId, surfaceId: reopenedPanelId)
guard tab.focusedPanelId != reopenedPanelId else { return }
if let focusedPanelId = tab.focusedPanelId,
let preReopenFocusedPanelId,
focusedPanelId != preReopenFocusedPanelId {
return
}
tab.focusPanel(reopenedPanelId)
}
private func reopenClosedBrowserPanel(
_ snapshot: ClosedBrowserPanelRestoreSnapshot,
in workspace: Workspace

View file

@ -1039,11 +1039,88 @@ final class TabManagerReopenClosedBrowserFocusTests: XCTestCase {
XCTAssertTrue(isFocusedPanelBrowser(in: workspace1))
}
func testReopenFromDifferentWorkspaceWinsAgainstSingleDeferredStaleFocus() {
let manager = TabManager()
guard let workspace1 = manager.selectedWorkspace,
let preReopenPanelId = workspace1.focusedPanelId,
let closedBrowserId = manager.openBrowser(url: URL(string: "https://example.com/stale-focus-cross-ws")) else {
XCTFail("Expected initial workspace state and browser panel")
return
}
drainMainQueue()
XCTAssertTrue(workspace1.closePanel(closedBrowserId, force: true))
drainMainQueue()
let panelIdsBeforeReopen = Set(workspace1.panels.keys)
let workspace2 = manager.addWorkspace()
XCTAssertEqual(manager.selectedTabId, workspace2.id)
XCTAssertTrue(manager.reopenMostRecentlyClosedBrowserPanel())
guard let reopenedPanelId = singleNewPanelId(in: workspace1, comparedTo: panelIdsBeforeReopen) else {
XCTFail("Expected reopened browser panel ID")
return
}
// Simulate one delayed stale focus callback from the panel that was focused before reopen.
DispatchQueue.main.async {
workspace1.focusPanel(preReopenPanelId)
}
drainMainQueue()
drainMainQueue()
drainMainQueue()
XCTAssertEqual(manager.selectedTabId, workspace1.id)
XCTAssertEqual(workspace1.focusedPanelId, reopenedPanelId)
XCTAssertTrue(workspace1.panels[reopenedPanelId] is BrowserPanel)
}
func testReopenInSameWorkspaceWinsAgainstSingleDeferredStaleFocus() {
let manager = TabManager()
guard let workspace = manager.selectedWorkspace,
let preReopenPanelId = workspace.focusedPanelId,
let closedBrowserId = manager.openBrowser(url: URL(string: "https://example.com/stale-focus-same-ws")) else {
XCTFail("Expected initial workspace state and browser panel")
return
}
drainMainQueue()
XCTAssertTrue(workspace.closePanel(closedBrowserId, force: true))
drainMainQueue()
let panelIdsBeforeReopen = Set(workspace.panels.keys)
XCTAssertTrue(manager.reopenMostRecentlyClosedBrowserPanel())
guard let reopenedPanelId = singleNewPanelId(in: workspace, comparedTo: panelIdsBeforeReopen) else {
XCTFail("Expected reopened browser panel ID")
return
}
// Simulate one delayed stale focus callback from the panel that was focused before reopen.
DispatchQueue.main.async {
workspace.focusPanel(preReopenPanelId)
}
drainMainQueue()
drainMainQueue()
drainMainQueue()
XCTAssertEqual(manager.selectedTabId, workspace.id)
XCTAssertEqual(workspace.focusedPanelId, reopenedPanelId)
XCTAssertTrue(workspace.panels[reopenedPanelId] is BrowserPanel)
}
private func isFocusedPanelBrowser(in workspace: Workspace) -> Bool {
guard let focusedPanelId = workspace.focusedPanelId else { return false }
return workspace.panels[focusedPanelId] is BrowserPanel
}
private func singleNewPanelId(in workspace: Workspace, comparedTo previousPanelIds: Set<UUID>) -> UUID? {
let newPanelIds = Set(workspace.panels.keys).subtracting(previousPanelIds)
guard newPanelIds.count == 1 else { return nil }
return newPanelIds.first
}
private func drainMainQueue() {
let expectation = expectation(description: "drain main queue")
DispatchQueue.main.async {