How cmux Handles Window Focus and Prevents Focus Stealing: A Deep Dive into AppKit and Bonsplit
cmux implements a layered focus-management strategy that synchronizes window-level activation, AppKit first-responder state, and model-level panel selection while explicitly blocking accidental focus-stealing through socket command gating, omnibar suppression, and programmatic split safeguards.
The manaflow-ai/cmux terminal multiplexer built on AppKit, SwiftUI, and a custom split-pane engine (Bonsplit) faces unique focus-management challenges. Because UI actions like splits, tab moves, and socket commands constantly mutate the visual hierarchy, cmux maintains strict coordination between the window-level focus, AppKit first-responder, and model-level "focused panel" to prevent unwanted focus theft.
Window-Level Focus Management
All window activation operations in cmux route through a single chokepoint in Sources/AppDelegate.swift. The focusMainWindow(windowId:) method handles bringing windows to the foreground and claiming application activation.
Activating Windows via AppDelegate
When a command requests window focus, cmux calls AppDelegate.focusMainWindow(windowId:) to ensure the target window becomes key and the app activates:
// Sources/AppDelegate.swift
func focusMainWindow(windowId: UUID) -> Bool {
guard let window = mainWindow(for: windowId) else { return false }
window.makeKeyAndOrderFront(nil) // bring to front
NSApp.activate(ignoringOtherApps: true) // become the active app
return true
}
Higher-level focus actions, including TerminalController.focusWindow(_:) and V2 socket commands (focus_window), eventually delegate to this method. For example, TerminalController wraps the call in a synchronous main-thread block:
// Sources/TerminalController.swift – focusWindow()
let ok = v2MainSync { AppDelegate.shared?.focusMainWindow(windowId: windowId) ?? false }
Per-Panel Focus Synchronization
Each surface (terminal, browser, or other content) lives inside a panel managed by the Workspace class. The Workspace.focusPanel(_:previousHostedView:trigger:) method in Sources/Workspace.swift serves as the single source of truth for aligning model state with UI state.
The Workspace.focusPanel Method
This centralized method updates four critical elements simultaneously:
- Model state: Updates
focusedPanelIdviamarkExplicitFocusIntent(on:) - Bonsplit pane: Selects the pane containing the panel using
focusPaneandselectTab - AppKit first-responder: Makes the panel's native view (
GhosttySurfaceScrollViewfor terminals,WKWebViewfor browsers) the first responder - Focus override: Respects
AppFocusState.overrideIsFocusedfor automation scenarios
// Sources/Workspace.swift – focusPanel()
guard let tabId = surfaceIdFromPanelId(panelId) else { return }
let targetPaneId = bonsplitController.allPaneIds.first { ... }
if let targetPaneId, !selectionAlreadyConverged {
bonsplitController.focusPane(targetPaneId) // pane → focused
}
if !selectionAlreadyConverged {
bonsplitController.selectTab(tabId) // tab → selected
}
applyTabSelection(tabId: tabId, inPane: targetPaneId,
reassertAppKitFocus: !shouldSuppressReentrantRefocus,
focusIntent: activationIntent,
previousTerminalHostedView: previousTerminalHostedView)
Re-asserting Focus After Non-Focus Splits
When splits occur without explicit focus intent, Bonsplit's asynchronous layout could temporarily diverge from the desired focus state. The reassertFocusAfterNonFocusSplit() method guarantees that newly-created panels receive focus even when layout races occur:
// Sources/Workspace.swift – reassertFocusAfterNonFocusSplit()
if focusedPanelId == splitPanelId {
focusPanel(preferredPanelId, previousHostedView: allowPreviousHostedView ? previousHostedView : nil)
}
Preventing Focus Stealing in cmux
cmux employs multiple defensive layers to ensure focus only changes when explicitly requested, neutralizing transient UI race conditions that could pull focus away from user-intended targets.
Socket Command Gating
Only authorized socket commands may mutate focus. The TerminalController.socketCommandAllowsInAppFocusMutations() static method checks the socket's focus-override flag and command key to determine if the operation should proceed:
// Sources/TerminalController.swift – socketCommandAllowsInAppFocusMutations()
static func socketCommandAllowsInAppFocusMutations() -> Bool {
// Returns true for commands that are documented to modify focus
}
All UI-mutating calls—including focusWindow, focusSurface, and focus_pane—wrap their logic with this guard. This prevents arbitrary remote clients from stealing focus unless the policy explicitly permits it.
Browser Omnibar Suppression
When a browser panel receives focus, the omnibar (address bar) would otherwise auto-focus immediately, pulling first-responder status away from terminals. BrowserPanel.suppressOmnibarAutofocus(for:) sets a timestamp that the panel checks before auto-focusing:
// Sources/Panels/BrowserPanel.swift – suppressOmnibarAutofocus()
func suppressOmniboxAutofocus(for seconds: TimeInterval) {
suppressOmnibarAutofocusUntil = Date().addingTimeInterval(seconds)
}
func shouldSuppressOmnibarAutofocus() -> Bool {
if let until = suppressOmnibarAutofocusUntil { return Date() < until }
return false
}
The focus path that moves a web view into focus calls this suppression for approximately one second before invoking window.makeFirstResponder(webView):
// Sources/TerminalController.swift – v2BrowserFocus()
browserPanel.suppressOmnibarAutofocus(for: 1.0)
window.makeFirstResponder(webView)
Safeguards for Programmatic Splits
When creating splits programmatically via newTerminalSplit or newBrowserSplit, the old panel's becomeFirstResponder side-effects could race with Bonsplit's layout. The code suppresses the old view's re-parent focus, explicitly calls focusPanel on the new panel, and runs delayed re-assertions to guarantee convergence:
// Sources/Workspace.swift – split creation (excerpt)
previousHostedView?.suppressReparentFocus()
focusPanel(newPanel.id, previousHostedView: previousHostedView)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
previousHostedView?.clearSuppressReparentFocus()
}
Three nested asyncAfter blocks progressively clear suppression and re-assert focus, ensuring any transient divergence resolves within approximately 150 milliseconds.
Modal UI Protection
Before any focus change, Workspace.isCommandPaletteVisibleForWorkspaceWindow() checks whether the global command-palette UI is active. If visible, focus changes are ignored to prevent stealing the palette's first-responder status:
guard !isCommandPaletteVisibleForWorkspaceWindow() else { return }
Similar guards exist for alerts and other modal dialogs, ensuring that focus_main_window cannot trigger from within close-confirmation dialogs or similar modal contexts.
Summary
- Centralized window activation: All window-level focus routes through
AppDelegate.focusMainWindow(windowId:)inSources/AppDelegate.swift, ensuring consistentNSApp.activatebehavior. - Unified panel focus:
Workspace.focusPanel(_:previousHostedView:trigger:)inSources/Workspace.swiftsynchronizes model state, Bonsplit pane selection, and AppKit first-responder in a single transaction. - Socket-level security:
TerminalController.socketCommandAllowsInAppFocusMutations()gates focus-mutating commands, preventing unauthorized remote focus changes. - Browser focus protection:
BrowserPanel.suppressOmnibarAutofocus(for:)prevents the address bar from stealing focus when switching to browser panels. - Split creation safety: Suppression of re-parent focus and delayed re-assertion logic eliminates race conditions during programmatic split operations.
- Modal safeguards: Visibility checks for command palettes and dialogs block external focus changes while modal UI owns first-responder.
Frequently Asked Questions
How does cmux prevent a terminal command from stealing focus when opening a browser split?
cmux uses socket command gating via TerminalController.socketCommandAllowsInAppFocusMutations() to verify that only explicitly authorized commands may request focus changes. When a browser split opens, the system also invokes BrowserPanel.suppressOmnibarAutofocus(for: 1.0) to prevent the address bar from immediately capturing first-responder status, ensuring the terminal maintains focus unless the user explicitly requests otherwise.
What happens if Bonsplit's asynchronous layout conflicts with a focus change request?
When programmatic splits occur, cmux calls previousHostedView?.suppressReparentFocus() to disable the old view's focus side-effects, then explicitly focuses the new panel via Workspace.focusPanel(). The system runs three nested DispatchQueue.main.asyncAfter delays (at 0.05s, 0.1s, and 0.15s) to progressively clear suppression and re-assert focus, guaranteeing convergence even when Bonsplit's layout animation temporarily diverges from the model state.
Can automation scripts force cmux to appear focused without triggering focus-stealing side effects?
Yes. cmux provides AppFocusState.overrideIsFocused, which allows automation to force a specific focus state without triggering the standard focus mutation guards. This override bypasses the normal socket command gating while still respecting modal UI protections, enabling scripted interactions to maintain consistent window appearance without disrupting the user's current focus context.
Why does cmux check for command palette visibility before changing focus?
The isCommandPaletteVisibleForWorkspaceWindow() check prevents external focus requests from interrupting user interaction with the global command palette. If the palette is visible, focus changes return early, preserving the palette's first-responder status. This safeguard ensures that background processes or socket commands cannot pull focus away from active text input in the command palette or modal dialogs.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →