From add233c5043264d47ecc6d3339a383f41a241ae8 Mon Sep 17 00:00:00 2001 From: Taylor Mullen Date: Tue, 15 Apr 2025 21:41:08 -0700 Subject: Initial commit of Gemini Code CLI This commit introduces the initial codebase for the Gemini Code CLI, a command-line interface designed to facilitate interaction with the Gemini API for software engineering tasks. The code was migrated from a previous git repository as a single squashed commit. Core Features & Components: * **Gemini Integration:** Leverages the `@google/genai` SDK to interact with the Gemini models, supporting chat history, streaming responses, and function calling (tools). * **Terminal UI:** Built with Ink (React for CLIs) providing an interactive chat interface within the terminal, including input prompts, message display, loading indicators, and tool interaction elements. * **Tooling Framework:** Implements a robust tool system allowing Gemini to interact with the local environment. Includes tools for: * File system listing (`ls`) * File reading (`read-file`) * Content searching (`grep`) * File globbing (`glob`) * File editing (`edit`) * File writing (`write-file`) * Executing bash commands (`terminal`) * **State Management:** Handles the streaming state of Gemini responses and manages the conversation history. * **Configuration:** Parses command-line arguments (`yargs`) and loads environment variables (`dotenv`) for setup. * **Project Structure:** Organized into `core`, `ui`, `tools`, `config`, and `utils` directories using TypeScript. Includes basic build (`tsc`) and start scripts. This initial version establishes the foundation for a powerful CLI tool enabling developers to use Gemini for coding assistance directly in their terminal environment. --- Created by yours truly: __Gemini Code__ --- packages/cli/src/core/historyUpdater.ts | 173 ++++++++++++++++++++++++++++++++ 1 file changed, 173 insertions(+) create mode 100644 packages/cli/src/core/historyUpdater.ts (limited to 'packages/cli/src/core/historyUpdater.ts') diff --git a/packages/cli/src/core/historyUpdater.ts b/packages/cli/src/core/historyUpdater.ts new file mode 100644 index 00000000..39eaca6a --- /dev/null +++ b/packages/cli/src/core/historyUpdater.ts @@ -0,0 +1,173 @@ +import { Part } from "@google/genai"; +import { toolRegistry } from "../tools/tool-registry.js"; +import { HistoryItem, IndividualToolCallDisplay, ToolCallEvent, ToolCallStatus, ToolConfirmationOutcome, ToolEditConfirmationDetails, ToolExecuteConfirmationDetails } from "../ui/types.js"; +import { ToolResultDisplay } from "../tools/ToolResult.js"; + +/** + * Processes a tool call chunk and updates the history state accordingly. + * Manages adding new tool groups or updating existing ones. + * Resides here as its primary effect is updating history based on tool events. + */ +export const handleToolCallChunk = ( + chunk: ToolCallEvent, + setHistory: React.Dispatch>, + submitQuery: (query: Part) => Promise, + getNextMessageId: () => number, + currentToolGroupIdRef: React.MutableRefObject +): void => { + const toolDefinition = toolRegistry.getTool(chunk.name); + const description = toolDefinition?.getDescription + ? toolDefinition.getDescription(chunk.args) + : ''; + const toolDisplayName = toolDefinition?.displayName ?? chunk.name; + let confirmationDetails = chunk.confirmationDetails; + if (confirmationDetails) { + const originalConfirmationDetails = confirmationDetails; + const historyUpdatingConfirm = async (outcome: ToolConfirmationOutcome) => { + originalConfirmationDetails.onConfirm(outcome); + + if (outcome === ToolConfirmationOutcome.Cancel) { + let resultDisplay: ToolResultDisplay | undefined; + if ('fileDiff' in originalConfirmationDetails) { + resultDisplay = { fileDiff: (originalConfirmationDetails as ToolEditConfirmationDetails).fileDiff }; + } else { + resultDisplay = `~~${(originalConfirmationDetails as ToolExecuteConfirmationDetails).command}~~`; + } + handleToolCallChunk({ ...chunk, status: ToolCallStatus.Canceled, confirmationDetails: undefined, resultDisplay, }, setHistory, submitQuery, getNextMessageId, currentToolGroupIdRef); + const functionResponse: Part = { + functionResponse: { + name: chunk.name, + response: { "error": "User rejected function call." }, + }, + } + await submitQuery(functionResponse); + } else { + const tool = toolRegistry.getTool(chunk.name) + if (!tool) { + throw new Error(`Tool "${chunk.name}" not found or is not registered.`); + } + + handleToolCallChunk({ ...chunk, status: ToolCallStatus.Invoked, resultDisplay: "Executing...", confirmationDetails: undefined }, setHistory, submitQuery, getNextMessageId, currentToolGroupIdRef); + + const result = await tool.execute(chunk.args); + + handleToolCallChunk({ ...chunk, status: ToolCallStatus.Invoked, resultDisplay: result.returnDisplay, confirmationDetails: undefined }, setHistory, submitQuery, getNextMessageId, currentToolGroupIdRef); + + const functionResponse: Part = { + functionResponse: { + name: chunk.name, + id: chunk.callId, + response: { "output": result.llmContent }, + }, + } + + await submitQuery(functionResponse); + } + } + + confirmationDetails = { + ...originalConfirmationDetails, + onConfirm: historyUpdatingConfirm, + }; + } + const toolDetail: IndividualToolCallDisplay = { + callId: chunk.callId, + name: toolDisplayName, + description, + resultDisplay: chunk.resultDisplay, + status: chunk.status, + confirmationDetails: confirmationDetails, + }; + + const activeGroupId = currentToolGroupIdRef.current; + setHistory(prev => { + if (chunk.status === ToolCallStatus.Pending) { + if (activeGroupId === null) { + // Start a new tool group + const newGroupId = getNextMessageId(); + currentToolGroupIdRef.current = newGroupId; + return [ + ...prev, + { id: newGroupId, type: 'tool_group', tools: [toolDetail] } as HistoryItem + ]; + } + + // Add to existing tool group + return prev.map(item => + item.id === activeGroupId && item.type === 'tool_group' + ? item.tools.some(t => t.callId === toolDetail.callId) + ? item // Tool already listed as pending + : { ...item, tools: [...item.tools, toolDetail] } + : item + ); + } + + // Update the status of a pending tool within the active group + if (activeGroupId === null) { + // Log if an invoked tool arrives without an active group context + console.warn("Received invoked tool status without an active tool group ID:", chunk); + return prev; + } + + return prev.map(item => + item.id === activeGroupId && item.type === 'tool_group' + ? { + ...item, + tools: item.tools.map(t => + t.callId === toolDetail.callId + ? { ...t, ...toolDetail, status: chunk.status } // Update details & status + : t + ) + } + : item + ); + }); +}; + +/** + * Appends an error or informational message to the history, attempting to attach + * it to the last non-user message or creating a new entry. + */ +export const addErrorMessageToHistory = ( + error: any, + setHistory: React.Dispatch>, + getNextMessageId: () => number +): void => { + const isAbort = error.name === 'AbortError'; + const errorType = isAbort ? 'info' : 'error'; + const errorText = isAbort + ? '[Request cancelled by user]' + : `[Error: ${error.message || 'Unknown error'}]`; + + setHistory(prev => { + const reversedHistory = [...prev].reverse(); + // Find the last message that isn't from the user to append the error/info to + const lastBotMessageIndex = reversedHistory.findIndex(item => item.type !== 'user'); + const originalIndex = lastBotMessageIndex !== -1 ? prev.length - 1 - lastBotMessageIndex : -1; + + if (originalIndex !== -1) { + // Append error to the last relevant message + return prev.map((item, index) => { + if (index === originalIndex) { + let baseText = ''; + // Determine base text based on item type + if (item.type === 'gemini') baseText = item.text ?? ''; + else if (item.type === 'tool_group') baseText = `Tool execution (${item.tools.length} calls)`; + else if (item.type === 'error' || item.type === 'info') baseText = item.text ?? ''; + // Safely handle potential undefined text + + const updatedText = (baseText + (baseText && !baseText.endsWith('\n') ? '\n' : '') + errorText).trim(); + // Reuse existing ID, update type and text + return { ...item, type: errorType, text: updatedText }; + } + return item; + }); + } else { + // No previous message to append to, add a new error item + return [ + ...prev, + { id: getNextMessageId(), type: errorType, text: errorText } as HistoryItem + ]; + } + }); +}; \ No newline at end of file -- cgit v1.2.3