mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-14 05:42:54 -07:00
@@ -1127,18 +1127,21 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
|||||||
}
|
}
|
||||||
}, [config, historyManager]);
|
}, [config, historyManager]);
|
||||||
|
|
||||||
const cancelHandlerRef = useRef<(shouldRestorePrompt?: boolean) => void>(
|
const cancelHandlerRef = useRef<
|
||||||
() => {},
|
(shouldRestorePrompt?: boolean, clearBuffer?: boolean) => void
|
||||||
);
|
>(() => {});
|
||||||
|
|
||||||
const onCancelSubmit = useCallback((shouldRestorePrompt?: boolean) => {
|
const onCancelSubmit = useCallback(
|
||||||
if (shouldRestorePrompt) {
|
(shouldRestorePrompt?: boolean, clearBuffer: boolean = false) => {
|
||||||
setPendingRestorePrompt(true);
|
if (shouldRestorePrompt) {
|
||||||
} else {
|
setPendingRestorePrompt(true);
|
||||||
setPendingRestorePrompt(false);
|
} else {
|
||||||
cancelHandlerRef.current(false);
|
setPendingRestorePrompt(false);
|
||||||
}
|
cancelHandlerRef.current(false, clearBuffer);
|
||||||
}, []);
|
}
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (pendingRestorePrompt) {
|
if (pendingRestorePrompt) {
|
||||||
@@ -1321,18 +1324,18 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
|||||||
});
|
});
|
||||||
|
|
||||||
cancelHandlerRef.current = useCallback(
|
cancelHandlerRef.current = useCallback(
|
||||||
(shouldRestorePrompt: boolean = true) => {
|
(shouldRestorePrompt: boolean = true, clearBuffer: boolean = false) => {
|
||||||
if (isToolAwaitingConfirmation(pendingHistoryItems)) {
|
if (!clearBuffer && isToolAwaitingConfirmation(pendingHistoryItems)) {
|
||||||
return; // Don't clear - user may be composing a follow-up message
|
return; // Don't clear - user may be composing a follow-up message
|
||||||
}
|
}
|
||||||
if (isToolExecuting(pendingHistoryItems)) {
|
|
||||||
buffer.setText(''); // Clear for Ctrl+C cancellation
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If cancelling (shouldRestorePrompt=false), never modify the buffer
|
// If cancelling (shouldRestorePrompt=false):
|
||||||
// User is in control - preserve whatever text they typed, pasted, or restored
|
|
||||||
if (!shouldRestorePrompt) {
|
if (!shouldRestorePrompt) {
|
||||||
|
// Clear the buffer if explicitly requested (e.g., Ctrl+C)
|
||||||
|
if (clearBuffer) {
|
||||||
|
buffer.setText('');
|
||||||
|
}
|
||||||
|
// Otherwise (e.g., Escape), user is in control - preserve whatever text they typed
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -202,6 +202,6 @@ describe('useAgentStream', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(mockLegacyAgentProtocol.abort).toHaveBeenCalled();
|
expect(mockLegacyAgentProtocol.abort).toHaveBeenCalled();
|
||||||
expect(mockOnCancelSubmit).toHaveBeenCalledWith(false);
|
expect(mockOnCancelSubmit).toHaveBeenCalledWith(false, true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -36,11 +36,15 @@ import type { UseHistoryManagerReturn } from './useHistoryManager.js';
|
|||||||
import { useSessionStats } from '../contexts/SessionContext.js';
|
import { useSessionStats } from '../contexts/SessionContext.js';
|
||||||
import { useStateAndRef } from './useStateAndRef.js';
|
import { useStateAndRef } from './useStateAndRef.js';
|
||||||
import { type MinimalTrackedToolCall } from './useTurnActivityMonitor.js';
|
import { type MinimalTrackedToolCall } from './useTurnActivityMonitor.js';
|
||||||
|
import { useKeypress } from './useKeypress.js';
|
||||||
|
|
||||||
export interface UseAgentStreamOptions {
|
export interface UseAgentStreamOptions {
|
||||||
agent?: AgentProtocol;
|
agent?: AgentProtocol;
|
||||||
addItem: UseHistoryManagerReturn['addItem'];
|
addItem: UseHistoryManagerReturn['addItem'];
|
||||||
onCancelSubmit: (shouldRestorePrompt?: boolean) => void;
|
onCancelSubmit: (
|
||||||
|
shouldRestorePrompt?: boolean,
|
||||||
|
clearBuffer?: boolean,
|
||||||
|
) => void;
|
||||||
isShellFocused?: boolean;
|
isShellFocused?: boolean;
|
||||||
logger?: Logger | null;
|
logger?: Logger | null;
|
||||||
}
|
}
|
||||||
@@ -120,13 +124,16 @@ export const useAgentStream = ({
|
|||||||
}
|
}
|
||||||
}, [addItem, pendingHistoryItemRef, setPendingHistoryItem]);
|
}, [addItem, pendingHistoryItemRef, setPendingHistoryItem]);
|
||||||
|
|
||||||
const cancelOngoingRequest = useCallback(async () => {
|
const cancelOngoingRequest = useCallback(
|
||||||
if (agent) {
|
async (clearBuffer: boolean = true) => {
|
||||||
await agent.abort();
|
if (agent) {
|
||||||
setStreamingState(StreamingState.Idle);
|
await agent.abort();
|
||||||
onCancelSubmit(false);
|
setStreamingState(StreamingState.Idle);
|
||||||
}
|
onCancelSubmit(false, clearBuffer);
|
||||||
}, [agent, onCancelSubmit]);
|
}
|
||||||
|
},
|
||||||
|
[agent, onCancelSubmit],
|
||||||
|
);
|
||||||
|
|
||||||
// TODO: Support native handleApprovalModeChange for Plan Mode
|
// TODO: Support native handleApprovalModeChange for Plan Mode
|
||||||
const handleApprovalModeChange = useCallback(
|
const handleApprovalModeChange = useCallback(
|
||||||
@@ -322,6 +329,21 @@ export const useAgentStream = ({
|
|||||||
return () => unsubscribe?.();
|
return () => unsubscribe?.();
|
||||||
}, [agent, handleEvent]);
|
}, [agent, handleEvent]);
|
||||||
|
|
||||||
|
useKeypress(
|
||||||
|
(key) => {
|
||||||
|
if (key.name === 'escape' && !isShellFocused) {
|
||||||
|
void cancelOngoingRequest(false);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
isActive:
|
||||||
|
streamingState === StreamingState.Responding ||
|
||||||
|
streamingState === StreamingState.WaitingForConfirmation,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const submitQuery = useCallback(
|
const submitQuery = useCallback(
|
||||||
async (
|
async (
|
||||||
query: Part[] | string,
|
query: Part[] | string,
|
||||||
|
|||||||
@@ -1637,7 +1637,7 @@ describe('useGeminiStream', () => {
|
|||||||
|
|
||||||
simulateEscapeKeyPress();
|
simulateEscapeKeyPress();
|
||||||
|
|
||||||
expect(cancelSubmitSpy).toHaveBeenCalledWith(false);
|
expect(cancelSubmitSpy).toHaveBeenCalledWith(false, false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call setShellInputFocused(false) when escape is pressed', async () => {
|
it('should call setShellInputFocused(false) when escape is pressed', async () => {
|
||||||
|
|||||||
@@ -227,7 +227,10 @@ export const useGeminiStream = (
|
|||||||
performMemoryRefresh: () => Promise<void>,
|
performMemoryRefresh: () => Promise<void>,
|
||||||
modelSwitchedFromQuotaError: boolean,
|
modelSwitchedFromQuotaError: boolean,
|
||||||
setModelSwitchedFromQuotaError: React.Dispatch<React.SetStateAction<boolean>>,
|
setModelSwitchedFromQuotaError: React.Dispatch<React.SetStateAction<boolean>>,
|
||||||
onCancelSubmit: (shouldRestorePrompt?: boolean) => void,
|
onCancelSubmit: (
|
||||||
|
shouldRestorePrompt?: boolean,
|
||||||
|
clearBuffer?: boolean,
|
||||||
|
) => void,
|
||||||
setShellInputFocused: (value: boolean) => void,
|
setShellInputFocused: (value: boolean) => void,
|
||||||
terminalWidth: number,
|
terminalWidth: number,
|
||||||
terminalHeight: number,
|
terminalHeight: number,
|
||||||
@@ -803,100 +806,129 @@ export const useGeminiStream = (
|
|||||||
[addItem, config, isLowErrorVerbosity],
|
[addItem, config, isLowErrorVerbosity],
|
||||||
);
|
);
|
||||||
|
|
||||||
const cancelOngoingRequest = useCallback(() => {
|
const cancelOngoingRequest = useCallback(
|
||||||
if (
|
(clearBuffer: boolean = true) => {
|
||||||
streamingState !== StreamingState.Responding &&
|
// If we are already cancelled, do nothing
|
||||||
streamingState !== StreamingState.WaitingForConfirmation
|
if (turnCancelledRef.current) {
|
||||||
) {
|
if (clearBuffer) {
|
||||||
return;
|
onCancelSubmit(false, true);
|
||||||
}
|
}
|
||||||
if (turnCancelledRef.current) {
|
return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
turnCancelledRef.current = true;
|
|
||||||
setRetryStatus(null);
|
|
||||||
|
|
||||||
// A full cancellation means no tools have produced a final result yet.
|
|
||||||
// This determines if we show a generic "Request cancelled" message.
|
|
||||||
const isFullCancellation = !toolCalls.some(
|
|
||||||
(tc) => tc.status === 'success' || tc.status === 'error',
|
|
||||||
);
|
|
||||||
|
|
||||||
// Ensure we have an abort controller, creating one if it doesn't exist.
|
|
||||||
if (!abortControllerRef.current) {
|
|
||||||
abortControllerRef.current = new AbortController();
|
|
||||||
}
|
|
||||||
|
|
||||||
// The order is important here.
|
|
||||||
// 1. Fire the signal to interrupt any active async operations.
|
|
||||||
abortControllerRef.current.abort();
|
|
||||||
// 2. Call the imperative cancel to clear the queue of pending tools.
|
|
||||||
cancelAllToolCalls(abortControllerRef.current.signal);
|
|
||||||
|
|
||||||
if (pendingHistoryItemRef.current) {
|
|
||||||
const isShellCommand =
|
|
||||||
pendingHistoryItemRef.current.type === 'tool_group' &&
|
|
||||||
pendingHistoryItemRef.current.tools.some(
|
|
||||||
(t) => t.name === SHELL_COMMAND_NAME,
|
|
||||||
);
|
|
||||||
|
|
||||||
// If it is a shell command, we update the status to Canceled and clear the output
|
|
||||||
// to avoid artifacts, then add it to history immediately.
|
|
||||||
if (isShellCommand) {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
||||||
const toolGroup = pendingHistoryItemRef.current as HistoryItemToolGroup;
|
|
||||||
const updatedTools = toolGroup.tools.map((tool) => {
|
|
||||||
if (tool.name === SHELL_COMMAND_NAME) {
|
|
||||||
return {
|
|
||||||
...tool,
|
|
||||||
status: CoreToolCallStatus.Cancelled,
|
|
||||||
resultDisplay: tool.resultDisplay,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return tool;
|
|
||||||
});
|
|
||||||
addItem({ ...toolGroup, tools: updatedTools } as HistoryItemWithoutId);
|
|
||||||
} else {
|
|
||||||
addItem(pendingHistoryItemRef.current);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
setPendingHistoryItem(null);
|
|
||||||
|
|
||||||
// If it was a full cancellation, add the info message now.
|
const hasActiveTools = toolCalls.some(
|
||||||
// Otherwise, we let handleCompletedTools figure out the next step,
|
(tc) =>
|
||||||
// which might involve sending partial results back to the model.
|
tc.status === CoreToolCallStatus.Executing ||
|
||||||
if (isFullCancellation) {
|
tc.status === CoreToolCallStatus.Scheduled ||
|
||||||
// If shell is active, we delay this message to ensure correct ordering
|
tc.status === CoreToolCallStatus.Validating,
|
||||||
// (Shell item first, then Info message).
|
);
|
||||||
if (!activeShellPtyId) {
|
|
||||||
addItem({
|
// If we are not responding, not waiting for confirmation, and have no active tools,
|
||||||
type: MessageType.INFO,
|
// there is nothing to abort.
|
||||||
text: 'Request cancelled.',
|
if (
|
||||||
});
|
streamingState === StreamingState.Idle &&
|
||||||
setIsResponding(false);
|
!isRespondingRef.current &&
|
||||||
|
!hasActiveTools
|
||||||
|
) {
|
||||||
|
// Even if we are "idle", if we are called with clearBuffer=true (Ctrl+C),
|
||||||
|
// we still want to clear the buffer.
|
||||||
|
if (clearBuffer) {
|
||||||
|
onCancelSubmit(false, true);
|
||||||
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
onCancelSubmit(false);
|
turnCancelledRef.current = true;
|
||||||
setShellInputFocused(false);
|
setRetryStatus(null);
|
||||||
}, [
|
|
||||||
streamingState,
|
// A full cancellation means no tools have produced a final result yet.
|
||||||
addItem,
|
// This determines if we show a generic "Request cancelled" message.
|
||||||
setPendingHistoryItem,
|
const isFullCancellation = !toolCalls.some(
|
||||||
onCancelSubmit,
|
(tc) => tc.status === 'success' || tc.status === 'error',
|
||||||
pendingHistoryItemRef,
|
);
|
||||||
setShellInputFocused,
|
|
||||||
cancelAllToolCalls,
|
// Ensure we have an abort controller, creating one if it doesn't exist.
|
||||||
toolCalls,
|
if (!abortControllerRef.current) {
|
||||||
activeShellPtyId,
|
abortControllerRef.current = new AbortController();
|
||||||
setIsResponding,
|
}
|
||||||
]);
|
|
||||||
|
// The order is important here.
|
||||||
|
// 1. Fire the signal to interrupt any active async operations.
|
||||||
|
abortControllerRef.current.abort();
|
||||||
|
// 2. Call the imperative cancel to clear the queue of pending tools.
|
||||||
|
cancelAllToolCalls(abortControllerRef.current.signal);
|
||||||
|
|
||||||
|
if (pendingHistoryItemRef.current) {
|
||||||
|
// If it is a shell command, we update the status to Canceled and clear the output
|
||||||
|
// to avoid artifacts, then add it to history immediately.
|
||||||
|
if (
|
||||||
|
pendingHistoryItemRef.current.type === 'tool_group' &&
|
||||||
|
pendingHistoryItemRef.current.tools.some(
|
||||||
|
(t) => t.name === SHELL_COMMAND_NAME,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
const toolGroup = pendingHistoryItemRef.current;
|
||||||
|
const updatedTools = toolGroup.tools.map((tool) => {
|
||||||
|
if (tool.name === SHELL_COMMAND_NAME) {
|
||||||
|
return {
|
||||||
|
...tool,
|
||||||
|
status: CoreToolCallStatus.Cancelled,
|
||||||
|
resultDisplay: tool.resultDisplay,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return tool;
|
||||||
|
});
|
||||||
|
const newToolGroup: HistoryItemToolGroup = {
|
||||||
|
...toolGroup,
|
||||||
|
tools: updatedTools,
|
||||||
|
};
|
||||||
|
addItem(newToolGroup);
|
||||||
|
} else {
|
||||||
|
addItem(pendingHistoryItemRef.current);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setPendingHistoryItem(null);
|
||||||
|
|
||||||
|
// If it was a full cancellation, add the info message now.
|
||||||
|
// Otherwise, we let handleCompletedTools figure out the next step,
|
||||||
|
// which might involve sending partial results back to the model.
|
||||||
|
if (isFullCancellation) {
|
||||||
|
// If shell is active, we delay this message to ensure correct ordering
|
||||||
|
// (Shell item first, then Info message).
|
||||||
|
if (!activeShellPtyId) {
|
||||||
|
addItem({
|
||||||
|
type: MessageType.INFO,
|
||||||
|
text: 'Request cancelled.',
|
||||||
|
});
|
||||||
|
setIsResponding(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onCancelSubmit(false, clearBuffer);
|
||||||
|
setShellInputFocused(false);
|
||||||
|
},
|
||||||
|
[
|
||||||
|
streamingState,
|
||||||
|
addItem,
|
||||||
|
setPendingHistoryItem,
|
||||||
|
onCancelSubmit,
|
||||||
|
pendingHistoryItemRef,
|
||||||
|
isRespondingRef,
|
||||||
|
setShellInputFocused,
|
||||||
|
cancelAllToolCalls,
|
||||||
|
toolCalls,
|
||||||
|
activeShellPtyId,
|
||||||
|
setIsResponding,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
useKeypress(
|
useKeypress(
|
||||||
(key) => {
|
(key) => {
|
||||||
if (key.name === 'escape' && !isShellFocused) {
|
if (key.name === 'escape' && !isShellFocused) {
|
||||||
cancelOngoingRequest();
|
cancelOngoingRequest(false);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isActive:
|
isActive:
|
||||||
|
|||||||
Reference in New Issue
Block a user