Skip to content

fix(terminal): forward deleteBackward text input commands - #5494

Closed
richardgetz wants to merge 11 commits into
manaflow-ai:mainfrom
richardgetz:fix-astropad-backspace-cmux
Closed

richardgetz wants to merge 11 commits into
manaflow-ai:mainfrom
richardgetz:fix-astropad-backspace-cmux

Conversation

@richardgetz

@richardgetz richardgetz commented Jun 6, 2026 •

Copy link
Copy Markdown

Summary

  • Fix Astropad Workbench Backspace input being mis-forwarded in cmux terminal sessions.
  • Add Swift Testing regression coverage for a synthetic Backspace event with empty characters and an unreliable keycode.
  • Normalize AppKit deleteBackward: text-input commands into canonical Ghostty Backspace events.

Problem

When remote-controlling my Mac from an iPhone with Astropad Workbench, tapping the on-screen Backspace key inside a cmux terminal did not delete text. In my Codex/tmux session, it appeared as bad input and could type a instead.

The same Backspace key works correctly outside this path:

  • Astropad Workbench -> Ghostty standalone: Backspace sends DEL 0x7f.
  • Physical keyboard -> cmux terminal: Backspace sends DEL 0x7f.
  • Ctrl-H -> cmux terminal: sends BS 0x08.

That narrows the issue to cmux's embedded Ghostty/AppKit input forwarding path, not tmux, stty, or Ghostty configuration.

Fix

Synthetic input can arrive as deleteBackward: through interpretKeyEvents with empty or unreliable raw NSEvent fields. cmux previously discarded doCommand(by:) commands to suppress system beeps.

This change captures deleteBackward: only while processing interpretKeyEvents and forwards it to Ghostty as canonical Backspace/DEL: text 0x7f with kVK_Delete.

Testing

  • git diff --check origin/main..HEAD
  • Built and launched tagged Debug app locally
  • Verified Astropad Backspace in terminal input path

View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.


Summary by cubic

Fixes Backspace not working in cmux terminals with Astropad Workbench by forwarding AppKit deleteBackward: and keyCode-0 “empty” events as canonical Backspace (DEL), even when raw fields look like the A key. Also prevents stale key-up mapping and keeps modified key combos on their original path.

  • Bug Fixes

    • Forward deleteBackward: during interpretKeyEvents as Backspace/DEL (0x7F, kVK_Delete), ignoring inconsistent raw fields (e.g., characters: "a", keyCode: kVK_ANSI_A); DEBUG tests validate canonical press text.
    • Handle Astropad “empty” Backspace events (no chars, keyCode == 0) that don’t emit deleteBackward: by sending DEL; gated to plain keys only (no Control/Option/Command), with tests confirming the fallback and that modified commands are untouched.
    • Avoid stale release mapping by not synthesizing a kVK_Delete keyUp; the later physical keyUp forwards with its original keycode, with DEBUG tests covering both cases.
  • Refactors

    • Use a disjoint modifier check for clearer, safer gating of the text-input command fallback.

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

Review in cubic

Summary by CodeRabbit

  • Bug Fixes

    • Improved keyboard handling so responder-chain text-input commands (including IME/compose input and delete/backspace) are consistently converted to terminal key events, suppressing system beeps and ensuring correct press/release pairing and modifier preservation when forwarded.
  • Tests

    • Added serialized, debug-only tests that validate forwarding behavior and observed key press/release details for text-input commands.

@vercel

vercel Bot commented Jun 6, 2026

Copy link
Copy Markdown

@richardgetz is attempting to deploy a commit to the Manaflow Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Jun 6, 2026 •

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 85aef514-51e0-4d67-be55-0d72295f9efa

📥 Commits

Reviewing files that changed from the base of the PR and between 020aeaa and ce31f5b.

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

📝 Walkthrough

Walkthrough

Adds responder-chain text-input command forwarding to GhosttyNSView: maps NSResponder selectors (currently deleteBackward) to Ghostty keycodes/text, accumulates commands during interpretKeyEvents, and synthesizes matching Ghostty press/release events.

Changes

Text-Input Command Forwarding

Layer / File(s) Summary
Command enum and accumulator state
Sources/GhosttyTerminalView.swift
Adds GhosttyTextInputCommand enum and keyCommandAccumulator state to collect responder-chain text-input commands.
doCommand capture
Sources/GhosttyTerminalView.swift
doCommand(by:) recognizes representable selectors and appends corresponding GhosttyTextInputCommand values to the accumulator, returning early to suppress the system beep.
keyDown initialization and routing
Sources/GhosttyTerminalView.swift
keyDown initializes/defers clearing keyTextAccumulator and keyCommandAccumulator, runs interpretKeyEvents, prefers accumulated IME/insertText, otherwise filters and forwards accumulated responder commands, and falls back to an empty-keyCode backspace mapping.
Conversion helpers and forwarding policy
Sources/GhosttyTerminalView.swift
Adds sendTextInputCommand(...) helpers (with DEBUG timing/logging) that construct Ghostty key events from commands and shouldForwardTextInputCommand/fallback helpers to gate forwarding (e.g., special-case deleteBackward).
Test validation of command forwarding
cmuxTests/TerminalAndGhosttyTests.swift
Debug-only serialized tests spin up a live AppKit window + TerminalSurface/GhosttyNSView, override debug handlers to trigger deleteBackward (or not), synthesize different keyDowns, and assert forwarded Ghostty press/release keycode, unshifted codepoint, text, and modifier routing.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • manaflow-ai/cmux#4382: Modifies GhosttyTerminalView input/interpretKeyEvents pipeline overlapping with responder-command forwarding changes.
  • manaflow-ai/cmux#3986: Changes the keyDown debug text-input handling and intersects with the debug forwarding pipeline.

Suggested reviewers

  • jesstelford

Poem

🐰 I nibble selectors, translate their song,
I stash small commands where accumulators belong,
Delete becomes a codepoint, then a press, then a rest,
Ghostty receives the whisper and sends it on its quest,
Hooray for routed inputs — neat, small, and strong.

🚥 Pre-merge checks | ✅ 18 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 17.65% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (18 passed)
Check name Status Explanation
Title check ✅ Passed The title directly summarizes the main change: forwarding deleteBackward text input commands to fix Backspace in terminal sessions.
Description check ✅ Passed The description covers all template sections: Summary (what changed and why), Testing (how tested and verified), and includes a filled Checklist. Demo video is not applicable for this terminal fix.
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 NSView subclass is implicitly MainActor. New private enum is a value type. keyCommandAccumulator scoped to keyDown method, accessed only on MainActor.
Cmux Swift Blocking Runtime ✅ Passed No blocking synchronization introduced. New code adds text input command forwarding via simple enum, defer-managed arrays, and non-blocking wrapper methods for responding to deleteBackward commands.
Cmux No Hacky Sleeps ✅ Passed Check scope excludes Swift; PR modifies only Swift files (GhosttyTerminalView.swift and cmuxTests/TerminalAndGhosttyTests.swift). Swift timing rules covered separately.
Cmux Algorithmic Complexity ✅ Passed PR implements tiny fixed-size command accumulation per keyDown (0-5 items max), single-pass filtering O(n where n≈1-5), and no nested scans or user-data scaling. Tests are properly #if DEBUG gated.
Cmux Swift Concurrency ✅ Passed PR adds text input command forwarding with no new Dispatch queues, Combine patterns, completion handlers, or fire-and-forget Tasks in cmux code.
Cmux Swift @Concurrent ✅ Passed All new Swift code in this PR is synchronous. New helper methods are lightweight, non-async functions on a MainActor NSView subclass with no concurrent annotation violations.
Cmux Swift File And Package Boundaries ✅ Passed Adds <250 lines to existing 16.3k-line file; all additions are AppKit↔Ghostty bridge glue in GhosttyNSView, not independently testable domain logic requiring package extraction.
Cmux Swift Logging ✅ Passed All production logging complies with swift-logging.md: no print/NSLog outside DEBUG guards; no ad hoc file logging; all DEBUG code properly gated.
Cmux User-Facing Error Privacy ✅ Passed No user-facing errors expose sensitive info. PR adds internal text input command logic with DEBUG-guarded logging and DEBUG-only tests.
Cmux Full Internationalization ✅ Passed All changes are DEBUG-only code, internal implementation details, or literal protocol tokens that don't require localization per full-internationalization.md exceptions.
Cmux Swiftui State Layout ✅ Passed PR modifies AppKit-level text input handling with no SwiftUI state pattern changes; existing ObservableObject remains unchanged; test-only DEBUG code contains no SwiftUI violations.
Cmux Architecture Rethink ✅ Passed Scoped responder-chain command accumulators with no timing, locks, observers, or split lifecycle. Single owner. Clear invariants. Test-only debug handlers properly guarded.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR does not introduce or materially change user-visible NSWindow/NSPanel/NSWindowController. Test-only DEBUG fixtures are exempted by rule. Lint script passes.
Cmux Source Artifacts ✅ Passed All changed files are legitimate product code, tests, and config. No artifacts (logs, caches, build output, temp folders, or scratch dirs) detected in the diff.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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 Jun 6, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

Fixes Backspace input being mis-forwarded in cmux terminal sessions when remote-controlling via Astropad Workbench. Synthetic events from Astropad arrive through interpretKeyEvents with empty or unreliable NSEvent raw fields (e.g., characters: "a", keyCode: kVK_ANSI_A), so the old code discarded the deleteBackward: responder-chain command entirely and the wrong character was sent.

  • Introduces GhosttyTextInputCommand enum and keyCommandAccumulator to capture AppKit responder-chain commands during interpretKeyEvents, then normalises deleteBackward: to a canonical Ghostty Backspace event (kVK_Delete, text 0x7F).
  • Adds a second fallback path (emptyKeyCodeZeroBackspaceFallbackCommand) for Astropad events where deleteBackward: is never emitted but the raw event carries keyCode 0 with empty characters.
  • Adds four serialised DEBUG-only Swift Testing regression tests covering: canonical Backspace forwarding, no stale release mapping, the empty-keyCode fallback, and preservation of the modified-key path (Ctrl+Opt+H stays as kVK_ANSI_H).

Confidence Score: 5/5

Safe to merge. The change is well-scoped: two new dispatch branches that only fire on events that would otherwise fall through to an incorrect or silent path, with both paths gated by the same modifier disjoint check.

The previous reviewer concerns about a stale textInputCommandReleaseKeyCodes dictionary and a parallel release side-channel have both been fully resolved — the synthesised key-up is gone entirely, and the regression test confirms the later physical key-up is forwarded with its own original keycode. The two new production branches are mutually exclusive with the IME text path and the normal key path, modifier gating is consistent, and the withCString lifetime is safe. No actor isolation changes, no new blocking primitives, no sensitive data in logs.

No files require special attention.

Important Files Changed

Filename Overview
Sources/GhosttyTerminalView.swift Adds GhosttyTextInputCommand enum, keyCommandAccumulator, and two new dispatch branches in keyDown to normalise deleteBackward: and empty-keyCode-zero events to canonical Ghostty Backspace; no stale release side-channel introduced; modifier gating is consistent across both paths.
cmuxTests/TerminalAndGhosttyTests.swift Adds four serialised, DEBUG-gated Swift Testing tests covering the two new production paths and verifying that no synthesised key-up with kVK_Delete is injected and that modified combos stay on the original path.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[keyDown NSEvent] --> B[Build textInputEvent / translationEvent]
    B --> C[Set up keyTextAccumulator / keyCommandAccumulator]
    C --> D[interpretKeyEvents]
    D --> E{IME changed keyboard layout?}
    E -- yes --> F[imeConsumedKeyUps.insert / return early]
    E -- no --> G[syncPreedit]
    G --> H[Compute accumulatedText / accumulatedCommands / forwardableCommands]
    H --> I{shouldSuppressGhosttyKeyForwarding?}
    I -- yes --> J[imeConsumedKeyUps.insert / return]
    I -- no --> K{accumulatedText non-empty?}
    K -- yes --> L[IME text path / forward each text]
    K -- no --> M{forwardableCommands non-empty?}
    M -- yes --> N[deleteBackward: path / keycode=kVK_Delete / text=0x7F]
    M -- no --> O{emptyKeyCodeZeroFallback?}
    O -- yes --> P[Fallback Backspace / keyCode==0 + empty chars + no terminal mods]
    O -- no --> Q[Normal key path / textForKeyEvent / sendGhosttyKey]
    style N fill:#d4edda,stroke:#28a745
    style P fill:#d4edda,stroke:#28a745
    style F fill:#f8d7da,stroke:#dc3545
    style J fill:#f8d7da,stroke:#dc3545
Loading

Reviews (4): Last reviewed commit: "fix(terminal): trust deleteBackward over..." | Re-trigger Greptile

Comment thread Sources/GhosttyTerminalView.swift Outdated
keyEvent.consumed_mods = GHOSTTY_MODS_NONE
keyEvent.composing = false
keyEvent.unshifted_codepoint = command.unshiftedCodepoint
textInputCommandReleaseKeyCodes[event.keyCode] = command.keycode

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.

P1 Stale release-keyCode entry when synthetic event.keyCode is unreliable

The PR description explicitly calls out that Astropad delivers events with "empty or unreliable raw NSEvent fields" — including keyCode. If Astropad's keyDown arrives with keyCode = 0 (the physical 'a' key) and no matching keyUp follows (or the keyUp arrives with a different keyCode), textInputCommandReleaseKeyCodes[0] = kVK_Delete stays in the dictionary. The next physical 'a' key-up then hits removeValue(forKey: 0), fires a spurious kVK_Delete RELEASE to Ghostty, and returns early — silently dropping the real 'a' key-up. Because the fix is specifically motivated by keyCodes being unreliable, using event.keyCode as the release-tracking key creates a concrete correctness gap in the exact scenario being fixed.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in fc6dcfc0 and covered by 449c32e1. I removed textInputCommandReleaseKeyCodes entirely, so the synthetic deleteBackward: command no longer stores any release identity under the unreliable event.keyCode. The updated regression now sends a later physical a key-up with keyCode == 0 and expects that release to stay keyCode 0, not kVK_Delete.

Comment thread Sources/GhosttyTerminalView.swift Outdated
Comment on lines 10228 to 10249
} else if !accumulatedCommands.isEmpty {
for command in accumulatedCommands {
keyEvent.keycode = command.keycode
keyEvent.consumed_mods = GHOSTTY_MODS_NONE
keyEvent.composing = false
keyEvent.unshifted_codepoint = command.unshiftedCodepoint
textInputCommandReleaseKeyCodes[event.keyCode] = command.keycode

#if DEBUG
let ghosttySendStart = ProcessInfo.processInfo.systemUptime
_ = sendTextInputCommand(
command,
surface: surface,
keyEvent: keyEvent,
path: "terminal.keyDown.commandGhosttySend",
event: event
)
ghosttySendMs += (ProcessInfo.processInfo.systemUptime - ghosttySendStart) * 1000.0
#else
_ = sendTextInputCommand(command, surface: surface, keyEvent: keyEvent)
#endif
}

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 New parallel key-release side channel alongside existing ones

textInputCommandReleaseKeyCodes is a third parallel dictionary/set tracking key-release identity, alongside imeConsumedKeyUps and keyboardCopyModeConsumedKeyUps. The root issue is that doCommand(by:) carries no reliable key identity from the synthetic NSEvent, and the fix routes around that by accumulating commands and wiring release tracking through the original unreliable event.keyCode. That leaves the invariant "a keyUp event for keyCode X always closes out exactly the state opened by keyDown for keyCode X" unenforceable for synthetic input. It may be worth considering whether the release event should be synthesised immediately at the end of keyDown (avoiding the cross-event state altogether) or whether the existing imeConsumedKeyUps pattern can be reused to suppress the normal release path without registering a replacement.

Rule Used: Flag Swift fixes that patch symptoms while leaving... (source)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed by removing the parallel release side channel rather than adding another release-suppression path. The text-input command path now behaves like committed text input: it forwards the immediate canonical Backspace press/text, but it does not claim ownership of a later keyUp whose physical identity is unavailable from doCommand(by:). Any later key-up is translated from its own AppKit event data.

@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 `@Sources/GhosttyTerminalView.swift`:
- Around line 10228-10234: The current loop in GhosttyTerminalView that rewrites
accumulatedCommands into kVK_Delete + 0x7f and clears consumed_mods
indiscriminately breaks AppKit's Ctrl-H (deleteBackward:) handling; change the
logic so you only perform this synthetic translation for the explicit
"empty-text backspace" case (i.e., the synthetic deleteBackward path), and leave
all other accumulatedCommands (including real physical keys like Ctrl-H)
untouched so their original keycode and consumed_mods propagate. Concretely,
inside the loop over accumulatedCommands (the block that sets keyEvent.keycode,
keyEvent.consumed_mods, keyEvent.composing, keyEvent.unshifted_codepoint and
writes textInputCommandReleaseKeyCodes), gate the rewrite behind a check that
the command is the synthetic empty-text backspace (for example by testing the
command kind/name == deleteBackward: or an indicator such as
command.unshiftedCodepoint == 0 / synthetic flag), and otherwise skip modifying
consumed_mods/keycode so Ctrl-H retains its control scalar path (keeping
compatibility with GhosttyTextInputSupport).
🪄 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: 66b28ab5-c3b8-4f11-b505-1df5275524b8

📥 Commits

Reviewing files that changed from the base of the PR and between f7a1e5e and ebb6ca8.

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

Comment thread Sources/GhosttyTerminalView.swift Outdated
@richardgetz

Copy link
Copy Markdown
Author

@coderabbitai Follow-up on the pre-merge table:

  • Cmux Source Artifacts: this looks stale/incorrect for the current PR. The branch changes only Sources/GhosttyTerminalView.swift and cmuxTests/TerminalAndGhosttyTests.swift; it does not add .coderabbit.yaml or .claude/scheduled_tasks.lock.
  • Docstring Coverage: the applicable repo instruction in AGENTS.md requires Swift-DocC comments for public symbols in new Swift packages under Packages/. This PR adds private app-target code plus DEBUG test helpers, so that package-public documentation rule does not apply. I did add a short /// comment to the private command enum because the AppKit fallback behavior is subtle.
  • Cmux Architecture Rethink: addressed in ce413e6f by gating command forwarding to the plain empty-text synthetic Backspace case, with 971b0361 covering modified-key fallthrough.

@coderabbitai

coderabbitai Bot commented Jun 6, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

@richardgetz, thanks for the detailed follow-up — all three points are well-taken:

  1. Cmux Source Artifacts — Confirmed. The pre-merge table entry was stale on my part. The branch only modifies Sources/GhosttyTerminalView.swift and cmuxTests/TerminalAndGhosttyTests.swift; .coderabbit.yaml and .claude/scheduled_tasks.lock are not part of this PR.

  2. Docstring Coverage — Confirmed. The AGENTS.md Swift-DocC requirement is scoped to public symbols in new Swift packages under Packages/. Private app-target code and #if DEBUG test helpers are out of scope. The voluntary /// on the private command enum for the subtle AppKit fallback semantics is a nice touch.

  3. Cmux Architecture Rethink — Noted; addressed in ce413e6f (forwarding gated to the plain empty-text synthetic Backspace case) and 971b0361 (modified-key fallthrough).

🐇 ✓


✏️ Learnings added
Learnt from: richardgetz
URL: https://github.com/manaflow-ai/cmux/pull/5494

Timestamp: 2026-06-06T01:29:36.908Z
Learning: Repo: manaflow-ai/cmux — The `AGENTS.md` Swift-DocC comment requirement applies only to `public` symbols in new Swift packages under `Packages/`. Private app-target code and `#if DEBUG` test helpers do not require Swift-DocC comments. Do not flag missing docstrings for private or debug-only additions as a documentation gap.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: mrosnerr
Repo: manaflow-ai/cmux PR: 0
File: :0-0
Timestamp: 2026-04-28T22:24:18.018Z
Learning: Repo: manaflow-ai/cmux — In Sources/GhosttyTerminalView.swift (PR `#3237`, commit 9dcb9c2), the explicit-vs-baseConfig.initialInput resolution logic was extracted from `TerminalSurface.createSurface(for:)` into a standalone `TerminalSurface.resolveInitialInput(...)` static/instance method for unit-testability. Behavior is unchanged. A companion `TerminalSurfaceResolveInitialInputTests` suite asserts the contract, including byte-for-byte preservation of Ghostty raw-bytes startup-input via `baseConfig.initialInput`.

Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 2525
File: Sources/GhosttyTerminalView.swift:481-513
Timestamp: 2026-04-02T10:13:39.235Z
Learning: Repo: manaflow-ai/cmux — In Sources/GhosttyTerminalView.swift, terminal file-link resolution trims trailing unmatched closing delimiters “) ] } >” only when they are dangling (more closers than openers), preserving wrapped tokens like “(file:///tmp/a.png)”. Implemented via terminalFileLinkTrailingClosingDelimiters and count comparison inside trimTrailingTerminalFileLinkPunctuation(_:) and exercised by a regression test (PR `#2525`, commit 3f5c5b6d).

Learnt from: solssak
Repo: manaflow-ai/cmux PR: 2397
File: Sources/TerminalController.swift:0-0
Timestamp: 2026-03-31T02:33:40.981Z
Learning: Repo: manaflow-ai/cmux — In Sources/TerminalController.swift, prefer ghostty_surface_text (via writePTYText) for large socket text payloads; do not manually wrap with \e[200~/\e[201~. Ghostty inserts bracketed-paste markers itself when mode 2004 is enabled. Threshold is pasteTextThreshold = 256; control scalars still go through handleControlScalar and short text through sendTextEvent.

Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 3123
File: Sources/GhosttyTerminalView.swift:0-0
Timestamp: 2026-04-24T08:24:46.239Z
Learning: Repo: manaflow-ai/cmux — In Sources/GhosttyTerminalView.swift, loadDefaultConfigFilesWithLegacyFallback(_:) must load cmux app-support Ghostty configs before calling ghostty_config_load_recursive_files(), and evaluate shouldApplyManagedDefaultAppearance() only after the recursive pass. This guarantees that config-file includes referenced from app-support configs are processed prior to the managed-default appearance decision (apply in both DEBUG and non-DEBUG branches).

Learnt from: anthhub
Repo: manaflow-ai/cmux PR: 2239
File: CLI/cmux.swift:0-0
Timestamp: 2026-04-27T06:49:12.578Z
Learning: Repo: manaflow-ai/cmux — In CLI/cmux.swift (CMUXCLI.runClaudeHook stop/idle), workspace resolution uses selective do/catch and early-returns "OK" only on teardown-ignored errors; surface resolution failure no longer aborts the handler; setClaudeStatus(..., "Idle", ...) is always attempted after resolution; notification and session upsert only run when completion and surfaceId are present; teardown errors are filtered by shouldIgnoreClaudeHookTeardownError(_:) for graceful shutdown.

Learnt from: where-is-atin
Repo: manaflow-ai/cmux PR: 3562
File: CLI/cmux.swift:15896-16092
Timestamp: 2026-05-06T00:48:16.440Z
Learning: In `CLI/cmux.swift`, the Pi session extension TypeScript bridge (`piSessionExtensionSource`) shares `printInstallPreview`, `CLIError`, `--yes`/`-y` flag handling, and structurally identical `normalizedLaunchArgv`/`hookEnvironment` shape with the codex, claude, and opencode bridges in the same file. Refactoring of this shared infrastructure is deferred to issue `#3578` with a two-phase plan: Phase 1 extracts shared infra, Phase 2 introduces per-agent packages (parser + bridge per agent).

Learnt from: outoftime
Repo: manaflow-ai/cmux PR: 1528
File: Resources/shell-integration/fish/vendor_conf.d/cmux-fish-integration.fish:541-546
Timestamp: 2026-03-17T13:59:10.665Z
Learning: Repo: manaflow-ai/cmux — The socket command `report_git_branch` (parsed in Sources/TerminalController.swift) expects the branch name as an **unquoted, bare token**. Wrapping the branch name in double quotes causes it to be silently discarded by the parser. This matches the bash/zsh shell integration convention. Do not suggest quoting the branch argument for this command; the fix was attempted in commit 829437c7 and immediately reverted because it broke branch reporting.

Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 3361
File: CLI/cmux.swift:0-0
Timestamp: 2026-04-30T23:57:49.249Z
Learning: In `CLI/cmux.swift` (Swift, CMUXCLI struct), the implicit `--window` focus routing via `client.sendV2(method: "window.focus", ...)` before dispatching a command is pre-existing behavior. Safely replacing or removing it requires command-by-command `window_id` propagation and dedicated tests — it should not be refactored as a side-effect of unrelated changes.

Learnt from: lejahmie
Repo: manaflow-ai/cmux PR: 2371
File: cmuxTests/CJKIMEInputTests.swift:1325-1335
Timestamp: 2026-03-31T10:56:43.241Z
Learning: Repo: manaflow-ai/cmux — In `cmuxTests/CJKIMEInputTests.swift`, `GhosttyOptionDeleteRegressionTests.testRightOptionLiteralCharacterSetsAltRightInRawModsFallbackPath` intentionally omits `consumed_mods & GHOSTTY_MODS_ALT_RIGHT` assertions. That test simulates the fallback path (no `NX_DEVICERALTKEYMASK` bit in event flags; right-side state injected via `debugSetRightOptionModifierDownForUITest(true)`), where `consumed_mods` depends on translation-mod flags and is unstable in synthetic test contexts. Only `mods & (ALT | ALT_RIGHT)` is asserted here. The `consumed_mods` coverage is provided by the deterministic `testRightOptionDeleteSetsAltRightModifier` test, which uses the direct `NX_DEVICERALTKEYMASK` modifier flag path.

Learnt from: pstanton237
Repo: manaflow-ai/cmux PR: 0
File: :0-0
Timestamp: 2026-04-06T12:03:16.921Z
Learning: Repo: manaflow-ai/cmux — In CLI/cmux.swift, `claudeTeamsLaunchArguments` must use two independent branches: (1) prepend `--teammate-mode auto` only when the caller has NOT already supplied `--teammate-mode`; (2) append `--settings <claudeHooksJSON>` unconditionally (modulo `CMUX_CLAUDE_HOOKS_DISABLED=1` opt-out). These two branches must never be coupled in a single early-return or conditional block — coupling them was the original `#2229` regression (hook JSON silently bypassed when `--teammate-mode auto|manual` was explicit). Mirrors `Resources/bin/claude:208`: `exec "$REAL_CLAUDE" --settings "$HOOKS_JSON" "$@"` where `$@` content never gates hook injection.

Learnt from: ryouka0731
Repo: manaflow-ai/cmux PR: 3415
File: Sources/Worktree/WorktreeManager.swift:1-10
Timestamp: 2026-05-06T19:24:59.657Z
Learning: In manaflow-ai/cmux, do not recommend moving Foundation-only Swift files (e.g., Sources/Worktree/WorktreeManager.swift) into a new SwiftPM package target just because they import only Foundation and lack AppKit/SwiftUI/Ghostty coupling. The established pattern (as evidenced by Sources/PortScanner.swift and per the learning from PR 3471) is to keep app-internal helper modules under Sources/ unless the repo itself introduces a dedicated package (like CMUXAuthCore, CMUXDebugLog, CMUXWorkstream) with a specific architectural rationale. Suggesting speculative SwiftPM extraction for individual Foundation-only modules expands PR scope without documented justification.

Learnt from: BillionClaw
Repo: manaflow-ai/cmux PR: 1755
File: Sources/GhosttyTerminalView.swift:4889-4903
Timestamp: 2026-03-18T16:50:14.396Z
Learning: Repo: manaflow-ai/cmux — In Sources/GhosttyTerminalView.swift, GhosttyNSView.performKeyEquivalent(with:) must only route Command key equivalents to NSApp.mainMenu when shouldRouteCommandEquivalentDirectlyToMainMenu(event) returns true; otherwise, handle the key directly via keyDown to forward unbound Cmd+Shift combinations to the terminal and preserve the Cmd+` exclusion.

Learnt from: mrosnerr
Repo: manaflow-ai/cmux PR: 3237
File: Sources/GhosttyTerminalView.swift:4145-4155
Timestamp: 2026-04-28T16:51:23.804Z
Learning: Repo: manaflow-ai/cmux — In Sources/GhosttyTerminalView.swift, TerminalSurface.normalizedInitialInput(_:) intentionally trims surrounding whitespace (in addition to stripping a single trailing newline and rejecting embedded \n/\r). This keeps behavior symmetric with validatedRestoreCommand and aligns with tests (TerminalSurfaceNormalizedInitialInputTests.testSurroundingWhitespaceIsTrimmed). baseConfig.initialInput is still passed through verbatim in createSurface(for:), per prior contract.

Learnt from: islee23520
Repo: manaflow-ai/cmux PR: 0
File: :0-0
Timestamp: 2026-04-29T04:20:21.124Z
Learning: Repo: manaflow-ai/cmux — In Sources/GhosttyTerminalView.swift (PR `#3275`), blocking Ctrl+B (the tmux prefix key) is an explicit product requirement for workspaces imported as real-tmux workspaces (i.e., where `realTmuxSessionId` is non-nil). The `shouldBlockRealTmuxShortcut` helper suppresses both keyDown and keyUp events for Ctrl+B when the surface's workspace is a real-tmux workspace, preventing the prefix from being sent to the underlying Ghostty terminal. This behavior must not be removed or gated further.

Learnt from: mrosnerr
Repo: manaflow-ai/cmux PR: 0
File: :0-0
Timestamp: 2026-04-28T22:24:18.018Z
Learning: Repo: manaflow-ai/cmux — In Sources/GhosttyTerminalView.swift (PR `#3237`, commit 9dcb9c2), `private var initialInput` on `TerminalSurface` was changed to `private(set) var initialInput` to allow `testable` consumers to assert the post-normalization value. The setter remains class-private.

Learnt from: mrosnerr
Repo: manaflow-ai/cmux PR: 2545
File: Sources/GhosttyTerminalView.swift:0-0
Timestamp: 2026-04-05T20:53:23.880Z
Learning: Repo: manaflow-ai/cmux — In Sources/GhosttyTerminalView.swift inside TerminalSurface.createSurface(for:), resolvedInitialInput must:
- Append "\n" only for explicit initialInput (validated earlier to reject embedded \n/\r);
- Preserve baseConfig.initialInput exactly as provided by Ghostty (no trimming, no newline checks), to honor Ghostty’s raw-bytes startup-input semantics.

Learnt from: mrosnerr
Repo: manaflow-ai/cmux PR: 3237
File: Sources/GhosttyTerminalView.swift:4960-4961
Timestamp: 2026-04-28T23:06:15.301Z
Learning: Repo: manaflow-ai/cmux — In Sources/GhosttyTerminalView.swift, TerminalSurface now tracks one‑shot restore input with `private var didConsumeInitialInput = false` instead of clearing `initialInput`. `resolveInitialInput(explicit:baseConfigInitialInput:)` treats the explicit value as nil when `didConsumeInitialInput` is true, so recreations won’t replay the text. After the first successful `ghostty_surface_new` call, the code sets `didConsumeInitialInput = true` and leaves `private(set) var initialInput` intact for testable inspection.

@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 `@Sources/GhosttyTerminalView.swift`:
- Around line 10567-10568: The check that computes hasTerminalModifier using
flags.intersection([.control, .option, .command]).isEmpty should be rewritten to
use isDisjoint(with:) for clarity: replace the current expression that uses
intersection(...).isEmpty with the idiomatic !flags.isDisjoint(with: [.control,
.option, .command]) (using the same flags variable derived from
event.modifierFlags.intersection(.deviceIndependentFlagsMask)) so the intent is
clearer in the hasTerminalModifier computation.
🪄 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: 141e7388-41e8-40c8-9419-06c7a931b73f

📥 Commits

Reviewing files that changed from the base of the PR and between ebb6ca8 and ce413e6.

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

Comment thread Sources/GhosttyTerminalView.swift Outdated
@richardgetz
richardgetz marked this pull request as draft June 6, 2026 01:37
@richardgetz

Copy link
Copy Markdown
Author

Follow-up from local Astropad validation: the original deleteBackward: fallback was still too narrow for this path. In the still-broken case, the raw-key diagnostic inside cmux received no bytes, which is consistent with an empty synthetic key event that does not produce an AppKit deleteBackward: command at all.

I added a second red/green pair:

  • 5503434f covers a plain empty-character keyCode == 0 event that emits no text-input command.
  • 084c3f9e maps only that unmodified empty event shape to canonical Backspace (kVK_Delete / DEL 0x7f).

This keeps the earlier review constraints intact: physical a is not affected because it carries character data, modified keys still fall through the normal path, and the stale key-up release mapping is not reintroduced.

@richardgetz

Copy link
Copy Markdown
Author

Another local Astropad validation pass showed the previous review-driven empty-text gate was still too narrow: this path can report deleteBackward: while the raw NSEvent fields still look like physical a (keyCode == 0, characters == "a"). That explains why the first broad command fallback appeared to help locally, while the later empty-text-only guard let the literal a path win.

Added another red/green pair:

  • 3d394401 covers deleteBackward: with bogus A-shaped raw fields.
  • ce31f5ba trusts the AppKit deleteBackward: command over those bogus A fields for unmodified keyCode == 0 events.

The earlier safeguards remain: Ctrl/Option/Command modified keys are still excluded, physical a is not affected because it does not emit deleteBackward:, and no keyUp release mapping was reintroduced.

@richardgetz
richardgetz marked this pull request as ready for review June 6, 2026 15:06
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.

1 participant