packages/cli/src/ui/hooks/useReactToolScheduler.ts (280 lines of code) (raw):

/** * @license * Copyright 2025 Google LLC * SPDX-License-Identifier: Apache-2.0 */ import { Config, ToolCallRequestInfo, ExecutingToolCall, ScheduledToolCall, ValidatingToolCall, WaitingToolCall, CompletedToolCall, CancelledToolCall, CoreToolScheduler, OutputUpdateHandler, AllToolCallsCompleteHandler, ToolCallsUpdateHandler, Tool, ToolCall, Status as CoreStatus, EditorType, } from '@google/gemini-cli-core'; import { useCallback, useState, useMemo } from 'react'; import { HistoryItemToolGroup, IndividualToolCallDisplay, ToolCallStatus, HistoryItemWithoutId, } from '../types.js'; export type ScheduleFn = ( request: ToolCallRequestInfo | ToolCallRequestInfo[], signal: AbortSignal, ) => void; export type MarkToolsAsSubmittedFn = (callIds: string[]) => void; export type TrackedScheduledToolCall = ScheduledToolCall & { responseSubmittedToGemini?: boolean; }; export type TrackedValidatingToolCall = ValidatingToolCall & { responseSubmittedToGemini?: boolean; }; export type TrackedWaitingToolCall = WaitingToolCall & { responseSubmittedToGemini?: boolean; }; export type TrackedExecutingToolCall = ExecutingToolCall & { responseSubmittedToGemini?: boolean; }; export type TrackedCompletedToolCall = CompletedToolCall & { responseSubmittedToGemini?: boolean; }; export type TrackedCancelledToolCall = CancelledToolCall & { responseSubmittedToGemini?: boolean; }; export type TrackedToolCall = | TrackedScheduledToolCall | TrackedValidatingToolCall | TrackedWaitingToolCall | TrackedExecutingToolCall | TrackedCompletedToolCall | TrackedCancelledToolCall; export function useReactToolScheduler( onComplete: (tools: CompletedToolCall[]) => void, config: Config, setPendingHistoryItem: React.Dispatch< React.SetStateAction<HistoryItemWithoutId | null> >, getPreferredEditor: () => EditorType | undefined, ): [TrackedToolCall[], ScheduleFn, MarkToolsAsSubmittedFn] { const [toolCallsForDisplay, setToolCallsForDisplay] = useState< TrackedToolCall[] >([]); const outputUpdateHandler: OutputUpdateHandler = useCallback( (toolCallId, outputChunk) => { setPendingHistoryItem((prevItem) => { if (prevItem?.type === 'tool_group') { return { ...prevItem, tools: prevItem.tools.map((toolDisplay) => toolDisplay.callId === toolCallId && toolDisplay.status === ToolCallStatus.Executing ? { ...toolDisplay, resultDisplay: outputChunk } : toolDisplay, ), }; } return prevItem; }); setToolCallsForDisplay((prevCalls) => prevCalls.map((tc) => { if (tc.request.callId === toolCallId && tc.status === 'executing') { const executingTc = tc as TrackedExecutingToolCall; return { ...executingTc, liveOutput: outputChunk }; } return tc; }), ); }, [setPendingHistoryItem], ); const allToolCallsCompleteHandler: AllToolCallsCompleteHandler = useCallback( (completedToolCalls) => { onComplete(completedToolCalls); }, [onComplete], ); const toolCallsUpdateHandler: ToolCallsUpdateHandler = useCallback( (updatedCoreToolCalls: ToolCall[]) => { setToolCallsForDisplay((prevTrackedCalls) => updatedCoreToolCalls.map((coreTc) => { const existingTrackedCall = prevTrackedCalls.find( (ptc) => ptc.request.callId === coreTc.request.callId, ); const newTrackedCall: TrackedToolCall = { ...coreTc, responseSubmittedToGemini: existingTrackedCall?.responseSubmittedToGemini ?? false, } as TrackedToolCall; return newTrackedCall; }), ); }, [setToolCallsForDisplay], ); const scheduler = useMemo( () => new CoreToolScheduler({ toolRegistry: config.getToolRegistry(), outputUpdateHandler, onAllToolCallsComplete: allToolCallsCompleteHandler, onToolCallsUpdate: toolCallsUpdateHandler, approvalMode: config.getApprovalMode(), getPreferredEditor, config, }), [ config, outputUpdateHandler, allToolCallsCompleteHandler, toolCallsUpdateHandler, getPreferredEditor, ], ); const schedule: ScheduleFn = useCallback( ( request: ToolCallRequestInfo | ToolCallRequestInfo[], signal: AbortSignal, ) => { scheduler.schedule(request, signal); }, [scheduler], ); const markToolsAsSubmitted: MarkToolsAsSubmittedFn = useCallback( (callIdsToMark: string[]) => { setToolCallsForDisplay((prevCalls) => prevCalls.map((tc) => callIdsToMark.includes(tc.request.callId) ? { ...tc, responseSubmittedToGemini: true } : tc, ), ); }, [], ); return [toolCallsForDisplay, schedule, markToolsAsSubmitted]; } /** * Maps a CoreToolScheduler status to the UI's ToolCallStatus enum. */ function mapCoreStatusToDisplayStatus(coreStatus: CoreStatus): ToolCallStatus { switch (coreStatus) { case 'validating': return ToolCallStatus.Executing; case 'awaiting_approval': return ToolCallStatus.Confirming; case 'executing': return ToolCallStatus.Executing; case 'success': return ToolCallStatus.Success; case 'cancelled': return ToolCallStatus.Canceled; case 'error': return ToolCallStatus.Error; case 'scheduled': return ToolCallStatus.Pending; default: { const exhaustiveCheck: never = coreStatus; console.warn(`Unknown core status encountered: ${exhaustiveCheck}`); return ToolCallStatus.Error; } } } /** * Transforms `TrackedToolCall` objects into `HistoryItemToolGroup` objects for UI display. */ export function mapToDisplay( toolOrTools: TrackedToolCall[] | TrackedToolCall, ): HistoryItemToolGroup { const toolCalls = Array.isArray(toolOrTools) ? toolOrTools : [toolOrTools]; const toolDisplays = toolCalls.map( (trackedCall): IndividualToolCallDisplay => { let displayName = trackedCall.request.name; let description = ''; let renderOutputAsMarkdown = false; const currentToolInstance = 'tool' in trackedCall && trackedCall.tool ? (trackedCall as { tool: Tool }).tool : undefined; if (currentToolInstance) { displayName = currentToolInstance.displayName; description = currentToolInstance.getDescription( trackedCall.request.args, ); renderOutputAsMarkdown = currentToolInstance.isOutputMarkdown; } else if ('request' in trackedCall && 'args' in trackedCall.request) { description = JSON.stringify(trackedCall.request.args); } const baseDisplayProperties: Omit< IndividualToolCallDisplay, 'status' | 'resultDisplay' | 'confirmationDetails' > = { callId: trackedCall.request.callId, name: displayName, description, renderOutputAsMarkdown, }; switch (trackedCall.status) { case 'success': return { ...baseDisplayProperties, status: mapCoreStatusToDisplayStatus(trackedCall.status), resultDisplay: trackedCall.response.resultDisplay, confirmationDetails: undefined, }; case 'error': return { ...baseDisplayProperties, name: currentToolInstance?.displayName ?? trackedCall.request.name, status: mapCoreStatusToDisplayStatus(trackedCall.status), resultDisplay: trackedCall.response.resultDisplay, confirmationDetails: undefined, }; case 'cancelled': return { ...baseDisplayProperties, status: mapCoreStatusToDisplayStatus(trackedCall.status), resultDisplay: trackedCall.response.resultDisplay, confirmationDetails: undefined, }; case 'awaiting_approval': return { ...baseDisplayProperties, status: mapCoreStatusToDisplayStatus(trackedCall.status), resultDisplay: undefined, confirmationDetails: trackedCall.confirmationDetails, }; case 'executing': return { ...baseDisplayProperties, status: mapCoreStatusToDisplayStatus(trackedCall.status), resultDisplay: (trackedCall as TrackedExecutingToolCall).liveOutput ?? undefined, confirmationDetails: undefined, }; case 'validating': // Fallthrough case 'scheduled': return { ...baseDisplayProperties, status: mapCoreStatusToDisplayStatus(trackedCall.status), resultDisplay: undefined, confirmationDetails: undefined, }; default: { const exhaustiveCheck: never = trackedCall; return { callId: (exhaustiveCheck as TrackedToolCall).request.callId, name: 'Unknown Tool', description: 'Encountered an unknown tool call state.', status: ToolCallStatus.Error, resultDisplay: 'Unknown tool call state', confirmationDetails: undefined, renderOutputAsMarkdown: false, }; } } }, ); return { type: 'tool_group', tools: toolDisplays, }; }