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' ;
2025-08-26 00:04:53 +02:00
import type {
2025-06-11 15:33:09 -04:00
Config ,
2025-08-28 16:42:54 -07:00
EditorType ,
2025-04-19 19:45:42 +01:00
GeminiClient ,
2025-06-13 21:21:40 -07:00
ServerGeminiChatCompressedEvent ,
2025-08-28 16:42:54 -07:00
ServerGeminiContentEvent as ContentEvent ,
2025-07-22 06:57:11 +09:00
ServerGeminiFinishedEvent ,
2025-08-28 16:42:54 -07:00
ServerGeminiStreamEvent as GeminiEvent ,
2025-08-26 00:04:53 +02:00
ThoughtSummary ,
2025-08-28 16:42:54 -07:00
ToolCallRequestInfo ,
GeminiErrorEventValue ,
2025-08-26 00:04:53 +02:00
} from '@google/gemini-cli-core' ;
import {
GeminiEventType as ServerGeminiEventType ,
2025-04-19 19:45:42 +01:00
getErrorMessage ,
isNodeError ,
2025-05-21 07:36:22 +00:00
MessageSenderType ,
2025-06-05 16:04:25 -04:00
logUserPrompt ,
2025-06-11 15:33:09 -04:00
GitService ,
2025-06-23 18:37:41 -07:00
UnauthorizedError ,
2025-06-22 09:26:48 -05:00
UserPromptEvent ,
2025-07-09 10:18:15 -04:00
DEFAULT_GEMINI_FLASH_MODEL ,
2025-08-25 16:06:47 -04:00
logConversationFinishedEvent ,
ConversationFinishedEvent ,
ApprovalMode ,
2025-08-13 17:57:11 +00:00
parseAndFormatApiError ,
2025-09-14 20:20:21 -07:00
ToolConfirmationOutcome ,
2025-09-09 01:14:15 -04:00
promptIdContext ,
2025-06-25 05:41:11 -07:00
} from '@google/gemini-cli-core' ;
2025-07-22 06:57:11 +09:00
import { type Part , type PartListUnion , FinishReason } from '@google/genai' ;
2025-08-26 00:04:53 +02:00
import type {
2025-06-11 15:33:09 -04:00
HistoryItem ,
2025-05-07 12:57:19 -07:00
HistoryItemWithoutId ,
2025-05-14 22:14:15 +00:00
HistoryItemToolGroup ,
2025-07-07 16:45:44 -04:00
SlashCommandProcessorResult ,
2025-04-19 19:45:42 +01:00
} from '../types.js' ;
2025-08-26 00:04:53 +02:00
import { StreamingState , MessageType , ToolCallStatus } from '../types.js' ;
2025-08-26 11:51:27 +08:00
import { isAtCommand , isSlashCommand } 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-08-26 00:04:53 +02:00
import type { UseHistoryManagerReturn } from './useHistoryManager.js' ;
2025-05-21 07:36:22 +00:00
import { useLogger } from './useLogger.js' ;
2025-06-01 14:16:24 -07:00
import {
useReactToolScheduler ,
mapToDisplay as mapTrackedToolCallsToDisplay ,
2025-09-14 20:20:21 -07:00
type TrackedToolCall ,
type TrackedCompletedToolCall ,
type TrackedCancelledToolCall ,
type TrackedWaitingToolCall ,
2025-06-01 14:16:24 -07:00
} from './useReactToolScheduler.js' ;
2025-09-14 20:20:21 -07:00
import { promises as fs } from 'node:fs' ;
import path from 'node:path' ;
2025-07-10 00:19:30 +05:30
import { useSessionStats } from '../contexts/SessionContext.js' ;
2025-08-12 14:05:49 -07:00
import { useKeypress } from './useKeypress.js' ;
2025-08-28 16:42:54 -07:00
import type { LoadedSettings } from '../../config/settings.js' ;
2025-04-28 12:38:07 -07:00
2025-05-14 22:14:15 +00:00
enum StreamProcessingStatus {
Completed ,
UserCancelled ,
Error ,
}
2025-09-14 20:20:21 -07:00
const EDIT_TOOL_NAMES = new Set ( [ 'replace' , 'write_file' ] ) ;
2025-09-16 19:40:51 -07:00
function showCitations ( settings : LoadedSettings ) : boolean {
2025-09-02 09:36:24 -07:00
const enabled = settings ? . merged ? . ui ? . showCitations ;
if ( enabled !== undefined ) {
return enabled ;
}
2025-09-16 19:40:51 -07:00
return true ;
2025-09-02 09:36:24 -07:00
}
2025-05-14 22:14:15 +00:00
/ * *
2025-06-01 14:16:24 -07:00
* Manages the Gemini stream , including user input , command processing ,
* API interaction , and tool call lifecycle .
2025-05-14 22:14:15 +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
export const useGeminiStream = (
2025-06-15 22:09:30 -04:00
geminiClient : GeminiClient ,
2025-06-11 15:33:09 -04:00
history : HistoryItem [ ] ,
2025-05-06 16:20:28 -07:00
addItem : UseHistoryManagerReturn [ 'addItem' ] ,
2025-04-20 21:06:22 +01:00
config : Config ,
2025-08-28 16:42:54 -07:00
settings : LoadedSettings ,
2025-05-13 23:55:49 +00:00
onDebugMessage : ( message : string ) = > void ,
2025-05-23 08:47:19 -07:00
handleSlashCommand : (
cmd : PartListUnion ,
2025-07-07 16:45:44 -04:00
) = > Promise < SlashCommandProcessorResult | false > ,
2025-05-19 16:11:45 -07:00
shellModeActive : boolean ,
2025-06-12 02:21:54 +01:00
getPreferredEditor : ( ) = > EditorType | undefined ,
2025-09-05 15:35:41 -07:00
onAuthError : ( error : string ) = > void ,
2025-06-22 01:35:36 -04:00
performMemoryRefresh : ( ) = > Promise < void > ,
2025-07-09 13:55:56 -04:00
modelSwitchedFromQuotaError : boolean ,
setModelSwitchedFromQuotaError : React.Dispatch < React.SetStateAction < boolean > > ,
2025-08-05 14:55:54 -07:00
onEditorClose : ( ) = > void ,
2025-08-06 15:19:10 -04:00
onCancelSubmit : ( ) = > void ,
2025-09-11 13:27:27 -07:00
setShellInputFocused : ( value : boolean ) = > void ,
terminalWidth : number ,
terminalHeight : number ,
isShellFocused? : 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-06-20 23:01:44 -04:00
const turnCancelledRef = useRef ( false ) ;
2025-05-16 16:45:58 +00:00
const [ isResponding , setIsResponding ] = useState < boolean > ( false ) ;
2025-06-15 11:19:05 -07:00
const [ thought , setThought ] = useState < ThoughtSummary | null > ( null ) ;
2025-09-17 15:37:13 -07:00
const [ pendingHistoryItem , pendingHistoryItemRef , setPendingHistoryItem ] =
2025-05-07 12:57:19 -07:00
useStateAndRef < HistoryItemWithoutId | null > ( null ) ;
2025-06-22 01:35:36 -04:00
const processedMemoryToolsRef = useRef < Set < string > > ( new Set ( ) ) ;
2025-07-10 00:19:30 +05:30
const { startNewPrompt , getPromptCount } = useSessionStats ( ) ;
2025-08-20 10:55:47 +09:00
const storage = config . storage ;
const logger = useLogger ( storage ) ;
2025-06-11 15:33:09 -04:00
const gitService = useMemo ( ( ) = > {
if ( ! config . getProjectRoot ( ) ) {
return ;
}
2025-08-20 10:55:47 +09:00
return new GitService ( config . getProjectRoot ( ) , storage ) ;
} , [ config , storage ] ) ;
2025-06-01 14:16:24 -07:00
2025-06-08 15:42:49 -07:00
const [ toolCalls , scheduleToolCalls , markToolsAsSubmitted ] =
useReactToolScheduler (
2025-06-27 16:39:54 -07:00
async ( completedToolCallsFromScheduler ) = > {
2025-06-08 15:42:49 -07:00
// This onComplete is called when ALL scheduled tools for a given batch are done.
if ( completedToolCallsFromScheduler . length > 0 ) {
// Add the final state of these tools to the history for display.
addItem (
mapTrackedToolCallsToDisplay (
completedToolCallsFromScheduler as TrackedToolCall [ ] ,
) ,
Date . now ( ) ,
) ;
2025-06-27 16:39:54 -07:00
2025-10-06 13:34:00 -06:00
// Record tool calls with full metadata before sending responses.
try {
const currentModel =
config . getGeminiClient ( ) . getCurrentSequenceModel ( ) ? ?
config . getModel ( ) ;
config
. getGeminiClient ( )
. getChat ( )
. recordCompletedToolCalls (
currentModel ,
completedToolCallsFromScheduler ,
) ;
} catch ( error ) {
console . error (
` Error recording completed tool call information: ${ error } ` ,
) ;
}
2025-06-27 16:39:54 -07:00
// Handle tool response submission immediately when tools complete
await handleCompletedTools (
completedToolCallsFromScheduler as TrackedToolCall [ ] ,
) ;
2025-06-08 15:42:49 -07:00
}
} ,
config ,
2025-06-12 02:21:54 +01:00
getPreferredEditor ,
2025-08-05 14:55:54 -07:00
onEditorClose ,
2025-06-08 15:42:49 -07:00
) ;
2025-06-01 14:16:24 -07:00
const pendingToolCallGroupDisplay = useMemo (
( ) = >
toolCalls . length ? mapTrackedToolCallsToDisplay ( toolCalls ) : undefined ,
2025-05-22 05:57:53 +00:00
[ toolCalls ] ,
) ;
2025-04-17 18:06:21 -04:00
2025-09-11 13:27:27 -07:00
const activeToolPtyId = useMemo ( ( ) = > {
const executingShellTool = toolCalls ? . find (
( tc ) = >
tc . status === 'executing' && tc . request . name === 'run_shell_command' ,
) ;
if ( executingShellTool ) {
return ( executingShellTool as { pid? : number } ) . pid ;
}
return undefined ;
} , [ toolCalls ] ) ;
2025-07-14 20:25:16 -07:00
const loopDetectedRef = useRef ( false ) ;
2025-09-10 22:20:13 -07:00
const [
loopDetectionConfirmationRequest ,
setLoopDetectionConfirmationRequest ,
] = useState < {
onComplete : ( result : { userSelection : 'disable' | 'keep' } ) = > void ;
} | null > ( null ) ;
2025-07-14 20:25:16 -07:00
2025-05-16 16:45:58 +00:00
const onExec = useCallback ( async ( done : Promise < void > ) = > {
setIsResponding ( true ) ;
await done ;
setIsResponding ( false ) ;
} , [ ] ) ;
2025-09-11 13:27:27 -07:00
const { handleShellCommand , activeShellPtyId } = 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-06-15 22:09:30 -04:00
geminiClient ,
2025-09-11 13:27:27 -07:00
setShellInputFocused ,
terminalWidth ,
terminalHeight ,
2025-04-30 00:26:07 +00:00
) ;
2025-09-11 13:27:27 -07:00
const activePtyId = activeShellPtyId || activeToolPtyId ;
useEffect ( ( ) = > {
if ( ! activePtyId ) {
setShellInputFocused ( false ) ;
}
} , [ activePtyId , setShellInputFocused ] ) ;
2025-05-24 00:44:17 -07:00
const streamingState = useMemo ( ( ) = > {
2025-06-01 14:16:24 -07:00
if ( toolCalls . some ( ( tc ) = > tc . status === 'awaiting_approval' ) ) {
2025-05-24 00:44:17 -07:00
return StreamingState . WaitingForConfirmation ;
}
if (
isResponding ||
toolCalls . some (
2025-06-01 14:16:24 -07:00
( tc ) = >
tc . status === 'executing' ||
tc . status === 'scheduled' ||
2025-06-19 18:25:23 -07:00
tc . status === 'validating' ||
( ( tc . status === 'success' ||
tc . status === 'error' ||
tc . status === 'cancelled' ) &&
! ( tc as TrackedCompletedToolCall | TrackedCancelledToolCall )
. responseSubmittedToGemini ) ,
2025-05-24 00:44:17 -07:00
)
) {
return StreamingState . Responding ;
}
return StreamingState . Idle ;
} , [ isResponding , toolCalls ] ) ;
2025-08-25 16:06:47 -04:00
useEffect ( ( ) = > {
if (
config . getApprovalMode ( ) === ApprovalMode . YOLO &&
streamingState === StreamingState . Idle
) {
const lastUserMessageIndex = history . findLastIndex (
( item : HistoryItem ) = > item . type === MessageType . USER ,
) ;
const turnCount =
lastUserMessageIndex === - 1 ? 0 : history.length - lastUserMessageIndex ;
if ( turnCount > 0 ) {
logConversationFinishedEvent (
config ,
new ConversationFinishedEvent ( config . getApprovalMode ( ) , turnCount ) ,
) ;
}
}
} , [ streamingState , config , history ] ) ;
2025-08-12 09:43:57 +05:30
const cancelOngoingRequest = useCallback ( ( ) = > {
if ( streamingState !== StreamingState . Responding ) {
return ;
}
if ( turnCancelledRef . current ) {
return ;
}
turnCancelledRef . current = true ;
abortControllerRef . current ? . abort ( ) ;
if ( pendingHistoryItemRef . current ) {
addItem ( pendingHistoryItemRef . current , Date . now ( ) ) ;
}
addItem (
{
type : MessageType . INFO ,
text : 'Request cancelled.' ,
} ,
Date . now ( ) ,
) ;
setPendingHistoryItem ( null ) ;
onCancelSubmit ( ) ;
setIsResponding ( false ) ;
2025-09-11 13:27:27 -07:00
setShellInputFocused ( false ) ;
2025-08-12 09:43:57 +05:30
} , [
streamingState ,
addItem ,
setPendingHistoryItem ,
onCancelSubmit ,
pendingHistoryItemRef ,
2025-09-11 13:27:27 -07:00
setShellInputFocused ,
2025-08-12 09:43:57 +05:30
] ) ;
2025-08-12 14:05:49 -07:00
useKeypress (
( key ) = > {
2025-09-11 13:27:27 -07:00
if ( key . name === 'escape' && ! isShellFocused ) {
2025-08-12 14:05:49 -07:00
cancelOngoingRequest ( ) ;
}
} ,
{ isActive : streamingState === StreamingState . Responding } ,
) ;
2025-04-17 18:06:21 -04:00
2025-05-24 00:44:17 -07:00
const prepareQueryForGemini = useCallback (
async (
query : PartListUnion ,
userMessageTimestamp : number ,
2025-05-30 01:35:03 -07:00
abortSignal : AbortSignal ,
2025-07-10 00:19:30 +05:30
prompt_id : string ,
2025-05-24 00:44:17 -07:00
) : Promise < {
queryToSend : PartListUnion | null ;
shouldProceed : boolean ;
} > = > {
2025-06-20 23:01:44 -04:00
if ( turnCancelledRef . current ) {
return { queryToSend : null , shouldProceed : false } ;
}
2025-05-24 00:44:17 -07:00
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
2025-08-26 11:51:27 +08:00
const slashCommandResult = isSlashCommand ( trimmedQuery )
? await handleSlashCommand ( trimmedQuery )
: false ;
2025-07-07 16:45:44 -04:00
if ( slashCommandResult ) {
2025-07-22 00:34:55 -04:00
switch ( slashCommandResult . type ) {
case 'schedule_tool' : {
const { toolName , toolArgs } = slashCommandResult ;
const toolCallRequest : ToolCallRequestInfo = {
callId : ` ${ toolName } - ${ Date . now ( ) } - ${ Math . random ( ) . toString ( 16 ) . slice ( 2 ) } ` ,
name : toolName ,
args : toolArgs ,
isClientInitiated : true ,
prompt_id ,
} ;
scheduleToolCalls ( [ toolCallRequest ] , abortSignal ) ;
return { queryToSend : null , shouldProceed : false } ;
}
case 'submit_prompt' : {
localQueryToSendToGemini = slashCommandResult . content ;
2025-07-07 16:45:44 -04:00
2025-07-22 00:34:55 -04:00
return {
queryToSend : localQueryToSendToGemini ,
shouldProceed : true ,
} ;
}
case 'handled' : {
return { queryToSend : null , shouldProceed : false } ;
}
default : {
const unreachable : never = slashCommandResult ;
throw new Error (
` Unhandled slash command result type: ${ unreachable } ` ,
) ;
}
}
2025-05-23 08:47:19 -07:00
}
2025-05-30 01:35:03 -07:00
if ( shellModeActive && handleShellCommand ( trimmedQuery , abortSignal ) ) {
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 ,
2025-05-30 01:35:03 -07:00
signal : abortSignal ,
2025-05-24 00:44:17 -07:00
} ) ;
2025-08-20 15:51:31 -04:00
// Add user's turn after @ command processing is done.
addItem (
{ type : MessageType . USER , text : trimmedQuery } ,
userMessageTimestamp ,
) ;
2025-05-24 00:44:17 -07:00
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 ,
2025-06-01 14:16:24 -07:00
scheduleToolCalls ,
2025-05-24 00:44:17 -07:00
] ,
) ;
2025-04-20 20:20:40 +01:00
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 = > {
2025-06-20 23:01:44 -04:00
if ( turnCancelledRef . current ) {
// Prevents additional output after a user initiated cancel.
return '' ;
}
2025-05-24 00:44:17 -07:00
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-06-20 23:01:44 -04:00
if ( turnCancelledRef . current ) {
return ;
}
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 ) ;
2025-07-28 23:27:33 +05:30
setThought ( null ) ; // Reset thought when user cancels
2025-05-24 00:44:17 -07:00
} ,
2025-07-28 23:27:33 +05:30
[ addItem , pendingHistoryItemRef , setPendingHistoryItem , setThought ] ,
2025-05-24 00:44:17 -07:00
) ;
2025-05-14 22:14:15 +00:00
2025-05-24 00:44:17 -07:00
const handleErrorEvent = useCallback (
2025-08-28 16:42:54 -07:00
( eventValue : GeminiErrorEventValue , userMessageTimestamp : number ) = > {
2025-05-24 00:44:17 -07:00
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 (
2025-06-23 17:30:13 -04:00
{
type : MessageType . ERROR ,
2025-06-23 23:43:00 -04:00
text : parseAndFormatApiError (
eventValue . error ,
2025-07-11 14:08:49 -07:00
config . getContentGeneratorConfig ( ) ? . authType ,
2025-07-09 10:18:15 -04:00
undefined ,
config . getModel ( ) ,
DEFAULT_GEMINI_FLASH_MODEL ,
2025-06-23 23:43:00 -04:00
) ,
2025-06-23 17:30:13 -04:00
} ,
2025-05-24 00:44:17 -07:00
userMessageTimestamp ,
) ;
2025-07-28 23:27:33 +05:30
setThought ( null ) ; // Reset thought when there's an error
2025-05-24 00:44:17 -07:00
} ,
2025-07-28 23:27:33 +05:30
[ addItem , pendingHistoryItemRef , setPendingHistoryItem , config , setThought ] ,
2025-05-24 00:44:17 -07:00
) ;
2025-05-14 22:14:15 +00:00
2025-08-28 16:42:54 -07:00
const handleCitationEvent = useCallback (
( text : string , userMessageTimestamp : number ) = > {
2025-09-16 19:40:51 -07:00
if ( ! showCitations ( settings ) ) {
2025-08-28 16:42:54 -07:00
return ;
}
2025-09-02 09:36:24 -07:00
2025-08-28 16:42:54 -07:00
if ( pendingHistoryItemRef . current ) {
addItem ( pendingHistoryItemRef . current , userMessageTimestamp ) ;
setPendingHistoryItem ( null ) ;
}
addItem ( { type : MessageType . INFO , text } , userMessageTimestamp ) ;
} ,
2025-09-16 19:40:51 -07:00
[ addItem , pendingHistoryItemRef , setPendingHistoryItem , settings ] ,
2025-08-28 16:42:54 -07:00
) ;
2025-07-22 06:57:11 +09:00
const handleFinishedEvent = useCallback (
( event : ServerGeminiFinishedEvent , userMessageTimestamp : number ) = > {
2025-09-02 23:29:07 -06:00
const finishReason = event . value . reason ;
if ( ! finishReason ) {
return ;
}
2025-07-22 06:57:11 +09:00
const finishReasonMessages : Record < FinishReason , string | undefined > = {
[ FinishReason . FINISH_REASON_UNSPECIFIED ] : undefined ,
[ FinishReason . STOP ] : undefined ,
[ FinishReason . MAX_TOKENS ] : 'Response truncated due to token limits.' ,
[ FinishReason . SAFETY ] : 'Response stopped due to safety reasons.' ,
[ FinishReason . RECITATION ] : 'Response stopped due to recitation policy.' ,
[ FinishReason . LANGUAGE ] :
'Response stopped due to unsupported language.' ,
[ FinishReason . BLOCKLIST ] : 'Response stopped due to forbidden terms.' ,
[ FinishReason . PROHIBITED_CONTENT ] :
'Response stopped due to prohibited content.' ,
[ FinishReason . SPII ] :
'Response stopped due to sensitive personally identifiable information.' ,
[ FinishReason . OTHER ] : 'Response stopped for other reasons.' ,
[ FinishReason . MALFORMED_FUNCTION_CALL ] :
'Response stopped due to malformed function call.' ,
[ FinishReason . IMAGE_SAFETY ] :
'Response stopped due to image safety violations.' ,
[ FinishReason . UNEXPECTED_TOOL_CALL ] :
'Response stopped due to unexpected tool call.' ,
} ;
const message = finishReasonMessages [ finishReason ] ;
if ( message ) {
addItem (
{
type : 'info' ,
text : ` ⚠️ ${ message } ` ,
} ,
userMessageTimestamp ,
) ;
}
} ,
[ addItem ] ,
) ;
2025-06-03 19:19:49 +00:00
const handleChatCompressionEvent = useCallback (
2025-09-17 13:12:06 -07:00
(
eventValue : ServerGeminiChatCompressedEvent [ 'value' ] ,
userMessageTimestamp : number ,
) = > {
if ( pendingHistoryItemRef . current ) {
addItem ( pendingHistoryItemRef . current , userMessageTimestamp ) ;
setPendingHistoryItem ( null ) ;
}
return addItem (
2025-06-03 19:19:49 +00:00
{
type : 'info' ,
2025-06-13 21:21:40 -07:00
text :
` IMPORTANT: This conversation approached the input token limit for ${ config . getModel ( ) } . ` +
` A compressed context will be sent for future messages (compressed from: ` +
2025-06-17 08:44:54 -07:00
` ${ eventValue ? . originalTokenCount ? ? 'unknown' } to ` +
` ${ eventValue ? . newTokenCount ? ? 'unknown' } tokens). ` ,
2025-06-03 19:19:49 +00:00
} ,
Date . now ( ) ,
2025-09-17 13:12:06 -07:00
) ;
} ,
[ addItem , config , pendingHistoryItemRef , setPendingHistoryItem ] ,
2025-06-03 19:19:49 +00:00
) ;
2025-07-11 07:55:03 -07:00
const handleMaxSessionTurnsEvent = useCallback (
( ) = >
addItem (
{
type : 'info' ,
text :
` The session has reached the maximum number of turns: ${ config . getMaxSessionTurns ( ) } . ` +
` Please update this limit in your setting.json file. ` ,
} ,
Date . now ( ) ,
) ,
[ addItem , config ] ,
) ;
2025-09-10 22:20:13 -07:00
const handleLoopDetectionConfirmation = useCallback (
( result : { userSelection : 'disable' | 'keep' } ) = > {
setLoopDetectionConfirmationRequest ( null ) ;
if ( result . userSelection === 'disable' ) {
config . getGeminiClient ( ) . getLoopDetectionService ( ) . disableForSession ( ) ;
addItem (
{
type : 'info' ,
text : ` Loop detection has been disabled for this session. Please try your request again. ` ,
} ,
Date . now ( ) ,
) ;
} else {
addItem (
{
type : 'info' ,
text : ` A potential loop was detected. This can happen due to repetitive tool calls or other model behavior. The request has been halted. ` ,
} ,
Date . now ( ) ,
) ;
}
} ,
[ config , addItem ] ,
) ;
2025-07-14 20:25:16 -07:00
const handleLoopDetectedEvent = useCallback ( ( ) = > {
2025-09-10 22:20:13 -07:00
// Show the confirmation dialog to choose whether to disable loop detection
setLoopDetectionConfirmationRequest ( {
onComplete : handleLoopDetectionConfirmation ,
} ) ;
} , [ handleLoopDetectionConfirmation ] ) ;
2025-07-14 20:25:16 -07:00
2025-05-24 00:44:17 -07:00
const processGeminiStreamEvents = useCallback (
async (
stream : AsyncIterable < GeminiEvent > ,
userMessageTimestamp : number ,
2025-06-08 15:42:49 -07:00
signal : AbortSignal ,
2025-05-24 00:44:17 -07:00
) : Promise < StreamProcessingStatus > = > {
let geminiMessageBuffer = '' ;
const toolCallRequests : ToolCallRequestInfo [ ] = [ ] ;
for await ( const event of stream ) {
2025-06-03 19:19:49 +00:00
switch ( event . type ) {
2025-06-15 11:19:05 -07:00
case ServerGeminiEventType . Thought :
setThought ( event . value ) ;
break ;
2025-06-03 19:19:49 +00:00
case ServerGeminiEventType . Content :
geminiMessageBuffer = handleContentEvent (
event . value ,
geminiMessageBuffer ,
userMessageTimestamp ,
) ;
break ;
case ServerGeminiEventType . ToolCallRequest :
toolCallRequests . push ( event . value ) ;
break ;
case ServerGeminiEventType . UserCancelled :
handleUserCancelledEvent ( userMessageTimestamp ) ;
break ;
case ServerGeminiEventType . Error :
handleErrorEvent ( event . value , userMessageTimestamp ) ;
break ;
case ServerGeminiEventType . ChatCompressed :
2025-09-17 13:12:06 -07:00
handleChatCompressionEvent ( event . value , userMessageTimestamp ) ;
2025-06-03 19:19:49 +00:00
break ;
case ServerGeminiEventType . ToolCallConfirmation :
case ServerGeminiEventType . ToolCallResponse :
2025-06-11 21:59:46 -07:00
// do nothing
2025-06-03 19:19:49 +00:00
break ;
2025-07-11 07:55:03 -07:00
case ServerGeminiEventType . MaxSessionTurns :
handleMaxSessionTurnsEvent ( ) ;
break ;
2025-07-22 06:57:11 +09:00
case ServerGeminiEventType . Finished :
handleFinishedEvent (
event as ServerGeminiFinishedEvent ,
userMessageTimestamp ,
) ;
break ;
2025-08-28 16:42:54 -07:00
case ServerGeminiEventType . Citation :
handleCitationEvent ( event . value , userMessageTimestamp ) ;
break ;
2025-07-14 20:25:16 -07:00
case ServerGeminiEventType . LoopDetected :
// handle later because we want to move pending history to history
// before we add loop detected message to history
loopDetectedRef . current = true ;
break ;
2025-09-03 22:00:16 -04:00
case ServerGeminiEventType . Retry :
// Will add the missing logic later
break ;
2025-06-03 19:19:49 +00:00
default : {
// enforces exhaustive switch-case
const unreachable : never = event ;
return unreachable ;
}
2025-05-24 00:44:17 -07:00
}
2025-05-14 22:14:15 +00:00
}
2025-06-01 14:16:24 -07:00
if ( toolCallRequests . length > 0 ) {
2025-06-08 15:42:49 -07:00
scheduleToolCalls ( toolCallRequests , signal ) ;
2025-06-01 14:16:24 -07:00
}
2025-05-24 00:44:17 -07:00
return StreamProcessingStatus . Completed ;
} ,
[
handleContentEvent ,
handleUserCancelledEvent ,
handleErrorEvent ,
2025-06-01 14:16:24 -07:00
scheduleToolCalls ,
2025-06-03 19:19:49 +00:00
handleChatCompressionEvent ,
2025-07-22 06:57:11 +09:00
handleFinishedEvent ,
2025-07-11 07:55:03 -07:00
handleMaxSessionTurnsEvent ,
2025-08-28 16:42:54 -07:00
handleCitationEvent ,
2025-05-24 00:44:17 -07:00
] ,
) ;
2025-05-22 05:57:53 +00:00
2025-05-14 22:14:15 +00:00
const submitQuery = useCallback (
2025-07-10 00:19:30 +05:30
async (
query : PartListUnion ,
options ? : { isContinuation : boolean } ,
prompt_id? : string ,
) = > {
2025-05-24 00:44:17 -07:00
if (
2025-06-19 18:25:23 -07:00
( streamingState === StreamingState . Responding ||
streamingState === StreamingState . WaitingForConfirmation ) &&
! options ? . isContinuation
2025-05-24 00:44:17 -07:00
)
return ;
2025-05-14 22:14:15 +00:00
const userMessageTimestamp = Date . now ( ) ;
2025-07-09 13:55:56 -04:00
// Reset quota error flag when starting a new query (not a continuation)
if ( ! options ? . isContinuation ) {
setModelSwitchedFromQuotaError ( false ) ;
config . setQuotaErrorOccurred ( false ) ;
}
2025-05-30 00:46:43 -07:00
abortControllerRef . current = new AbortController ( ) ;
2025-05-30 01:35:03 -07:00
const abortSignal = abortControllerRef . current . signal ;
2025-06-20 23:01:44 -04:00
turnCancelledRef . current = false ;
2025-05-14 22:14:15 +00:00
2025-07-10 00:19:30 +05:30
if ( ! prompt_id ) {
prompt_id = config . getSessionId ( ) + '########' + getPromptCount ( ) ;
}
2025-09-09 01:14:15 -04:00
return promptIdContext . run ( prompt_id , async ( ) = > {
const { queryToSend , shouldProceed } = await prepareQueryForGemini (
query ,
2025-05-14 22:14:15 +00:00
userMessageTimestamp ,
2025-06-08 15:42:49 -07:00
abortSignal ,
2025-09-09 01:14:15 -04:00
prompt_id ,
2025-04-29 15:39:36 -07:00
) ;
2025-04-19 19:45:42 +01:00
2025-09-09 01:14:15 -04:00
if ( ! shouldProceed || queryToSend === null ) {
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-09-09 01:14:15 -04:00
if ( ! options ? . isContinuation ) {
2025-09-22 14:31:06 -07:00
if ( typeof queryToSend === 'string' ) {
// logging the text prompts only for now
const promptText = queryToSend ;
logUserPrompt (
config ,
new UserPromptEvent (
promptText . length ,
prompt_id ,
config . getContentGeneratorConfig ( ) ? . authType ,
promptText ,
) ,
) ;
}
2025-09-09 01:14:15 -04:00
startNewPrompt ( ) ;
setThought ( null ) ; // Reset thought when starting a new prompt
2025-05-07 12:57:19 -07:00
}
2025-09-09 01:14:15 -04:00
setIsResponding ( true ) ;
setInitError ( null ) ;
try {
const stream = geminiClient . sendMessageStream (
queryToSend ,
abortSignal ,
prompt_id ,
) ;
const processingStatus = await processGeminiStreamEvents (
stream ,
2025-05-06 16:20:28 -07:00
userMessageTimestamp ,
2025-09-09 01:14:15 -04:00
abortSignal ,
2025-04-17 18:06:21 -04:00
) ;
2025-09-09 01:14:15 -04:00
if ( processingStatus === StreamProcessingStatus . UserCancelled ) {
return ;
}
if ( pendingHistoryItemRef . current ) {
addItem ( pendingHistoryItemRef . current , userMessageTimestamp ) ;
setPendingHistoryItem ( null ) ;
}
if ( loopDetectedRef . current ) {
loopDetectedRef . current = false ;
handleLoopDetectedEvent ( ) ;
}
} catch ( error : unknown ) {
if ( error instanceof UnauthorizedError ) {
onAuthError ( 'Session expired or is unauthorized.' ) ;
} else if ( ! isNodeError ( error ) || error . name !== 'AbortError' ) {
addItem (
{
type : MessageType . ERROR ,
text : parseAndFormatApiError (
getErrorMessage ( error ) || 'Unknown error' ,
config . getContentGeneratorConfig ( ) ? . authType ,
undefined ,
config . getModel ( ) ,
DEFAULT_GEMINI_FLASH_MODEL ,
) ,
} ,
userMessageTimestamp ,
) ;
}
} finally {
setIsResponding ( false ) ;
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-09-09 01:14:15 -04:00
} ) ;
2025-04-17 18:06:21 -04:00
} ,
2025-04-19 19:45:42 +01:00
[
2025-06-01 14:16:24 -07:00
streamingState ,
2025-07-09 13:55:56 -04:00
setModelSwitchedFromQuotaError ,
2025-05-24 00:44:17 -07:00
prepareQueryForGemini ,
processGeminiStreamEvents ,
pendingHistoryItemRef ,
2025-06-01 14:16:24 -07:00
addItem ,
setPendingHistoryItem ,
setInitError ,
2025-06-05 21:33:24 +00:00
geminiClient ,
2025-06-19 16:52:22 -07:00
onAuthError ,
2025-06-23 23:43:00 -04:00
config ,
2025-07-10 00:19:30 +05:30
startNewPrompt ,
getPromptCount ,
2025-07-14 20:25:16 -07:00
handleLoopDetectedEvent ,
2025-04-19 19:45:42 +01:00
] ,
2025-04-17 18:06:21 -04:00
) ;
2025-09-14 20:20:21 -07:00
const handleApprovalModeChange = useCallback (
async ( newApprovalMode : ApprovalMode ) = > {
// Auto-approve pending tool calls when switching to auto-approval modes
if (
newApprovalMode === ApprovalMode . YOLO ||
newApprovalMode === ApprovalMode . AUTO_EDIT
) {
let awaitingApprovalCalls = toolCalls . filter (
( call ) : call is TrackedWaitingToolCall = >
call . status === 'awaiting_approval' ,
) ;
// For AUTO_EDIT mode, only approve edit tools (replace, write_file)
if ( newApprovalMode === ApprovalMode . AUTO_EDIT ) {
awaitingApprovalCalls = awaitingApprovalCalls . filter ( ( call ) = >
EDIT_TOOL_NAMES . has ( call . request . name ) ,
) ;
}
// Process pending tool calls sequentially to reduce UI chaos
for ( const call of awaitingApprovalCalls ) {
if ( call . confirmationDetails ? . onConfirm ) {
try {
await call . confirmationDetails . onConfirm (
ToolConfirmationOutcome . ProceedOnce ,
) ;
} catch ( error ) {
console . error (
` Failed to auto-approve tool call ${ call . request . callId } : ` ,
error ,
) ;
}
}
}
}
} ,
[ toolCalls ] ,
) ;
2025-06-27 16:39:54 -07:00
const handleCompletedTools = useCallback (
async ( completedToolCallsFromScheduler : TrackedToolCall [ ] ) = > {
2025-06-22 01:35:36 -04:00
if ( isResponding ) {
return ;
}
2025-06-01 14:16:24 -07:00
2025-06-27 16:39:54 -07:00
const completedAndReadyToSubmitTools =
completedToolCallsFromScheduler . filter (
(
tc : TrackedToolCall ,
) : tc is TrackedCompletedToolCall | TrackedCancelledToolCall = > {
const isTerminalState =
tc . status === 'success' ||
tc . status === 'error' ||
tc . status === 'cancelled' ;
if ( isTerminalState ) {
const completedOrCancelledCall = tc as
| TrackedCompletedToolCall
| TrackedCancelledToolCall ;
return (
completedOrCancelledCall . response ? . responseParts !== undefined
) ;
}
return false ;
} ,
) ;
2025-06-22 01:35:36 -04:00
// Finalize any client-initiated tools as soon as they are done.
const clientTools = completedAndReadyToSubmitTools . filter (
( t ) = > t . request . isClientInitiated ,
) ;
if ( clientTools . length > 0 ) {
markToolsAsSubmitted ( clientTools . map ( ( t ) = > t . request . callId ) ) ;
}
// Identify new, successful save_memory calls that we haven't processed yet.
const newSuccessfulMemorySaves = completedAndReadyToSubmitTools . filter (
( t ) = >
t . request . name === 'save_memory' &&
t . status === 'success' &&
! processedMemoryToolsRef . current . has ( t . request . callId ) ,
) ;
if ( newSuccessfulMemorySaves . length > 0 ) {
// Perform the refresh only if there are new ones.
void performMemoryRefresh ( ) ;
// Mark them as processed so we don't do this again on the next render.
newSuccessfulMemorySaves . forEach ( ( t ) = >
processedMemoryToolsRef . current . add ( t . request . callId ) ,
) ;
}
const geminiTools = completedAndReadyToSubmitTools . filter (
( t ) = > ! t . request . isClientInitiated ,
) ;
if ( geminiTools . length === 0 ) {
return ;
}
2025-06-01 14:16:24 -07:00
2025-06-08 11:14:45 -07:00
// If all the tools were cancelled, don't submit a response to Gemini.
2025-06-22 01:35:36 -04:00
const allToolsCancelled = geminiTools . every (
2025-06-08 11:14:45 -07:00
( tc ) = > tc . status === 'cancelled' ,
) ;
if ( allToolsCancelled ) {
if ( geminiClient ) {
// We need to manually add the function responses to the history
// so the model knows the tools were cancelled.
2025-08-22 14:12:05 -07:00
const combinedParts = geminiTools . flatMap (
2025-06-08 11:14:45 -07:00
( toolCall ) = > toolCall . response . responseParts ,
) ;
2025-07-05 13:56:39 -07:00
geminiClient . addHistory ( {
role : 'user' ,
parts : combinedParts ,
} ) ;
2025-06-08 11:14:45 -07:00
}
2025-06-22 01:35:36 -04:00
const callIdsToMarkAsSubmitted = geminiTools . map (
2025-06-08 11:14:45 -07:00
( toolCall ) = > toolCall . request . callId ,
) ;
markToolsAsSubmitted ( callIdsToMarkAsSubmitted ) ;
return ;
}
2025-08-22 14:12:05 -07:00
const responsesToSend : Part [ ] = geminiTools . flatMap (
2025-06-22 01:35:36 -04:00
( toolCall ) = > toolCall . response . responseParts ,
) ;
const callIdsToMarkAsSubmitted = geminiTools . map (
2025-06-01 14:16:24 -07:00
( toolCall ) = > toolCall . request . callId ,
) ;
2025-07-10 00:19:30 +05:30
const prompt_ids = geminiTools . map (
( toolCall ) = > toolCall . request . prompt_id ,
) ;
2025-06-01 14:16:24 -07:00
markToolsAsSubmitted ( callIdsToMarkAsSubmitted ) ;
2025-07-09 13:55:56 -04:00
// Don't continue if model was switched due to quota error
if ( modelSwitchedFromQuotaError ) {
return ;
}
2025-07-10 00:19:30 +05:30
submitQuery (
2025-08-22 14:12:05 -07:00
responsesToSend ,
2025-07-10 00:19:30 +05:30
{
isContinuation : true ,
} ,
prompt_ids [ 0 ] ,
) ;
2025-06-27 16:39:54 -07:00
} ,
[
isResponding ,
submitQuery ,
markToolsAsSubmitted ,
geminiClient ,
performMemoryRefresh ,
2025-07-09 13:55:56 -04:00
modelSwitchedFromQuotaError ,
2025-06-27 16:39:54 -07:00
] ,
) ;
2025-06-01 14:16:24 -07:00
2025-09-17 15:37:13 -07:00
const pendingHistoryItems = useMemo (
( ) = >
[ pendingHistoryItem , pendingToolCallGroupDisplay ] . filter (
( i ) = > i !== undefined && i !== null ,
) ,
[ pendingHistoryItem , pendingToolCallGroupDisplay ] ,
) ;
2025-05-16 16:45:58 +00:00
2025-06-11 15:33:09 -04:00
useEffect ( ( ) = > {
const saveRestorableToolCalls = async ( ) = > {
2025-06-20 00:39:15 -04:00
if ( ! config . getCheckpointingEnabled ( ) ) {
2025-06-11 15:33:09 -04:00
return ;
}
const restorableToolCalls = toolCalls . filter (
( toolCall ) = >
2025-09-14 20:20:21 -07:00
EDIT_TOOL_NAMES . has ( toolCall . request . name ) &&
2025-06-11 15:33:09 -04:00
toolCall . status === 'awaiting_approval' ,
) ;
if ( restorableToolCalls . length > 0 ) {
2025-08-20 10:55:47 +09:00
const checkpointDir = storage . getProjectTempCheckpointsDir ( ) ;
2025-06-11 15:33:09 -04:00
if ( ! checkpointDir ) {
return ;
}
try {
await fs . mkdir ( checkpointDir , { recursive : true } ) ;
} catch ( error ) {
if ( ! isNodeError ( error ) || error . code !== 'EEXIST' ) {
onDebugMessage (
` Failed to create checkpoint directory: ${ getErrorMessage ( error ) } ` ,
) ;
return ;
}
}
for ( const toolCall of restorableToolCalls ) {
const filePath = toolCall . request . args [ 'file_path' ] as string ;
if ( ! filePath ) {
onDebugMessage (
` Skipping restorable tool call due to missing file_path: ${ toolCall . request . name } ` ,
) ;
continue ;
}
try {
2025-08-22 08:29:52 -07:00
if ( ! gitService ) {
onDebugMessage (
` Checkpointing is enabled but Git service is not available. Failed to create snapshot for ${ filePath } . Ensure Git is installed and working properly. ` ,
) ;
continue ;
}
let commitHash : string | undefined ;
try {
commitHash = await gitService . createFileSnapshot (
` Snapshot for ${ toolCall . request . name } ` ,
) ;
} catch ( error ) {
onDebugMessage (
` Failed to create new snapshot: ${ getErrorMessage ( error ) } . Attempting to use current commit. ` ,
) ;
}
2025-06-11 15:33:09 -04:00
if ( ! commitHash ) {
2025-08-22 08:29:52 -07:00
commitHash = await gitService . getCurrentCommitHash ( ) ;
2025-06-11 15:33:09 -04:00
}
if ( ! commitHash ) {
onDebugMessage (
2025-08-22 08:29:52 -07:00
` Failed to create snapshot for ${ filePath } . Checkpointing may not be working properly. Ensure Git is installed and the project directory is accessible. ` ,
2025-06-11 15:33:09 -04:00
) ;
continue ;
}
const timestamp = new Date ( )
. toISOString ( )
. replace ( /:/g , '-' )
. replace ( /\./g , '_' ) ;
const toolName = toolCall . request . name ;
const fileName = path . basename ( filePath ) ;
const toolCallWithSnapshotFileName = ` ${ timestamp } - ${ fileName } - ${ toolName } .json ` ;
const clientHistory = await geminiClient ? . getHistory ( ) ;
const toolCallWithSnapshotFilePath = path . join (
checkpointDir ,
toolCallWithSnapshotFileName ,
) ;
await fs . writeFile (
toolCallWithSnapshotFilePath ,
JSON . stringify (
{
history ,
clientHistory ,
toolCall : {
name : toolCall.request.name ,
args : toolCall.request.args ,
} ,
commitHash ,
filePath ,
} ,
null ,
2 ,
) ,
) ;
} catch ( error ) {
onDebugMessage (
2025-08-22 08:29:52 -07:00
` Failed to create checkpoint for ${ filePath } : ${ getErrorMessage (
2025-06-11 15:33:09 -04:00
error ,
2025-08-22 08:29:52 -07:00
) } . This may indicate a problem with Git or file system permissions . ` ,
2025-06-11 15:33:09 -04:00
) ;
}
}
}
} ;
saveRestorableToolCalls ( ) ;
2025-08-20 10:55:47 +09:00
} , [
toolCalls ,
config ,
onDebugMessage ,
gitService ,
history ,
geminiClient ,
storage ,
] ) ;
2025-06-11 15:33:09 -04:00
2025-04-29 23:38:26 +00:00
return {
streamingState ,
submitQuery ,
initError ,
2025-05-22 05:57:53 +00:00
pendingHistoryItems ,
2025-06-15 11:19:05 -07:00
thought ,
2025-08-12 09:43:57 +05:30
cancelOngoingRequest ,
2025-09-14 20:20:21 -07:00
pendingToolCalls : toolCalls ,
handleApprovalModeChange ,
2025-09-11 13:27:27 -07:00
activePtyId ,
2025-09-10 22:20:13 -07:00
loopDetectionConfirmationRequest ,
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
} ;