feat(tui): consider Ctrl+C clearing a non-empty composer before it arms exit (Esc/Ctrl+C split revisited)
- Dominant language
- Rust
- Stars
- 41k
- Forks
- 3.6k
- Avg merge
- 13h 59m
- Merged PRs (30d)
- 299
Description
## Problem
Arriving from other agents, `Ctrl+C` is the muscle-memory "erase what I typed" key, and the other agents do implement it that way:
- Claude Code (their reference, "Interrupt, or clear input"): *"Interrupts a running operation. If nothing is running, the first press clears the prompt input and a second press exits Claude Code"* — https://code.claude.com/docs/en/interactive-mode
- opencode binds both `input_clear` and `app_exit` to `ctrl+c` (`packages/tui/src/config/keybind.ts:161` and `:48`).
In Codewhale the same gesture does something else when a draft is present: it arms the exit prompt, and a second press quits the app. Plain `Ctrl+C` never reaches a clear path here — the copy shortcut requires `Cmd` or `Ctrl+Shift` (`crates/tui/src/tui/key_shortcuts.rs:29-40`), so an unmodified press goes straight to the exit-arming table below.
Current decision table (`crates/tui/src/tui/mouse_ui.rs:1775-1788`, handled at `crates/tui/src/tui/ui/event_loop.rs:5962-5975`):
1. transcript selection active → copy;
2. turn in flight → cancel;
3. exit already armed → exit;
4. otherwise → **arm the exit prompt** (toast "press Ctrl+C again", `crates/tui/src/tui/app.rs:5447-5456`).
Nothing in that order looks at a non-empty composer. The draft is not lost — `Esc` clears it (`crates/tui/src/tui/composer_ui.rs:42` → `event_loop.rs:6097-6100`), `Ctrl+U` does the same (`event_loop.rs:6619`), and `Ctrl+Z` restores the cleared text (`app/composer.rs:1534`, `docs/KEYBINDINGS.md:50-51`) — but the outcome of the gesture (an exit prompt, then exit) is the one a newcomer least expects, and the mismatch only becomes visible when the app closes.
Prior art, stated up front: this split was chosen deliberately — #1757 discussed the `Esc`/`Ctrl+C` matrix and settled on Esc clearing a non-empty composer while Ctrl+C stays the standard terminal SIGINT path, with the principle that each key does one clear thing; #1771 then added the recoverable-clear buffer. So this is not "Ctrl+C is broken". It is a question of whether that choice is worth revisiting now that the arriving muscle memory is the other agents' behaviour.
## Proposed solution
Option A — follow the other agents: with a non-empty composer, nothing in flight and no selection, `Ctrl+C` clears the draft through the same recoverable path `Esc` uses (so `Ctrl+Z` still restores it), and the arm-exit behaviour returns once the composer is empty. The SIGINT path for an empty composer is unchanged, which is the case where the standard terminal expectation actually applies.
Option B — keep the split and close the discoverability gap: the armed-exit toast could say that the draft is kept and that `Esc` / `Ctrl+Z` clear or restore it, so the user learns the difference at the moment it matters instead of after quitting.
Both are small; A changes a key's meaning, B changes one string.
## Use case
Pasting a long prompt, then changing your mind and wanting the field empty without select-all. Today that is `Esc` or `Ctrl+U` — neither is what a user coming from opencode / Claude Code / Codex reaches for, and `Ctrl+U` is documented (`docs/KEYBINDINGS.md:50`) but only after you know to look.
## Alternatives considered
- Do nothing: the capability exists (`Esc`, `Ctrl+U`, `Ctrl+W` are all documented at `docs/KEYBINDINGS.md:39-51`), so this is only about which key performs it.
- Clearing on every Ctrl+C including mid-turn: rejected — in flight, Ctrl+C must stay "cancel", otherwise there is no way out of a running turn.
- Following opencode literally (first press clears, second exits with no armed window): the current two-second arm window (`app.rs:5447-5456`) is a deliberate safety property worth keeping.
## Impact
Small but recurring for anyone whose habit comes from the other agents: the expected "field is now empty" turns into an exit prompt, and the second press quits. No usage data behind this; the argument is consistency with sibling tools, and the counter-argument (#1757's "one key, one meaning") is equally strong — which is why this is raised as a proposal for the maintainer's call rather than a defect report.
## Additional context
- Decision table and handler: `mouse_ui.rs:1775-1788`, `event_loop.rs:5962-5975`; unit-tested table at `crates/tui/src/tui/ui/tests.rs:16043+` (`ctrl_c_disposition_*`).
- Esc → `EscapeAction::ClearInput`: `composer_ui.rs:42`; recoverable clear: `app/composer.rs:1534`; `Ctrl+U`: `event_loop.rs:6619`; `Ctrl+Z` restore: `event_loop.rs:6625`.
- Docs: `docs/KEYBINDINGS.md:39` (Esc), `:50-51` (`Ctrl-U` / `Ctrl-Z`).
- Related: #1757 (the Esc/Ctrl+C matrix that produced the current behaviour), #1771 (`Ctrl+Z` restore), #1337 (Ctrl+C copy convention on Windows).
Contributor guide
Research direction
Read the Ctrl+C decision table in crates/tui/src/tui/mouse_ui.rs and its handler in crates/tui/src/tui/ui/event_loop.rs, then inspect the ctrl_c_disposition_* tests in crates/tui/src/tui/ui/tests.rs. Confirm whether the maintainer chooses Option A or B, trace the existing recoverable clear path in app/composer.rs, and update the relevant behavior, tests, or toast text so the selected interaction is covered.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- cli
- Issue type
- Feature
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 42/100