11import { Box , Text , useApp , useInput } from "ink" ;
2- import { useEffect , useMemo , useReducer , useState } from "react" ;
2+ import { useEffect , useMemo , useReducer , useRef , useState } from "react" ;
33import { type AgentBundle , createAgent } from "../agent/agent.js" ;
44import { ConfigError } from "../agent/config.js" ;
55import { initialState , reducer } from "../agent/events.js" ;
@@ -15,7 +15,7 @@ import { buildAttachmentPrompt, collectAttachments } from "./attachments.js";
1515import { CompactionBanner } from "./CompactionBanner.js" ;
1616import { FirstRunSetup } from "./FirstRunSetup.js" ;
1717import { HistoryStore } from "./history-store.js" ;
18- import { Input } from "./Input.js" ;
18+ import { Input , type InputHandle } from "./Input.js" ;
1919import { MessageList } from "./MessageList.js" ;
2020import { Permission } from "./Permission.js" ;
2121import { Status } from "./Status.js" ;
@@ -97,6 +97,7 @@ function ChatApp({ bundle, onExit }: ChatAppProps) {
9797 return next . length > 50 ? next . slice ( next . length - 50 ) : next ;
9898 } ) ;
9999 const [ tasks , setTasks ] = useState < readonly Task [ ] > ( ( ) => bundle . toolContext . tasks . list ( ) ) ;
100+ const inputRef = useRef < InputHandle | null > ( null ) ;
100101
101102 const registry = useMemo ( ( ) => {
102103 const reg = new CommandRegistry ( ) ;
@@ -220,19 +221,21 @@ function ChatApp({ bundle, onExit }: ChatAppProps) {
220221 } ) ;
221222 } ;
222223
223- // Ctrl-C semantics:
224- // • Any open overlay (Permission, UserQuery) gets dismissed first
225- // so the user is never trapped behind a prompt with no escape.
226- // • While the agent is busy: abort the turn. Stays in the app.
227- // • A second Ctrl-C within DOUBLE_TAP_MS exits, regardless of
228- // whether the previous press aborted or just landed a hint.
229- // "Twice real fast" is universally understood as "I want out."
230- // • While idle at the prompt: first press posts a hint + arms the
231- // exit window so the second tap exits.
224+ // Ctrl-C semantics, in priority order:
225+ // 1. Open overlay (Permission, UserQuery) → dismiss it. Never trap
226+ // the user behind a prompt with no escape.
227+ // 2. Agent busy → abort the turn. Stay in the app.
228+ // 3. Idle, input has typed text → wipe the input. Lets the user
229+ // back out of a half-written prompt without having to backspace.
230+ // 4. Idle, input empty → post a "Press Ctrl-C again to exit" hint
231+ // and arm the double-tap exit window.
232+ //
233+ // At any state, a second Ctrl-C within DOUBLE_TAP_MS exits cleanly —
234+ // "twice real fast" is universally understood as "I want out."
232235 //
233236 // 1000ms is "real fast" — wide enough that intentional double-taps
234237 // register, tight enough that mashing Ctrl-C twice while flustered
235- // doesn't accidentally exit. Tunable here if testers want it longer.
238+ // doesn't accidentally exit.
236239 const exitTimerRef = useMemo ( ( ) => ( { deadline : 0 } ) , [ ] ) ;
237240 const handleAbort = ( ) => {
238241 const DOUBLE_TAP_MS = 1000 ;
@@ -243,11 +246,7 @@ function ChatApp({ bundle, onExit }: ChatAppProps) {
243246 }
244247 exitTimerRef . deadline = now + DOUBLE_TAP_MS ;
245248
246- // Overlays first: dismissing them brings the user back to a usable
247- // input row without exiting the agent loop. Permission denies the
248- // pending request (and any tool waiting on it surfaces the denial
249- // to the model as a normal tool error); UserQuery cancels the
250- // pending question.
249+ // Overlays first.
251250 if ( permRequest ) {
252251 bundle . permissions . respond ( permRequest . id , "deny" ) ;
253252 if ( busy ) {
@@ -273,6 +272,11 @@ function ChatApp({ bundle, onExit }: ChatAppProps) {
273272 // gets the user out without confirmation theater.
274273 return ;
275274 }
275+
276+ // Idle with typed input → clear it, don't fall through to the
277+ // exit-hint branch. The user is bailing on a prompt, not the app.
278+ if ( inputRef . current ?. clearIfHasText ( ) ) return ;
279+
276280 appendStatus ( "Press Ctrl-C again to exit." ) ;
277281 } ;
278282
@@ -328,6 +332,7 @@ function ChatApp({ bundle, onExit }: ChatAppProps) {
328332 />
329333 ) : (
330334 < Input
335+ ref = { inputRef }
331336 disabled = { busy }
332337 onSubmit = { handleSubmit }
333338 onAbort = { handleAbort }
0 commit comments