Skip to content

Fix terminals freezing when first responder drifts#2505

Merged
austinywang merged 8 commits intomainfrom
issue-2438-terminals-freezing
Apr 6, 2026
Merged

Fix terminals freezing when first responder drifts#2505
austinywang merged 8 commits intomainfrom
issue-2438-terminals-freezing

Conversation

@austinywang
Copy link
Copy Markdown
Contributor

@austinywang austinywang commented Apr 1, 2026

Summary

  • Adds repairFocusedTerminalKeyboardRoutingIfNeeded to detect when the window's first responder has drifted away from the focused terminal surface (e.g., to nil, the window itself, a hidden/detached view, or a view in a different window) and automatically restores focus before the keyDown is dispatched
  • Regression test verifies that typing into a terminal with a broken first responder triggers the repair and forwards the keystroke into Ghostty

Fixes #2438

Test plan

  • Regression test: testWindowSendEventRepairsLostFirstResponderForFocusedTerminalTyping
  • Manual: open multiple workspaces/splits, switch between them rapidly, verify typing never freezes
  • Manual: verify Cmd+shortcuts still work (repair is skipped for .command modifier)
  • Manual: verify search overlay still receives keystrokes (repair is skipped when search is active)

🤖 Generated with Claude Code


Summary by cubic

Fixes terminal typing freezes by repairing keyboard routing to the active terminal surface or search field when the window’s first responder drifts, including field editor cases. Fixes #2438.

  • Bug Fixes
    • Added repairFocusedTerminalKeyboardRoutingIfNeeded (invoked from NSWindow.sendEvent on .keyDown) to detect responder drift—including nil, window, hidden/detached/different-window views, wrong same-window view, and field-editor ownership—and restore focus to the terminal surface or mounted search field; skips when .command is pressed, a sheet is attached, or the command palette is visible.
    • When a background-image is configured, force macos-background-from-layer = false so Ghostty renders images; keep the layer-backed background for solid colors and refresh it after config reloads.
    • Added regression tests for nil/window drift, visible same-window drift, and search typing repair (preserves the first typed character).
    • Fixed DEBUG logging by coercing focus and surface tokens to String for stable output.

Written for commit b131701. Summary will update on new commits.

Summary by CodeRabbit

  • Bug Fixes

    • Automatically restores and reroutes typing to the intended terminal view when focus is lost or an incorrect responder intercepts key events; limited to plain typing (excludes command/modifier cases and attached sheets/overlays).
  • Appearance

    • Honors configured background images instead of forcing legacy layer-backed backgrounds.
  • Tests

    • Added tests validating focus repair for terminals and the search field, including preserved initial input.

@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 1, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment Apr 6, 2026 7:16am

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 1, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds key-down focus-repair logic invoked from a swizzled NSWindow.sendEvent. It computes a key-routing owner for the current responder, validates responder ↔ window/surface ownership and visibility, resolves the active workspace + focused terminal panel, and calls terminalPanel.hostedView.ensureFocus(...) when repair is needed.

Changes

Cohort / File(s) Summary
AppDelegate — focus repair
Sources/AppDelegate.swift
Added repairFocusedTerminalKeyboardRoutingIfNeeded(window:event:), helpers to derive key-routing owner, validate responder-window/surface ancestry and visibility, decide when to repair, and wired call into swizzled NSWindow.cmux_sendEvent. DEBUG-only timing accumulator added.
Tests — routing & search focus
cmuxTests/AppDelegateShortcutRoutingTests.swift
Added FocusableTestView and three test cases covering lost-first-responder repair, stray on-screen responder repair, and terminal-search typing repair (preserves typed input). Includes private helpers to locate editable NSTextField and detect field-editor ownership.
Ghostty terminal focus & config
Sources/GhosttyTerminalView.swift
Added hasConfiguredBackgroundImage(_:); conditional override of macos-background-from-layer when a background image exists; added responderMatchesPreferredKeyboardFocus(_:); refactored restoreSearchFocus to attempt immediate focus on mounted search field (focusMountedSearchFieldIfAvailable) with field-editor-aware ownership checks. Minor DEBUG log formatting tweaks.
Vendored dependency pin
vendor/bonsplit
Updated submodule pointer to a newer commit (vendor code version bump).

Sequence Diagram(s)

sequenceDiagram
    participant Window as NSWindow
    participant App as AppDelegate
    participant WS as WorkspaceResolver
    participant Panel as TerminalPanel
    participant Ghost as GhosttyNSView

    Window->>App: cmux_sendEvent(event: keyDown)
    App->>App: repairFocusedTerminalKeyboardRoutingIfNeeded(window,event)
    alt event passes gates
        App->>WS: resolve active workspace + focused panel
        WS->>Panel: locate focused terminal panel
        Panel->>Ghost: hostedView.ensureFocus(workspace.id, panelId)
        Ghost-->>App: focus ensured / no-op if already matching
    else gates fail
        App-->>Window: continue normal sendEvent
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐰 I hopped along the window's way,

and nudged lost keys back into play.
A cursor sighed, the letters flew,
the terminal woke — hello, anew!
🥕✨

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 3.45% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Out of Scope Changes check ❓ Inconclusive Most changes align with fixing responder drift, but the vendor/bonsplit submodule update and background-image handling changes appear tangentially related or unscoped. Clarify why the vendor/bonsplit submodule was updated and confirm background-image changes are necessary for the keyboard routing fix.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Fix terminals freezing when first responder drifts' accurately describes the main change: detecting and repairing lost first responder focus to prevent terminal freezing.
Linked Issues check ✅ Passed The PR partially addresses issue #2438 by fixing the responder-drift failure mode with the new repair routine, but does not fix the separate main-thread deadlock failure mode identified in comments.
Description check ✅ Passed The PR description covers the main change (focus repair logic) and includes a test plan with one automated test checked off, but lacks a demo video and does not explicitly confirm all checklist items.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue-2438-terminals-freezing

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 1, 2026

Greptile Summary

This PR fixes terminal-freezing caused by the macOS first-responder drifting away from the focused Ghostty surface (to nil, the window itself, a hidden view, or a view in a different window). It intercepts every keyDown event in NSWindow.sendEvent, diagnoses the drift with responderNeedsFocusedTerminalKeyRepair, and calls ensureFocus to restore the correct first responder before the event is dispatched to cmux_sendEvent.

Key changes:

  • keyRoutingOwnerView — unwraps field-editor NSTextViews to their delegate NSView before any window/visibility checks.
  • responderNeedsFocusedTerminalKeyRepair — returns true for the pathological states (nil, NSWindow, hidden view, wrong-window view) that cause key routing to stall, and returns false for any legitimate responder so the fast-path is a no-op.
  • repairFocusedTerminalKeyboardRoutingIfNeeded — gate-keeps with multiple early-exit guards (non-keyDown, command modifier, non-terminal window, attached sheet, command palette visible, search active) before attempting repair; adds focusRepairMs timing to the existing per-keystroke breakdown log.
  • Regression test follows the required two-commit structure (050cd83f = failing test, 1a95fb9c = fix) as mandated by the project's regression test commit policy.

Confidence Score: 5/5

  • Safe to merge — the fix is narrow and well-guarded, with no P0/P1 findings.
  • All remaining comments are P2 style/clarification suggestions. The core logic is correct: repair runs only when a diagnosable drift state is detected, early-exit guards protect latency-sensitive non-repair paths, timing instrumentation is added to the existing breakdown log, and the two-commit regression-test structure is correctly followed per project policy.
  • No files require special attention.

Important Files Changed

Filename Overview
Sources/AppDelegate.swift Adds three new helpers — keyRoutingOwnerView, responderNeedsFocusedTerminalKeyRepair, and repairFocusedTerminalKeyboardRoutingIfNeeded — hooked into the sendEvent path before cmux_sendEvent. Guards are well-structured (skips command keys, attached sheets, command palette, search overlay). DEBUG timing instrumentation added correctly. No P0/P1 issues found.
cmuxTests/AppDelegateShortcutRoutingTests.swift Adds testWindowSendEventRepairsLostFirstResponderForFocusedTerminalTyping, which breaks focus with makeFirstResponder(nil) then verifies sendEvent triggers repair and restores firstResponder. Key-forwarding assertion is DEBUG-only (inherent limitation of the test seam) but first-responder identity is verified in all configurations.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[NSWindow.sendEvent keyDown] --> B{isMainTerminalWindow?}
    B -- No --> Z[cmux_sendEvent  no repair]
    B -- Yes --> C{attachedSheet?}
    C -- Yes --> Z
    C -- No --> D{commandPaletteVisible?}
    D -- Yes --> Z
    D -- No --> E{focusedTerminalPanel found?}
    E -- No --> Z
    E -- Yes --> F{searchState active?}
    F -- Yes --> Z
    F -- No --> G[responderNeedsFocusedTerminalKeyRepair]
    G --> H{firstResponder nil\nor NSWindow?}
    H -- Yes --> REPAIR
    H -- No --> I{cmuxOwningGhosttyView found?}
    I -- Yes --> J{wrong window\nor hidden?}
    J -- Yes --> REPAIR
    J -- No --> K[return false]
    I -- No --> L{keyRoutingOwnerView found?}
    L -- No --> K
    L -- Yes --> M{wrong window / hidden\n/ no superview?}
    M -- Yes --> REPAIR
    M -- No --> K
    K --> Z
    REPAIR[ensureFocus → restore firstResponder] --> Z
    Z --> N[Event dispatched to correct first responder]
Loading

Reviews (1): Last reviewed commit: "Repair focused terminal key routing afte..." | Re-trigger Greptile

Comment thread cmuxTests/AppDelegateShortcutRoutingTests.swift
Comment thread Sources/AppDelegate.swift Outdated
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 2 files

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
cmuxTests/AppDelegateShortcutRoutingTests.swift (1)

3435-3441: Restore prior debug observer instead of always clearing to nil

Line 3435 overwrites a static global observer, and Line 3440 always clears it to nil. That can accidentally drop an existing observer and create cross-test interference. Save/restore the previous value in defer.

Suggested patch
 `#if` DEBUG
         var forwardedKeyDownCount = 0
+        let previousKeyEventObserver = GhosttyNSView.debugGhosttySurfaceKeyEventObserver
         GhosttyNSView.debugGhosttySurfaceKeyEventObserver = { keyEvent in
+            previousKeyEventObserver?(keyEvent)
             guard keyEvent.action == GHOSTTY_ACTION_PRESS, keyEvent.keycode == 0 else { return }
             forwardedKeyDownCount += 1
         }
         defer {
-            GhosttyNSView.debugGhosttySurfaceKeyEventObserver = nil
+            GhosttyNSView.debugGhosttySurfaceKeyEventObserver = previousKeyEventObserver
         }
 `#endif`
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmuxTests/AppDelegateShortcutRoutingTests.swift` around lines 3435 - 3441,
The test overwrites the global GhosttyNSView.debugGhosttySurfaceKeyEventObserver
and then unconditionally clears it to nil, which can drop an existing observer
and cause cross-test interference; fix by capturing the previous observer into a
local (e.g. let previousObserver =
GhosttyNSView.debugGhosttySurfaceKeyEventObserver) before assigning the new
closure that increments forwardedKeyDownCount, and in the defer restore the
previousObserver (GhosttyNSView.debugGhosttySurfaceKeyEventObserver =
previousObserver) instead of setting it to nil.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Sources/AppDelegate.swift`:
- Around line 4949-4952: The recovery guard uses contextForMainWindow(window)
which can miss stale ObjectIdentifier entries; change the logic to use the
reindexing resolver by calling contextForMainTerminalWindow(window) as the
primary or as a fallback when contextForMainWindow(window) returns nil so the
repair path always resolves the window (keep the rest of the guard: workspace =
context.tabManager.selectedWorkspace, panelId = workspace.focusedPanelId,
terminalPanel = workspace.terminalPanel(for: panelId)). Ensure you reference
contextForMainTerminalWindow(_:), contextForMainWindow(_:),
tabManager.selectedWorkspace, focusedPanelId, and terminalPanel(for:) when
making the update.
- Around line 4913-4918: The current check in cmuxOwningGhosttyView(for:) treats
any GhosttyNSView in the same window as healthy; change it to also consider
whether that ghostty view actually corresponds to the currently focused panel —
if ghosttyView is in the same window but its panel identifier does not match
workspace.focusedPanelId (or whatever property identifies the panel on
GhosttyNSView), return true so repair runs; in short, add a comparison between
ghosttyView's panel id/owner and workspace.focusedPanelId (or the focused panel
id source) and treat a mismatch as needing repair.

---

Nitpick comments:
In `@cmuxTests/AppDelegateShortcutRoutingTests.swift`:
- Around line 3435-3441: The test overwrites the global
GhosttyNSView.debugGhosttySurfaceKeyEventObserver and then unconditionally
clears it to nil, which can drop an existing observer and cause cross-test
interference; fix by capturing the previous observer into a local (e.g. let
previousObserver = GhosttyNSView.debugGhosttySurfaceKeyEventObserver) before
assigning the new closure that increments forwardedKeyDownCount, and in the
defer restore the previousObserver
(GhosttyNSView.debugGhosttySurfaceKeyEventObserver = previousObserver) instead
of setting it to nil.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0faab0c6-dc09-409a-828e-f9a9083720e4

📥 Commits

Reviewing files that changed from the base of the PR and between e9b2090 and 1a95fb9.

📒 Files selected for processing (2)
  • Sources/AppDelegate.swift
  • cmuxTests/AppDelegateShortcutRoutingTests.swift

Comment thread Sources/AppDelegate.swift
Comment thread Sources/AppDelegate.swift Outdated
@sghael
Copy link
Copy Markdown

sghael commented Apr 3, 2026

Note: this PR doesn't cover the main-thread deadlock scenario

This PR fixes the case where the first responder silently drifts and typing stops (app still responsive). But there's a second failure mode reported in #2438 where the entire app hangs (beach ball, not just frozen input).

I captured a sample trace of this deadlock — posted in this comment on #2438. The main thread is stuck here:

GhosttyTerminalView.updateNSView()
  → GhosttySurfaceScrollView.applyFirstResponderIfNeeded()
    → Thread.Futex.Deadline.wait
      → __ulock_wait2

100% of main thread samples are blocked on that futex — it's a deadlock during a SwiftUI layout pass, not a responder drift. Since the main thread is blocked, sendEvent never runs, so the repairFocusedTerminalKeyboardRoutingIfNeeded repair added here wouldn't get a chance to fire.

Just flagging so #2438 isn't closed as fully resolved by this PR alone — the deadlock in applyFirstResponderIfNeeded is a separate issue.

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 4 files (changes from recent commits).

You’re at about 95% of the monthly review limit. You may want to disable incremental reviews to conserve quota. Reviews will continue until that limit is exceeded. If you need help avoiding interruptions, please contact contact@cubic.dev.

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="Sources/AppDelegate.swift">

<violation number="1" location="Sources/AppDelegate.swift:4961">
P1: The new NSTextView/NSControl exemption skips repair when the open find bar has drifted focus back from terminal mode to the search field.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread Sources/AppDelegate.swift Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Sources/AppDelegate.swift`:
- Around line 4949-4963: The current check that returns false for any ownerView
is NSControl || responder is NSTextView is too broad; replace it with a targeted
whitelist: create a helper like isAllowedTextInputResponder(_ responder:
NSResponder) that explicitly checks for known same-window editor types (e.g.,
your app’s omnibar/find-field editor classes or specific control subclasses) and
use that helper in place of the blanket condition in the block containing
hostedView.responderMatchesPreferredKeyboardFocus(_:),
cmuxOwningGhosttyView(for:), and keyRoutingOwnerView(for:). Update the code path
to return false only when isAllowedTextInputResponder(...) is true and remove
the generic NSControl/NSTextView check so unrelated text inputs in other splits
won’t steal typing from the focused terminal.
- Around line 4896-4903: The current keyRoutingOwnerView(for:) function unsafely
dereferences NSTextView.delegate (editor.delegate as? NSView) on a hot .keyDown
path; remove that direct delegate access and instead determine the routing owner
via safe responder traversal: if responder is NSTextView and
editor.isFieldEditor, walk the responder chain (using editor.nextResponder /
nextResponder loop) or check editor.superview / editor.window?.firstResponder to
locate the nearest NSView owner, returning that NSView if found; otherwise
fallback to returning responder as? NSView. Update keyRoutingOwnerView(for:) to
avoid any direct use of editor.delegate so it cannot dereference the
unsafe-unretained delegate.

In `@Sources/GhosttyTerminalView.swift`:
- Around line 9268-9270: The mounted-field fast path calls
focusMountedSearchFieldIfAvailable(window:surfaceShort:) without checking
whether the mounted-focus request is still valid; mirror the other paths by
gating this call with canApplyMountedSearchFieldFocusRequest() so it only runs
when the active-panel intent matches (same guard used by
requestMountedSearchFieldFocus(...) and the .ghosttySearchFocus observer).
Update the fast path (and the similar section around lines 9296–9336) to call
canApplyMountedSearchFieldFocusRequest() before invoking
focusMountedSearchFieldIfAvailable(window:surfaceShort:) to prevent
restoreSearchFocus(window:) from making an unfocused panel’s search field first
responder.
- Around line 1426-1467: When we force Ghostty's native background via
loadDefaultConfigFilesWithLegacyFallback (the branch that calls
loadInlineGhosttyConfig with "macos-background-from-layer = false"), clear the
host-owned background fill so the separate CALayer background
(GhosttySurfaceScrollView.backgroundView) doesn't remain under Ghostty's
composed image; update the same branch in
loadDefaultConfigFilesWithLegacyFallback to, after calling
loadInlineGhosttyConfig, clear or reset the hosted background (via the
view-level code path used by GhosttyNSView.applySurfaceBackground or by directly
clearing GhosttySurfaceScrollView.backgroundView) so no leftover solid-color
fill remains when Ghostty renders its own image-based background.

In `@vendor/bonsplit`:
- Line 1: The vendor/bonsplit submodule pointer is referencing commit
e25dfe0e9a405bd610742901169b0c9c91980c25 which is not present on the submodule
remote origin/main (currently at 5c1567d7ecad030dd5b9eddf3821402e733b5b87);
either push the missing commit e25dfe0e... to the bonsplit repository's main
branch so origin/main includes it, or update the parent repo's vendor/bonsplit
pointer to match the existing origin/main commit (5c1567d7...), and add a short
commit message or PR description explaining why this bonsplit change is required
for the keyboard-routing repair so reviewers understand the coupling.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ac70465a-e441-4df0-9d7d-90347c6fdc0d

📥 Commits

Reviewing files that changed from the base of the PR and between 1a95fb9 and c9025fe.

📒 Files selected for processing (4)
  • Sources/AppDelegate.swift
  • Sources/GhosttyTerminalView.swift
  • cmuxTests/AppDelegateShortcutRoutingTests.swift
  • vendor/bonsplit

Comment thread Sources/AppDelegate.swift
Comment thread Sources/AppDelegate.swift Outdated
Comment thread Sources/GhosttyTerminalView.swift
Comment thread Sources/GhosttyTerminalView.swift
Comment thread vendor/bonsplit Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (2)
Sources/AppDelegate.swift (2)

5015-5023: ⚠️ Potential issue | 🟠 Major

Don’t exempt every NSControl/NSTextView from terminal repair.

After hostedView.responderMatchesPreferredKeyboardFocus(...) already preserves the focused terminal’s own surface/search target, the fallback at Line 5021 still treats any other text input as intentional. A stale browser omnibar or find field in the same window will keep swallowing plain typing, so this responder-drift path still won’t repair that class of freeze. Remove the blanket exemption, or replace it with a tiny explicit whitelist of same-panel editors that are actually allowed to win.

💡 Narrow the fallback
         guard let ownerView = keyRoutingOwnerView(for: responder) else {
             return true
         }
         if ownerView === window.contentView {
             return true
         }
-        if ownerView is NSControl || responder is NSTextView {
-            return false
-        }
         return true

Based on learnings: In AppDelegate.repairFocusedTerminalKeyboardRoutingIfNeeded(window:event:), determine whether to repair focus by asking the focused terminal’s hosted view if the current first responder already matches that panel’s preferred keyboard target.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/AppDelegate.swift` around lines 5015 - 5023, In
AppDelegate.repairFocusedTerminalKeyboardRoutingIfNeeded(window:event:), remove
the blanket exemption that returns false for any ownerView is NSControl ||
responder is NSTextView; instead, call the focused terminal’s hosted view method
hostedView.responderMatchesPreferredKeyboardFocus(...) to determine whether the
current first responder should win, and only allow a very small explicit
whitelist (e.g., same-panel editor classes you know are allowed) to bypass
repair; update the logic around ownerView and window.contentView to fall through
to repair when neither the hosted view affirms a match nor the responder is on
the explicit whitelist.

4956-5010: ⚠️ Potential issue | 🔴 Critical

Eliminate unsafe field-editor delegate reads from the new key-repair path.

keyRoutingOwnerView(...) still dereferences NSTextView.delegate, and this same flow immediately calls hostedView.responderMatchesPreferredKeyboardFocus(...), whose implementation also does that. AppKit keeps that delegate unsafe-unretained, so responder teardown/reparenting can trap here on ordinary .keyDown handling—the exact state this repair is trying to recover from. Resolve field-editor ownership via responder/superview traversal or tracked ownership instead of reading delegate in this hot path.

🐛 Safer owner resolution pattern for this file
 private func keyRoutingOwnerView(for responder: NSResponder?) -> NSView? {
     guard let responder else { return nil }
     if let editor = responder as? NSTextView,
-       editor.isFieldEditor,
-       let delegateView = editor.delegate as? NSView {
-        return delegateView
+       editor.isFieldEditor {
+        var current: NSResponder? = editor.nextResponder
+        while let candidate = current {
+            if let view = candidate as? NSView {
+                return view
+            }
+            current = candidate.nextResponder
+        }
+        if let superview = editor.superview {
+            return superview
+        }
     }
     return responder as? NSView
 }

You'll want the same no-delegate rule in GhosttySurfaceScrollView.responderMatchesPreferredKeyboardFocus(_:), since this repair path calls that helper before returning.

Run this read-only check to confirm the current hot path still depends on delegate as? NSView:

#!/bin/bash
set -euo pipefail

echo '--- AppDelegate repair helpers ---'
sed -n '4956,5025p' Sources/AppDelegate.swift

echo
echo '--- GhosttySurfaceScrollView focus matcher ---'
rg -n -C3 'func responderMatchesPreferredKeyboardFocus|delegate as\? NSView' Sources/GhosttyTerminalView.swift

echo
echo '--- Existing in-file AppKit safety note ---'
sed -n '13571,13584p' Sources/AppDelegate.swift

Expected result: both helpers on the new .keyDown repair path still show delegate as? NSView, while the lower AppKit note in this file explains why that read is unsafe.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/AppDelegate.swift` around lines 4956 - 5010, keyRoutingOwnerView(...)
currently reads NSTextView.delegate (unsafe); change it to avoid delegate access
by resolving field-editor ownership via responder/superview traversal: if
responder is NSTextView and responder.isFieldEditor, walk responder.superview
(and if needed its superview chain) to find the NSView that represents the
field’s owner (or return nil if none), otherwise return responder as? NSView as
before; update responderNeedsFocusedTerminalKeyRepair path to use this
non-delegating resolution and also apply the same no-delegate approach inside
GhosttySurfaceScrollView.responderMatchesPreferredKeyboardFocus(_:) so neither
helper ever casts .delegate as? NSView.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@Sources/AppDelegate.swift`:
- Around line 5015-5023: In
AppDelegate.repairFocusedTerminalKeyboardRoutingIfNeeded(window:event:), remove
the blanket exemption that returns false for any ownerView is NSControl ||
responder is NSTextView; instead, call the focused terminal’s hosted view method
hostedView.responderMatchesPreferredKeyboardFocus(...) to determine whether the
current first responder should win, and only allow a very small explicit
whitelist (e.g., same-panel editor classes you know are allowed) to bypass
repair; update the logic around ownerView and window.contentView to fall through
to repair when neither the hosted view affirms a match nor the responder is on
the explicit whitelist.
- Around line 4956-5010: keyRoutingOwnerView(...) currently reads
NSTextView.delegate (unsafe); change it to avoid delegate access by resolving
field-editor ownership via responder/superview traversal: if responder is
NSTextView and responder.isFieldEditor, walk responder.superview (and if needed
its superview chain) to find the NSView that represents the field’s owner (or
return nil if none), otherwise return responder as? NSView as before; update
responderNeedsFocusedTerminalKeyRepair path to use this non-delegating
resolution and also apply the same no-delegate approach inside
GhosttySurfaceScrollView.responderMatchesPreferredKeyboardFocus(_:) so neither
helper ever casts .delegate as? NSView.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: faf34315-023c-43b3-8a95-cc16f88813b8

📥 Commits

Reviewing files that changed from the base of the PR and between c9025fe and bbd4aa6.

📒 Files selected for processing (3)
  • Sources/AppDelegate.swift
  • Sources/GhosttyTerminalView.swift
  • cmuxTests/AppDelegateShortcutRoutingTests.swift
✅ Files skipped from review due to trivial changes (1)
  • cmuxTests/AppDelegateShortcutRoutingTests.swift
🚧 Files skipped from review as they are similar to previous changes (1)
  • Sources/GhosttyTerminalView.swift

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (2)
Sources/GhosttyTerminalView.swift (2)

9624-9633: ⚠️ Potential issue | 🟠 Major

Add the active-panel guard to the mounted-field fast path.

Line 8639 and the .ghosttySearchFocus path already gate search-field focus with canApplyMountedSearchFieldFocusRequest(), but this direct path does not. If restoreSearchFocus(window:) reaches it during stale panel-intent restoration, an unfocused panel can become first responder and steal typing from the actual focused target.

Minimal fix
 `@discardableResult`
 private func focusMountedSearchFieldIfAvailable(
     window: NSWindow,
     surfaceShort: String
 ) -> Bool {
+    guard canApplyMountedSearchFieldFocusRequest() else {
+        return false
+    }
     guard let overlay = searchOverlayHostingView,
           overlay.superview === self,
           let field = findEditableSearchField(in: overlay) else {
         return false
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/GhosttyTerminalView.swift` around lines 9624 - 9633, The
mounted-field fast path in
focusMountedSearchFieldIfAvailable(window:surfaceShort:) is missing the same
active-panel check used by the .ghosttySearchFocus path; add a guard that calls
canApplyMountedSearchFieldFocusRequest() (or otherwise verifies the panel is
active) before proceeding to use searchOverlayHostingView and
findEditableSearchField so restoreSearchFocus(window:) cannot focus a
stale/inactive panel and steal first responder; keep the existing early-return
false behavior if the guard fails.

1475-1499: ⚠️ Potential issue | 🟠 Major

Clear the host-owned background fill when background-image is configured.

This branch disables Ghostty’s layer shortcut, but Line 4911 still pushes a color into hostedView.setBackgroundColor(...), and Line 8435 still paints backgroundView. That leaves a second solid fill under Ghostty’s native image background, so translucent/background-image compositing is still wrong. Skip or clear the hosted fill whenever this override forces macos-background-from-layer = false.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/GhosttyTerminalView.swift` around lines 1475 - 1499, When
hasConfiguredBackgroundImage(config) is true and you force Ghostty's native
background via loadInlineGhosttyConfig("macos-background-from-layer = false",
...), also clear/skip the host-owned solid fill so a second solid layer isn't
painted under Ghostty's image; update the branch that sets the
"cmux-layer-bg-image-override" to call the host view clearing routine (e.g.
hostedView.setBackgroundColor(...) to clear/transparent) and/or disable
backgroundView painting (reference backgroundView.paint) so the host fill is not
rendered when macos-background-from-layer is false.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@Sources/GhosttyTerminalView.swift`:
- Around line 9624-9633: The mounted-field fast path in
focusMountedSearchFieldIfAvailable(window:surfaceShort:) is missing the same
active-panel check used by the .ghosttySearchFocus path; add a guard that calls
canApplyMountedSearchFieldFocusRequest() (or otherwise verifies the panel is
active) before proceeding to use searchOverlayHostingView and
findEditableSearchField so restoreSearchFocus(window:) cannot focus a
stale/inactive panel and steal first responder; keep the existing early-return
false behavior if the guard fails.
- Around line 1475-1499: When hasConfiguredBackgroundImage(config) is true and
you force Ghostty's native background via
loadInlineGhosttyConfig("macos-background-from-layer = false", ...), also
clear/skip the host-owned solid fill so a second solid layer isn't painted under
Ghostty's image; update the branch that sets the "cmux-layer-bg-image-override"
to call the host view clearing routine (e.g. hostedView.setBackgroundColor(...)
to clear/transparent) and/or disable backgroundView painting (reference
backgroundView.paint) so the host fill is not rendered when
macos-background-from-layer is false.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 18b27ab1-1b96-434e-9c0a-26225af1d1e4

📥 Commits

Reviewing files that changed from the base of the PR and between bbd4aa6 and 201501e.

📒 Files selected for processing (1)
  • Sources/GhosttyTerminalView.swift

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Terminals freezing

2 participants