2025-04-18 17:44:24 -07:00
|
|
|
/**
|
|
|
|
|
* @license
|
|
|
|
|
* Copyright 2025 Google LLC
|
|
|
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
|
|
|
*/
|
|
|
|
|
|
2025-05-22 05:57:53 +00:00
|
|
|
import { useState, useRef, useCallback, useEffect, useMemo } from 'react';
|
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__
2025-04-15 21:41:08 -07:00
|
|
|
import { useInput } from 'ink';
|
2025-04-18 18:08:43 -04:00
|
|
|
import {
|
2025-04-19 19:45:42 +01:00
|
|
|
GeminiClient,
|
2025-05-06 16:20:28 -07:00
|
|
|
GeminiEventType as ServerGeminiEventType,
|
2025-05-14 22:14:15 +00:00
|
|
|
ServerGeminiStreamEvent as GeminiEvent,
|
|
|
|
|
ServerGeminiContentEvent as ContentEvent,
|
|
|
|
|
ServerGeminiErrorEvent as ErrorEvent,
|
2025-04-19 19:45:42 +01:00
|
|
|
getErrorMessage,
|
|
|
|
|
isNodeError,
|
2025-04-20 21:06:22 +01:00
|
|
|
Config,
|
2025-05-21 07:36:22 +00:00
|
|
|
MessageSenderType,
|
|
|
|
|
ServerToolCallConfirmationDetails,
|
2025-04-21 10:53:11 -04:00
|
|
|
ToolCallResponseInfo,
|
2025-04-21 14:32:18 -04:00
|
|
|
ToolEditConfirmationDetails,
|
|
|
|
|
ToolExecuteConfirmationDetails,
|
2025-05-21 07:36:22 +00:00
|
|
|
ToolResultDisplay,
|
2025-05-22 05:57:53 +00:00
|
|
|
ToolCallRequestInfo,
|
2025-04-19 19:45:42 +01:00
|
|
|
} from '@gemini-code/server';
|
2025-05-26 14:17:56 -07:00
|
|
|
import { type PartListUnion, type Part } from '@google/genai';
|
2025-04-19 19:45:42 +01:00
|
|
|
import {
|
2025-04-21 11:49:46 -07:00
|
|
|
StreamingState,
|
2025-04-19 19:45:42 +01:00
|
|
|
ToolCallStatus,
|
2025-05-07 12:57:19 -07:00
|
|
|
HistoryItemWithoutId,
|
2025-05-14 22:14:15 +00:00
|
|
|
HistoryItemToolGroup,
|
2025-05-14 12:37:17 -07:00
|
|
|
MessageType,
|
2025-04-19 19:45:42 +01:00
|
|
|
} from '../types.js';
|
2025-05-02 14:39:39 -07:00
|
|
|
import { isAtCommand } from '../utils/commandUtils.js';
|
2025-04-30 00:26:07 +00:00
|
|
|
import { useShellCommandProcessor } from './shellCommandProcessor.js';
|
2025-05-02 14:39:39 -07:00
|
|
|
import { handleAtCommand } from './atCommandProcessor.js';
|
2025-05-07 21:15:41 -07:00
|
|
|
import { findLastSafeSplitPoint } from '../utils/markdownUtilities.js';
|
2025-05-07 12:57:19 -07:00
|
|
|
import { useStateAndRef } from './useStateAndRef.js';
|
2025-05-06 16:20:28 -07:00
|
|
|
import { UseHistoryManagerReturn } from './useHistoryManager.js';
|
2025-05-21 07:36:22 +00:00
|
|
|
import { useLogger } from './useLogger.js';
|
2025-05-22 05:57:53 +00:00
|
|
|
import { useToolScheduler, mapToDisplay } from './useToolScheduler.js';
|
2025-05-26 14:17:56 -07:00
|
|
|
import { GeminiChat } from '@gemini-code/server/src/core/geminiChat.js';
|
2025-04-28 12:38:07 -07:00
|
|
|
|
2025-05-29 22:30:18 +00:00
|
|
|
export function mergePartListUnions(list: PartListUnion[]): PartListUnion {
|
|
|
|
|
const resultParts: PartListUnion = [];
|
|
|
|
|
for (const item of list) {
|
|
|
|
|
if (Array.isArray(item)) {
|
|
|
|
|
resultParts.push(...item);
|
|
|
|
|
} else {
|
|
|
|
|
resultParts.push(item);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return resultParts;
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-14 22:14:15 +00:00
|
|
|
enum StreamProcessingStatus {
|
|
|
|
|
Completed,
|
|
|
|
|
UserCancelled,
|
|
|
|
|
Error,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Hook to manage the Gemini stream, handle user input, process commands,
|
|
|
|
|
* and interact with the Gemini API and history manager.
|
|
|
|
|
*/
|
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__
2025-04-15 21:41:08 -07:00
|
|
|
export const useGeminiStream = (
|
2025-05-06 16:20:28 -07:00
|
|
|
addItem: UseHistoryManagerReturn['addItem'],
|
2025-05-05 17:52:29 +00:00
|
|
|
refreshStatic: () => void,
|
2025-05-05 20:48:34 +00:00
|
|
|
setShowHelp: React.Dispatch<React.SetStateAction<boolean>>,
|
2025-04-20 21:06:22 +01:00
|
|
|
config: Config,
|
2025-05-13 23:55:49 +00:00
|
|
|
onDebugMessage: (message: string) => void,
|
2025-05-23 08:47:19 -07:00
|
|
|
handleSlashCommand: (
|
|
|
|
|
cmd: PartListUnion,
|
|
|
|
|
) => import('./slashCommandProcessor.js').SlashCommandActionReturn | boolean,
|
2025-05-19 16:11:45 -07:00
|
|
|
shellModeActive: boolean,
|
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__
2025-04-15 21:41:08 -07:00
|
|
|
) => {
|
2025-04-17 18:06:21 -04:00
|
|
|
const [initError, setInitError] = useState<string | null>(null);
|
|
|
|
|
const abortControllerRef = useRef<AbortController | null>(null);
|
2025-05-26 14:17:56 -07:00
|
|
|
const chatSessionRef = useRef<GeminiChat | null>(null);
|
2025-04-17 18:06:21 -04:00
|
|
|
const geminiClientRef = useRef<GeminiClient | null>(null);
|
2025-05-16 16:45:58 +00:00
|
|
|
const [isResponding, setIsResponding] = useState<boolean>(false);
|
2025-05-07 12:57:19 -07:00
|
|
|
const [pendingHistoryItemRef, setPendingHistoryItem] =
|
|
|
|
|
useStateAndRef<HistoryItemWithoutId | null>(null);
|
2025-05-21 07:36:22 +00:00
|
|
|
const logger = useLogger();
|
2025-05-27 15:40:18 -07:00
|
|
|
const [toolCalls, schedule, cancel] = useToolScheduler(
|
|
|
|
|
(tools) => {
|
|
|
|
|
if (tools.length) {
|
|
|
|
|
addItem(mapToDisplay(tools), Date.now());
|
2025-05-29 22:30:18 +00:00
|
|
|
const toolResponses = tools
|
|
|
|
|
.filter(
|
|
|
|
|
(t) =>
|
|
|
|
|
t.status === 'error' ||
|
|
|
|
|
t.status === 'cancelled' ||
|
|
|
|
|
t.status === 'success',
|
|
|
|
|
)
|
|
|
|
|
.map((t) => t.response.responseParts);
|
|
|
|
|
|
|
|
|
|
submitQuery(mergePartListUnions(toolResponses));
|
2025-05-27 15:40:18 -07:00
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
config,
|
|
|
|
|
setPendingHistoryItem,
|
|
|
|
|
);
|
2025-05-22 05:57:53 +00:00
|
|
|
const pendingToolCalls = useMemo(
|
|
|
|
|
() => (toolCalls.length ? mapToDisplay(toolCalls) : undefined),
|
|
|
|
|
[toolCalls],
|
|
|
|
|
);
|
2025-04-17 18:06:21 -04:00
|
|
|
|
2025-05-16 16:45:58 +00:00
|
|
|
const onExec = useCallback(async (done: Promise<void>) => {
|
|
|
|
|
setIsResponding(true);
|
|
|
|
|
await done;
|
|
|
|
|
setIsResponding(false);
|
|
|
|
|
}, []);
|
2025-04-30 00:26:07 +00:00
|
|
|
const { handleShellCommand } = useShellCommandProcessor(
|
2025-05-06 16:20:28 -07:00
|
|
|
addItem,
|
2025-05-21 13:16:50 -07:00
|
|
|
setPendingHistoryItem,
|
2025-05-16 16:45:58 +00:00
|
|
|
onExec,
|
2025-05-13 23:55:49 +00:00
|
|
|
onDebugMessage,
|
2025-04-30 00:26:07 +00:00
|
|
|
config,
|
|
|
|
|
);
|
|
|
|
|
|
2025-05-24 00:44:17 -07:00
|
|
|
const streamingState = useMemo(() => {
|
|
|
|
|
if (toolCalls.some((t) => t.status === 'awaiting_approval')) {
|
|
|
|
|
return StreamingState.WaitingForConfirmation;
|
|
|
|
|
}
|
|
|
|
|
if (
|
|
|
|
|
isResponding ||
|
|
|
|
|
toolCalls.some(
|
2025-05-25 16:01:10 -07:00
|
|
|
(t) =>
|
|
|
|
|
t.status === 'executing' ||
|
|
|
|
|
t.status === 'scheduled' ||
|
|
|
|
|
t.status === 'validating',
|
2025-05-24 00:44:17 -07:00
|
|
|
)
|
|
|
|
|
) {
|
|
|
|
|
return StreamingState.Responding;
|
|
|
|
|
}
|
|
|
|
|
return StreamingState.Idle;
|
|
|
|
|
}, [isResponding, toolCalls]);
|
|
|
|
|
|
2025-04-17 18:06:21 -04:00
|
|
|
useEffect(() => {
|
|
|
|
|
setInitError(null);
|
|
|
|
|
if (!geminiClientRef.current) {
|
|
|
|
|
try {
|
2025-04-22 11:01:09 -07:00
|
|
|
geminiClientRef.current = new GeminiClient(config);
|
2025-04-18 17:47:49 -04:00
|
|
|
} catch (error: unknown) {
|
2025-05-06 16:20:28 -07:00
|
|
|
const errorMsg = `Failed to initialize client: ${getErrorMessage(error) || 'Unknown error'}`;
|
|
|
|
|
setInitError(errorMsg);
|
2025-05-14 12:37:17 -07:00
|
|
|
addItem({ type: MessageType.ERROR, text: errorMsg }, Date.now());
|
2025-04-17 18:06:21 -04:00
|
|
|
}
|
|
|
|
|
}
|
2025-05-06 16:20:28 -07:00
|
|
|
}, [config, addItem]);
|
2025-04-17 18:06:21 -04:00
|
|
|
|
2025-05-06 16:20:28 -07:00
|
|
|
useInput((_input, key) => {
|
2025-05-09 23:29:02 -07:00
|
|
|
if (streamingState !== StreamingState.Idle && key.escape) {
|
2025-04-17 18:06:21 -04:00
|
|
|
abortControllerRef.current?.abort();
|
2025-05-22 05:57:53 +00:00
|
|
|
cancel();
|
2025-04-17 18:06:21 -04:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2025-05-24 00:44:17 -07:00
|
|
|
const prepareQueryForGemini = useCallback(
|
|
|
|
|
async (
|
|
|
|
|
query: PartListUnion,
|
|
|
|
|
userMessageTimestamp: number,
|
|
|
|
|
signal: AbortSignal,
|
|
|
|
|
): Promise<{
|
|
|
|
|
queryToSend: PartListUnion | null;
|
|
|
|
|
shouldProceed: boolean;
|
|
|
|
|
}> => {
|
|
|
|
|
if (typeof query === 'string' && query.trim().length === 0) {
|
|
|
|
|
return { queryToSend: null, shouldProceed: false };
|
|
|
|
|
}
|
2025-04-17 18:06:21 -04:00
|
|
|
|
2025-05-24 00:44:17 -07:00
|
|
|
let localQueryToSendToGemini: PartListUnion | null = null;
|
2025-04-29 13:29:57 -07:00
|
|
|
|
2025-05-24 00:44:17 -07:00
|
|
|
if (typeof query === 'string') {
|
|
|
|
|
const trimmedQuery = query.trim();
|
|
|
|
|
onDebugMessage(`User query: '${trimmedQuery}'`);
|
|
|
|
|
await logger?.logMessage(MessageSenderType.USER, trimmedQuery);
|
2025-05-05 20:48:34 +00:00
|
|
|
|
2025-05-24 00:44:17 -07:00
|
|
|
// Handle UI-only commands first
|
|
|
|
|
const slashCommandResult = handleSlashCommand(trimmedQuery);
|
|
|
|
|
if (typeof slashCommandResult === 'boolean' && slashCommandResult) {
|
|
|
|
|
// Command was handled, and it doesn't require a tool call from here
|
|
|
|
|
return { queryToSend: null, shouldProceed: false };
|
|
|
|
|
} else if (
|
|
|
|
|
typeof slashCommandResult === 'object' &&
|
|
|
|
|
slashCommandResult.shouldScheduleTool
|
|
|
|
|
) {
|
|
|
|
|
// Slash command wants to schedule a tool call (e.g., /memory add)
|
|
|
|
|
const { toolName, toolArgs } = slashCommandResult;
|
|
|
|
|
if (toolName && toolArgs) {
|
|
|
|
|
const toolCallRequest: ToolCallRequestInfo = {
|
|
|
|
|
callId: `${toolName}-${Date.now()}-${Math.random().toString(16).slice(2)}`,
|
|
|
|
|
name: toolName,
|
|
|
|
|
args: toolArgs,
|
|
|
|
|
};
|
|
|
|
|
schedule([toolCallRequest]); // schedule expects an array or single object
|
|
|
|
|
}
|
|
|
|
|
return { queryToSend: null, shouldProceed: false }; // Handled by scheduling the tool
|
2025-05-23 08:47:19 -07:00
|
|
|
}
|
|
|
|
|
|
2025-05-24 00:44:17 -07:00
|
|
|
if (shellModeActive && handleShellCommand(trimmedQuery)) {
|
2025-05-14 22:14:15 +00:00
|
|
|
return { queryToSend: null, shouldProceed: false };
|
2025-04-29 15:39:36 -07:00
|
|
|
}
|
2025-05-24 00:44:17 -07:00
|
|
|
|
|
|
|
|
// Handle @-commands (which might involve tool calls)
|
|
|
|
|
if (isAtCommand(trimmedQuery)) {
|
|
|
|
|
const atCommandResult = await handleAtCommand({
|
|
|
|
|
query: trimmedQuery,
|
|
|
|
|
config,
|
|
|
|
|
addItem,
|
|
|
|
|
onDebugMessage,
|
|
|
|
|
messageId: userMessageTimestamp,
|
|
|
|
|
signal,
|
|
|
|
|
});
|
|
|
|
|
if (!atCommandResult.shouldProceed) {
|
|
|
|
|
return { queryToSend: null, shouldProceed: false };
|
|
|
|
|
}
|
|
|
|
|
localQueryToSendToGemini = atCommandResult.processedQuery;
|
|
|
|
|
} else {
|
|
|
|
|
// Normal query for Gemini
|
|
|
|
|
addItem(
|
|
|
|
|
{ type: MessageType.USER, text: trimmedQuery },
|
|
|
|
|
userMessageTimestamp,
|
|
|
|
|
);
|
|
|
|
|
localQueryToSendToGemini = trimmedQuery;
|
|
|
|
|
}
|
2025-04-29 13:29:57 -07:00
|
|
|
} else {
|
2025-05-24 00:44:17 -07:00
|
|
|
// It's a function response (PartListUnion that isn't a string)
|
|
|
|
|
localQueryToSendToGemini = query;
|
2025-04-20 20:20:40 +01:00
|
|
|
}
|
2025-05-14 22:14:15 +00:00
|
|
|
|
2025-05-24 00:44:17 -07:00
|
|
|
if (localQueryToSendToGemini === null) {
|
|
|
|
|
onDebugMessage(
|
|
|
|
|
'Query processing resulted in null, not sending to Gemini.',
|
|
|
|
|
);
|
|
|
|
|
return { queryToSend: null, shouldProceed: false };
|
|
|
|
|
}
|
|
|
|
|
return { queryToSend: localQueryToSendToGemini, shouldProceed: true };
|
|
|
|
|
},
|
|
|
|
|
[
|
|
|
|
|
config,
|
|
|
|
|
addItem,
|
|
|
|
|
onDebugMessage,
|
|
|
|
|
handleShellCommand,
|
|
|
|
|
handleSlashCommand,
|
|
|
|
|
logger,
|
|
|
|
|
shellModeActive,
|
|
|
|
|
schedule,
|
|
|
|
|
],
|
|
|
|
|
);
|
2025-04-20 20:20:40 +01:00
|
|
|
|
2025-05-24 00:44:17 -07:00
|
|
|
const ensureChatSession = useCallback(async (): Promise<{
|
2025-05-14 22:14:15 +00:00
|
|
|
client: GeminiClient | null;
|
2025-05-26 14:17:56 -07:00
|
|
|
chat: GeminiChat | null;
|
2025-05-14 22:14:15 +00:00
|
|
|
}> => {
|
|
|
|
|
const currentClient = geminiClientRef.current;
|
|
|
|
|
if (!currentClient) {
|
|
|
|
|
const errorMsg = 'Gemini client is not available.';
|
|
|
|
|
setInitError(errorMsg);
|
|
|
|
|
addItem({ type: MessageType.ERROR, text: errorMsg }, Date.now());
|
|
|
|
|
return { client: null, chat: null };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!chatSessionRef.current) {
|
|
|
|
|
try {
|
|
|
|
|
chatSessionRef.current = await currentClient.startChat();
|
|
|
|
|
} catch (err: unknown) {
|
|
|
|
|
const errorMsg = `Failed to start chat: ${getErrorMessage(err)}`;
|
2025-05-06 16:20:28 -07:00
|
|
|
setInitError(errorMsg);
|
2025-05-14 12:37:17 -07:00
|
|
|
addItem({ type: MessageType.ERROR, text: errorMsg }, Date.now());
|
2025-05-14 22:14:15 +00:00
|
|
|
return { client: currentClient, chat: null };
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return { client: currentClient, chat: chatSessionRef.current };
|
2025-05-24 00:44:17 -07:00
|
|
|
}, [addItem]);
|
2025-05-14 22:14:15 +00:00
|
|
|
|
|
|
|
|
// --- UI Helper Functions (used by event handlers) ---
|
|
|
|
|
const updateFunctionResponseUI = (
|
|
|
|
|
toolResponse: ToolCallResponseInfo,
|
|
|
|
|
status: ToolCallStatus,
|
|
|
|
|
) => {
|
|
|
|
|
setPendingHistoryItem((item) =>
|
|
|
|
|
item?.type === 'tool_group'
|
|
|
|
|
? {
|
|
|
|
|
...item,
|
2025-05-16 16:45:58 +00:00
|
|
|
tools: item.tools.map((tool) =>
|
|
|
|
|
tool.callId === toolResponse.callId
|
|
|
|
|
? {
|
|
|
|
|
...tool,
|
|
|
|
|
status,
|
|
|
|
|
resultDisplay: toolResponse.resultDisplay,
|
|
|
|
|
}
|
|
|
|
|
: tool,
|
|
|
|
|
),
|
2025-05-14 22:14:15 +00:00
|
|
|
}
|
2025-05-16 16:45:58 +00:00
|
|
|
: item,
|
2025-05-14 22:14:15 +00:00
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
2025-05-22 05:57:53 +00:00
|
|
|
// Extracted declineToolExecution to be part of wireConfirmationSubmission's closure
|
|
|
|
|
// or could be a standalone helper if more params are passed.
|
|
|
|
|
// TODO: handle file diff result display stuff
|
|
|
|
|
function _declineToolExecution(
|
|
|
|
|
declineMessage: string,
|
|
|
|
|
status: ToolCallStatus,
|
|
|
|
|
request: ServerToolCallConfirmationDetails['request'],
|
|
|
|
|
originalDetails: ServerToolCallConfirmationDetails['details'],
|
|
|
|
|
) {
|
|
|
|
|
let resultDisplay: ToolResultDisplay | undefined;
|
|
|
|
|
if ('fileDiff' in originalDetails) {
|
|
|
|
|
resultDisplay = {
|
|
|
|
|
fileDiff: (originalDetails as ToolEditConfirmationDetails).fileDiff,
|
|
|
|
|
fileName: (originalDetails as ToolEditConfirmationDetails).fileName,
|
2025-05-14 22:14:15 +00:00
|
|
|
};
|
2025-05-22 05:57:53 +00:00
|
|
|
} else {
|
|
|
|
|
resultDisplay = `~~${(originalDetails as ToolExecuteConfirmationDetails).command}~~`;
|
2025-05-14 22:14:15 +00:00
|
|
|
}
|
2025-05-22 05:57:53 +00:00
|
|
|
const functionResponse: Part = {
|
|
|
|
|
functionResponse: {
|
|
|
|
|
id: request.callId,
|
|
|
|
|
name: request.name,
|
|
|
|
|
response: { error: declineMessage },
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
const responseInfo: ToolCallResponseInfo = {
|
|
|
|
|
callId: request.callId,
|
2025-05-29 22:30:18 +00:00
|
|
|
responseParts: functionResponse,
|
2025-05-22 05:57:53 +00:00
|
|
|
resultDisplay,
|
|
|
|
|
error: new Error(declineMessage),
|
|
|
|
|
};
|
|
|
|
|
const history = chatSessionRef.current?.getHistory();
|
|
|
|
|
if (history) {
|
|
|
|
|
history.push({ role: 'model', parts: [functionResponse] });
|
|
|
|
|
}
|
|
|
|
|
updateFunctionResponseUI(responseInfo, status);
|
2025-05-24 00:44:17 -07:00
|
|
|
|
2025-05-22 05:57:53 +00:00
|
|
|
if (pendingHistoryItemRef.current) {
|
|
|
|
|
addItem(pendingHistoryItemRef.current, Date.now());
|
|
|
|
|
setPendingHistoryItem(null);
|
|
|
|
|
}
|
|
|
|
|
setIsResponding(false);
|
|
|
|
|
}
|
2025-05-14 22:14:15 +00:00
|
|
|
|
|
|
|
|
// --- Stream Event Handlers ---
|
2025-05-24 00:44:17 -07:00
|
|
|
|
|
|
|
|
const handleContentEvent = useCallback(
|
|
|
|
|
(
|
|
|
|
|
eventValue: ContentEvent['value'],
|
|
|
|
|
currentGeminiMessageBuffer: string,
|
|
|
|
|
userMessageTimestamp: number,
|
|
|
|
|
): string => {
|
|
|
|
|
let newGeminiMessageBuffer = currentGeminiMessageBuffer + eventValue;
|
|
|
|
|
if (
|
|
|
|
|
pendingHistoryItemRef.current?.type !== 'gemini' &&
|
|
|
|
|
pendingHistoryItemRef.current?.type !== 'gemini_content'
|
|
|
|
|
) {
|
|
|
|
|
if (pendingHistoryItemRef.current) {
|
|
|
|
|
addItem(pendingHistoryItemRef.current, userMessageTimestamp);
|
|
|
|
|
}
|
|
|
|
|
setPendingHistoryItem({ type: 'gemini', text: '' });
|
|
|
|
|
newGeminiMessageBuffer = eventValue;
|
|
|
|
|
}
|
|
|
|
|
// Split large messages for better rendering performance. Ideally,
|
|
|
|
|
// we should maximize the amount of output sent to <Static />.
|
|
|
|
|
const splitPoint = findLastSafeSplitPoint(newGeminiMessageBuffer);
|
|
|
|
|
if (splitPoint === newGeminiMessageBuffer.length) {
|
|
|
|
|
// Update the existing message with accumulated content
|
|
|
|
|
setPendingHistoryItem((item) => ({
|
|
|
|
|
type: item?.type as 'gemini' | 'gemini_content',
|
|
|
|
|
text: newGeminiMessageBuffer,
|
|
|
|
|
}));
|
|
|
|
|
} else {
|
|
|
|
|
// This indicates that we need to split up this Gemini Message.
|
|
|
|
|
// Splitting a message is primarily a performance consideration. There is a
|
|
|
|
|
// <Static> component at the root of App.tsx which takes care of rendering
|
|
|
|
|
// content statically or dynamically. Everything but the last message is
|
|
|
|
|
// treated as static in order to prevent re-rendering an entire message history
|
|
|
|
|
// multiple times per-second (as streaming occurs). Prior to this change you'd
|
|
|
|
|
// see heavy flickering of the terminal. This ensures that larger messages get
|
|
|
|
|
// broken up so that there are more "statically" rendered.
|
|
|
|
|
const beforeText = newGeminiMessageBuffer.substring(0, splitPoint);
|
|
|
|
|
const afterText = newGeminiMessageBuffer.substring(splitPoint);
|
|
|
|
|
addItem(
|
|
|
|
|
{
|
|
|
|
|
type: pendingHistoryItemRef.current?.type as
|
|
|
|
|
| 'gemini'
|
|
|
|
|
| 'gemini_content',
|
|
|
|
|
text: beforeText,
|
|
|
|
|
},
|
|
|
|
|
userMessageTimestamp,
|
|
|
|
|
);
|
|
|
|
|
setPendingHistoryItem({ type: 'gemini_content', text: afterText });
|
|
|
|
|
newGeminiMessageBuffer = afterText;
|
|
|
|
|
}
|
|
|
|
|
return newGeminiMessageBuffer;
|
|
|
|
|
},
|
|
|
|
|
[addItem, pendingHistoryItemRef, setPendingHistoryItem],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const handleUserCancelledEvent = useCallback(
|
|
|
|
|
(userMessageTimestamp: number) => {
|
2025-05-14 22:14:15 +00:00
|
|
|
if (pendingHistoryItemRef.current) {
|
2025-05-24 00:44:17 -07:00
|
|
|
if (pendingHistoryItemRef.current.type === 'tool_group') {
|
|
|
|
|
const updatedTools = pendingHistoryItemRef.current.tools.map(
|
|
|
|
|
(tool) =>
|
|
|
|
|
tool.status === ToolCallStatus.Pending ||
|
|
|
|
|
tool.status === ToolCallStatus.Confirming ||
|
|
|
|
|
tool.status === ToolCallStatus.Executing
|
|
|
|
|
? { ...tool, status: ToolCallStatus.Canceled }
|
|
|
|
|
: tool,
|
|
|
|
|
);
|
|
|
|
|
const pendingItem: HistoryItemToolGroup = {
|
|
|
|
|
...pendingHistoryItemRef.current,
|
|
|
|
|
tools: updatedTools,
|
|
|
|
|
};
|
|
|
|
|
addItem(pendingItem, userMessageTimestamp);
|
|
|
|
|
} else {
|
|
|
|
|
addItem(pendingHistoryItemRef.current, userMessageTimestamp);
|
|
|
|
|
}
|
|
|
|
|
setPendingHistoryItem(null);
|
2025-05-14 22:14:15 +00:00
|
|
|
}
|
|
|
|
|
addItem(
|
2025-05-24 00:44:17 -07:00
|
|
|
{ type: MessageType.INFO, text: 'User cancelled the request.' },
|
2025-05-14 22:14:15 +00:00
|
|
|
userMessageTimestamp,
|
|
|
|
|
);
|
2025-05-24 00:44:17 -07:00
|
|
|
setIsResponding(false);
|
|
|
|
|
cancel();
|
|
|
|
|
},
|
|
|
|
|
[addItem, pendingHistoryItemRef, setPendingHistoryItem, cancel],
|
|
|
|
|
);
|
2025-05-14 22:14:15 +00:00
|
|
|
|
2025-05-24 00:44:17 -07:00
|
|
|
const handleErrorEvent = useCallback(
|
|
|
|
|
(eventValue: ErrorEvent['value'], userMessageTimestamp: number) => {
|
|
|
|
|
if (pendingHistoryItemRef.current) {
|
2025-05-14 22:14:15 +00:00
|
|
|
addItem(pendingHistoryItemRef.current, userMessageTimestamp);
|
2025-05-24 00:44:17 -07:00
|
|
|
setPendingHistoryItem(null);
|
2025-05-14 22:14:15 +00:00
|
|
|
}
|
2025-05-24 00:44:17 -07:00
|
|
|
addItem(
|
|
|
|
|
{ type: MessageType.ERROR, text: `[API Error: ${eventValue.message}]` },
|
|
|
|
|
userMessageTimestamp,
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
[addItem, pendingHistoryItemRef, setPendingHistoryItem],
|
|
|
|
|
);
|
2025-05-14 22:14:15 +00:00
|
|
|
|
2025-05-24 00:44:17 -07:00
|
|
|
const processGeminiStreamEvents = useCallback(
|
|
|
|
|
async (
|
|
|
|
|
stream: AsyncIterable<GeminiEvent>,
|
|
|
|
|
userMessageTimestamp: number,
|
|
|
|
|
): Promise<StreamProcessingStatus> => {
|
|
|
|
|
let geminiMessageBuffer = '';
|
|
|
|
|
const toolCallRequests: ToolCallRequestInfo[] = [];
|
|
|
|
|
for await (const event of stream) {
|
|
|
|
|
if (event.type === ServerGeminiEventType.Content) {
|
|
|
|
|
geminiMessageBuffer = handleContentEvent(
|
|
|
|
|
event.value,
|
|
|
|
|
geminiMessageBuffer,
|
|
|
|
|
userMessageTimestamp,
|
|
|
|
|
);
|
|
|
|
|
} else if (event.type === ServerGeminiEventType.ToolCallRequest) {
|
|
|
|
|
toolCallRequests.push(event.value);
|
|
|
|
|
} else if (event.type === ServerGeminiEventType.UserCancelled) {
|
|
|
|
|
handleUserCancelledEvent(userMessageTimestamp);
|
|
|
|
|
cancel();
|
|
|
|
|
return StreamProcessingStatus.UserCancelled;
|
|
|
|
|
} else if (event.type === ServerGeminiEventType.Error) {
|
|
|
|
|
handleErrorEvent(event.value, userMessageTimestamp);
|
|
|
|
|
return StreamProcessingStatus.Error;
|
|
|
|
|
}
|
2025-05-14 22:14:15 +00:00
|
|
|
}
|
2025-05-24 00:44:17 -07:00
|
|
|
schedule(toolCallRequests);
|
|
|
|
|
return StreamProcessingStatus.Completed;
|
|
|
|
|
},
|
|
|
|
|
[
|
|
|
|
|
handleContentEvent,
|
|
|
|
|
handleUserCancelledEvent,
|
|
|
|
|
cancel,
|
|
|
|
|
handleErrorEvent,
|
|
|
|
|
schedule,
|
|
|
|
|
],
|
|
|
|
|
);
|
2025-05-22 05:57:53 +00:00
|
|
|
|
2025-05-14 22:14:15 +00:00
|
|
|
const submitQuery = useCallback(
|
|
|
|
|
async (query: PartListUnion) => {
|
2025-05-24 00:44:17 -07:00
|
|
|
if (
|
|
|
|
|
streamingState === StreamingState.Responding ||
|
|
|
|
|
streamingState === StreamingState.WaitingForConfirmation
|
|
|
|
|
)
|
|
|
|
|
return;
|
2025-05-14 22:14:15 +00:00
|
|
|
|
|
|
|
|
const userMessageTimestamp = Date.now();
|
|
|
|
|
setShowHelp(false);
|
|
|
|
|
|
|
|
|
|
abortControllerRef.current ??= new AbortController();
|
|
|
|
|
const signal = abortControllerRef.current.signal;
|
|
|
|
|
|
|
|
|
|
const { queryToSend, shouldProceed } = await prepareQueryForGemini(
|
|
|
|
|
query,
|
|
|
|
|
userMessageTimestamp,
|
|
|
|
|
signal,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (!shouldProceed || queryToSend === null) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const { client, chat } = await ensureChatSession();
|
|
|
|
|
|
|
|
|
|
if (!client || !chat) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-04-17 18:06:21 -04:00
|
|
|
|
2025-05-16 16:45:58 +00:00
|
|
|
setIsResponding(true);
|
2025-04-17 18:06:21 -04:00
|
|
|
setInitError(null);
|
2025-04-19 19:45:42 +01:00
|
|
|
|
2025-04-17 18:06:21 -04:00
|
|
|
try {
|
2025-05-14 22:14:15 +00:00
|
|
|
const stream = client.sendMessageStream(chat, queryToSend, signal);
|
|
|
|
|
const processingStatus = await processGeminiStreamEvents(
|
|
|
|
|
stream,
|
|
|
|
|
userMessageTimestamp,
|
2025-04-29 15:39:36 -07:00
|
|
|
);
|
2025-04-19 19:45:42 +01:00
|
|
|
|
2025-05-24 00:44:17 -07:00
|
|
|
if (processingStatus === StreamProcessingStatus.UserCancelled) {
|
2025-05-14 22:14:15 +00:00
|
|
|
return;
|
2025-05-14 12:37:17 -07:00
|
|
|
}
|
2025-04-21 14:32:18 -04:00
|
|
|
|
2025-05-07 12:57:19 -07:00
|
|
|
if (pendingHistoryItemRef.current) {
|
|
|
|
|
addItem(pendingHistoryItemRef.current, userMessageTimestamp);
|
|
|
|
|
setPendingHistoryItem(null);
|
|
|
|
|
}
|
2025-04-18 17:47:49 -04:00
|
|
|
} catch (error: unknown) {
|
|
|
|
|
if (!isNodeError(error) || error.name !== 'AbortError') {
|
2025-05-06 16:20:28 -07:00
|
|
|
addItem(
|
2025-04-17 18:06:21 -04:00
|
|
|
{
|
2025-05-14 12:37:17 -07:00
|
|
|
type: MessageType.ERROR,
|
2025-05-15 23:51:53 -07:00
|
|
|
text: `[Stream Error: ${getErrorMessage(error) || 'Unknown error'}]`,
|
2025-04-17 18:06:21 -04:00
|
|
|
},
|
2025-05-06 16:20:28 -07:00
|
|
|
userMessageTimestamp,
|
2025-04-17 18:06:21 -04:00
|
|
|
);
|
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__
2025-04-15 21:41:08 -07:00
|
|
|
}
|
2025-04-17 18:06:21 -04:00
|
|
|
} finally {
|
2025-05-20 23:56:43 -07:00
|
|
|
abortControllerRef.current = null; // Always reset
|
2025-05-16 16:45:58 +00:00
|
|
|
setIsResponding(false);
|
2025-04-21 14:32:18 -04:00
|
|
|
}
|
2025-04-17 18:06:21 -04:00
|
|
|
},
|
2025-04-19 19:45:42 +01:00
|
|
|
[
|
2025-05-07 12:57:19 -07:00
|
|
|
setShowHelp,
|
2025-05-06 16:20:28 -07:00
|
|
|
addItem,
|
2025-05-14 22:14:15 +00:00
|
|
|
setInitError,
|
2025-05-24 00:44:17 -07:00
|
|
|
ensureChatSession,
|
|
|
|
|
prepareQueryForGemini,
|
|
|
|
|
processGeminiStreamEvents,
|
|
|
|
|
setPendingHistoryItem,
|
|
|
|
|
pendingHistoryItemRef,
|
|
|
|
|
streamingState,
|
2025-04-19 19:45:42 +01:00
|
|
|
],
|
2025-04-17 18:06:21 -04:00
|
|
|
);
|
|
|
|
|
|
2025-05-22 05:57:53 +00:00
|
|
|
const pendingHistoryItems = [
|
|
|
|
|
pendingHistoryItemRef.current,
|
|
|
|
|
pendingToolCalls,
|
|
|
|
|
].filter((i) => i !== undefined && i !== null);
|
2025-05-16 16:45:58 +00:00
|
|
|
|
2025-04-29 23:38:26 +00:00
|
|
|
return {
|
|
|
|
|
streamingState,
|
|
|
|
|
submitQuery,
|
|
|
|
|
initError,
|
2025-05-22 05:57:53 +00:00
|
|
|
pendingHistoryItems,
|
2025-04-29 23:38:26 +00:00
|
|
|
};
|
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__
2025-04-15 21:41:08 -07:00
|
|
|
};
|