From 01022d737abd891bc87a4b899e79c4b2684f8903 Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Thu, 30 Jul 2026 04:26:48 -0500 Subject: [PATCH] feat(tui): double ESC discards the draft, even mid-stream MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirrors the CLI binding. Placed above the isBlocked early-return so it works while the agent streams — that is the whole gap, since Ctrl+C interrupts a running turn and only clears the composer when idle. Pushes the draft to history before clearing so Up recalls it. --- ui-tui/src/app/useInputHandlers.ts | 26 +++++++++++++++++++++++++- ui-tui/src/config/timing.ts | 6 ++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/ui-tui/src/app/useInputHandlers.ts b/ui-tui/src/app/useInputHandlers.ts index f9461919915..9624c6ec87d 100644 --- a/ui-tui/src/app/useInputHandlers.ts +++ b/ui-tui/src/app/useInputHandlers.ts @@ -3,7 +3,7 @@ import { useStore } from '@nanostores/react' import { useEffect, useRef } from 'react' import { DASHBOARD_TUI_MODE } from '../config/env.js' -import { TYPING_IDLE_MS } from '../config/timing.js' +import { DOUBLE_ESC_MS, TYPING_IDLE_MS } from '../config/timing.js' import { applyCompletion } from '../domain/slash.js' import type { ApprovalRespondResponse, @@ -320,9 +320,33 @@ export function useInputHandlers(ctx: InputHandlerContext): InputHandlerResult { }) } + // Double-Esc discards the draft, matching Claude Code / Gemini CLI. It + // sits above the isBlocked early-return on purpose: Ctrl+C interrupts a + // running turn rather than clearing, so while the agent streams there is + // otherwise no way to throw away a half-typed prompt. The draft is pushed + // to history first so Up recalls it. + const lastEscRef = useRef(0) + useInput((ch, key) => { const live = getUiState() + if (key.escape) { + const now = Date.now() + const isDouble = now - lastEscRef.current <= DOUBLE_ESC_MS + + lastEscRef.current = isDouble ? 0 : now + + if (isDouble && (cState.input || cState.inputBuf.length)) { + if (cState.input.trim()) { + cActions.pushHistory(cState.input) + } + + cActions.clearIn() + + return + } + } + if (isBlocked) { // When approval/clarify/confirm overlays are active, their own useInput // handlers must receive keystrokes (arrow keys, numbers, Enter). Only diff --git a/ui-tui/src/config/timing.ts b/ui-tui/src/config/timing.ts index 9a18796e168..abecd3e87e9 100644 --- a/ui-tui/src/config/timing.ts +++ b/ui-tui/src/config/timing.ts @@ -12,3 +12,9 @@ export const REASONING_PULSE_MS = 700 // responsive enough to track the drag, cheap enough to stay smooth, and the // trailing edge always lands the final width so the settled layout is exact. export const RESIZE_COALESCE_MS = 32 + +// Two Esc presses within this window discard the draft (Claude Code / +// Gemini CLI parity). Long enough for a deliberate double-tap, short +// enough that two unrelated Escs — dismissing a completion, then a +// selection — don't silently clear the composer. +export const DOUBLE_ESC_MS = 500