Add "Send Ctrl-F to Terminal" passthrough action (force-stop Claude Code agents) (#4993)#5011
Add "Send Ctrl-F to Terminal" passthrough action (force-stop Claude Code agents) (#4993)#5011austinywang wants to merge 2 commits into
Conversation
Issue #4993: inside a cmux-managed Claude Code session, CC only exposes force-stop as a TUI keybinding ("press Ctrl-F twice"). The maintainer framing is the durable one: any ctrl-chord a TUI reads off the raw tty is one keystroke-capture handler away from being swallowed, so the fix is a non-keyboard stop path rather than patching one binding. This adds a single shared action that delivers a faithfully Ghostty-encoded Ctrl-F to the focused terminal's PTY, bypassing cmux's shortcut/menu/ responder layers entirely. Encoding goes through the existing named-key path (`sendNamedKey("ctrl-f")`) so it respects the surface's current keyboard mode, matching a real keystroke. Invoke it twice to force-stop. Altitude: the general raw-key passthrough already exists on the CLI (`send_key <key>`); a key picker is wrong ergonomics for a quick GUI force-stop, so the GUI surfaces expose the concrete Ctrl-F chord while the socket command stays general. Default shortcut is unbound (binding plain Ctrl-F would be self-referential); users opt in via Settings. Shared model path: TabManager.sendCtrlFToFocusedTerminal(), wired to every entrypoint — customizable KeyboardShortcutSettings action, command palette, Find menu item, cmux.json shortcuts.bindings (auto), and `send_key ctrl-f` (now an explicit named case + help text). Docs derive from the single cmux-shortcuts.ts source; strings localized en/ja. Tests: behavioral guards that the ctrl-f chord stays recognized/deliverable (TerminalSurface.sendNamedKey) and that NamedKeySendResult.accepted maps delivery correctly. A red-first regression test does not apply: the byte encoding already worked generically; the defect was the missing non-keyboard affordance, which is GUI/menu/palette wiring exercised at integration level. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughAdds an opt-in shortcut and UI (menu, command palette, app dispatcher) to forward a Ctrl‑F named-key to the focused terminal, implements terminal-side mapping and delivery semantics, updates keyboard shortcut settings, adds tests, and inserts localization and web shortcut metadata. ChangesTerminal Ctrl+F Forwarding
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 17 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (17 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
Greptile SummaryThis PR adds a "Send Ctrl-F to Terminal" action as a structured, non-keyboard escape hatch to deliver
Confidence Score: 5/5Safe to merge — all changes are additive UI wiring with no mutations to existing state, auth, or persistence paths. The change adds a new forwarding path for a single control chord. All five entrypoints follow the existing patterns in the codebase exactly — the forceRefresh-on-.sent-only pattern matches TerminalController, the shortcut handler mirrors .attachTextBoxFile, and localization stubs cover every supported locale. No existing behavior is modified. No files require special attention. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[User wants to force-stop\nhung TUI agent] --> B{Entry point}
B --> C[Keyboard Shortcut\nunbound by default]
B --> D[Command Palette\npalette.terminalSendCtrlF]
B --> E[Find Menu\nSend Ctrl-F to Terminal]
B --> F[CLI Socket\nsend_key ctrl-f]
C --> G[AppDelegate\nmatchConfiguredShortcut]
D --> H[ContentView registry]
E --> I[cmuxApp.swift\nrestoreFindTargetFocus]
F --> J[TerminalController\nsendKey handler]
G --> K[TabManager\nsendCtrlFToFocusedTerminal]
H --> K
I --> K
J --> L[TerminalPanel\nsendNamedKeyResult ctrl-f]
K --> M{selectedTerminalPanel?}
M -- nil --> N[return false / beep]
M -- panel --> L
L --> O[TerminalSurface\nsendNamedKey ctrl-f]
O --> P[pendingKeyEvent\nkVK_ANSI_F + GHOSTTY_MODS_CTRL]
P --> Q{NamedKeySendResult}
Q -- .sent --> R[forceRefresh + return true]
Q -- .queued --> S[return true]
Q -- failure cases --> N
Reviews (2): Last reviewed commit: "Localize Ctrl-F passthrough strings for ..." | Re-trigger Greptile |
| } | ||
| } | ||
| }, | ||
| "command.terminalSendCtrlF.title": { | ||
| "extractionState": "manual", | ||
| "localizations": { | ||
| "en": { | ||
| "stringUnit": { | ||
| "state": "translated", | ||
| "value": "Send Ctrl-F to Terminal" | ||
| } | ||
| }, | ||
| "ja": { | ||
| "stringUnit": { | ||
| "state": "translated", | ||
| "value": "ターミナルにCtrl-Fを送信" | ||
| } | ||
| } | ||
| } | ||
| }, | ||
| "menu.find.sendCtrlFToTerminal": { | ||
| "extractionState": "manual", | ||
| "localizations": { | ||
| "en": { | ||
| "stringUnit": { | ||
| "state": "translated", | ||
| "value": "Send Ctrl-F to Terminal" | ||
| } | ||
| }, | ||
| "ja": { | ||
| "stringUnit": { | ||
| "state": "translated", | ||
| "value": "ターミナルにCtrl-Fを送信" | ||
| } | ||
| } | ||
| } | ||
| }, | ||
| "shortcut.sendCtrlFToTerminal.label": { | ||
| "extractionState": "manual", | ||
| "localizations": { | ||
| "en": { | ||
| "stringUnit": { | ||
| "state": "translated", | ||
| "value": "Send Ctrl-F to Terminal" | ||
| } | ||
| }, | ||
| "ja": { | ||
| "stringUnit": { | ||
| "state": "translated", | ||
| "value": "ターミナルにCtrl-Fを送信" | ||
| } | ||
| } | ||
| } | ||
| }, | ||
| "shortcut.attachTextBoxFile.label": { | ||
| "extractionState": "manual", | ||
| "localizations": { |
There was a problem hiding this comment.
Missing locale entries for 18 locales
The three new string keys (command.terminalSendCtrlF.title, menu.find.sendCtrlFToTerminal, shortcut.sendCtrlFToTerminal.label) only include en and ja localizations. The catalog already supports ar, bs, da, de, es, fr, it, km, ko, nb, pl, pt-BR, ru, th, tr, uk, zh-Hans, and zh-Hant. Every other entry added alongside a new action (e.g. shortcut.attachTextBoxFile.label) carries "state": "needs_review" stubs for every supported locale. Without those stubs, the string catalog is incomplete and users on non-en/non-ja builds will fall back to the defaultValue literal rather than a catalog-managed fallback — bypassing translation pipeline review for all 18 locales.
Rule Used: Flag production user-facing text that is not fully... (source)
There was a problem hiding this comment.
Fixed in fb798df — expanded all three new keys to the full 20-locale catalog set (en + ja translated; the other 18 locales seeded with the English value and state=needs_review, matching the placeholder convention existing untranslated entries use).
— Claude Code
There was a problem hiding this comment.
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/ContentView.swift`:
- Around line 7405-7416: The Localizable.xcstrings file only contains
command.terminalSendCtrlF.title for en and ja; add the missing localized entries
for the key "command.terminalSendCtrlF.title" for every locale listed in the app
catalog (ar, bs, da, de, es, fr, it, km, ko, nb, pl, pt-BR, ru, th, tr, uk,
zh-Hans, zh-Hant) so the CommandPaletteCommandContribution with commandId
"palette.terminalSendCtrlF" has a proper title in all supported locales; update
each locale’s Localizable.xcstrings (or corresponding .strings/.xtstrings files)
with the translated string (or fallback copy of English if translation not
available) and ensure the project resource catalog includes those locale files.
🪄 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: 2991c237-3ca8-463f-9e38-7980a20e01b0
📒 Files selected for processing (12)
Resources/Localizable.xcstringsSources/AppDelegate.swiftSources/ContentView+RightSidebarCommandPalette.swiftSources/ContentView.swiftSources/GhosttyTerminalView.swiftSources/KeyboardShortcutContext.swiftSources/KeyboardShortcutSettings.swiftSources/TabManager.swiftSources/TerminalController.swiftSources/cmuxApp.swiftcmuxTests/TerminalAndGhosttyTests.swiftweb/data/cmux-shortcuts.ts
CodeRabbit/Greptile flagged that the three new string keys (command.terminalSendCtrlF.title, menu.find.sendCtrlFToTerminal, shortcut.sendCtrlFToTerminal.label) only had en/ja while every other entry in Localizable.xcstrings carries all 20 catalog locales. Expand each to the full locale set, matching the file convention: en + ja translated, the other 18 locales seeded with the English value and state=needs_review (the same placeholder pattern existing untranslated entries use). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit fb798df. Configure here.
| case toggleTerminalCopyMode | ||
| case focusTextBoxInput | ||
| case attachTextBoxFile | ||
| case sendCtrlFToTerminal |
There was a problem hiding this comment.
Settings UI missing shortcut action
Medium Severity
sendCtrlFToTerminal was added to KeyboardShortcutSettings.Action and wired in the app, but not to the mirrored ShortcutAction enum used by the Settings keyboard-shortcuts UI and shortcuts.bindings catalog. Users cannot discover, bind, or reset this shortcut in Settings like adjacent terminal actions (focusTextBoxInput, attachTextBoxFile), despite the PR describing it as customizable.
Reviewed by Cursor Bugbot for commit fb798df. Configure here.
There was a problem hiding this comment.
False positive — there is no separate/mirrored ShortcutAction enum. The Settings keyboard-shortcuts UI is data-driven: cmuxApp.swift iterates KeyboardShortcutSettings.settingsVisibleActions (= Action.allCases.filter(isPublicShortcutAction), minus showHideAllWindows) and renders a ShortcutSettingRow per action. sendCtrlFToTerminal is public (isPublicShortcutAction defaults true; only the 5 sidebar-mode actions are excluded), so it already appears in Settings as a discoverable/bindable/resettable row. The shortcuts.bindings catalog is likewise generated by iterating publicShortcutActions in KeyboardShortcutSettingsFileStore+Template.swift, and cmux.json parsing resolves it via Action(rawValue: "sendCtrlFToTerminal"). No mirror to update.
— Claude Code


Fixes #4993.
Problem
Inside a cmux-managed Claude Code (CC) session, force-stopping a hung background agent is only exposed by CC as a TUI keybinding — its watchdog instructs "press Ctrl-F twice." Because that chord is read off the raw tty, it inherits every keystroke-capture layer between the window and the PTY, and there is no non-keyboard way to deliver it.
/cleardoes not help (CC preserves background agents by design), so a hung agent can't be cleared short of killing the session.The maintainer comment on the issue states the durable framing: "any ctrl-chord CC reads off the raw tty is one keystroke-capture handler away from being swallowed … the durable fix is a non-keyboard stop path."
Investigation (what actually swallows plain Ctrl-F today)
Tracing cmux's key-routing path on current
mainwith default settings, plain Ctrl-F is not captured by cmux and already reaches the PTY:find=⌘F,toggleFullScreen=⌘⌃F,switchRightSidebarToFind=⌃2.performKeyEquivalentswizzle returnsfalsefor non-⌘ chords, so the event flows toGhosttyTerminalView.keyDown's control fast path and is encoded to the PTY as^F(the only thing that intercepts^Fis keyboard copy-mode, which the user must explicitly toggle on).So the literal "cmux grabs Ctrl-F" does not reproduce in code. The real, durable gap — and what this PR delivers — is a non-keyboard affordance to deliver the chord, so force-stop works regardless of how fragile keystroke routing is for any given TUI chord.
Fix — one shared passthrough action, wired through every surface
A single shared model path,
TabManager.sendCtrlFToFocusedTerminal(), delivers a faithfully Ghostty-encoded Ctrl-F to the focused terminal's PTY via the existing named-key path (sendNamedKey("ctrl-f")), so it respects the surface's current keyboard-encoding mode and matches a real keystroke. It bypasses cmux's shortcut/menu/responder layers entirely. Invoke twice to force-stop.Entrypoints (per the repo's shared-behavior policy):
KeyboardShortcutSettings.Action.sendCtrlFToTerminal, editable in Settings, unbound by default (binding plain Ctrl-F would be self-referential; users opt in).palette.terminalSendCtrlF, searchable by "force stop / agent / claude / ctrl f / hung".send_key ctrl-f(already worked via the generic parser; now an explicit named case + help text).shortcuts.bindings.sendCtrlFToTerminal(auto-derived from the action).web/data/cmux-shortcuts.tssource.Altitude (why Ctrl-F specifically in the GUI, not an arbitrary-key picker)
The general raw-key passthrough already exists on the CLI (
send_key <key>). A key-picker is the wrong ergonomics for a quick GUI force-stop, so the GUI surfaces expose the concrete Ctrl-F chord (the issue's documented need) while the socket command stays general. This eliminates the class — a structured action has no keystroke left to intercept — without special-casing a keycode in the routing layer.Tests
Behavioral guards (added to the already-wired
TerminalAndGhosttyTests):testSendNamedKeyRecognizesCtrlFForceStopChord—ctrl-f/ctrl+fresolve to a recognized, deliverable chord (.surfaceUnavailableon a closed surface vs.unknownKeyfor garbage), locking the encoding the affordance depends on.testNamedKeySendResultAcceptedReflectsDelivery— the newNamedKeySendResult.acceptedmaps delivery vs failure correctly.On red-first: a red-first regression test does not apply here. The byte-level chord encoding already worked generically on
main; the defect was the absence of a non-keyboard affordance, which is GUI/menu/palette wiring exercised only at integration level, not a unit-reproducible capture bug. Stated plainly rather than faking a red/green.🤖 Generated with Claude Code
Need help on this PR? Tag
@codesmithwith what you need. Autofix is disabled.Note
Low Risk
UI and input-forwarding only; no auth, persistence, or security-sensitive paths; default shortcut is unbound and delivery is gated on a focused terminal.
Overview
Adds a Send Ctrl-F to Terminal action so users can deliver a Ghostty-encoded
^Fto the focused PTY without relying on keyboard routing—aimed at TUIs (e.g. Claude Code) that require Ctrl-F twice to force-stop hung background agents.TabManager.sendCtrlFToFocusedTerminal()callssendNamedKey("ctrl-f"), refreshes the surface on success, and reports delivery via newNamedKeySendResult.accepted. The named-key parser now mapsctrl-f/ctrl+f.The action is wired through Settings (
sendCtrlFToTerminal, unbound by default), command palette, Find menu (with focus restore), AppDelegate shortcut handling (event consumed only when delivery succeeds), socket help forsend_key ctrl-f, web shortcut docs, and localization. Unit tests lockctrl-frecognition and theacceptedflag.Reviewed by Cursor Bugbot for commit fb798df. Bugbot is set up for automated code reviews on this repo. Configure here.
Summary by cubic
Adds a non-keyboard action to send Ctrl‑F to the focused terminal, giving a reliable way to force‑stop hung Claude Code agents even when keystrokes are intercepted. Invoke it twice to trigger the stop.
TabManager.sendCtrlFToFocusedTerminal()which forwards a Ghostty‑encoded Ctrl‑F viasendNamedKey("ctrl-f"); extends the named‑key map to recognizectrl-f/ctrl+fand addsNamedKeySendResult.acceptedto report delivery.sendCtrlFToTerminal, unbound by default), and CLI/socketsend_key ctrl-f(help text updated).Written for commit fb798df. Summary will update on new commits.
Summary by CodeRabbit
New Features
Documentation
Tests