From 2d1a6af890da1e9437cd1a1774e2c7fc7ad32957 Mon Sep 17 00:00:00 2001 From: JAYADITYA <96861162+JayadityaGit@users.noreply.github.com> Date: Tue, 12 Aug 2025 09:43:57 +0530 Subject: [PATCH] feat(cli): support single Ctrl+C to cancel streaming, preserving double Ctrl+C to exit (#5838) --- packages/cli/src/ui/App.tsx | 5 ++ .../messages/ToolConfirmationMessage.tsx | 4 +- packages/cli/src/ui/hooks/useGeminiStream.ts | 52 ++++++++++++------- 3 files changed, 40 insertions(+), 21 deletions(-) diff --git a/packages/cli/src/ui/App.tsx b/packages/cli/src/ui/App.tsx index c4f67294..e952d6b2 100644 --- a/packages/cli/src/ui/App.tsx +++ b/packages/cli/src/ui/App.tsx @@ -545,6 +545,7 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => { initError, pendingHistoryItems: pendingGeminiHistoryItems, thought, + cancelOngoingRequest, } = useGeminiStream( config.getGeminiClient(), history, @@ -655,6 +656,9 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => { if (isAuthenticating) { return; } + if (!ctrlCPressedOnce) { + cancelOngoingRequest?.(); + } handleExit(ctrlCPressedOnce, setCtrlCPressedOnce, ctrlCTimerRef); } else if (keyMatchers[Command.EXIT](key)) { if (buffer.text.length > 0) { @@ -686,6 +690,7 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => { ctrlDTimerRef, handleSlashCommand, isAuthenticating, + cancelOngoingRequest, ], ); diff --git a/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx b/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx index fcdb743f..88b25b86 100644 --- a/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx +++ b/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx @@ -56,9 +56,9 @@ export const ToolConfirmationMessage: React.FC< onConfirm(outcome); }; - useInput((_, key) => { + useInput((input, key) => { if (!isFocused) return; - if (key.escape) { + if (key.escape || (key.ctrl && (input === 'c' || input === 'C'))) { handleConfirm(ToolConfirmationOutcome.Cancel); } }); diff --git a/packages/cli/src/ui/hooks/useGeminiStream.ts b/packages/cli/src/ui/hooks/useGeminiStream.ts index 58bec431..6385d267 100644 --- a/packages/cli/src/ui/hooks/useGeminiStream.ts +++ b/packages/cli/src/ui/hooks/useGeminiStream.ts @@ -183,26 +183,39 @@ export const useGeminiStream = ( return StreamingState.Idle; }, [isResponding, toolCalls]); + const cancelOngoingRequest = useCallback(() => { + if (streamingState !== StreamingState.Responding) { + return; + } + if (turnCancelledRef.current) { + return; + } + turnCancelledRef.current = true; + abortControllerRef.current?.abort(); + if (pendingHistoryItemRef.current) { + addItem(pendingHistoryItemRef.current, Date.now()); + } + addItem( + { + type: MessageType.INFO, + text: 'Request cancelled.', + }, + Date.now(), + ); + setPendingHistoryItem(null); + onCancelSubmit(); + setIsResponding(false); + }, [ + streamingState, + addItem, + setPendingHistoryItem, + onCancelSubmit, + pendingHistoryItemRef, + ]); + useInput((_input, key) => { - if (streamingState === StreamingState.Responding && key.escape) { - if (turnCancelledRef.current) { - return; - } - turnCancelledRef.current = true; - abortControllerRef.current?.abort(); - if (pendingHistoryItemRef.current) { - addItem(pendingHistoryItemRef.current, Date.now()); - } - addItem( - { - type: MessageType.INFO, - text: 'Request cancelled.', - }, - Date.now(), - ); - setPendingHistoryItem(null); - onCancelSubmit(); - setIsResponding(false); + if (key.escape) { + cancelOngoingRequest(); } }); @@ -954,5 +967,6 @@ export const useGeminiStream = ( initError, pendingHistoryItems, thought, + cancelOngoingRequest, }; };