diff --git a/Sources/AppDelegate.swift b/Sources/AppDelegate.swift index 7d72cb08..5db4367f 100644 --- a/Sources/AppDelegate.swift +++ b/Sources/AppDelegate.swift @@ -9968,7 +9968,20 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent context.sidebarSelectionState.selection = .tabs bringToFront(window) - context.tabManager.focusTabFromNotification(tabId, surfaceId: surfaceId) + guard context.tabManager.focusTabFromNotification(tabId, surfaceId: surfaceId) else { +#if DEBUG + recordMultiWindowNotificationOpenFailureIfNeeded( + tabId: tabId, + surfaceId: surfaceId, + notificationId: notificationId, + reason: "focus_failed" + ) + if ProcessInfo.processInfo.environment["CMUX_UI_TEST_JUMP_UNREAD_SETUP"] == "1" { + writeJumpUnreadTestData(["jumpUnreadOpenResult": "0"]) + } +#endif + return false + } #if DEBUG // UI test support: Jump-to-unread asserts that the correct workspace/panel is focused. @@ -10033,7 +10046,17 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent sidebarSelectionState?.selection = .tabs bringToFront(window) - tabManager.focusTabFromNotification(tabId, surfaceId: surfaceId) + guard tabManager.focusTabFromNotification(tabId, surfaceId: surfaceId) else { +#if DEBUG + if ProcessInfo.processInfo.environment["CMUX_UI_TEST_JUMP_UNREAD_SETUP"] == "1" { + writeJumpUnreadTestData([ + "jumpUnreadFallbackFail": "focus_failed", + "jumpUnreadOpenResult": "0", + ]) + } +#endif + return false + } #if DEBUG recordJumpUnreadFocusFromModelIfNeeded( diff --git a/Sources/TabManager.swift b/Sources/TabManager.swift index 2556f229..ceb99add 100644 --- a/Sources/TabManager.swift +++ b/Sources/TabManager.swift @@ -2203,9 +2203,24 @@ class TabManager: ObservableObject { } } - func focusTabFromNotification(_ tabId: UUID, surfaceId: UUID? = nil) { + @discardableResult + func focusTabFromNotification(_ tabId: UUID, surfaceId: UUID? = nil) -> Bool { let wasSelected = selectedTabId == tabId - guard let tab = tabs.first(where: { $0.id == tabId }) else { return } + guard let tab = tabs.first(where: { $0.id == tabId }) else { +#if DEBUG + dlog("notification.focus.fail tab=\(tabId.uuidString.prefix(5)) reason=missingTab") +#endif + return false + } + if let surfaceId, tab.panels[surfaceId] == nil { +#if DEBUG + dlog( + "notification.focus.fail tab=\(tabId.uuidString.prefix(5)) " + + "panel=\(surfaceId.uuidString.prefix(5)) reason=missingPanel" + ) +#endif + return false + } let desiredPanelId = surfaceId ?? tab.focusedPanelId #if DEBUG if let desiredPanelId { @@ -2232,6 +2247,7 @@ class TabManager: ObservableObject { tab.triggerNotificationFocusFlash(panelId: targetPanelId, requiresSplit: false, shouldFocus: true) notificationStore.markRead(forTabId: tabId, surfaceId: targetPanelId) } + return true } func focusSurface(tabId: UUID, surfaceId: UUID) { diff --git a/Sources/TerminalController.swift b/Sources/TerminalController.swift index bf4862c6..70fe4a0f 100644 --- a/Sources/TerminalController.swift +++ b/Sources/TerminalController.swift @@ -11099,7 +11099,9 @@ class TerminalController { result = "ERROR: Surface not found" return } - tabManager.focusTabFromNotification(tab.id, surfaceId: surfaceId) + if !tabManager.focusTabFromNotification(tab.id, surfaceId: surfaceId) { + result = "ERROR: Focus failed" + } } return result } diff --git a/cmuxTests/CmuxWebViewKeyEquivalentTests.swift b/cmuxTests/CmuxWebViewKeyEquivalentTests.swift index da61d8b9..3713877a 100644 --- a/cmuxTests/CmuxWebViewKeyEquivalentTests.swift +++ b/cmuxTests/CmuxWebViewKeyEquivalentTests.swift @@ -5505,7 +5505,7 @@ final class TabManagerNotificationFocusTests: XCTestCase { XCTAssertTrue(workspace.toggleSplitZoom(panelId: leftPanelId), "Expected split zoom to enable") XCTAssertTrue(workspace.bonsplitController.isSplitZoomed, "Expected workspace to start zoomed") - manager.focusTabFromNotification(workspace.id, surfaceId: rightPanel.id) + XCTAssertTrue(manager.focusTabFromNotification(workspace.id, surfaceId: rightPanel.id)) drainMainQueue() drainMainQueue() @@ -5515,6 +5515,16 @@ final class TabManagerNotificationFocusTests: XCTestCase { ) XCTAssertEqual(workspace.focusedPanelId, rightPanel.id, "Expected notification target panel to be focused") } + + func testFocusTabFromNotificationReturnsFalseForMissingPanel() { + let manager = TabManager() + guard let workspace = manager.selectedWorkspace else { + XCTFail("Expected selected workspace") + return + } + + XCTAssertFalse(manager.focusTabFromNotification(workspace.id, surfaceId: UUID())) + } } @MainActor