Fix crash when a focused SwiftUI view outlives its hosting view
Summary
cmux aborts with Fatal error: Attempted to read an unowned reference but object ... was already destroyed inside SwiftUI's KeyViewProxy.nextResponder.getter. Sentry has this one crash under seven issues because each walker groups separately: CMUXTERM-MACOS-3Z73 (0.65.0, shortcut routing), 3Z6B (menu activeBrowserPanel), 3TDX (WebViewRepresentable.responderChainContains), 2FTW (BrowserPanel.responderChainContains), 1SSX (AppKit _firstResponderForEvent), 24Y3 and 29G9 (SwiftUI FirstResponderObserver during makeFirstResponder).
SwiftUI puts a private KeyViewProxy subview into an NSHostingView for each focusable element. Disassembly of macOS 27 SwiftUI shows its nextResponder returns focusBridge.host, an unowned reference to the hosting view. When the hosting view is freed while anything still holds the proxy, every nextResponder read traps. Once a window's first responder is such a proxy, there is no safe exit: a key event, a cmux walk, and makeFirstResponder(nil) (through SwiftUI's own observer) all crash. So guarding cmux's own walks would only move the crash into AppKit.
This PR replaces the nextResponder getter on SwiftUI.KeyViewProxy only. A proxy with no superview returns nil; every other proxy calls SwiftUI's original getter. While a proxy is attached, its host is its superview and that is what nextResponder returns. A freed host clears its subviews' superview, so the guard conservatively ends the chain for detached proxies, including a temporary detach from a still-live host, and preserves attached behavior. If SwiftUI renames the class, the install is a no-op.
This is a workaround for a SwiftUI bug, installed beside cmux's existing NSWindow/NSApplication swizzles. I could not find the exact cmux transition that leaves a window's first responder on a freed proxy: plain removal, subviews =, replaceSubview, ancestor removal, content swaps, cross-window moves and restoring a detached proxy are all reset correctly by AppKit. The guard protects main-thread responder walks through that detached state; it does not identify or eliminate the originating lifetime transition.
Focus recovery stays in the existing window event path. Before dispatching terminal key events, repairFocusedTerminalKeyboardRoutingIfNeeded treats detached responders as invalid focus owners and restores the selected terminal. This covers plain and Command key events; recovery for browser-only, sheet, and auxiliary-window states has not been established by a live stale-proxy reproduction. The getter itself does not mutate focus during responder-chain traversal.
During CI validation, the shortcut-routing suite also stalled while all 14 utility executor workers were scanning temporary files from unused CloudFilePreviewCache instances. Cleanup now starts only when a remote preview is requested and fetches metadata only for matching cache directories. The focus-proxy fixtures wait for actual focus and host release; the IME fixtures explicitly request native surface startup before dispatching input.
Testing
- Documentation follow-up
83c41ce33b372bb360c8a1712c139a8579e69db5adds DocC comments and clarifies the detached-proxy contract. Every line outside///comments is unchanged from the verified parent. All 6 scoped local checks passed again; new-head hosted CI is pending. - Runtime/test code at
fcda01111ba236c8bea73a13464afdd32d4844c7: CI 37585194272 passed app/test compilation and all 27 selected suites. XCTest reported 371 tests, 4 skipped, 0 failures; Swift Testing reported 240 tests in 19 suites passed. The skips cover existing libghostty/window-server/WebKit host limitations. - Both proxy regressions executed and passed: released host (0.051 s) and attached host (0.009 s). The two previously failing IME input cases passed (0.366 s and 0.150 s), and
AppDelegateShortcutRoutingTests.testTextBoxMentionFileSuggestionsRefreshCachedMissespassed (1.238 s). - Red/green evidence: 37581970340 at
527c79ad6d8failed the attached-proxy setup, both IME fixtures, and the mention-index test. The latter three also failed at the original PR head. The green run retains all 13 original suites and adds the related suites selected by the cache change; the entire shortcut-routing suite ran so the mention test retained the preceding Files-store churn. python3 scripts/verify-local.py: 6/6 selected checks passed.git diff --checkpassed. The exact-head tagged fleet build passed; build receipt details include the job and artifact digest.- The original author reports a standalone macOS 27.0.1 repro: retaining a proxy after releasing its host aborts during responder reads, key dispatch, and focus changes without the guard; those operations complete with it.
Changelog
Fixed: avoid crashes when event routing reaches a SwiftUI focus proxy whose host was released, and defer unnecessary preview-cache cleanup during workspace creation
Proof
No visible change; the crash repro output is in Testing.
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.
Summary by cubic
Fixes the crash when a SwiftUI KeyViewProxy (a private focusable subview) outlives its hosting view. SwiftUI's nextResponder getter returns an unowned reference to the host, so once the host is freed, every responder-chain read aborts with "Attempted to read an unowned reference"; cmux's shortcut routing, AppKit's event routing, and SwiftUI's own first-responder observer all hit the same state.
- Installs a guard on
SwiftUI.KeyViewProxy.nextRespondernext to cmux's existing swizzles: a proxy with no superview returnsnil, everything else calls SwiftUI's original getter. Since a freed host clears its subviews'superview, the guard only changes the chain for proxies that would crash; if SwiftUI renames the class, the install is a no-op. - Adds tests covering a detached proxy ending the chain and an attached proxy still forwarding to its host; the fixtures wait on SwiftUI focus lifecycle state instead of fixed delays, and the IME tests now demand a native runtime before synthetic input.
- Defers
CloudFilePreviewCachecleanup until a preview is actually opened, so unused Files stores no longer saturate the executor with temp-directory scans at startup.
[!NOTE]
Medium Risk
Runtime method replacement on a private SwiftUI class affects global keyboard/focus routing; behavior is narrowly scoped to detached proxies but is inherently fragile across OS updates.
Overview
Adds a runtime guard on SwiftUI’s private KeyViewProxy so nextResponder returns nil when the proxy is detached (superview == nil), avoiding fatal crashes from SwiftUI’s unowned host reference during responder-chain walks (shortcuts, AppKit routing, focus). The guard is installed once alongside existing window/application swizzles in AppDelegate; if the class is absent, install is a no-op.
CloudFilePreviewCache no longer kicks off stale-directory cleanup at init—cleanup runs on first preview materialize, with cancellation checks and metadata reads only for matching preview folders, reducing executor churn when many Files stores never open previews.
Tests: new Swift Testing suite for detached vs attached proxy behavior; CJK IME hosted-terminal fixtures now start the native surface and reconcile geometry before synthetic input.
Reviewed by Cursor Bugbot for commit 83c41ce33b372bb360c8a1712c139a8579e69db5. Bugbot is set up for automated code reviews on this repo. Configure here.
Summary by CodeRabbit
- Bug Fixes
- Improved keyboard responder handling for detached SwiftUI views: their responder chain now ends when they are no longer attached to a host view, while attached views continue forwarding through the chain.
- Preview cache cleanup now starts when a preview is first materialized, checks for cancellation during cleanup, and can stop promptly when cancelled.
- Tests
- Added regression coverage for responder behavior in both detached and attached views.