Skip to content

Fix #2347 terminal focus and surface recovery#2354

Merged
austinywang merged 4 commits intomainfrom
issue-2347-cursor-stuck-input-frozen
Mar 30, 2026
Merged

Fix #2347 terminal focus and surface recovery#2354
austinywang merged 4 commits intomainfrom
issue-2347-cursor-stuck-input-frozen

Conversation

@austinywang
Copy link
Copy Markdown
Contributor

@austinywang austinywang commented Mar 30, 2026

Summary

  • add regression coverage for missing-surface key input and suppressed reparent focus recovery
  • recover terminal input by restarting background surface creation when keyDown arrives during a transient surface miss
  • reassert Ghostty focus after reparent suppression clears when the surface still owns first responder

Testing

  • ./scripts/reload.sh --tag issue-2347 --launch
  • Tests not run locally per repo policy; regression commits are split so CI can show red then green

Summary by cubic

Fixes #2347 by reliably recovering terminal input when the surface is briefly missing and safely restoring focus after reparent suppression. Adds lifecycle guards to avoid recreating closed surfaces and prevents stale focus replay.

  • Bug Fixes

    • On keyDown with a missing surface, queue background surface creation even if the view is temporarily detached; add debug logs.
    • Gate all runtime surface creation behind allowsRuntimeSurfaceCreation() so closed/closing portals aren’t recreated.
    • During recovery, clear desired focus if the first responder moved; don’t replay focus. When reparent suppression clears, reassert focus only if visible, active, key window, and still first responder; defer if hidden/tiny and start the surface if nil.
  • Tests

    • Add debug-only regressions for detached-view input recovery, no-recreate on closed lifecycle, no-focus replay after responder changes, and focus reassertion after reparent suppression (debugDesiredFocusState() included).

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

Summary by CodeRabbit

  • Bug Fixes

    • Improved keyboard input recovery when a terminal view is detached or its runtime surface is missing, with safer gating so recovery doesn't occur during closed/tearing-down sessions.
    • Enhanced focus reassertion to respect window, visibility and responder eligibility, reducing accidental focus steals.
  • Tests

    • Added debug-only regression tests covering input recovery and focus reassertion in edge cases.

@vercel
Copy link
Copy Markdown

vercel Bot commented Mar 30, 2026

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

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment Mar 30, 2026 9:52am

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 30, 2026

📝 Walkthrough

Walkthrough

Adds guarded runtime surface creation and active input-recovery paths: keyDown now triggers background surface startup when the runtime surface is missing; focus reassertion is gated by portal lifecycle and view/window eligibility; debug helpers and regression tests for detached/suppressed-focus scenarios were added.

Changes

Cohort / File(s) Summary
Surface Recovery & Focus Management
Sources/GhosttyTerminalView.swift
Added TerminalSurface.allowsRuntimeSurfaceCreation() and DEBUG debugDesiredFocusState(). Added GhosttyNSView.requestInputRecoveryAfterSurfaceMiss(reason:). Guarded runtime surface creation paths and changed keyDown, resignFirstResponder, clearSuppressReparentFocus, and reassertTerminalSurfaceFocus() to request background surface startup and more strictly gate focus reassertion by lifecycle/visibility/responder eligibility.
Terminal Focus Recovery Tests
cmuxTests/TerminalAndGhosttyTests.swift
Added debug-only regression tests plus FocusProbeView helper to simulate detach/close races and verify missing-surface keyDown triggers background surface recreation or respects portal close lifecycle.
Workspace Focus Reassertion Tests
cmuxTests/WorkspaceUnitTests.swift
Added a debug-only test that verifies clearSuppressReparentFocus() reasserts Ghostty desired-focus when the surface remains first responder.

Sequence Diagram(s)

sequenceDiagram
    participant User as User
    participant GNV as GhosttyNSView
    participant ES as ensureSurfaceReadyForInput
    participant Recovery as requestInputRecoveryAfterSurfaceMiss
    participant TS as TerminalSurface
    participant BG as BackgroundStarter

    User->>GNV: keyDown
    GNV->>ES: ensureSurfaceReadyForInput()
    ES-->>GNV: fails (surface == nil)
    GNV->>Recovery: requestInputRecoveryAfterSurfaceMiss(reason)
    Recovery->>TS: set desiredFocus / recordExternalFocusState(true)
    Recovery->>TS: requestBackgroundSurfaceStartIfNeeded()
    TS->>BG: start background surface
    BG-->>TS: surface recreated
    TS-->>GNV: surface available / focus reasserted
    GNV-->>User: input processed (after recovery)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~40 minutes

Possibly related PRs

Poem

🐰 I nudged a lost surface back to view,

A key pressed soft, a tiny rescue.
Desired focus set, background wakes—hooray!
Reattached, reborn, the terminal can play. ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 4.17% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The PR title directly refers to issue #2347 and summarizes the main changes: fixing terminal focus and surface recovery, which is the primary focus of the changeset.
Description check ✅ Passed The PR description includes Summary and Testing sections covering what changed and how it was tested; however, it lacks a Demo Video section and is missing the Review Trigger checklist items specified in the template.
Linked Issues check ✅ Passed The PR addresses all key coding requirements from issue #2347: adding guards to prevent silent input loss when surface is nil, recovering focus via background surface restart on keyDown, reasserting Ghostty focus after reparent suppression clears, and preventing stale focus replay when responder changes.
Out of Scope Changes check ✅ Passed All changes directly relate to the #2347 objectives: adding lifecycle guards for surface creation, recovering input via background surface restart, reasserting focus after reparent suppression, and debug-only regression tests for these scenarios.

✏️ 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-2347-cursor-stuck-input-frozen

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.

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 3 files

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Mar 30, 2026

Greptile Summary

This PR fixes two related terminal focus/input recovery bugs (issue #2347): (1) when a keyDown arrives while the Ghostty surface is transiently absent, input is no longer silently dropped — instead requestInputRecoveryAfterSurfaceMiss triggers a background surface restart and pre-sets the desired focus state so the terminal comes back live; (2) clearSuppressReparentFocus now checks all preconditions (first-responder ownership, window key state, visibility, geometry) and, when they are met, calls reassertTerminalSurfaceFocus to re-fire the Ghostty focus signal after reparent suppression clears, with a deferred scheduleAutomaticFirstResponderApply fallback for the hidden/tiny-geometry case.

Both code paths are covered by new debug-only regression tests following the repo's two-commit CI policy, and a small debugDesiredFocusState() seam is added to TerminalSurface to make the focus bit observable from tests without exposing it in production.

  • requestInputRecoveryAfterSurfaceMiss — new helper called from keyDown on surface miss; sets desiredFocus, records external focus state, and queues a background surface start
  • clearSuppressReparentFocus — extended with multi-condition guard block and a call to reassertTerminalSurfaceFocus; defers via scheduleAutomaticFirstResponderApply when the view is hidden or has unusable geometry
  • reassertTerminalSurfaceFocus — now also requests background surface start when terminalSurface.surface == nil before calling setFocus(true)
  • debugDesiredFocusState() — debug-only accessor on TerminalSurface exposing desiredFocusState for test assertions
  • Two regression tests added: one in TerminalAndGhosttyTests (missing-surface keyDown), one in WorkspaceUnitTests (reparent-focus suppression clearance)

Confidence Score: 5/5

Safe to merge — both code paths have regression test coverage, all remaining findings are minor style/cleanup P2 items.

The logic changes are well-scoped: the keyDown recovery path is guarded by the existing ensureSurfaceReadyForInput nil-check and delegates to the already-tested requestBackgroundSurfaceStartIfNeeded; the clearSuppressReparentFocus path adds conservative guards before reasserting focus and falls back gracefully when conditions aren't met. Both fixes are exercised by dedicated regression tests. The only findings are two P2 style issues (a #if DEBUG / #endif indentation mismatch, and a surfaceShort variable computed before early-return guards in release builds on a non-hot path).

No files require special attention beyond the minor style items in Sources/GhosttyTerminalView.swift.

Important Files Changed

Filename Overview
Sources/GhosttyTerminalView.swift Adds requestInputRecoveryAfterSurfaceMiss helper called on missing-surface keyDown, debugDesiredFocusState() debug accessor, surface-nil guard in reassertTerminalSurfaceFocus, and a multi-condition focus-reassertion block in clearSuppressReparentFocus; two minor P2 style issues (indentation mismatch, eager dead-store in release).
cmuxTests/TerminalAndGhosttyTests.swift Adds testKeyDownRecoversReleasedSurfaceWhileHostedViewIsDetached — a debug-only regression test that simulates the missing-surface race by releasing the surface, detaching the view, sending a keyDown, and asserting background surface recreation is triggered within 1 s.
cmuxTests/WorkspaceUnitTests.swift Adds testClearSuppressReparentFocusReassertsGhosttyFocusForCurrentFirstResponder — a debug-only regression test that suppresses reparent focus on the left split pane, makes its surface view first responder, then clears suppression and asserts the Ghostty desiredFocusState bit flips.

Sequence Diagram

sequenceDiagram
    participant User
    participant GhosttyNSView
    participant TerminalSurface
    participant Ghostty

    Note over GhosttyNSView,TerminalSurface: Scenario 1 — keyDown with missing surface
    User->>GhosttyNSView: keyDown(event)
    GhosttyNSView->>GhosttyNSView: ensureSurfaceReadyForInput()
    GhosttyNSView-->>GhosttyNSView: nil (surface absent)
    GhosttyNSView->>GhosttyNSView: requestInputRecoveryAfterSurfaceMiss()
    GhosttyNSView->>TerminalSurface: recordExternalFocusState(true)
    GhosttyNSView->>TerminalSurface: requestBackgroundSurfaceStartIfNeeded()
    TerminalSurface-->>Ghostty: createSurface() [async, next run-loop]
    Ghostty-->>TerminalSurface: surface ready

    Note over GhosttySurfaceScrollView,TerminalSurface: Scenario 2 — reparent focus recovery
    participant GhosttySurfaceScrollView
    GhosttySurfaceScrollView->>GhosttySurfaceScrollView: clearSuppressReparentFocus()
    GhosttySurfaceScrollView->>GhosttySurfaceScrollView: suppressingReparentFocus = false
    GhosttySurfaceScrollView->>GhosttySurfaceScrollView: check guards (desiredFocus, firstResponder, active, visible, keyWindow, geometry)
    alt all guards pass
        GhosttySurfaceScrollView->>GhosttySurfaceScrollView: reassertTerminalSurfaceFocus()
        alt surface == nil
            GhosttySurfaceScrollView->>TerminalSurface: requestBackgroundSurfaceStartIfNeeded()
        end
        GhosttySurfaceScrollView->>TerminalSurface: setFocus(true)
        TerminalSurface->>Ghostty: ghostty_surface_set_focus(true)
    else hidden or too small
        GhosttySurfaceScrollView->>GhosttySurfaceScrollView: scheduleAutomaticFirstResponderApply()
    end
Loading

Reviews (1): Last reviewed commit: "Fix issue #2347 terminal focus and surfa..." | Re-trigger Greptile

}

#if DEBUG
#if DEBUG
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Mismatched #if DEBUG / #endif indentation

The #if DEBUG directive was de-indented from 4 spaces to column 0, but its matching #endif on line 3499 ( #endif) is still indented with 4 spaces. All other #if DEBUG / #endif pairs in this file use consistent column-0 placement. The compiler is fine with mixed indentation, but it's inconsistent with the rest of the file.

Suggested change
#if DEBUG
#if DEBUG

return size.width > 1 && size.height > 1
}()
let isHiddenForFocus = isHiddenOrHasHiddenAncestor || surfaceView.isHiddenOrHasHiddenAncestor
let surfaceShort = surfaceView.terminalSurface?.id.uuidString.prefix(5) ?? "nil"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 surfaceShort computed before early-return guards

surfaceShort is only consumed inside #if DEBUG blocks further down, yet it is computed eagerly here — before the four guard statements that will short-circuit the function in the common "nothing to do" case. In release builds this is a dead allocation on every non-matching call.

Consider wrapping the declaration:

#if DEBUG
let surfaceShort = surfaceView.terminalSurface?.id.uuidString.prefix(5) ?? "nil"
#endif

hasUsablePortalGeometry and isHiddenForFocus are used in the non-debug guard so they need to stay where they are — only surfaceShort is affected.

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: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Sources/GhosttyTerminalView.swift (1)

5707-5713: ⚠️ Potential issue | 🟠 Major

The recovery keypress is still lost.

On Line 5712, super.keyDown(with:) falls back into the NSTextInputClient path, but insertText(_:, replacementRange:) ultimately calls sendTextToSurface(_:), which returns immediately while surface == nil. The async restart brings the terminal back, but the key that triggered recovery never reaches it, so users still need a second keypress after the miss.

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

In `@Sources/GhosttyTerminalView.swift` around lines 5707 - 5713, The keypress is
lost because you call super.keyDown(with:) when surface == nil and the
NSTextInputClient path drops the input in sendTextToSurface(_:) which returns
while the surface is nil; instead, do not call super.keyDown(with:) in the
guard-fail path—capture and buffer the original NSEvent (or derived text) into a
pending recovery slot (e.g. pendingRecoveryKeypress), call
requestInputRecoveryAfterSurfaceMiss(reason:) and start the async recovery, and
when ensureSurfaceReadyForInput() / your surface-ready callback runs, replay the
buffered event by calling the same code path that delivers text to the surface
(e.g. call sendTextToSurface(_:) or insertText(_:replacementRange:) once surface
is non-nil); ensure the buffer is cleared and access is thread-safe.
🤖 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/GhosttyTerminalView.swift`:
- Around line 8884-8911: The current path clears suppressingReparentFocus and
reasserts only the terminal surface (reassertTerminalSurfaceFocus) which
restores AppKit first responder but does not restore Bonsplit/pane focus state
or invoke the surfaceView's onFocus callback; update the code that runs when
suppression is removed (the block after the guards where
reassertTerminalSurfaceFocus is called) to also restore pane focus state by
calling the same method used elsewhere to set Bonsplit/current-panel (e.g.
focusPanel(...) or the internal function that updates Bonsplit state) and invoke
surfaceView.onFocus?() (or the equivalent onFocus handler) so the pane-level
focus state and any on-focus side effects run in addition to
reassertTerminalSurfaceFocus.

---

Outside diff comments:
In `@Sources/GhosttyTerminalView.swift`:
- Around line 5707-5713: The keypress is lost because you call
super.keyDown(with:) when surface == nil and the NSTextInputClient path drops
the input in sendTextToSurface(_:) which returns while the surface is nil;
instead, do not call super.keyDown(with:) in the guard-fail path—capture and
buffer the original NSEvent (or derived text) into a pending recovery slot (e.g.
pendingRecoveryKeypress), call requestInputRecoveryAfterSurfaceMiss(reason:) and
start the async recovery, and when ensureSurfaceReadyForInput() / your
surface-ready callback runs, replay the buffered event by calling the same code
path that delivers text to the surface (e.g. call sendTextToSurface(_:) or
insertText(_:replacementRange:) once surface is non-nil); ensure the buffer is
cleared and access is thread-safe.
🪄 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: cf26f198-c8e9-4143-baba-124e82edf87e

📥 Commits

Reviewing files that changed from the base of the PR and between 29c0f52 and 87c1de7.

📒 Files selected for processing (3)
  • Sources/GhosttyTerminalView.swift
  • cmuxTests/TerminalAndGhosttyTests.swift
  • cmuxTests/WorkspaceUnitTests.swift

Comment on lines 8884 to +8911
surfaceView.suppressingReparentFocus = false
let hasUsablePortalGeometry: Bool = {
let size = bounds.size
return size.width > 1 && size.height > 1
}()
let isHiddenForFocus = isHiddenOrHasHiddenAncestor || surfaceView.isHiddenOrHasHiddenAncestor
let surfaceShort = surfaceView.terminalSurface?.id.uuidString.prefix(5) ?? "nil"

guard surfaceView.desiredFocus else { return }
guard isSurfaceViewFirstResponder() else { return }
guard isActive else { return }
guard surfaceView.isVisibleInUI else { return }
guard let window, window.isKeyWindow else { return }
guard !isHiddenForFocus, hasUsablePortalGeometry else {
#if DEBUG
dlog(
"focus.reparent.resume.defer surface=\(surfaceShort) " +
"reason=hidden_or_tiny hidden=\(isHiddenForFocus ? 1 : 0) " +
"frame=\(String(format: "%.1fx%.1f", bounds.width, bounds.height))"
)
#endif
scheduleAutomaticFirstResponderApply(reason: "clearSuppressReparentFocus.hiddenOrTiny")
return
}
#if DEBUG
dlog("focus.reparent.resume surface=\(surfaceShort) firstResponder=\(String(describing: window.firstResponder))")
#endif
reassertTerminalSurfaceFocus(reason: "clearSuppressReparentFocus")
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

This path restores Ghostty focus but not pane focus state.

If suppression masked the original becomeFirstResponder(), onFocus?() never ran. Reasserting only the Ghostty surface here can leave Bonsplit/current-panel state pointing at the pane chosen by focusPanel(...) while AppKit first responder and terminal input stay on this surface.

💡 Suggested fix
         guard !isHiddenForFocus, hasUsablePortalGeometry else {
 `#if` DEBUG
             dlog(
                 "focus.reparent.resume.defer surface=\(surfaceShort) " +
                 "reason=hidden_or_tiny hidden=\(isHiddenForFocus ? 1 : 0) " +
                 "frame=\(String(format: "%.1fx%.1f", bounds.width, bounds.height))"
             )
 `#endif`
             scheduleAutomaticFirstResponderApply(reason: "clearSuppressReparentFocus.hiddenOrTiny")
             return
         }
+        surfaceView.onFocus?()
 `#if` DEBUG
         dlog("focus.reparent.resume surface=\(surfaceShort) firstResponder=\(String(describing: window.firstResponder))")
 `#endif`
         reassertTerminalSurfaceFocus(reason: "clearSuppressReparentFocus")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/GhosttyTerminalView.swift` around lines 8884 - 8911, The current path
clears suppressingReparentFocus and reasserts only the terminal surface
(reassertTerminalSurfaceFocus) which restores AppKit first responder but does
not restore Bonsplit/pane focus state or invoke the surfaceView's onFocus
callback; update the code that runs when suppression is removed (the block after
the guards where reassertTerminalSurfaceFocus is called) to also restore pane
focus state by calling the same method used elsewhere to set
Bonsplit/current-panel (e.g. focusPanel(...) or the internal function that
updates Bonsplit state) and invoke surfaceView.onFocus?() (or the equivalent
onFocus handler) so the pane-level focus state and any on-focus side effects run
in addition to reassertTerminalSurfaceFocus.

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: 1

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

8908-8936: ⚠️ Potential issue | 🟠 Major

Replay pane focus before reasserting Ghostty focus.

When reparent suppression swallowed becomeFirstResponder(), this block restores Ghostty focus but never replays surfaceView.onFocus?(), so Bonsplit/current-panel state can stay stale even though AppKit first responder is back on the terminal.

💡 Proposed fix
         guard !isHiddenForFocus, hasUsablePortalGeometry else {
 `#if` DEBUG
             dlog(
                 "focus.reparent.resume.defer surface=\(surfaceShort) " +
                 "reason=hidden_or_tiny hidden=\(isHiddenForFocus ? 1 : 0) " +
                 "frame=\(String(format: "%.1fx%.1f", bounds.width, bounds.height))"
             )
 `#endif`
             scheduleAutomaticFirstResponderApply(reason: "clearSuppressReparentFocus.hiddenOrTiny")
             return
         }
+        surfaceView.onFocus?()
 `#if` DEBUG
         dlog("focus.reparent.resume surface=\(surfaceShort) firstResponder=\(String(describing: window.firstResponder))")
 `#endif`
         reassertTerminalSurfaceFocus(reason: "clearSuppressReparentFocus")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/GhosttyTerminalView.swift` around lines 8908 - 8936,
clearSuppressReparentFocus currently reasserts terminal focus but doesn't replay
the suppressed focus callback, leaving Bonsplit/current-panel state stale;
update clearSuppressReparentFocus to invoke the surfaceView focus handler
(surfaceView.onFocus?() / appropriate onFocus method) or otherwise replay the
focus event on surfaceView before calling reassertTerminalSurfaceFocus (and keep
the existing early-exit checks and scheduleAutomaticFirstResponderApply behavior
intact) so UI state is updated when reparent suppression previously swallowed
becomeFirstResponder().
🧹 Nitpick comments (1)
cmuxTests/TerminalAndGhosttyTests.swift (1)

1693-1700: Harden the negative assertion window for closed-surface recovery.

A single main-queue drain can miss delayed async recreation. Prefer an inverted expectation over a short interval to catch late regressions deterministically.

♻️ Suggested test hardening
-        let drained = expectation(description: "background recovery drained")
-        DispatchQueue.main.async { drained.fulfill() }
-        wait(for: [drained], timeout: 1.0)
+        let recreated = XCTNSPredicateExpectation(
+            predicate: NSPredicate { _, _ in
+                surface.surface != nil
+            },
+            object: NSObject()
+        )
+        recreated.isInverted = true
+        wait(for: [recreated], timeout: 0.25)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmuxTests/TerminalAndGhosttyTests.swift` around lines 1693 - 1700, Replace
the single main-queue drain/expectation with an inverted expectation that waits
a short interval to catch any delayed recreation: create an expectation (e.g.,
"no-surface-recreation") set isInverted = true, observe or poll surface.surface
(the Ghostty runtime surface) and fulfill the expectation if it becomes non-nil,
then wait for that short timeout; after the wait assert that surface.surface is
still nil to ensure a late async keyDown recovery did not recreate the Ghostty
surface.
🤖 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/GhosttyTerminalView.swift`:
- Around line 5064-5072: requestInputRecoveryAfterSurfaceMiss(reason:) currently
only queues surface recreation but doesn't propagate the intended focus state,
so createSurface(for:) will initialize the new TerminalSurface from
TerminalSurface.desiredFocusState (which may be stale) instead of
GhosttyNSView.desiredFocus; update requestInputRecoveryAfterSurfaceMiss(reason:)
to mirror setFocus(true) used by reassertTerminalSurfaceFocus(reason:) by
explicitly propagating the view's desired focus intent to the recreated surface
(e.g., call the same focus propagation path or set the new surface's desired
focus to GhosttyNSView.desiredFocus / call setFocus(true) logic) so the
recreated surface receives first-responder intent; apply the same change to the
second occurrence of this helper (the block also present around the 5732-5738
area).

---

Duplicate comments:
In `@Sources/GhosttyTerminalView.swift`:
- Around line 8908-8936: clearSuppressReparentFocus currently reasserts terminal
focus but doesn't replay the suppressed focus callback, leaving
Bonsplit/current-panel state stale; update clearSuppressReparentFocus to invoke
the surfaceView focus handler (surfaceView.onFocus?() / appropriate onFocus
method) or otherwise replay the focus event on surfaceView before calling
reassertTerminalSurfaceFocus (and keep the existing early-exit checks and
scheduleAutomaticFirstResponderApply behavior intact) so UI state is updated
when reparent suppression previously swallowed becomeFirstResponder().

---

Nitpick comments:
In `@cmuxTests/TerminalAndGhosttyTests.swift`:
- Around line 1693-1700: Replace the single main-queue drain/expectation with an
inverted expectation that waits a short interval to catch any delayed
recreation: create an expectation (e.g., "no-surface-recreation") set isInverted
= true, observe or poll surface.surface (the Ghostty runtime surface) and
fulfill the expectation if it becomes non-nil, then wait for that short timeout;
after the wait assert that surface.surface is still nil to ensure a late async
keyDown recovery did not recreate the Ghostty surface.
🪄 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: 97179386-43c4-4f7c-ad92-d80c0c05942e

📥 Commits

Reviewing files that changed from the base of the PR and between 87c1de7 and 5686090.

📒 Files selected for processing (2)
  • Sources/GhosttyTerminalView.swift
  • cmuxTests/TerminalAndGhosttyTests.swift

Comment on lines +5064 to +5072
private func requestInputRecoveryAfterSurfaceMiss(reason: String) {
terminalSurface?.requestBackgroundSurfaceStartIfNeeded()
#if DEBUG
dlog(
"focus.input_recovery surface=\(terminalSurface?.id.uuidString.prefix(5) ?? "nil") " +
"reason=\(reason) inWindow=\(window != nil ? 1 : 0)"
)
#endif
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Propagate focus intent on the missing-surface key path.

This helper only queues recreation, but createSurface(for:) initializes the new runtime surface from TerminalSurface.desiredFocusState, not GhosttyNSView.desiredFocus. If first-responder acquisition happened while the runtime surface was nil, that state is still stale here, so the recreated surface can come back unfocused and the next key still goes nowhere. Mirror the setFocus(true) behavior that reassertTerminalSurfaceFocus(reason:) already uses.

💡 Proposed fix
     private func requestInputRecoveryAfterSurfaceMiss(reason: String) {
+        desiredFocus = true
+        terminalSurface?.setFocus(true)
         terminalSurface?.requestBackgroundSurfaceStartIfNeeded()
 `#if` DEBUG
         dlog(
             "focus.input_recovery surface=\(terminalSurface?.id.uuidString.prefix(5) ?? "nil") " +

Also applies to: 5732-5738

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

In `@Sources/GhosttyTerminalView.swift` around lines 5064 - 5072,
requestInputRecoveryAfterSurfaceMiss(reason:) currently only queues surface
recreation but doesn't propagate the intended focus state, so
createSurface(for:) will initialize the new TerminalSurface from
TerminalSurface.desiredFocusState (which may be stale) instead of
GhosttyNSView.desiredFocus; update requestInputRecoveryAfterSurfaceMiss(reason:)
to mirror setFocus(true) used by reassertTerminalSurfaceFocus(reason:) by
explicitly propagating the view's desired focus intent to the recreated surface
(e.g., call the same focus propagation path or set the new surface's desired
focus to GhosttyNSView.desiredFocus / call setFocus(true) logic) so the
recreated surface receives first-responder intent; apply the same change to the
second occurrence of this helper (the block also present around the 5732-5738
area).

@austinywang austinywang merged commit ae59e57 into main Mar 30, 2026
16 checks passed
bn-l pushed a commit to bn-l/cmux that referenced this pull request Apr 3, 2026
…#2354)

* Add regressions for issue manaflow-ai#2347 terminal focus loss

* Fix issue manaflow-ai#2347 terminal focus and surface recovery

* Add regressions for missing-surface recovery review cases

* Fix missing-surface recovery lifecycle and focus replay
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.

Terminal cursor gets stuck in output text, becomes completely unresponsive to input

1 participant