From 931ae76df170b0a9429cd77b3f58a3aebdb42cc9 Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Mon, 16 Mar 2026 08:12:51 -0500 Subject: [PATCH] fix(cli): Ctrl+C clears input, second Ctrl+C exits TUI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit InputBar is now a controlled component — input state lives in app.tsx. Ctrl+C when input has text clears it; Ctrl+C with empty input exits, giving the standard two-press-to-quit behaviour expected in TUIs. Co-Authored-By: Claude Sonnet 4.6 --- packages/cli/src/tui/app.tsx | 13 +++++++- packages/cli/src/tui/components/input-bar.tsx | 33 ++++++++++++------- 2 files changed, 33 insertions(+), 13 deletions(-) diff --git a/packages/cli/src/tui/app.tsx b/packages/cli/src/tui/app.tsx index 21c0803..247b30e 100644 --- a/packages/cli/src/tui/app.tsx +++ b/packages/cli/src/tui/app.tsx @@ -73,6 +73,9 @@ export function TuiApp({ const [sidebarSelectedIndex, setSidebarSelectedIndex] = useState(0); + // Controlled input state — held here so Ctrl+C can clear it + const [tuiInput, setTuiInput] = useState(''); + const handleLocalCommand = useCallback( (parsed: ParsedCommand) => { switch (parsed.command) { @@ -153,8 +156,14 @@ export function TuiApp({ ); useInput((ch, key) => { + // Ctrl+C: clear input if non-empty; exit if already empty (second press) if (key.ctrl && ch === 'c') { - exit(); + if (tuiInput) { + setTuiInput(''); + } else { + exit(); + } + return; } // Ctrl+L: toggle sidebar (refresh on open) if (key.ctrl && ch === 'l') { @@ -260,6 +269,8 @@ export function TuiApp({ )} void; onSubmit: (value: string) => void; onSystemMessage?: (message: string) => void; onLocalCommand?: (parsed: ParsedCommand) => void; @@ -18,6 +22,8 @@ export interface InputBarProps { } export function InputBar({ + value: input, + onChange: setInput, onSubmit, onSystemMessage, onLocalCommand, @@ -27,7 +33,6 @@ export function InputBar({ placeholder: placeholderOverride, allCommands, }: InputBarProps) { - const [input, setInput] = useState(''); const [showAutocomplete, setShowAutocomplete] = useState(false); const [autocompleteIndex, setAutocompleteIndex] = useState(0); @@ -36,15 +41,18 @@ export function InputBar({ // Determine which commands to show in autocomplete const availableCommands = allCommands ?? commandRegistry.getAll(); - const handleChange = useCallback((value: string) => { - setInput(value); - if (value.startsWith('/')) { - setShowAutocomplete(true); - setAutocompleteIndex(0); - } else { - setShowAutocomplete(false); - } - }, []); + const handleChange = useCallback( + (value: string) => { + setInput(value); + if (value.startsWith('/')) { + setShowAutocomplete(true); + setAutocompleteIndex(0); + } else { + setShowAutocomplete(false); + } + }, + [setInput], + ); const handleSubmit = useCallback( (value: string) => { @@ -92,6 +100,7 @@ export function InputBar({ isStreaming, connected, addToHistory, + setInput, ], ); @@ -116,7 +125,7 @@ export function InputBar({ return true; } return false; - }, [showAutocomplete, input, availableCommands, autocompleteIndex]); + }, [showAutocomplete, input, availableCommands, autocompleteIndex, setInput]); useInput((ch, key) => { // Escape: hide autocomplete