feat: Ctrl+C interrupts first, exits on second press (#71)#72
Conversation
Ctrl+C exited the REPL immediately. Now the first Ctrl+C interrupts the in-flight run (like Esc) and arms a short window; a second Ctrl+C inside the window exits. So you can stop the LLM mid-thought without leaving the conversation, and a single accidental press never quits. Ctrl+D still exits immediately (EOF). While a modal key-capture is pending (the y/a/n permission prompt), the original immediate exit is preserved. - input.ts: two-stage Ctrl+C (first → `interrupt` + "(press Ctrl+C again to exit)" hint + 2s armed window; second → `exit`); Ctrl+D split out; window timer is unref'd and cleared on close. Tests: - textarea-ctrl-c.test.ts (unit): interrupt-first, second-exits, window disarm (fake timers), typing-between, Ctrl+D immediate, modal-capture immediate exit. - ctrl-c-interrupt.e2e.test.ts (integration, real REPL): first Ctrl+C aborts the run and stays in the conversation; second exits. - textarea-interrupt.test.ts: updated the stale "ctrl+c exits" assertion. - steering.e2e.test.ts: cleanup exits use Ctrl+D (Ctrl+C now interrupts). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
✅ VerificationFull suite + typecheck + lint (clean)
Ctrl+C tests (7)Behavior confirmed
FlakinessThe timing-based e2e ( Regressions handledExisting steering e2e tests used a single Ctrl+C to exit at cleanup; switched those to Ctrl+D (immediate exit, unchanged by this PR) and updated the one stale |
|
✅ PR OK — verified end-to-end (posted as comment; GitHub blocks self-approval). Closes #71: Ctrl+C is now two-stage — interrupt first, exit only on a second press — exactly the requested behavior. Verified the wiring on both sides:
Verification: build ✓ · typecheck ✓ · lint ✓ · full suite 719/719. New tests cover interrupt-first / second-exits / window-disarm (fake timers) / typing-between-presses / Ctrl+D-immediate / modal-immediate, plus an e2e proving the first Ctrl+C aborts the run and stays in the conversation while the second exits. Re-ran the timing-based e2e 3× → 16/16 each; the two stale assertions ( Correct, conventional, well-tested. LGTM. |
Closes #71.
Problem
Ctrl+C exited the REPL immediately. When the LLM is mid-generation you could interrupt with Esc, but Ctrl+C — the reflex for "stop" — quit the whole conversation.
Fix
Make Ctrl+C two-stage (matching Node/Python/Claude Code conventions):
interrupt(the interactive REPL aborts the in-flight run; a no-op when idle) and arm a short window, showing(press Ctrl+C again to exit).exit.Implementation is entirely in
input.ts(the textarea owns the keyboard). The disarm timer isunref()'d and cleared on close.Note on rebase
Rebased onto latest
main— it now includes the merged bracketed-paste change (#60/#70). Both features coexist inonKeypress(paste capture runs first, then the Ctrl+C handler).Tests
textarea-ctrl-c.test.ts(unit, 6): interrupt-first / second-exits / window-disarm (fake timers) / typing-between-presses / Ctrl+D immediate / modal-capture immediate-exit.ctrl-c-interrupt.e2e.test.ts(integration, real REPL + agent loop): first Ctrl+C aborts the run and stays in the conversation (run-mode promise unresolved); second Ctrl+C exits.textarea-interrupt.test.ts: updated the stale "ctrl+c exits" assertion to the new contract.steering.e2e.test.ts: cleanup exits switched to Ctrl+D (since Ctrl+C now interrupts).Verification
🤖 Generated with Claude Code