Merge pull request #257 from manaflow-ai/feat-reopen-browser-focus-fix

Fix reopened browser focus after workspace switch
This commit is contained in:
Lawrence Chen 2026-02-21 03:08:24 -08:00 committed by GitHub
commit 7dbc80d48e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 114 additions and 8 deletions

View file

@ -1618,7 +1618,11 @@ class TabManager: ObservableObject {
selectedTabId = targetWorkspace.id
}
if reopenClosedBrowserPanel(snapshot, in: targetWorkspace) {
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)
return true
}
}
@ -1629,14 +1633,14 @@ class TabManager: ObservableObject {
private func reopenClosedBrowserPanel(
_ snapshot: ClosedBrowserPanelRestoreSnapshot,
in workspace: Workspace
) -> Bool {
) -> UUID? {
if let originalPane = workspace.bonsplitController.allPaneIds.first(where: { $0.id == snapshot.originalPaneId }),
let browserPanel = workspace.newBrowserSurface(inPane: originalPane, url: snapshot.url, focus: true) {
let tabCount = workspace.bonsplitController.tabs(inPane: originalPane).count
let maxIndex = max(0, tabCount - 1)
let targetIndex = min(max(snapshot.originalTabIndex, 0), maxIndex)
_ = workspace.reorderSurface(panelId: browserPanel.id, toIndex: targetIndex)
return true
return browserPanel.id
}
if let orientation = snapshot.fallbackSplitOrientation,
@ -1644,19 +1648,19 @@ class TabManager: ObservableObject {
let anchorPane = workspace.bonsplitController.allPaneIds.first(where: { $0.id == fallbackAnchorPaneId }),
let anchorTab = workspace.bonsplitController.selectedTab(inPane: anchorPane) ?? workspace.bonsplitController.tabs(inPane: anchorPane).first,
let anchorPanelId = workspace.panelIdFromSurfaceId(anchorTab.id),
workspace.newBrowserSplit(
let browserPanelId = workspace.newBrowserSplit(
from: anchorPanelId,
orientation: orientation,
insertFirst: snapshot.fallbackSplitInsertFirst,
url: snapshot.url
) != nil {
return true
)?.id {
return browserPanelId
}
guard let focusedPane = workspace.bonsplitController.focusedPaneId ?? workspace.bonsplitController.allPaneIds.first else {
return false
return nil
}
return workspace.newBrowserSurface(inPane: focusedPane, url: snapshot.url, focus: true) != nil
return workspace.newBrowserSurface(inPane: focusedPane, url: snapshot.url, focus: true)?.id
}
/// Flash the currently focused panel so the user can visually confirm focus.

View file

@ -2461,6 +2461,9 @@ extension Workspace: BonsplitDelegate {
case .markAsUnread:
guard let panelId = panelIdFromSurfaceId(tab.id) else { return }
markPanelUnread(panelId)
case .markAsRead:
guard let panelId = panelIdFromSurfaceId(tab.id) else { return }
markPanelRead(panelId)
}
}

View file

@ -818,6 +818,13 @@ final class UpdateChannelSettingsTests: XCTestCase {
XCTAssertTrue(resolved.usedFallback)
}
func testResolvedFeedFallsBackWhenInfoFeedEmpty() {
let resolved = UpdateFeedResolver.resolvedFeedURLString(infoFeedURL: "")
XCTAssertEqual(resolved.url, UpdateFeedResolver.fallbackFeedURL)
XCTAssertFalse(resolved.isNightly)
XCTAssertTrue(resolved.usedFallback)
}
func testResolvedFeedUsesInfoFeedForStableChannel() {
let infoFeed = "https://example.com/custom/appcast.xml"
let resolved = UpdateFeedResolver.resolvedFeedURLString(infoFeedURL: infoFeed)
@ -954,6 +961,98 @@ final class TabManagerSurfaceCreationTests: XCTestCase {
}
}
@MainActor
final class TabManagerReopenClosedBrowserFocusTests: XCTestCase {
func testReopenFromDifferentWorkspaceFocusesReopenedBrowser() {
let manager = TabManager()
guard let workspace1 = manager.selectedWorkspace,
let closedBrowserId = manager.openBrowser(url: URL(string: "https://example.com/ws-switch")) else {
XCTFail("Expected initial workspace and browser panel")
return
}
drainMainQueue()
XCTAssertTrue(workspace1.closePanel(closedBrowserId, force: true))
drainMainQueue()
let workspace2 = manager.addWorkspace()
XCTAssertEqual(manager.selectedTabId, workspace2.id)
XCTAssertTrue(manager.reopenMostRecentlyClosedBrowserPanel())
drainMainQueue()
XCTAssertEqual(manager.selectedTabId, workspace1.id)
XCTAssertTrue(isFocusedPanelBrowser(in: workspace1))
}
func testReopenFallsBackToCurrentWorkspaceAndFocusesBrowserWhenOriginalWorkspaceDeleted() {
let manager = TabManager()
guard let originalWorkspace = manager.selectedWorkspace,
let closedBrowserId = manager.openBrowser(url: URL(string: "https://example.com/deleted-ws")) else {
XCTFail("Expected initial workspace and browser panel")
return
}
drainMainQueue()
XCTAssertTrue(originalWorkspace.closePanel(closedBrowserId, force: true))
drainMainQueue()
let currentWorkspace = manager.addWorkspace()
manager.closeWorkspace(originalWorkspace)
XCTAssertEqual(manager.selectedTabId, currentWorkspace.id)
XCTAssertFalse(manager.tabs.contains(where: { $0.id == originalWorkspace.id }))
XCTAssertTrue(manager.reopenMostRecentlyClosedBrowserPanel())
drainMainQueue()
XCTAssertEqual(manager.selectedTabId, currentWorkspace.id)
XCTAssertTrue(isFocusedPanelBrowser(in: currentWorkspace))
}
func testReopenCollapsedSplitFromDifferentWorkspaceFocusesBrowser() {
let manager = TabManager()
guard let workspace1 = manager.selectedWorkspace,
let sourcePanelId = workspace1.focusedPanelId,
let splitBrowserId = manager.newBrowserSplit(
tabId: workspace1.id,
fromPanelId: sourcePanelId,
orientation: .horizontal,
insertFirst: false,
url: URL(string: "https://example.com/collapsed-split")
) else {
XCTFail("Expected to create browser split")
return
}
drainMainQueue()
XCTAssertTrue(workspace1.closePanel(splitBrowserId, force: true))
drainMainQueue()
let workspace2 = manager.addWorkspace()
XCTAssertEqual(manager.selectedTabId, workspace2.id)
XCTAssertTrue(manager.reopenMostRecentlyClosedBrowserPanel())
drainMainQueue()
XCTAssertEqual(manager.selectedTabId, workspace1.id)
XCTAssertTrue(isFocusedPanelBrowser(in: workspace1))
}
private func isFocusedPanelBrowser(in workspace: Workspace) -> Bool {
guard let focusedPanelId = workspace.focusedPanelId else { return false }
return workspace.panels[focusedPanelId] is BrowserPanel
}
private func drainMainQueue() {
let expectation = expectation(description: "drain main queue")
DispatchQueue.main.async {
expectation.fulfill()
}
wait(for: [expectation], timeout: 1.0)
}
}
@MainActor
final class WorkspacePanelGitBranchTests: XCTestCase {
func testClosingFocusedSplitRestoresBranchForRemainingFocusedPanel() {