From 6bd989480375e57ed328f6a40ed067aca142cb63 Mon Sep 17 00:00:00 2001 From: Lawrence Chen Date: Tue, 17 Mar 2026 17:38:03 -0700 Subject: [PATCH 1/2] Avoid menu resync on window activation --- Sources/AppDelegate.swift | 19 +++++++++++++++++++ Sources/cmuxApp.swift | 2 +- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/Sources/AppDelegate.swift b/Sources/AppDelegate.swift index 8be1eaf851bb..677c46eedb8d 100644 --- a/Sources/AppDelegate.swift +++ b/Sources/AppDelegate.swift @@ -4912,6 +4912,25 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent return nil } + /// Resolve the best current `TabManager` for menu/UI reads without mutating + /// the app-wide active window pointers. + func preferredTabManager(preferredWindow: NSWindow? = nil) -> TabManager? { + if let preferredWindow, + let context = contextForMainWindow(preferredWindow) { + return context.tabManager + } + if let context = contextForMainWindow(NSApp.keyWindow) { + return context.tabManager + } + if let context = contextForMainWindow(NSApp.mainWindow) { + return context.tabManager + } + if let activeManager = tabManager { + return activeManager + } + return mainWindowContexts.values.first?.tabManager + } + /// Re-sync app-level active window pointers from the currently focused main terminal window. /// This keeps menu/shortcut actions window-scoped even if the cached `tabManager` drifts. @discardableResult diff --git a/Sources/cmuxApp.swift b/Sources/cmuxApp.swift index c07000450d07..6e8b312fa024 100644 --- a/Sources/cmuxApp.swift +++ b/Sources/cmuxApp.swift @@ -840,7 +840,7 @@ struct cmuxApp: App { } private var activeTabManager: TabManager { - AppDelegate.shared?.synchronizeActiveMainWindowContext( + appDelegate.preferredTabManager( preferredWindow: NSApp.keyWindow ?? NSApp.mainWindow ) ?? tabManager } From 17c5ea62c1d84417ed7804eb47517e8463906506 Mon Sep 17 00:00:00 2001 From: Lawrence Chen Date: Tue, 17 Mar 2026 17:57:11 -0700 Subject: [PATCH 2/2] Centralize window activation focus restore --- Sources/AppDelegate.swift | 52 +++++++++++++++++++++++++++++-- Sources/GhosttyTerminalView.swift | 12 ------- 2 files changed, 49 insertions(+), 15 deletions(-) diff --git a/Sources/AppDelegate.swift b/Sources/AppDelegate.swift index 677c46eedb8d..e1e8b3c3eae3 100644 --- a/Sources/AppDelegate.swift +++ b/Sources/AppDelegate.swift @@ -10451,7 +10451,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent queue: .main ) { [weak self] note in guard let self, let window = note.object as? NSWindow else { return } - self.setActiveMainWindow(window) + guard let context = self.setActiveMainWindow(window) else { return } + self.restoreFocusedTerminalOnWindowActivation(window: window, context: context) } } @@ -10495,8 +10496,52 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent return tabManager?.selectedWorkspace?.browserPanel(for: panelId) } - private func setActiveMainWindow(_ window: NSWindow) { - guard let context = contextForMainTerminalWindow(window) else { return } + private func restoreFocusedTerminalOnWindowActivation( + window: NSWindow, + context: MainWindowContext + ) { + guard window.isKeyWindow, + let workspace = context.tabManager.selectedWorkspace, + let focusedPanelId = workspace.focusedPanelId, + let terminalPanel = workspace.terminalPanel(for: focusedPanelId) else { + return + } +#if DEBUG + dlog( + "mainWindow.focusRestore window={\(debugWindowToken(window))} " + + "workspace=\(workspace.id.uuidString.prefix(5)) panel=\(focusedPanelId.uuidString.prefix(5))" + ) +#endif + terminalPanel.hostedView.ensureFocus(for: workspace.id, surfaceId: focusedPanelId) + } + + @discardableResult + private func setActiveMainWindow(_ window: NSWindow) -> MainWindowContext? { + guard let context = contextForMainTerminalWindow(window) else { return nil } + let sameTabManager = tabManager === context.tabManager + let sameSidebarState = sidebarState === context.sidebarState + let sameSidebarSelectionState = sidebarSelectionState === context.sidebarSelectionState + if sameTabManager { + let repairedSidebarState = !sameSidebarState && sidebarState == nil + let repairedSidebarSelectionState = + !sameSidebarSelectionState && sidebarSelectionState == nil + if repairedSidebarState { + sidebarState = context.sidebarState + } + if repairedSidebarSelectionState { + sidebarSelectionState = context.sidebarSelectionState + } + TerminalController.shared.setActiveTabManager(context.tabManager) +#if DEBUG + dlog( + "mainWindow.active window={\(debugWindowToken(window))} context={\(debugContextToken(context))} " + + "nochange=1 repairSidebar=\(repairedSidebarState ? 1 : 0) " + + "repairSelection=\(repairedSidebarSelectionState ? 1 : 0) " + + "\(debugShortcutRouteSnapshot())" + ) +#endif + return context + } #if DEBUG let beforeManagerToken = debugManagerToken(tabManager) #endif @@ -10509,6 +10554,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent "mainWindow.active window={\(debugWindowToken(window))} context={\(debugContextToken(context))} beforeMgr=\(beforeManagerToken) afterMgr=\(debugManagerToken(tabManager)) \(debugShortcutRouteSnapshot())" ) #endif + return context } private func unregisterMainWindow(_ window: NSWindow) { diff --git a/Sources/GhosttyTerminalView.swift b/Sources/GhosttyTerminalView.swift index 2c241ffa4a06..96f188e10e30 100644 --- a/Sources/GhosttyTerminalView.swift +++ b/Sources/GhosttyTerminalView.swift @@ -6696,18 +6696,6 @@ final class GhosttySurfaceScrollView: NSView { windowObservers.forEach { NotificationCenter.default.removeObserver($0) } windowObservers.removeAll() guard let window else { return } - windowObservers.append(NotificationCenter.default.addObserver( - forName: NSWindow.didBecomeKeyNotification, - object: window, - queue: .main - ) { [weak self] _ in - guard let self else { return } - let searchActive = self.surfaceView.terminalSurface?.searchState != nil -#if DEBUG - dlog("find.window.didBecomeKey surface=\(self.surfaceView.terminalSurface?.id.uuidString.prefix(5) ?? "nil") searchActive=\(searchActive) focusTarget=\(self.searchFocusTarget) firstResponder=\(String(describing: self.window?.firstResponder))") -#endif - self.scheduleAutomaticFirstResponderApply(reason: "didBecomeKey") - }) windowObservers.append(NotificationCenter.default.addObserver( forName: NSWindow.didResignKeyNotification, object: window,