mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-30 23:14:32 -07:00
feat(core, cli): Implement sequential approval. (#11593)
This commit is contained in:
@@ -111,6 +111,7 @@ export const useGeminiStream = (
|
||||
const [initError, setInitError] = useState<string | null>(null);
|
||||
const abortControllerRef = useRef<AbortController | null>(null);
|
||||
const turnCancelledRef = useRef(false);
|
||||
const activeQueryIdRef = useRef<string | null>(null);
|
||||
const [isResponding, setIsResponding] = useState<boolean>(false);
|
||||
const [thought, setThought] = useState<ThoughtSummary | null>(null);
|
||||
const [pendingHistoryItem, pendingHistoryItemRef, setPendingHistoryItem] =
|
||||
@@ -126,47 +127,55 @@ export const useGeminiStream = (
|
||||
return new GitService(config.getProjectRoot(), storage);
|
||||
}, [config, storage]);
|
||||
|
||||
const [toolCalls, scheduleToolCalls, markToolsAsSubmitted] =
|
||||
useReactToolScheduler(
|
||||
async (completedToolCallsFromScheduler) => {
|
||||
// This onComplete is called when ALL scheduled tools for a given batch are done.
|
||||
if (completedToolCallsFromScheduler.length > 0) {
|
||||
// Add the final state of these tools to the history for display.
|
||||
addItem(
|
||||
mapTrackedToolCallsToDisplay(
|
||||
completedToolCallsFromScheduler as TrackedToolCall[],
|
||||
),
|
||||
Date.now(),
|
||||
);
|
||||
|
||||
// Record tool calls with full metadata before sending responses.
|
||||
try {
|
||||
const currentModel =
|
||||
config.getGeminiClient().getCurrentSequenceModel() ??
|
||||
config.getModel();
|
||||
config
|
||||
.getGeminiClient()
|
||||
.getChat()
|
||||
.recordCompletedToolCalls(
|
||||
currentModel,
|
||||
completedToolCallsFromScheduler,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Error recording completed tool call information: ${error}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Handle tool response submission immediately when tools complete
|
||||
await handleCompletedTools(
|
||||
const [
|
||||
toolCalls,
|
||||
scheduleToolCalls,
|
||||
markToolsAsSubmitted,
|
||||
setToolCallsForDisplay,
|
||||
cancelAllToolCalls,
|
||||
] = useReactToolScheduler(
|
||||
async (completedToolCallsFromScheduler) => {
|
||||
// This onComplete is called when ALL scheduled tools for a given batch are done.
|
||||
if (completedToolCallsFromScheduler.length > 0) {
|
||||
// Add the final state of these tools to the history for display.
|
||||
addItem(
|
||||
mapTrackedToolCallsToDisplay(
|
||||
completedToolCallsFromScheduler as TrackedToolCall[],
|
||||
),
|
||||
Date.now(),
|
||||
);
|
||||
|
||||
// Clear the live-updating display now that the final state is in history.
|
||||
setToolCallsForDisplay([]);
|
||||
|
||||
// Record tool calls with full metadata before sending responses.
|
||||
try {
|
||||
const currentModel =
|
||||
config.getGeminiClient().getCurrentSequenceModel() ??
|
||||
config.getModel();
|
||||
config
|
||||
.getGeminiClient()
|
||||
.getChat()
|
||||
.recordCompletedToolCalls(
|
||||
currentModel,
|
||||
completedToolCallsFromScheduler,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Error recording completed tool call information: ${error}`,
|
||||
);
|
||||
}
|
||||
},
|
||||
config,
|
||||
getPreferredEditor,
|
||||
onEditorClose,
|
||||
);
|
||||
|
||||
// Handle tool response submission immediately when tools complete
|
||||
await handleCompletedTools(
|
||||
completedToolCallsFromScheduler as TrackedToolCall[],
|
||||
);
|
||||
}
|
||||
},
|
||||
config,
|
||||
getPreferredEditor,
|
||||
onEditorClose,
|
||||
);
|
||||
|
||||
const pendingToolCallGroupDisplay = useMemo(
|
||||
() =>
|
||||
@@ -265,27 +274,54 @@ export const useGeminiStream = (
|
||||
}, [streamingState, config, history]);
|
||||
|
||||
const cancelOngoingRequest = useCallback(() => {
|
||||
if (streamingState !== StreamingState.Responding) {
|
||||
if (
|
||||
streamingState !== StreamingState.Responding &&
|
||||
streamingState !== StreamingState.WaitingForConfirmation
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (turnCancelledRef.current) {
|
||||
return;
|
||||
}
|
||||
turnCancelledRef.current = true;
|
||||
abortControllerRef.current?.abort();
|
||||
|
||||
// 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) {
|
||||
addItem(pendingHistoryItemRef.current, Date.now());
|
||||
}
|
||||
addItem(
|
||||
{
|
||||
type: MessageType.INFO,
|
||||
text: 'Request cancelled.',
|
||||
},
|
||||
Date.now(),
|
||||
);
|
||||
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) {
|
||||
addItem(
|
||||
{
|
||||
type: MessageType.INFO,
|
||||
text: 'Request cancelled.',
|
||||
},
|
||||
Date.now(),
|
||||
);
|
||||
setIsResponding(false);
|
||||
}
|
||||
|
||||
onCancelSubmit();
|
||||
setIsResponding(false);
|
||||
setShellInputFocused(false);
|
||||
}, [
|
||||
streamingState,
|
||||
@@ -294,6 +330,8 @@ export const useGeminiStream = (
|
||||
onCancelSubmit,
|
||||
pendingHistoryItemRef,
|
||||
setShellInputFocused,
|
||||
cancelAllToolCalls,
|
||||
toolCalls,
|
||||
]);
|
||||
|
||||
useKeypress(
|
||||
@@ -302,7 +340,11 @@ export const useGeminiStream = (
|
||||
cancelOngoingRequest();
|
||||
}
|
||||
},
|
||||
{ isActive: streamingState === StreamingState.Responding },
|
||||
{
|
||||
isActive:
|
||||
streamingState === StreamingState.Responding ||
|
||||
streamingState === StreamingState.WaitingForConfirmation,
|
||||
},
|
||||
);
|
||||
|
||||
const prepareQueryForGemini = useCallback(
|
||||
@@ -764,6 +806,8 @@ export const useGeminiStream = (
|
||||
options?: { isContinuation: boolean },
|
||||
prompt_id?: string,
|
||||
) => {
|
||||
const queryId = `${Date.now()}-${Math.random()}`;
|
||||
activeQueryIdRef.current = queryId;
|
||||
if (
|
||||
(streamingState === StreamingState.Responding ||
|
||||
streamingState === StreamingState.WaitingForConfirmation) &&
|
||||
@@ -901,7 +945,9 @@ export const useGeminiStream = (
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
setIsResponding(false);
|
||||
if (activeQueryIdRef.current === queryId) {
|
||||
setIsResponding(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
@@ -963,10 +1009,6 @@ export const useGeminiStream = (
|
||||
|
||||
const handleCompletedTools = useCallback(
|
||||
async (completedToolCallsFromScheduler: TrackedToolCall[]) => {
|
||||
if (isResponding) {
|
||||
return;
|
||||
}
|
||||
|
||||
const completedAndReadyToSubmitTools =
|
||||
completedToolCallsFromScheduler.filter(
|
||||
(
|
||||
@@ -1028,6 +1070,19 @@ export const useGeminiStream = (
|
||||
);
|
||||
|
||||
if (allToolsCancelled) {
|
||||
// If the turn was cancelled via the imperative escape key flow,
|
||||
// the cancellation message is added there. We check the ref to avoid duplication.
|
||||
if (!turnCancelledRef.current) {
|
||||
addItem(
|
||||
{
|
||||
type: MessageType.INFO,
|
||||
text: 'Request cancelled.',
|
||||
},
|
||||
Date.now(),
|
||||
);
|
||||
}
|
||||
setIsResponding(false);
|
||||
|
||||
if (geminiClient) {
|
||||
// We need to manually add the function responses to the history
|
||||
// so the model knows the tools were cancelled.
|
||||
@@ -1074,12 +1129,12 @@ export const useGeminiStream = (
|
||||
);
|
||||
},
|
||||
[
|
||||
isResponding,
|
||||
submitQuery,
|
||||
markToolsAsSubmitted,
|
||||
geminiClient,
|
||||
performMemoryRefresh,
|
||||
modelSwitchedFromQuotaError,
|
||||
addItem,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user