diff options
Diffstat (limited to 'packages/cli/src/ui')
| -rw-r--r-- | packages/cli/src/ui/App.tsx | 31 | ||||
| -rw-r--r-- | packages/cli/src/ui/components/Header.tsx | 2 | ||||
| -rw-r--r-- | packages/cli/src/ui/components/InputPrompt.tsx | 56 | ||||
| -rw-r--r-- | packages/cli/src/ui/components/messages/ToolGroupMessage.tsx | 2 | ||||
| -rw-r--r-- | packages/cli/src/ui/components/messages/ToolMessage.tsx | 144 | ||||
| -rw-r--r-- | packages/cli/src/ui/hooks/useAppEffects.ts | 4 | ||||
| -rw-r--r-- | packages/cli/src/ui/hooks/useGeminiStream.ts | 401 | ||||
| -rw-r--r-- | packages/cli/src/ui/types.ts | 9 |
8 files changed, 456 insertions, 193 deletions
diff --git a/packages/cli/src/ui/App.tsx b/packages/cli/src/ui/App.tsx index e1b45af9..fa9b6f9b 100644 --- a/packages/cli/src/ui/App.tsx +++ b/packages/cli/src/ui/App.tsx @@ -22,16 +22,20 @@ import { useStartupWarnings, useInitializationErrorEffect, } from './hooks/useAppEffects.js'; +import type { Config } from '@gemini-code/server'; interface AppProps { - directory: string; + config: Config; } -export const App = ({ directory }: AppProps) => { +export const App = ({ config }: AppProps) => { const [history, setHistory] = useState<HistoryItem[]>([]); const [startupWarnings, setStartupWarnings] = useState<string[]>([]); - const { streamingState, submitQuery, initError } = - useGeminiStream(setHistory); + const { streamingState, submitQuery, initError } = useGeminiStream( + setHistory, + config.getApiKey(), + config.getModel(), + ); const { elapsedTime, currentLoadingPhrase } = useLoadingIndicator(streamingState); @@ -61,12 +65,7 @@ export const App = ({ directory }: AppProps) => { !initError && !isWaitingForToolConfirmation; - const { - query, - setQuery, - handleSubmit: handleHistorySubmit, - inputKey, - } = useInputHistory({ + const { query, handleSubmit: handleHistorySubmit } = useInputHistory({ userMessages, onSubmit: submitQuery, isActive: isInputActive, @@ -74,7 +73,7 @@ export const App = ({ directory }: AppProps) => { return ( <Box flexDirection="column" padding={1} marginBottom={1} width="100%"> - <Header cwd={directory} /> + <Header cwd={config.getTargetDir()} /> {startupWarnings.length > 0 && ( <Box @@ -135,15 +134,7 @@ export const App = ({ directory }: AppProps) => { /> </Box> - {isInputActive && ( - <InputPrompt - query={query} - setQuery={setQuery} - onSubmit={handleHistorySubmit} - isActive={isInputActive} - forceKey={inputKey} - /> - )} + {isInputActive && <InputPrompt onSubmit={handleHistorySubmit} />} <Footer queryLength={query.length} /> <ITermDetectionWarning /> diff --git a/packages/cli/src/ui/components/Header.tsx b/packages/cli/src/ui/components/Header.tsx index d045706c..62649996 100644 --- a/packages/cli/src/ui/components/Header.tsx +++ b/packages/cli/src/ui/components/Header.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { Box, Text } from 'ink'; import { UI_WIDTH, BOX_PADDING_X } from '../constants.js'; -import { shortenPath } from '../../utils/paths.js'; +import { shortenPath } from '@gemini-code/server'; interface HeaderProps { cwd: string; diff --git a/packages/cli/src/ui/components/InputPrompt.tsx b/packages/cli/src/ui/components/InputPrompt.tsx index b5d0b2b5..86e760ee 100644 --- a/packages/cli/src/ui/components/InputPrompt.tsx +++ b/packages/cli/src/ui/components/InputPrompt.tsx @@ -5,41 +5,43 @@ */ import React from 'react'; -import { Box, Text } from 'ink'; +import { Box, useInput, useFocus } from 'ink'; import TextInput from 'ink-text-input'; -import { globalConfig } from '../../config/config.js'; interface InputPromptProps { - query: string; - setQuery: (value: string) => void; onSubmit: (value: string) => void; - isActive: boolean; - forceKey?: number; } -export const InputPrompt: React.FC<InputPromptProps> = ({ - query, - setQuery, - onSubmit, - isActive, - forceKey, -}) => { - const model = globalConfig.getModel(); +export const InputPrompt: React.FC<InputPromptProps> = ({ onSubmit }) => { + const [value, setValue] = React.useState(''); + const { isFocused } = useFocus({ autoFocus: true }); + + useInput( + (input, key) => { + if (key.return) { + if (value.trim()) { + onSubmit(value); + setValue(''); + } + } + }, + { isActive: isFocused }, + ); return ( - <Box marginTop={1} borderStyle="round" borderColor={'white'} paddingX={1}> - <Text color={'white'}>> </Text> - <Box flexGrow={1}> - <TextInput - key={forceKey?.toString()} - value={query} - onChange={setQuery} - onSubmit={onSubmit} - showCursor={true} - focus={isActive} - placeholder={`Ask Gemini (${model})... (try "/init" or "/help")`} - /> - </Box> + <Box + borderStyle="round" + borderColor={isFocused ? 'blue' : 'gray'} + paddingX={1} + > + <TextInput + value={value} + onChange={setValue} + placeholder="Enter your message or use tools..." + onSubmit={() => { + /* Empty to prevent double submission */ + }} + /> </Box> ); }; diff --git a/packages/cli/src/ui/components/messages/ToolGroupMessage.tsx b/packages/cli/src/ui/components/messages/ToolGroupMessage.tsx index b43d452b..0662e333 100644 --- a/packages/cli/src/ui/components/messages/ToolGroupMessage.tsx +++ b/packages/cli/src/ui/components/messages/ToolGroupMessage.tsx @@ -30,10 +30,12 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({ <React.Fragment key={tool.callId}> <ToolMessage key={tool.callId} // Use callId as the key + callId={tool.callId} // Pass callId name={tool.name} description={tool.description} resultDisplay={tool.resultDisplay} status={tool.status} + confirmationDetails={tool.confirmationDetails} // Pass confirmationDetails /> {tool.status === ToolCallStatus.Confirming && tool.confirmationDetails && ( diff --git a/packages/cli/src/ui/components/messages/ToolMessage.tsx b/packages/cli/src/ui/components/messages/ToolMessage.tsx index da93b18a..9c1dd36b 100644 --- a/packages/cli/src/ui/components/messages/ToolMessage.tsx +++ b/packages/cli/src/ui/components/messages/ToolMessage.tsx @@ -7,70 +7,110 @@ import React from 'react'; import { Box, Text } from 'ink'; import Spinner from 'ink-spinner'; -import { ToolCallStatus } from '../../types.js'; -import { ToolResultDisplay } from '../../../tools/tools.js'; +import { + IndividualToolCallDisplay, + ToolCallStatus, + ToolCallConfirmationDetails, + ToolEditConfirmationDetails, + ToolExecuteConfirmationDetails, +} from '../../types.js'; import { DiffRenderer } from './DiffRenderer.js'; -import { MarkdownRenderer } from '../../utils/MarkdownRenderer.js'; +import { FileDiff, ToolResultDisplay } from '../../../tools/tools.js'; -interface ToolMessageProps { - name: string; - description: string; - resultDisplay: ToolResultDisplay | undefined; - status: ToolCallStatus; -} - -export const ToolMessage: React.FC<ToolMessageProps> = ({ +export const ToolMessage: React.FC<IndividualToolCallDisplay> = ({ + callId, name, description, resultDisplay, status, + confirmationDetails, }) => { - const statusIndicatorWidth = 3; - const hasResult = - (status === ToolCallStatus.Invoked || status === ToolCallStatus.Canceled) && - resultDisplay && - resultDisplay.toString().trim().length > 0; + // Explicitly type the props to help the type checker + const typedConfirmationDetails = confirmationDetails as + | ToolCallConfirmationDetails + | undefined; + const typedResultDisplay = resultDisplay as ToolResultDisplay | undefined; + + let color = 'gray'; + let prefix = ''; + switch (status) { + case ToolCallStatus.Pending: + prefix = 'Pending:'; + break; + case ToolCallStatus.Invoked: + prefix = 'Executing:'; + break; + case ToolCallStatus.Confirming: + color = 'yellow'; + prefix = 'Confirm:'; + break; + case ToolCallStatus.Success: + color = 'green'; + prefix = 'Success:'; + break; + case ToolCallStatus.Error: + color = 'red'; + prefix = 'Error:'; + break; + default: + // Handle unexpected status if necessary, or just break + break; + } + + const title = `${prefix} ${name}`; return ( - <Box paddingX={1} paddingY={0} flexDirection="column"> - {/* Row for Status Indicator and Tool Info */} - <Box minHeight={1}> - {/* Status Indicator */} - <Box minWidth={statusIndicatorWidth}> - {status === ToolCallStatus.Pending && <Spinner type="dots" />} - {status === ToolCallStatus.Invoked && <Text color="green">✔</Text>} - {status === ToolCallStatus.Confirming && <Text color="blue">?</Text>} - {status === ToolCallStatus.Canceled && ( - <Text color="red" bold> - - + <Box key={callId} borderStyle="round" paddingX={1} flexDirection="column"> + <Box> + {status === ToolCallStatus.Invoked && ( + <Box marginRight={1}> + <Text color="blue"> + <Spinner type="dots" /> </Text> + </Box> + )} + <Text bold color={color}> + {title} + </Text> + <Text color={color}> + {status === ToolCallStatus.Error && typedResultDisplay + ? `: ${typedResultDisplay}` + : ` - ${description}`} + </Text> + </Box> + {status === ToolCallStatus.Confirming && typedConfirmationDetails && ( + <Box flexDirection="column" marginLeft={2}> + {/* Display diff for edit/write */} + {'fileDiff' in typedConfirmationDetails && ( + <DiffRenderer + diffContent={ + (typedConfirmationDetails as ToolEditConfirmationDetails) + .fileDiff + } + /> )} + {/* Display command for execute */} + {'command' in typedConfirmationDetails && ( + <Text color="yellow"> + Command:{' '} + { + (typedConfirmationDetails as ToolExecuteConfirmationDetails) + .command + } + </Text> + )} + {/* <ConfirmInput onConfirm={handleConfirm} isFocused={isFocused} /> */} </Box> - <Box> - <Text - color="blue" - wrap="truncate-end" - strikethrough={status === ToolCallStatus.Canceled} - > - <Text bold>{name}</Text> <Text color="gray">{description}</Text> - </Text> - </Box> - </Box> - - {hasResult && ( - <Box paddingLeft={statusIndicatorWidth}> - <Box flexShrink={1} flexDirection="row"> - <Text color="gray">↳ </Text> - {/* Use default text color (white) or gray instead of dimColor */} - {typeof resultDisplay === 'string' && ( - <Box flexDirection="column"> - {MarkdownRenderer.render(resultDisplay)} - </Box> - )} - {typeof resultDisplay === 'object' && ( - <DiffRenderer diffContent={resultDisplay.fileDiff} /> - )} - </Box> + )} + {status === ToolCallStatus.Success && typedResultDisplay && ( + <Box flexDirection="column" marginLeft={2}> + {typeof typedResultDisplay === 'string' ? ( + <Text>{typedResultDisplay}</Text> + ) : ( + <DiffRenderer + diffContent={(typedResultDisplay as FileDiff).fileDiff} + /> + )} </Box> )} </Box> diff --git a/packages/cli/src/ui/hooks/useAppEffects.ts b/packages/cli/src/ui/hooks/useAppEffects.ts index 9f1e5af1..4576ce33 100644 --- a/packages/cli/src/ui/hooks/useAppEffects.ts +++ b/packages/cli/src/ui/hooks/useAppEffects.ts @@ -8,8 +8,8 @@ import { useEffect } from 'react'; import fs from 'fs'; import path from 'path'; import os from 'os'; -import type { HistoryItem } from '../types.js'; -import { getErrorMessage } from '../../utils/errors.js'; +import { HistoryItem } from '../types.js'; +import { getErrorMessage } from '@gemini-code/server'; const warningsFilePath = path.join(os.tmpdir(), 'gemini-code-cli-warnings.txt'); diff --git a/packages/cli/src/ui/hooks/useGeminiStream.ts b/packages/cli/src/ui/hooks/useGeminiStream.ts index 8cbb5f51..56203179 100644 --- a/packages/cli/src/ui/hooks/useGeminiStream.ts +++ b/packages/cli/src/ui/hooks/useGeminiStream.ts @@ -4,20 +4,30 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { exec } from 'child_process'; +import { exec as _exec } from 'child_process'; import { useState, useRef, useCallback, useEffect } from 'react'; import { useInput } from 'ink'; -import { GeminiClient } from '../../core/gemini-client.js'; -import { type Chat, type PartListUnion } from '@google/genai'; -import { HistoryItem } from '../types.js'; +// Import server-side client and types import { - processGeminiStream, - StreamingState, -} from '../../core/gemini-stream.js'; -import { globalConfig } from '../../config/config.js'; -import { getErrorMessage, isNodeError } from '../../utils/errors.js'; + GeminiClient, + GeminiEventType as ServerGeminiEventType, // Rename to avoid conflict + getErrorMessage, + isNodeError, + ToolResult, +} from '@gemini-code/server'; +import type { Chat, PartListUnion, FunctionDeclaration } from '@google/genai'; +// Import CLI types +import { + HistoryItem, + IndividualToolCallDisplay, + ToolCallStatus, +} from '../types.js'; +import { Tool } from '../../tools/tools.js'; // CLI Tool definition +import { StreamingState } from '../../core/gemini-stream.js'; +// Import CLI tool registry +import { toolRegistry } from '../../tools/tool-registry.js'; -const allowlistedCommands = ['ls']; // TODO: make this configurable +const _allowlistedCommands = ['ls']; // Prefix with underscore since it's unused const addHistoryItem = ( setHistory: React.Dispatch<React.SetStateAction<HistoryItem[]>>, @@ -30,32 +40,36 @@ const addHistoryItem = ( ]); }; +// Hook now accepts apiKey and model export const useGeminiStream = ( setHistory: React.Dispatch<React.SetStateAction<HistoryItem[]>>, + apiKey: string, + model: string, ) => { const [streamingState, setStreamingState] = useState<StreamingState>( StreamingState.Idle, ); const [initError, setInitError] = useState<string | null>(null); const abortControllerRef = useRef<AbortController | null>(null); - const currentToolGroupIdRef = useRef<number | null>(null); const chatSessionRef = useRef<Chat | null>(null); const geminiClientRef = useRef<GeminiClient | null>(null); const messageIdCounterRef = useRef(0); + const currentGeminiMessageIdRef = useRef<number | null>(null); - // Initialize Client Effect (remains the same) + // Initialize Client Effect - uses props now useEffect(() => { setInitError(null); if (!geminiClientRef.current) { try { - geminiClientRef.current = new GeminiClient(globalConfig); + geminiClientRef.current = new GeminiClient(apiKey, model); } catch (error: unknown) { setInitError( `Failed to initialize client: ${getErrorMessage(error) || 'Unknown error'}`, ); } } - }, []); + // Dependency array includes apiKey and model now + }, [apiKey, model]); // Input Handling Effect (remains the same) useInput((input, key) => { @@ -70,17 +84,25 @@ export const useGeminiStream = ( return baseTimestamp + messageIdCounterRef.current; }, []); - // Submit Query Callback (updated to call processGeminiStream) + // Helper function to update Gemini message content + const updateGeminiMessage = useCallback( + (messageId: number, newContent: string) => { + setHistory((prevHistory) => + prevHistory.map((item) => + item.id === messageId && item.type === 'gemini' + ? { ...item, text: newContent } + : item, + ), + ); + }, + [setHistory], + ); + + // Improved submit query function const submitQuery = useCallback( async (query: PartListUnion) => { - if (streamingState === StreamingState.Responding) { - // No-op if already going. - return; - } - - if (typeof query === 'string' && query.toString().trim().length === 0) { - return; - } + if (streamingState === StreamingState.Responding) return; + if (typeof query === 'string' && query.trim().length === 0) return; const userMessageTimestamp = Date.now(); const client = geminiClientRef.current; @@ -90,101 +112,306 @@ export const useGeminiStream = ( } if (!chatSessionRef.current) { - chatSessionRef.current = await client.startChat(); + try { + // Use getFunctionDeclarations for startChat + const toolSchemas = toolRegistry.getFunctionDeclarations(); + chatSessionRef.current = await client.startChat(toolSchemas); + } catch (err: unknown) { + setInitError(`Failed to start chat: ${getErrorMessage(err)}`); + setStreamingState(StreamingState.Idle); + return; + } } - // Reset state setStreamingState(StreamingState.Responding); setInitError(null); - currentToolGroupIdRef.current = null; - messageIdCounterRef.current = 0; + messageIdCounterRef.current = 0; // Reset counter for new submission const chat = chatSessionRef.current; + let currentToolGroupId: number | null = null; + + // For function responses, we don't need to add a user message + if (typeof query === 'string') { + // Only add user message for string queries, not for function responses + addHistoryItem( + setHistory, + { type: 'user', text: query }, + userMessageTimestamp, + ); + } try { - // Add user message - if (typeof query === 'string') { - const trimmedQuery = query.toString(); - addHistoryItem( - setHistory, - { type: 'user', text: trimmedQuery }, - userMessageTimestamp, - ); + abortControllerRef.current = new AbortController(); + const signal = abortControllerRef.current.signal; + + // Get ServerTool descriptions for the server call + const serverTools: ServerTool[] = toolRegistry + .getAllTools() + .map((cliTool: Tool) => ({ + name: cliTool.name, + schema: cliTool.schema, + execute: (args: Record<string, unknown>) => + cliTool.execute(args as ToolArgs), // Pass execution + })); + + const stream = client.sendMessageStream( + chat, + query, + serverTools, + signal, + ); + + // Process the stream events from the server logic + let currentGeminiText = ''; // To accumulate message content + let hasInitialGeminiResponse = false; + + for await (const event of stream) { + if (signal.aborted) break; + + if (event.type === ServerGeminiEventType.Content) { + // For content events, accumulate the text and update an existing message or create a new one + currentGeminiText += event.value; + + if (!hasInitialGeminiResponse) { + // Create a new Gemini message if this is the first content event + hasInitialGeminiResponse = true; + const eventTimestamp = getNextMessageId(userMessageTimestamp); + currentGeminiMessageIdRef.current = eventTimestamp; - const maybeCommand = trimmedQuery.split(/\s+/)[0]; - if (allowlistedCommands.includes(maybeCommand)) { - exec(trimmedQuery, (error, stdout) => { - const timestamp = getNextMessageId(userMessageTimestamp); - // TODO: handle stderr, error addHistoryItem( setHistory, - { type: 'info', text: stdout }, - timestamp, + { type: 'gemini', text: currentGeminiText }, + eventTimestamp, ); - }); - return; - } - } else if ( - // HACK to detect errored function responses. - typeof query === 'object' && - query !== null && - !Array.isArray(query) && // Ensure it's a single Part object - 'functionResponse' in query && // Check if it's a function response Part - query.functionResponse?.response && // Check if response object exists - 'error' in query.functionResponse.response // Check specifically for the 'error' key - ) { - const history = chat.getHistory(); - history.push({ role: 'user', parts: [query] }); - return; - } + } else if (currentGeminiMessageIdRef.current !== null) { + // Update the existing message with accumulated content + updateGeminiMessage( + currentGeminiMessageIdRef.current, + currentGeminiText, + ); + } + } else if (event.type === ServerGeminiEventType.ToolCallRequest) { + // Reset the Gemini message tracking for the next response + currentGeminiText = ''; + hasInitialGeminiResponse = false; + currentGeminiMessageIdRef.current = null; - // Prepare for streaming - abortControllerRef.current = new AbortController(); - const signal = abortControllerRef.current.signal; + const { callId, name, args } = event.value; + + const cliTool = toolRegistry.getTool(name); // Get the full CLI tool + if (!cliTool) { + console.error(`CLI Tool "${name}" not found!`); + continue; + } - // --- Delegate to Stream Processor --- + if (currentToolGroupId === null) { + currentToolGroupId = getNextMessageId(userMessageTimestamp); + // Add explicit cast to Omit<HistoryItem, 'id'> + addHistoryItem( + setHistory, + { type: 'tool_group', tools: [] } as Omit<HistoryItem, 'id'>, + currentToolGroupId, + ); + } - const stream = client.sendMessageStream(chat, query, signal); + // Create the UI display object matching IndividualToolCallDisplay + const toolCallDisplay: IndividualToolCallDisplay = { + callId, + name, + description: cliTool.getDescription(args as ToolArgs), + status: ToolCallStatus.Pending, + resultDisplay: undefined, + confirmationDetails: undefined, + }; - const addHistoryItemFromStream = ( - itemData: Omit<HistoryItem, 'id'>, - id: number, - ) => { - addHistoryItem(setHistory, itemData, id); - }; - const getStreamMessageId = () => getNextMessageId(userMessageTimestamp); + // Add pending tool call to the UI history group + setHistory((prevHistory) => + prevHistory.map((item) => { + if ( + item.id === currentToolGroupId && + item.type === 'tool_group' + ) { + // Ensure item.tools exists and is an array before spreading + const currentTools = Array.isArray(item.tools) + ? item.tools + : []; + return { + ...item, + tools: [...currentTools, toolCallDisplay], // Add the complete display object + }; + } + return item; + }), + ); - // Call the renamed processor function - await processGeminiStream({ - stream, - signal, - setHistory, - submitQuery, - getNextMessageId: getStreamMessageId, - addHistoryItem: addHistoryItemFromStream, - currentToolGroupIdRef, - }); + // --- Tool Execution & Confirmation Logic --- + const confirmationDetails = await cliTool.shouldConfirmExecute( + args as ToolArgs, + ); + + if (confirmationDetails) { + setHistory((prevHistory) => + prevHistory.map((item) => { + if ( + item.id === currentToolGroupId && + item.type === 'tool_group' + ) { + return { + ...item, + tools: item.tools.map((tool) => + tool.callId === callId + ? { + ...tool, + status: ToolCallStatus.Confirming, + confirmationDetails, + } + : tool, + ), + }; + } + return item; + }), + ); + setStreamingState(StreamingState.WaitingForConfirmation); + return; + } + + try { + setHistory((prevHistory) => + prevHistory.map((item) => { + if ( + item.id === currentToolGroupId && + item.type === 'tool_group' + ) { + return { + ...item, + tools: item.tools.map((tool) => + tool.callId === callId + ? { ...tool, status: ToolCallStatus.Invoked } + : tool, + ), + }; + } + return item; + }), + ); + + const result: ToolResult = await cliTool.execute( + args as ToolArgs, + ); + const resultPart = { + functionResponse: { + name, + id: callId, + response: { output: result.llmContent }, + }, + }; + + setHistory((prevHistory) => + prevHistory.map((item) => { + if ( + item.id === currentToolGroupId && + item.type === 'tool_group' + ) { + return { + ...item, + tools: item.tools.map((tool) => + tool.callId === callId + ? { + ...tool, + status: ToolCallStatus.Success, + resultDisplay: result.returnDisplay, + } + : tool, + ), + }; + } + return item; + }), + ); + + // Execute the function and continue the stream + await submitQuery(resultPart); + return; + } catch (execError: unknown) { + const error = new Error( + `Tool execution failed: ${execError instanceof Error ? execError.message : String(execError)}`, + ); + const errorPart = { + functionResponse: { + name, + id: callId, + response: { + error: `Tool execution failed: ${error.message}`, + }, + }, + }; + setHistory((prevHistory) => + prevHistory.map((item) => { + if ( + item.id === currentToolGroupId && + item.type === 'tool_group' + ) { + return { + ...item, + tools: item.tools.map((tool) => + tool.callId === callId + ? { + ...tool, + status: ToolCallStatus.Error, + resultDisplay: `Error: ${error.message}`, + } + : tool, + ), + }; + } + return item; + }), + ); + await submitQuery(errorPart); + return; + } + } + } } catch (error: unknown) { - // (Error handling for stream initiation remains the same) - console.error('Error initiating stream:', error); if (!isNodeError(error) || error.name !== 'AbortError') { - // Use historyUpdater's function potentially? Or keep addHistoryItem here? - // Keeping addHistoryItem here for direct errors from this scope. + console.error('Error processing stream or executing tool:', error); addHistoryItem( setHistory, { type: 'error', - text: `[Error starting stream: ${getErrorMessage(error)}]`, + text: `[Error: ${getErrorMessage(error)}]`, }, getNextMessageId(userMessageTimestamp), ); } } finally { abortControllerRef.current = null; - setStreamingState(StreamingState.Idle); + // Only set to Idle if not waiting for confirmation + if (streamingState !== StreamingState.WaitingForConfirmation) { + setStreamingState(StreamingState.Idle); + } } }, - [setStreamingState, setHistory, initError, getNextMessageId], + // Dependencies need careful review - including updateGeminiMessage + [ + streamingState, + setHistory, + apiKey, + model, + getNextMessageId, + updateGeminiMessage, + ], ); return { streamingState, submitQuery, initError }; }; + +// Define ServerTool interface here if not importing from server (circular dep issue?) +interface ServerTool { + name: string; + schema: FunctionDeclaration; + execute(params: Record<string, unknown>): Promise<ToolResult>; +} + +// Define a more specific type for tool arguments to replace 'any' +type ToolArgs = Record<string, unknown>; diff --git a/packages/cli/src/ui/types.ts b/packages/cli/src/ui/types.ts index 6ff2f738..b0959ce4 100644 --- a/packages/cli/src/ui/types.ts +++ b/packages/cli/src/ui/types.ts @@ -7,10 +7,11 @@ import { ToolResultDisplay } from '../tools/tools.js'; export enum ToolCallStatus { - Pending, - Invoked, - Confirming, - Canceled, + Pending = 'Pending', + Invoked = 'Invoked', + Confirming = 'Confirming', + Success = 'Success', + Error = 'Error', } export interface ToolCallEvent { |
