Hmbown / Hmbown/Codewhale

feat(tui): consider Ctrl+C clearing a non-empty composer before it arms exit (Esc/Ctrl+C split revisited)

Open
#6,237 1 comment 0 reactions 0 assignees View on GitHub
enhancement
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.