Skip to content

Fix #3998: discard input for dead terminal panes - #4045

Closed
austinywang wants to merge 43 commits into
mainfrom
issue-3998-dead-pty-garbled-input
Closed

austinywang wants to merge 43 commits into
mainfrom
issue-3998-dead-pty-garbled-input

Conversation

@austinywang

@austinywang austinywang commented May 13, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • add a regression test for key input after a terminal child has exited
  • track terminal input lifecycle separately from the Ghostty surface pointer
  • discard keyboard and socket input once the child-exited state is observed

Local reproduction

  • Normal exit and external kill -9 of the pane shell both auto-closed the workspace/pane on the current debug build before it could be typed into.
  • A controlled no-consumer case reproduced the same routing fault: create a new focused workspace and send socket text while debug-terminals reports the focused surface with tty=nil; observed input was still accepted/rendered into the terminal grid instead of being dropped until a live PTY consumer exists.
  • This matches the issue's failure mode at the input-routing boundary: input was gated only on a Ghostty surface pointer, not terminal/PTY liveness.

Testing

  • Not run locally, per repo policy. Regression and fix are split into two commits so CI can show the test-only commit red and the fix commit green.

Note

Medium Risk
Touches terminal input routing, lifecycle, and concurrency around Ghostty surface callbacks; regressions could drop valid input or affect panel close/restore behavior.

Overview
Fixes dead-pane input by introducing an explicit terminal input lifecycle in GhosttyTerminalView (locked) that flips to child-exited from Ghostty callbacks/close paths, clears queued socket input, and blocks further sendText/sendInput/sendNamedKey and view key handling with process_exited semantics.

Also forces Ghostty wait_after_command off (no inheritance and overridden at runtime surface creation) so exited PTYs aren’t kept focusable, and updates bash/zsh shell integration to reset keyboard protocols at every prompt (with a Kitty alias).

Adds/updates regression tests covering dead-pane socket + key rejection, suppression of key forwarding to Ghostty, keyboard reset emission correctness, and ensuring startup-command tabs/splits don’t request/enable wait_after_command.

Reviewed by Cursor Bugbot for commit 53bb46d. Bugbot is set up for automated code reviews on this repo. Configure here.


Summary by cubic

Stops all input to dead terminal panes and returns process_exited for control‑socket and v2 API commands. Also disables wait_after_command everywhere and resets terminal keyboard protocols at each shell prompt to prevent garbled input; aligns with issue-3998.

  • Bug Fixes
    • Add a terminal input lifecycle with a lock: mark child‑exited in Ghostty callbacks and TabManager, clear queued socket input, gate create/attach/flush and all input paths, preserve the first teardown reason, and lock the pending socket‑input startup check to avoid races.
    • Gate input end‑to‑end: sendText/sendInput/sendNamedKey/paste and view keyDown/keyUp/flagsChanged/paste readiness respect ready/blocked/unavailable; dead panes swallow events before Ghostty; skip readiness waits when blocked.
    • Control‑socket and v2: surface.send_text/surface.send_key and legacy send return process_exited for dead panes and surface_unavailable when closed; refresh only when input applies; fix queued vs blocked reporting.
    • Shell integrations (bash/zsh): reset terminal keyboard protocols at each prompt; add _cmux_reset_kitty_keyboard_protocol alias; remove duplicate helpers. Tests verify the reset sequence and that typing “chart” emits plain bytes (no Kitty CSI‑u).
    • Disable wait_after_command globally: never inherit it and force false at runtime. Tests cover startup‑command tabs/splits, initial workspaces, remote splits, and templates that request it; runtime override verified.

Written for commit 53bb46d. Summary will update on new commits. Review in cubic

Summary by CodeRabbit

  • Bug Fixes

    • Terminal input is blocked after a child process exits, preventing input and new surface/attachment creation for dead panes.
    • DEL (0x7F) is correctly treated as Delete and discarded once a pane is marked dead.
  • New Features

    • Shell integrations now reset terminal keyboard protocol at each prompt to recover from crashed TUIs.
  • Tests

    • Added tests ensuring dead-pane key events are not forwarded and a regression preventing mis-encoded typed text.

Review Change Stack

@vercel

vercel Bot commented May 13, 2026 •

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment May 22, 2026 8:31pm
cmux-staging Building Building Preview, Comment May 22, 2026 8:31pm

@coderabbitai

coderabbitai Bot commented May 13, 2026 •

Copy link
Copy Markdown

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

This PR implements a terminal input blocking state machine that prevents input forwarding after a child process exits. TerminalSurface gains a lifecycle state (acceptingInput vs childExited) that is transitioned when Ghostty reports surface closure or explicit child-exit actions. All input entry points and surface creation/attachment are guarded by this state; queued socket input is discarded when blocked. The change is integrated into TerminalController input routing and TabManager panel closure, and verified with tests and shell-integration updates.

Changes

Child-exited input blocking

Layer / File(s) Summary
Terminal input lifecycle state machine
Sources/GhosttyTerminalView.swift
Introduces TerminalInputLifecycleState, input-lifecycle lock and helpers, acceptsTerminalInput property, pending-socket-input discard/flush scaffolding, SurfaceInputReadiness, and DEBUG markChildProcessExitedForTesting().
Child-exit trigger points
Sources/GhosttyTerminalView.swift
Ghostty runtime close_surface_cb and GHOSTTY_ACTION_SHOW_CHILD_EXITED handler mark the TerminalSurface as child-exited when confirmation is not required.
Input gating and surface constraints
Sources/GhosttyTerminalView.swift
sendText, sendNamedKey, sendInput, pending-socket enqueue/flush, allowsRuntimeSurfaceCreation(), attachToView, createSurface, and ensureSurfaceReadyForInput() check lifecycle; keyboard routing paths (becomeFirstResponder, performKeyEquivalent, keyDown, keyUp, flagsChanged, paste) short-circuit when blocked; sendInput maps DEL (0x7F) to Ghostty Delete.
TerminalController input routing
Sources/TerminalController.swift
Adds routeUnescapedInput / RoutedTerminalInputResult; routes input to attached terminalPanel.surface.sendInput() or falls back to terminalPanel.sendText() + background start; queued/refresh behavior uses acceptsTerminalInput.
TabManager child-exit marking
Sources/TabManager.swift
closePanelAfterChildExited() calls markChildProcessExited(reason: "closePanelAfterChildExited") after tab/panel validation.
Tests, CJK/IME helpers & regression, shell integration
cmuxTests/*, Resources/shell-integration/*
Adds dead-pane keyDown test (testDeadTerminalPaneKeyDownDoesNotForwardInputToGhostty), CJK/IME test helpers, regression test for stale Kitty keyboard encoding, makeHostedTerminalWindow accepts initialCommand, and adds bash/zsh helpers invoked on each prompt to reset keyboard protocol state.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • manaflow-ai/cmux#1640: Both PRs modify Sources/GhosttyTerminalView.swift key/input forwarding behavior around DEL (0x7F)—one treats DEL as a non-text/control Delete key and gating paths, while the other adjusts how control characters (including 0x7F) are handled to preserve correct modifier semantics (Option+Delete).
  • manaflow-ai/cmux#3694: Both PRs modify the macOS input-forwarding suppression behavior in GhosttyTerminalView.swift/GhosttyNSView around keyDown/keyUp event handling—main PR blocks forwarding when TerminalSurface has exited, while the retrieved PR suppresses forwarding during IME candidate/composition—so they are code-level related in the same keyboard routing paths.
  • manaflow-ai/cmux#3332: Both PRs modify Sources/GhosttyTerminalView.swift key-event routing in the AppKit performKeyEquivalent/menu-miss handling path—main PR adds acceptsTerminalInput gating to keyboard forwarding, while retrieved PR changes second-pass performKeyEquivalentAfterMenuMiss redispatch behavior for unbound Cmd+Shift events—so the changes are closely related at the same input-forwarding code level.

Poem

🐰 I hopped to the terminal, whiskers twitching at the keys,
The child process wandered off — the surface fell to ease.
Queued bytes like little carrots were quietly let go,
The rabbit guards the doorway where no stray input flows. 🥕


Caution

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

  • Ignore

❌ Failed checks (3 errors, 1 warning)

Check name Status Explanation Resolution
Cmux Swift Blocking Runtime ❌ Error NSLock in TerminalSurface.inputLifecycleLock protects state accessed from I/O thread callbacks without @MainActor isolation, violating manual-lock rule. Add @MainActor to TerminalSurface or use actor-based state. Dispatch callback-initiated lifecycle mutations to main via DispatchQueue.main, as done with markChildProcessExited.
Cmux Swift File And Package Boundaries ❌ Error Adds 279+ lines to 14,004-line GhosttyTerminalView.swift, exceeding 250-line threshold for oversized files. New terminal input lifecycle responsibility added, not extracted; file grows. Extract terminal input lifecycle state to new SwiftPM package with protocol API, or reduce file size by 200+ lines while adding this feature.
Cmux Architecture Rethink ❌ Error Lock-based input gating with split state ownership. Unresolved review comments: insertText bypasses lifecycle checks and performKeyEquivalent swallows menu shortcuts. Consolidate input-liveness state; revoke firstResponder when childExited; make one shared gate for all input paths; remove lock-based blocking.
Docstring Coverage ⚠️ Warning Docstring coverage is 5.26% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (11 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Cmux Swift Actor Isolation ✅ Passed PR introduces no new Swift 6 actor isolation mistakes. New shared mutable state uses NSLock. New methods are @MainActor. Existing TerminalSurface debt predates this PR and is not worsened.
Cmux No Hacky Sleeps ✅ Passed Shell integration adds deterministic escape sequences only, no sleeps or delays. Test code uses deterministic test scaffolding, which is allowed.
Cmux Swift Concurrency ✅ Passed PR uses modern concurrency patterns (NSLock, @MainActor for AppKit). No legacy patterns introduced: no DispatchQueue.global(), DispatchGroup, completion handlers, or problematic Tasks.
Cmux Swift @Concurrent ✅ Passed PR contains no @concurrent violations: no async functions added, no missing @concurrent on nonisolated async work, and no invalid @concurrent usage. Thread-safe state protection uses locks.
Cmux Swift Logging ✅ Passed No Swift logging violations found. All new production code either uses proper cmuxDebugLog (guarded by #if DEBUG) or contains no logging. Tests and shell scripts follow allowed patterns.
Cmux Swiftui State Layout ✅ Passed PR introduces no new SwiftUI violations. New properties are private/computed, not @Published. TerminalSurface is existing AppKit-bridge ObservableObject with internal state management only.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR introduces NSWindow creation only in test fixtures (cmuxTests/), which are allowed exceptions. No new windows in source files require identifier registration.
Title check ✅ Passed The title 'Fix #3998: discard input for dead terminal panes' accurately describes the main change in the changeset, which is preventing input from being forwarded to terminal panes after their child process exits.
Description check ✅ Passed PR description covers key changes, testing approach, and local reproduction. Includes summary of what changed and why.
✨ 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-3998-dead-pty-garbled-input

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

greptile-apps Bot commented May 13, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes #3998 by introducing a TerminalInputLifecycleState on TerminalSurface that tracks child-process exit independently of the Ghostty surface pointer, gating all input paths (keyboard events, socket send, flush) on that state via a lock-protected check callable from Ghostty callback threads. It also removes wait_after_command = true overrides everywhere and forces the flag to false at runtime, since cmux now owns the entire dead-pane lifecycle.

  • Input lifecycle gate — markChildProcessInputExited runs synchronously on the Ghostty callback thread under inputLifecycleLock, clears any queued socket input immediately, and upgrades GhosttyNSView's ensureSurfaceReadyForInput to return a typed SurfaceInputReadiness (.ready / .blocked / .unavailable) so keyDown, keyUp, flagsChanged, and paste all swallow events before they reach Ghostty for dead panes.
  • wait_after_command removed globally — three Workspace call sites and CmuxSurfaceConfigTemplate no longer inherit or set the Ghostty flag; createSurface hard-codes false; four new tests assert the invariant.
  • Shell integration prompt reset — _cmux_reset_terminal_keyboard_protocols is hoisted before the CMUX_PANEL_ID guard and now fires unconditionally at every interactive prompt to recover from crashed TUIs that leave Kitty keyboard protocol state pushed.

Confidence Score: 5/5

Safe to merge — the input-gating logic is consistent across all paths, the lock is correctly scoped to protect cross-thread access without holding it during Ghostty FFI calls, and the child-exit transition is idempotent.

The lifecycle gate, lock usage, and queue-drain shape are sound. Previous thread findings have all been addressed. The two remaining items are style-level observations that do not affect current behavior.

GhosttyTerminalView.swift — string-prefix dispatch in inputSendResult/namedKeySendResult couples result types to free-form log strings, and the .sent arm in enqueuePendingSocketKeyIfAllowed is unreachable dead code.

Important Files Changed

Filename Overview
Sources/GhosttyTerminalView.swift Core change: adds TerminalInputLifecycleState, inputLifecycleLock, and full input-gating across send/key/flush paths. Lock-protected queue drain and capture-then-flush shape are correctly split. Two minor style issues: string-prefix dispatch for result-type inference, and a dead .sent arm in the key-queue adapter.
Sources/TabManager.swift One-line addition: calls markChildProcessExited on the terminal surface in closePanelAfterChildExited before proceeding with panel teardown. Correct and minimal.
Sources/Workspace.swift Removes three waitAfterCommand = true overrides for startup-command tabs, remote splits, and initial workspace commands. The responsibility is now fully handled by the new runtime surface override and child-exit lifecycle.
Sources/WorkspaceSurfaceConfig.swift CmuxSurfaceConfigTemplate now always initialises waitAfterCommand = false, ignoring the inherited Ghostty flag. One-line change with clear explanatory comment.
Resources/shell-integration/cmux-bash-integration.bash Moves _cmux_reset_terminal_keyboard_protocols definition earlier and calls it unconditionally at every prompt (before the CMUX_PANEL_ID gate). Adds _cmux_reset_kitty_keyboard_protocol alias for backward compat. TTY guard inside the function prevents non-interactive misfire.
Resources/shell-integration/cmux-zsh-integration.zsh Parallel change to the bash integration: reset helper moved earlier, alias added, unconditional prompt-time call. Correct and symmetric.
cmuxTests/AppDelegateIssue2907RoutingTests.swift Adds testSurfaceSendTextAndKeyRejectDeadTerminalPane covering socket send_text, send_key, sendNamedKey and legacy send after markChildProcessExitedForTesting. Correctly skips in release builds.
cmuxTests/GhosttyCommandShiftForwardingTests.swift Adds testDeadTerminalPaneKeyDownDoesNotForwardInputToGhostty using the debug key-event observer to assert zero Ghostty calls after markChildProcessExitedForTesting. Clean regression test.
cmuxTests/WorkspaceSplitStartupCommandTests.swift Adds four tests verifying waitAfterCommand is never set: initial workspace, startup-command tab, remote split, and runtime surface override.
cmuxTests/CJKIMEInputTests.swift Strengthens the stale-Kitty-keyboard test: now types the full word 'chart', verifies the zsh reset helper exits cleanly and emits the exact expected escape sequence, and captures stderr for better failure diagnostics.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    GhosttyCallback["Ghostty callback thread"]
    MarkInputExited["markChildProcessInputExited()\nunder inputLifecycleLock"]
    MainQueue["DispatchQueue.main.async\nmarkChildProcessExited()"]
    GhosttyCallback --> MarkInputExited
    GhosttyCallback --> MainQueue
    TabManager["TabManager.closePanelAfterChildExited()"]
    TabManager --> MarkInputExited
    subgraph InputPaths["All input paths"]
        KeyDown["keyDown / keyUp / flagsChanged"]
        SendText["sendText / sendInput / sendNamedKey"]
        Flush["flushPendingSocketInput"]
    end
    MarkInputExited --> |"childExited state set"| InputPaths
    KeyDown --> |"SurfaceInputReadiness.blocked"| Discard["Discarded"]
    SendText --> |"blockReason != nil"| Discard
    Flush --> |"drain under lock"| Discard
Loading

Reviews (26): Last reviewed commit: "Merge remote-tracking branch 'origin/mai..." | Re-trigger Greptile

Comment thread Sources/GhosttyTerminalView.swift
Comment thread Sources/GhosttyTerminalView.swift Outdated
Comment thread Sources/GhosttyTerminalView.swift Outdated
coderabbitai[bot]
coderabbitai Bot previously requested changes May 13, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

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)

4433-4774: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Synchronize the child-exit gate with queued socket input.

markChildProcessExited(reason:) now flips inputLifecycleState and clears pendingSocketInputQueue, but the socket entry points still read/mutate both without any shared isolation. A concurrent sendText/sendNamedKey can race the exit transition and enqueue or flush one more payload after the pane is already marked dead, which breaks the new discard-after-exit contract.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/GhosttyTerminalView.swift` around lines 4433 - 4774, The
markChildProcessExited race happens because inputLifecycleState and
pendingSocketInputQueue/pendingSocketInputBytes are accessed without
synchronization; fix by protecting all reads/writes of inputLifecycleState and
pendingSocketInputQueue/pendingSocketInputBytes with a single lock (either reuse
debugMetadataLock or add a dedicated NSLock), updating
markChildProcessExited(reason:), terminalInputBlockReason(),
shouldForwardTerminalInput(reason:), discardPendingSocketInput(reason:) and any
socket-entry points (e.g. sendText/sendNamedKey equivalents) to acquire/release
that lock around checks and mutations so the transition to .childExited and the
clearing of the queues is atomic and prevents post-exit enqueues.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Sources/GhosttyTerminalView.swift`:
- Around line 6868-6869: The guard treating
terminalSurface?.acceptsTerminalInput == false the same as a missing surface
must return an explicit "blocked" state instead of nil so callers can silently
consume input; update the code around the guard (the check on
terminalSurface?.acceptsTerminalInput and the subsequent if let surface =
surface branch) to return a distinct .blocked enum/case (or a distinct value)
when the surface exists but acceptsTerminalInput is false, leave nil only for
the truly missing/unattached surface, and adjust callers like keyDown(with:) and
requestInputRecoveryAfterSurfaceMiss(reason:) to consume .blocked without
calling requestInputRecoveryAfterSurfaceMiss or super.keyDown(with:). Ensure you
reference the enum/case you introduce (e.g., .blocked) in the same type
currently returned by the function so downstream logic can branch on .blocked vs
nil.

In `@Sources/TerminalController.swift`:
- Around line 15226-15243: Extract the repeated attached-vs-unattached send
routing into a single `@MainActor` helper that takes the unescaped text and a
TerminalPanel (e.g., routeUnescapedInput(_ text: String, to terminalPanel:
TerminalPanel)); replace each verbatim block in sendInputToWorkspace,
sendInputToSurface, and the current site with calls to this helper; inside the
helper perform the conditional on terminalPanel.surface.surface != nil and call
terminalPanel.surface.sendInput(text) when attached, otherwise call
terminalPanel.sendText(text) and
terminalPanel.surface.requestBackgroundSurfaceStartIfNeeded() so all routing
logic is centralized for future lifecycle/gating changes.

---

Outside diff comments:
In `@Sources/GhosttyTerminalView.swift`:
- Around line 4433-4774: The markChildProcessExited race happens because
inputLifecycleState and pendingSocketInputQueue/pendingSocketInputBytes are
accessed without synchronization; fix by protecting all reads/writes of
inputLifecycleState and pendingSocketInputQueue/pendingSocketInputBytes with a
single lock (either reuse debugMetadataLock or add a dedicated NSLock), updating
markChildProcessExited(reason:), terminalInputBlockReason(),
shouldForwardTerminalInput(reason:), discardPendingSocketInput(reason:) and any
socket-entry points (e.g. sendText/sendNamedKey equivalents) to acquire/release
that lock around checks and mutations so the transition to .childExited and the
clearing of the queues is atomic and prevents post-exit enqueues.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 7c2176be-b72b-4c62-836f-3a8745b3a857

📥 Commits

Reviewing files that changed from the base of the PR and between be769af and 9305c65.

📒 Files selected for processing (4)
  • Sources/GhosttyTerminalView.swift
  • Sources/TabManager.swift
  • Sources/TerminalController.swift
  • cmuxTests/GhosttyCommandShiftForwardingTests.swift

Comment thread Sources/GhosttyTerminalView.swift Outdated
Comment thread Sources/TerminalController.swift
Comment thread Sources/GhosttyTerminalView.swift
coderabbitai[bot]
coderabbitai Bot previously requested changes May 13, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cmuxTests/CJKIMEInputTests.swift`:
- Around line 1624-1654: The helper cmuxZshTerminalKeyboardResetSequence
currently swallows sourcing errors (the "|| true") and never validates the
subprocess exit or output, so it can return empty data; remove the "|| true"
that masks source failures, capture standardError into a Pipe variable, call try
process.run(), waitUntilExit(), then validate process.terminationStatus (or
process.terminationReason) and the output payload and if the process failed or
produced empty output throw an error that includes the captured stderr and/or
stdout to fail fast and surface the integration failure.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: b9876028-3d34-4f29-bcdb-bae61df8831f

📥 Commits

Reviewing files that changed from the base of the PR and between 9305c65 and b5d3496.

📒 Files selected for processing (3)
  • Resources/shell-integration/cmux-bash-integration.bash
  • Resources/shell-integration/cmux-zsh-integration.zsh
  • cmuxTests/CJKIMEInputTests.swift

Comment thread cmuxTests/CJKIMEInputTests.swift
Comment thread Sources/GhosttyTerminalView.swift
coderabbitai[bot]
coderabbitai Bot previously requested changes May 13, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

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)

4427-4822: 🛠️ Refactor suggestion | 🟠 Major | 🏗️ Heavy lift

Extract the terminal-input lifecycle into its own helper/type.

This PR adds several hundred more lines of lifecycle, queueing, and routing logic to a production Swift file that is already well past the repo budget. Please move the new input-state machine/pending-socket logic out of GhosttyTerminalView.swift before merge.

As per coding guidelines, "do not accept more than 250 lines added to an existing production Swift file that is already over 800 lines, unless an extraction exception is met by removing/moving mixed responsibilities and decreasing total line count by more than 200 lines."

Also applies to: 5985-6070, 6944-7009

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/GhosttyTerminalView.swift` around lines 4427 - 4822, The new
terminal-input lifecycle, queueing and routing logic (types
PendingSocketInputDiscard, PendingSocketInputQueueSnapshot,
PendingSocketInputDrain, TerminalInputLifecycleState, the
pendingSocketInputQueue/Bytes/maxPendingSocketInputBytes state,
inputLifecycleLock, and all helper methods terminalInputBlockReasonLocked,
terminalInputBlockReason, clearPendingSocketInputLocked,
logPendingSocketInputDiscard, consumeTerminalInputIfAllowed,
markChildProcessExited, withInputLifecycleLock, etc.) should be extracted into a
dedicated helper type (e.g. TerminalInputLifecycleManager) in its own Swift
file; move the enums/structs and the queue + lock internals into that type, keep
synchronization inside it, and expose a narrow API (methods like
consumeTerminalInputIfAllowed(reason:), markChildProcessExited(reason:),
terminalInputBlockReason(), clearPendingSocketInputIfAny(), and
portal/attachment-related setters/getters) so GhosttyTerminalView holds a single
instance and delegates calls to it—update all references in GhosttyTerminalView
to use the new instance and preserve any `@MainActor` or debug logging calls by
forwarding context/IDs as parameters.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cmuxTests/CJKIMEInputTests.swift`:
- Around line 1883-1888: Reorder the test so the injected keyboard-reset bytes
from cmuxZshTerminalKeyboardResetSequence() are processed after the
clear-history path completes: call
hostedTerminal.surface.performBindingAction("clear_screen") and
hostedTerminal.surface.forceRefresh(reason: "unit.clearHistory") (and the
subsequent RunLoop.current.run(...)) first, then invoke try
processTerminalOutput(cmuxZshTerminalKeyboardResetSequence(), in:
hostedTerminal) so the reset happens at the fresh prompt and avoids leaving the
runtime surface in stale Kitty mode.

In `@Sources/GhosttyTerminalView.swift`:
- Around line 6976-6987: flagsChanged(with:) currently bypasses the readiness
checks and calls ghostty_surface_key directly; change flagsChanged(with:) to
call ensureSurfaceReadyForInput() and handle the returned SurfaceInputReadiness
the same way as keyDown/keyUp/performKeyEquivalent (i.e., do nothing on
.blocked/.unavailable and only call ghostty_surface_key when the readiness is
.ready(surface)), so modifier-only events are gated behind the same
blocked/unavailable logic as other input paths.
- Around line 4743-4758: The new reader terminalInputBlockReasonLocked() reads
portalLifecycleState under inputLifecycleLock but writers
(beginPortalCloseLifecycle, markPortalLifecycleClosed) still mutate
portalLifecycleState without that lock, causing a race; fix by ensuring all
accesses and mutations of portalLifecycleState are protected by the same
inputLifecycleLock (wrap reads in
terminalInputBlockReasonLocked/terminalInputBlockReason and wrap mutations in
beginPortalCloseLifecycle and markPortalLifecycleClosed, or move those mutations
onto the same synchronization context) so portalLifecycleState is only
read/written while holding inputLifecycleLock.

In `@Sources/TerminalController.swift`:
- Around line 15487-15488: The remaining send paths (e.g., the call to
Self.routeUnescapedInput(unescaped, to: terminalPanel) and the other similar
send exits) currently ignore the helper result so they skip the attached-surface
redraw; change these to capture the helper return value (the object/tuple that
exposes shouldForceRefresh), set success accordingly, and propagate its
shouldForceRefresh into the redraw path so the same refresh preserved by the
logic around lines 6818-6822 is applied; in short, replace the discarded-result
calls to routeUnescapedInput (and the other send paths noted) with assignments
that read .shouldForceRefresh and use that flag to trigger the redraw.

---

Outside diff comments:
In `@Sources/GhosttyTerminalView.swift`:
- Around line 4427-4822: The new terminal-input lifecycle, queueing and routing
logic (types PendingSocketInputDiscard, PendingSocketInputQueueSnapshot,
PendingSocketInputDrain, TerminalInputLifecycleState, the
pendingSocketInputQueue/Bytes/maxPendingSocketInputBytes state,
inputLifecycleLock, and all helper methods terminalInputBlockReasonLocked,
terminalInputBlockReason, clearPendingSocketInputLocked,
logPendingSocketInputDiscard, consumeTerminalInputIfAllowed,
markChildProcessExited, withInputLifecycleLock, etc.) should be extracted into a
dedicated helper type (e.g. TerminalInputLifecycleManager) in its own Swift
file; move the enums/structs and the queue + lock internals into that type, keep
synchronization inside it, and expose a narrow API (methods like
consumeTerminalInputIfAllowed(reason:), markChildProcessExited(reason:),
terminalInputBlockReason(), clearPendingSocketInputIfAny(), and
portal/attachment-related setters/getters) so GhosttyTerminalView holds a single
instance and delegates calls to it—update all references in GhosttyTerminalView
to use the new instance and preserve any `@MainActor` or debug logging calls by
forwarding context/IDs as parameters.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 18460a3f-0ae6-4203-83eb-6eb6af2feba0

📥 Commits

Reviewing files that changed from the base of the PR and between b5d3496 and fadf469.

📒 Files selected for processing (3)
  • Sources/GhosttyTerminalView.swift
  • Sources/TerminalController.swift
  • cmuxTests/CJKIMEInputTests.swift

Comment thread cmuxTests/CJKIMEInputTests.swift Outdated
Comment thread Sources/GhosttyTerminalView.swift
Comment thread Sources/GhosttyTerminalView.swift
Comment thread Sources/TerminalController.swift Outdated
Comment thread Sources/GhosttyTerminalView.swift Outdated
Comment thread Sources/GhosttyTerminalView.swift
coderabbitai[bot]
coderabbitai Bot previously requested changes May 13, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

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)

5725-5786: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

The lifecycle guard still races with the actual Ghostty write.

These paths check consumeTerminalInputIfAllowed() / drain the queue under inputLifecycleLock, then release the lock before calling ghostty_surface_text or ghostty_surface_key. A socket thread that passes the guard just before markChildProcessExited(reason:) flips the state can still deliver a final chunk/key after exit. To fully close this regression, the lifecycle transition and the C write need a single serialization point.

Also applies to: 6056-6097

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/GhosttyTerminalView.swift` around lines 5725 - 5786, The race happens
because consumeTerminalInputIfAllowed() (the inputLifecycleLock) is released
before the actual C surface writes (ghostty_surface_text / ghostty_surface_key)
performed by writeTextData(_:to:) and sendKeyEvent(surface:keycode:mods:), so a
thread can pass the guard then lose the race with
markChildProcessExited(reason:). Fix by serializing the lifecycle transition and
the C write on the same lock: ensure sendText(_:), sendNamedKey(_:), and
sendInput(_:) perform the actual calls into writeTextData and sendKeyEvent while
still holding the inputLifecycleLock (or refactor writeTextData/sendKeyEvent to
acquire that same lock internally), so the guard and the native write are atomic
with respect to markChildProcessExited(reason:).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Sources/GhosttyTerminalView.swift`:
- Around line 2072-2075: The code currently calls
callbackTerminalSurface?.markChildProcessExited(reason:) inside
DispatchQueue.main.async, leaving inputLifecycleState as .acceptingInput until
the async block runs and allowing a queued keyDown to reach a dead pane; change
this so that the lifecycle mutation is done synchronously: call
markChildProcessExited(reason:) (or otherwise set inputLifecycleState to the
child-exited state) immediately on the current thread before enqueueing the
DispatchQueue.main.async UI-close work, and leave only UI-only operations in the
async block; apply the same synchronous flip in the other identical site
referenced (the block around lines 3764-3766) and ensure
ensureSurfaceReadyForInput() will see the updated state.

---

Outside diff comments:
In `@Sources/GhosttyTerminalView.swift`:
- Around line 5725-5786: The race happens because
consumeTerminalInputIfAllowed() (the inputLifecycleLock) is released before the
actual C surface writes (ghostty_surface_text / ghostty_surface_key) performed
by writeTextData(_:to:) and sendKeyEvent(surface:keycode:mods:), so a thread can
pass the guard then lose the race with markChildProcessExited(reason:). Fix by
serializing the lifecycle transition and the C write on the same lock: ensure
sendText(_:), sendNamedKey(_:), and sendInput(_:) perform the actual calls into
writeTextData and sendKeyEvent while still holding the inputLifecycleLock (or
refactor writeTextData/sendKeyEvent to acquire that same lock internally), so
the guard and the native write are atomic with respect to
markChildProcessExited(reason:).
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 895ebf22-367a-4b8c-860a-e2d4a947a466

📥 Commits

Reviewing files that changed from the base of the PR and between fadf469 and 9fc629d.

📒 Files selected for processing (3)
  • Sources/GhosttyTerminalView.swift
  • Sources/TerminalController.swift
  • cmuxTests/CJKIMEInputTests.swift

Comment thread Sources/GhosttyTerminalView.swift
Comment thread Sources/GhosttyTerminalView.swift Outdated
coderabbitai[bot]
coderabbitai Bot previously requested changes May 13, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

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)

4418-4830: 🛠️ Refactor suggestion | 🟠 Major | 🏗️ Heavy lift

Extract the input lifecycle/queue state machine out of this file.

This PR adds a large chunk of non-view lifecycle and socket-queue logic to an already massive production file. Pulling TerminalInputLifecycleState plus the pending-socket queue/drain helpers into a dedicated type or companion file would keep the portal/view code reviewable and make this state machine much easier to test in isolation.

As per coding guidelines, "Flag Swift changes that add too much unrelated responsibility to one file: do not accept more than 250 lines added to an existing production Swift file that is already over 800 lines..."

Also applies to: 6000-6090

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/GhosttyTerminalView.swift` around lines 4418 - 4830, Extract the
input-lifecycle and pending-socket queue logic into a new type (e.g.
TerminalInputLifecycle or TerminalInputQueue) in its own file: move the
enums/structs TerminalInputLifecycleState, PendingSocketInputDiscard,
PendingSocketInputQueueSnapshot, PendingSocketInputDrain and all related stored
state (pendingSocketInputQueue, pendingSocketInputBytes,
maxPendingSocketInputBytes, inputLifecycleLock, inputLifecycleState) plus the
methods terminalInputBlockReasonLocked, terminalInputBlockReason,
clearPendingSocketInputLocked, logPendingSocketInputDiscard,
consumeTerminalInputIfAllowed, markChildProcessInputExited,
markChildProcessExited and portalBinding helpers into that new type; preserve
the locking semantics by making the lock private to the new type and expose
thread-safe APIs used by the view (e.g. canAcceptInput(),
consumeIfAllowed(reason:), markChildExited(reason:), snapshotQueue()) so call
sites in GhosttyTerminalView swap to those APIs and no external code references
the moved private symbols or the private vars directly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Sources/GhosttyTerminalView.swift`:
- Around line 6997-7008: NSTextInputClient.insertText currently calls
sendTextToSurface() directly and bypasses the readiness gate; update insertText
to call ensureSurfaceReadyForInput() and only forward text when it returns
.ready(surface) (use the returned surface), otherwise drop or handle the input
consistently with key-event handlers (e.g., treat .blocked/.unavailable as
no-op). Specifically change NSTexInputClient.insertText to consult
ensureSurfaceReadyForInput() before invoking sendTextToSurface(), matching the
behavior around markChildProcessInputExited and preventing ghostty_surface_key
calls when the surface isn't ready.
- Around line 7603-7609: The switch in ensureSurfaceReadyForInput() currently
returns true for the .blocked case which incorrectly consumes AppKit key
equivalents; change the .blocked branch to return false so blocked panes reject
terminal input instead of hijacking menu shortcuts (leave .ready handling as-is
and keep .unavailable returning false). Update the switch in
GhosttyTerminalView.swift (the ensureSurfaceReadyForInput() call/case handling)
so .blocked falls through to allow main menu key equivalents.

---

Outside diff comments:
In `@Sources/GhosttyTerminalView.swift`:
- Around line 4418-4830: Extract the input-lifecycle and pending-socket queue
logic into a new type (e.g. TerminalInputLifecycle or TerminalInputQueue) in its
own file: move the enums/structs TerminalInputLifecycleState,
PendingSocketInputDiscard, PendingSocketInputQueueSnapshot,
PendingSocketInputDrain and all related stored state (pendingSocketInputQueue,
pendingSocketInputBytes, maxPendingSocketInputBytes, inputLifecycleLock,
inputLifecycleState) plus the methods terminalInputBlockReasonLocked,
terminalInputBlockReason, clearPendingSocketInputLocked,
logPendingSocketInputDiscard, consumeTerminalInputIfAllowed,
markChildProcessInputExited, markChildProcessExited and portalBinding helpers
into that new type; preserve the locking semantics by making the lock private to
the new type and expose thread-safe APIs used by the view (e.g.
canAcceptInput(), consumeIfAllowed(reason:), markChildExited(reason:),
snapshotQueue()) so call sites in GhosttyTerminalView swap to those APIs and no
external code references the moved private symbols or the private vars directly.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 6c1ae43f-ef1e-4daa-8a72-7c33ed8c3057

📥 Commits

Reviewing files that changed from the base of the PR and between 9fc629d and 3bbf93b.

📒 Files selected for processing (3)
  • Sources/GhosttyTerminalView.swift
  • Sources/TabManager.swift
  • cmuxTests/CJKIMEInputTests.swift

Comment thread Sources/GhosttyTerminalView.swift
Comment thread Sources/GhosttyTerminalView.swift
Comment thread Sources/GhosttyTerminalView.swift Outdated
Comment thread Resources/shell-integration/cmux-zsh-integration.zsh
Comment thread Sources/TerminalController.swift Outdated
Comment thread Sources/GhosttyTerminalView.swift Outdated

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

No issues found across 10 files

Re-trigger cubic

@austinywang austinywang changed the title Discard input for dead terminal panes Fix #3998: discard input for dead terminal panes May 18, 2026
Comment thread Sources/Workspace.swift
…-3998-dead-pty-garbled-input

# Conflicts:
#	Sources/GhosttyTerminalView.swift

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit bfe100c. Configure here.

Comment thread Sources/GhosttyTerminalView.swift

This branch was successfully deployed

1 active deployment
Preview – cmux — 53bb46d3 Deployed May 22, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

stale-revisit Closed after 30+ days without activity; preserved for possible revisit or reopening.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants