summaryrefslogtreecommitdiff
path: root/packages/cli/src/core/historyUpdater.ts
diff options
context:
space:
mode:
authorTaylor Mullen <[email protected]>2025-04-15 21:41:08 -0700
committerTaylor Mullen <[email protected]>2025-04-17 13:19:55 -0400
commitadd233c5043264d47ecc6d3339a383f41a241ae8 (patch)
tree3d80d412ed805007132cf44257bbd7667005dcd8 /packages/cli/src/core/historyUpdater.ts
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__
Diffstat (limited to 'packages/cli/src/core/historyUpdater.ts')
-rw-r--r--packages/cli/src/core/historyUpdater.ts173
1 files changed, 173 insertions, 0 deletions
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<React.SetStateAction<HistoryItem[]>>,
+ submitQuery: (query: Part) => Promise<void>,
+ getNextMessageId: () => number,
+ currentToolGroupIdRef: React.MutableRefObject<number | null>
+): 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<React.SetStateAction<HistoryItem[]>>,
+ 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