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 {
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 ,
2026-02-13 11:14:35 +09:00
MessageBusType ,
2025-09-09 01:14:15 -04:00
promptIdContext ,
2025-10-09 10:22:26 -07:00
tokenLimit ,
2025-10-28 19:05:48 +00:00
debugLogger ,
2025-10-27 19:16:44 -04:00
runInDevTraceSpan ,
2025-12-09 10:08:23 -05:00
EDIT_TOOL_NAMES ,
2026-01-29 09:00:46 -05:00
ASK_USER_TOOL_NAME ,
2025-12-09 10:08:23 -05:00
processRestorableToolCalls ,
2025-12-17 15:12:59 -08:00
recordToolCallInteractions ,
2025-12-31 07:22:53 +08:00
ToolErrorType ,
2026-01-20 16:23:01 -08:00
ValidationRequiredError ,
2026-01-13 23:03:19 -05:00
coreEvents ,
CoreEvent ,
2026-02-13 17:20:14 -05:00
CoreToolCallStatus ,
2026-02-18 14:05:50 -08:00
buildUserSteeringHintPrompt ,
2026-02-26 18:26:16 -08:00
GeminiCliOperation ,
2026-02-24 14:31:41 -05:00
getPlanModeExitMessage ,
2026-01-13 23:03:19 -05:00
} from '@google/gemini-cli-core' ;
import type {
Config ,
EditorType ,
GeminiClient ,
ServerGeminiChatCompressedEvent ,
ServerGeminiContentEvent as ContentEvent ,
ServerGeminiFinishedEvent ,
ServerGeminiStreamEvent as GeminiEvent ,
ThoughtSummary ,
ToolCallRequestInfo ,
2026-01-30 09:53:09 -08:00
ToolCallResponseInfo ,
2026-01-13 23:03:19 -05:00
GeminiErrorEventValue ,
RetryAttemptPayload ,
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 ,
2026-02-09 19:24:41 -08:00
HistoryItemThinking ,
2025-05-07 12:57:19 -07:00
HistoryItemWithoutId ,
2025-05-14 22:14:15 +00:00
HistoryItemToolGroup ,
2026-02-18 14:05:50 -08:00
HistoryItemInfo ,
2026-01-27 16:06:24 -08:00
IndividualToolCallDisplay ,
2025-07-07 16:45:44 -04:00
SlashCommandProcessorResult ,
2025-11-13 19:11:06 -08:00
HistoryItemModel ,
2025-04-19 19:45:42 +01:00
} from '../types.js' ;
2026-02-13 17:20:14 -05:00
import { StreamingState , MessageType } 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' ;
2026-02-09 19:24:41 -08:00
import { getInlineThinkingMode } from '../utils/inlineThinkingMode.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-11-19 15:49:39 -08:00
import { SHELL_COMMAND_NAME } from '../constants.js' ;
2026-01-19 20:00:42 -05:00
import { mapToDisplay as mapTrackedToolCallsToDisplay } from './toolMapping.js' ;
2025-06-01 14:16:24 -07:00
import {
2026-01-21 00:18:42 -05:00
useToolScheduler ,
2025-09-14 20:20:21 -07:00
type TrackedToolCall ,
type TrackedCompletedToolCall ,
type TrackedCancelledToolCall ,
type TrackedWaitingToolCall ,
2026-01-30 09:53:09 -08:00
type TrackedExecutingToolCall ,
2026-01-21 00:18:42 -05:00
} from './useToolScheduler.js' ;
2026-02-17 18:41:43 -08:00
import { theme } from '../semantic-colors.js' ;
import { getToolGroupBorderAppearance } from '../utils/borderStyles.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
2026-01-30 09:53:09 -08:00
type ToolResponseWithParts = ToolCallResponseInfo & {
llmContent? : PartListUnion ;
} ;
interface ShellToolData {
pid? : number ;
command? : string ;
initialOutput? : string ;
}
2025-05-14 22:14:15 +00:00
enum StreamProcessingStatus {
Completed ,
UserCancelled ,
Error ,
}
2026-02-27 14:15:10 -05:00
const SUPPRESSED_TOOL_ERRORS_NOTE =
2026-03-02 20:32:50 -08:00
'Some internal tool attempts failed before this final error. Press F12 for diagnostics, or run /settings and change "Error Verbosity" to full for details.' ;
2026-02-27 14:15:10 -05:00
const LOW_VERBOSITY_FAILURE_NOTE =
2026-03-02 20:32:50 -08:00
'This request failed. Press F12 for diagnostics, or run /settings and change "Error Verbosity" to full for full details.' ;
2026-02-27 14:15:10 -05:00
2026-01-30 09:53:09 -08:00
function isShellToolData ( data : unknown ) : data is ShellToolData {
if ( typeof data !== 'object' || data === null ) {
return false ;
}
const d = data as Partial < ShellToolData > ;
return (
( d . pid === undefined || typeof d . pid === 'number' ) &&
( d . command === undefined || typeof d . command === 'string' ) &&
( d . initialOutput === undefined || typeof d . initialOutput === 'string' )
) ;
}
2025-09-16 19:40:51 -07:00
function showCitations ( settings : LoadedSettings ) : boolean {
2026-01-15 09:26:10 -08:00
const enabled = settings . merged . ui . showCitations ;
2025-09-02 09:36:24 -07:00
if ( enabled !== undefined ) {
return enabled ;
}
2025-09-16 19:40:51 -07:00
return true ;
2025-09-02 09:36:24 -07:00
}
2026-01-27 16:06:24 -08:00
/ * *
* Calculates the current streaming state based on tool call status and responding flag .
* /
function calculateStreamingState (
isResponding : boolean ,
toolCalls : TrackedToolCall [ ] ,
) : StreamingState {
2026-02-13 17:20:14 -05:00
if (
toolCalls . some ( ( tc ) = > tc . status === CoreToolCallStatus . AwaitingApproval )
) {
2026-01-27 16:06:24 -08:00
return StreamingState . WaitingForConfirmation ;
}
const isAnyToolActive = toolCalls . some ( ( tc ) = > {
// These statuses indicate active processing
if (
2026-02-13 17:20:14 -05:00
tc . status === CoreToolCallStatus . Executing ||
tc . status === CoreToolCallStatus . Scheduled ||
tc . status === CoreToolCallStatus . Validating
2026-01-27 16:06:24 -08:00
) {
return true ;
}
// Terminal statuses (success, error, cancelled) still count as "Responding"
// if the result hasn't been submitted back to Gemini yet.
if (
2026-02-13 17:20:14 -05:00
tc . status === CoreToolCallStatus . Success ||
tc . status === CoreToolCallStatus . Error ||
tc . status === CoreToolCallStatus . Cancelled
2026-01-27 16:06:24 -08:00
) {
return ! ( tc as TrackedCompletedToolCall | TrackedCancelledToolCall )
. responseSubmittedToGemini ;
}
return false ;
} ) ;
if ( isResponding || isAnyToolActive ) {
return StreamingState . Responding ;
}
return StreamingState . Idle ;
}
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-11-19 11:11:36 +08:00
onCancelSubmit : ( shouldRestorePrompt? : boolean ) = > void ,
2025-09-11 13:27:27 -07:00
setShellInputFocused : ( value : boolean ) = > void ,
terminalWidth : number ,
terminalHeight : number ,
isShellFocused? : boolean ,
2026-02-18 14:05:50 -08:00
consumeUserHint ? : ( ) = > string | null ,
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 ) ;
2026-01-13 23:03:19 -05:00
const [ retryStatus , setRetryStatus ] = useState < RetryAttemptPayload | null > (
null ,
) ;
2026-02-27 14:15:10 -05:00
const isLowErrorVerbosity = settings . merged . ui ? . errorVerbosity !== 'full' ;
const suppressedToolErrorCountRef = useRef ( 0 ) ;
const suppressedToolErrorNoteShownRef = useRef ( false ) ;
const lowVerbosityFailureNoteShownRef = useRef ( false ) ;
2025-04-17 18:06:21 -04:00
const abortControllerRef = useRef < AbortController | null > ( null ) ;
2025-06-20 23:01:44 -04:00
const turnCancelledRef = useRef ( false ) ;
2025-10-27 09:59:08 -07:00
const activeQueryIdRef = useRef < string | null > ( null ) ;
2026-02-24 14:31:41 -05:00
const previousApprovalModeRef = useRef < ApprovalMode > (
config . getApprovalMode ( ) ,
) ;
2026-03-10 15:41:16 -03:00
const [ isResponding , setIsRespondingState ] = useState < boolean > ( false ) ;
const isRespondingRef = useRef < boolean > ( false ) ;
const setIsResponding = useCallback (
( value : boolean ) = > {
setIsRespondingState ( value ) ;
isRespondingRef . current = value ;
} ,
[ setIsRespondingState ] ,
) ;
2026-02-09 19:24:41 -08:00
const [ thought , thoughtRef , setThought ] =
useStateAndRef < 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 ) ;
2026-02-09 19:24:41 -08:00
2026-01-13 06:19:53 -08:00
const [ lastGeminiActivityTime , setLastGeminiActivityTime ] =
useState < number > ( 0 ) ;
2026-01-27 16:06:24 -08:00
const [ pushedToolCallIds , pushedToolCallIdsRef , setPushedToolCallIds ] =
useStateAndRef < Set < string > > ( new Set ( ) ) ;
const [ _isFirstToolInGroup , isFirstToolInGroupRef , setIsFirstToolInGroup ] =
useStateAndRef < boolean > ( true ) ;
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
2026-01-13 23:03:19 -05:00
useEffect ( ( ) = > {
const handleRetryAttempt = ( payload : RetryAttemptPayload ) = > {
setRetryStatus ( payload ) ;
} ;
coreEvents . on ( CoreEvent . RetryAttempt , handleRetryAttempt ) ;
return ( ) = > {
coreEvents . off ( CoreEvent . RetryAttempt , handleRetryAttempt ) ;
} ;
} , [ ] ) ;
2025-10-27 09:59:08 -07:00
const [
toolCalls ,
scheduleToolCalls ,
markToolsAsSubmitted ,
setToolCallsForDisplay ,
cancelAllToolCalls ,
2025-11-21 12:19:34 -05:00
lastToolOutputTime ,
2026-01-21 00:18:42 -05:00
] = useToolScheduler (
2025-10-27 09:59:08 -07:00
async ( completedToolCallsFromScheduler ) = > {
// This onComplete is called when ALL scheduled tools for a given batch are done.
if ( completedToolCallsFromScheduler . length > 0 ) {
2026-01-27 16:06:24 -08:00
// Add only the tools that haven't been pushed to history yet.
const toolsToPush = completedToolCallsFromScheduler . filter (
( tc ) = > ! pushedToolCallIdsRef . current . has ( tc . request . callId ) ,
2025-10-27 09:59:08 -07:00
) ;
2026-01-27 16:06:24 -08:00
if ( toolsToPush . length > 0 ) {
addItem (
mapTrackedToolCallsToDisplay ( toolsToPush as TrackedToolCall [ ] , {
borderTop : isFirstToolInGroupRef.current ,
borderBottom : true ,
2026-02-17 18:41:43 -08:00
borderColor : theme.border.default ,
borderDimColor : false ,
2026-01-27 16:06:24 -08:00
} ) ,
) ;
}
2025-06-27 16:39:54 -07:00
2025-10-27 09:59:08 -07:00
// Clear the live-updating display now that the final state is in history.
setToolCallsForDisplay ( [ ] ) ;
2025-10-06 13:34:00 -06:00
2025-10-27 09:59:08 -07:00
// Record tool calls with full metadata before sending responses.
try {
const currentModel =
config . getGeminiClient ( ) . getCurrentSequenceModel ( ) ? ?
config . getModel ( ) ;
config
. getGeminiClient ( )
. getChat ( )
. recordCompletedToolCalls (
currentModel ,
completedToolCallsFromScheduler ,
) ;
2025-12-17 15:12:59 -08:00
await recordToolCallInteractions (
config ,
completedToolCallsFromScheduler ,
) ;
2025-10-27 09:59:08 -07:00
} catch ( error ) {
2025-10-28 19:05:48 +00:00
debugLogger . warn (
2025-10-27 09:59:08 -07:00
` Error recording completed tool call information: ${ error } ` ,
2025-06-27 16:39:54 -07:00
) ;
2025-06-08 15:42:49 -07:00
}
2025-10-27 09:59:08 -07:00
// Handle tool response submission immediately when tools complete
await handleCompletedTools (
completedToolCallsFromScheduler as TrackedToolCall [ ] ,
) ;
}
} ,
config ,
getPreferredEditor ,
) ;
2025-06-01 14:16:24 -07:00
2026-02-17 18:41:43 -08:00
const activeToolPtyId = useMemo ( ( ) = > {
const executingShellTool = toolCalls . find (
( tc ) = >
tc . status === 'executing' && tc . request . name === 'run_shell_command' ,
) ;
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
return ( executingShellTool as TrackedExecutingToolCall | undefined ) ? . pid ;
} , [ toolCalls ] ) ;
2026-03-10 15:41:16 -03:00
const onExec = useCallback (
async ( done : Promise < void > ) = > {
setIsResponding ( true ) ;
await done ;
setIsResponding ( false ) ;
} ,
[ setIsResponding ] ,
) ;
2026-02-17 18:41:43 -08:00
const {
handleShellCommand ,
activeShellPtyId ,
lastShellOutputTime ,
backgroundShellCount ,
isBackgroundShellVisible ,
toggleBackgroundShell ,
backgroundCurrentShell ,
registerBackgroundShell ,
dismissBackgroundShell ,
backgroundShells ,
} = useShellCommandProcessor (
addItem ,
setPendingHistoryItem ,
onExec ,
onDebugMessage ,
config ,
geminiClient ,
setShellInputFocused ,
terminalWidth ,
terminalHeight ,
activeToolPtyId ,
) ;
2026-01-27 16:06:24 -08:00
const streamingState = useMemo (
( ) = > calculateStreamingState ( isResponding , toolCalls ) ,
[ isResponding , toolCalls ] ,
2025-05-22 05:57:53 +00:00
) ;
2025-04-17 18:06:21 -04:00
2026-01-27 16:06:24 -08:00
// Reset tracking when a new batch of tools starts
useEffect ( ( ) = > {
if ( toolCalls . length > 0 ) {
const isNewBatch = ! toolCalls . some ( ( tc ) = >
pushedToolCallIdsRef . current . has ( tc . request . callId ) ,
) ;
if ( isNewBatch ) {
setPushedToolCallIds ( new Set ( ) ) ;
setIsFirstToolInGroup ( true ) ;
}
} else if ( streamingState === StreamingState . Idle ) {
// Clear when idle to be ready for next turn
setPushedToolCallIds ( new Set ( ) ) ;
setIsFirstToolInGroup ( true ) ;
}
} , [
toolCalls ,
pushedToolCallIdsRef ,
setPushedToolCallIds ,
setIsFirstToolInGroup ,
streamingState ,
] ) ;
// Push completed tools to history as they finish
useEffect ( ( ) = > {
const toolsToPush : TrackedToolCall [ ] = [ ] ;
for ( const tc of toolCalls ) {
if ( pushedToolCallIdsRef . current . has ( tc . request . callId ) ) continue ;
if (
tc . status === 'success' ||
tc . status === 'error' ||
tc . status === 'cancelled'
) {
toolsToPush . push ( tc ) ;
} else {
// Stop at first non-terminal tool to preserve order
break ;
}
}
if ( toolsToPush . length > 0 ) {
const newPushed = new Set ( pushedToolCallIdsRef . current ) ;
let isFirst = isFirstToolInGroupRef . current ;
for ( const tc of toolsToPush ) {
newPushed . add ( tc . request . callId ) ;
const isLastInBatch = tc === toolCalls [ toolCalls . length - 1 ] ;
const historyItem = mapTrackedToolCallsToDisplay ( tc , {
borderTop : isFirst ,
borderBottom : isLastInBatch ,
2026-02-17 18:41:43 -08:00
. . . getToolGroupBorderAppearance (
{ type : 'tool_group' , tools : toolCalls } ,
activeShellPtyId ,
! ! isShellFocused ,
[ ] ,
backgroundShells ,
) ,
2026-01-27 16:06:24 -08:00
} ) ;
addItem ( historyItem ) ;
isFirst = false ;
}
setPushedToolCallIds ( newPushed ) ;
setIsFirstToolInGroup ( false ) ;
}
} , [
toolCalls ,
pushedToolCallIdsRef ,
isFirstToolInGroupRef ,
setPushedToolCallIds ,
setIsFirstToolInGroup ,
addItem ,
2026-02-17 18:41:43 -08:00
activeShellPtyId ,
isShellFocused ,
backgroundShells ,
2026-01-27 16:06:24 -08:00
] ) ;
const pendingToolGroupItems = useMemo ( ( ) : HistoryItemWithoutId [ ] = > {
const remainingTools = toolCalls . filter (
( tc ) = > ! pushedToolCallIds . has ( tc . request . callId ) ,
) ;
const items : HistoryItemWithoutId [ ] = [ ] ;
2026-02-17 18:41:43 -08:00
const appearance = getToolGroupBorderAppearance (
{ type : 'tool_group' , tools : toolCalls } ,
activeShellPtyId ,
! ! isShellFocused ,
[ ] ,
backgroundShells ,
) ;
2026-01-27 16:06:24 -08:00
if ( remainingTools . length > 0 ) {
items . push (
mapTrackedToolCallsToDisplay ( remainingTools , {
borderTop : pushedToolCallIds.size === 0 ,
borderBottom : false , // Stay open to connect with the slice below
2026-02-17 18:41:43 -08:00
. . . appearance ,
2026-01-27 16:06:24 -08:00
} ) ,
) ;
}
// Always show a bottom border slice if we have ANY tools in the batch
// and we haven't finished pushing the whole batch to history yet.
// Once all tools are terminal and pushed, the last history item handles the closing border.
const allTerminal =
toolCalls . length > 0 &&
toolCalls . every (
( tc ) = >
tc . status === 'success' ||
tc . status === 'error' ||
tc . status === 'cancelled' ,
) ;
const allPushed =
toolCalls . length > 0 &&
toolCalls . every ( ( tc ) = > pushedToolCallIds . has ( tc . request . callId ) ) ;
const anyVisibleInHistory = pushedToolCallIds . size > 0 ;
const anyVisibleInPending = remainingTools . some ( ( tc ) = > {
2026-01-29 09:00:46 -05:00
// AskUser tools are rendered by AskUserDialog, not ToolGroupMessage
const isInProgress =
tc . status !== 'success' &&
tc . status !== 'error' &&
tc . status !== 'cancelled' ;
if ( tc . request . name === ASK_USER_TOOL_NAME && isInProgress ) {
return false ;
}
2026-01-27 16:06:24 -08:00
return (
tc . status !== 'scheduled' &&
tc . status !== 'validating' &&
tc . status !== 'awaiting_approval'
) ;
} ) ;
if (
toolCalls . length > 0 &&
! ( allTerminal && allPushed ) &&
( anyVisibleInHistory || anyVisibleInPending )
) {
items . push ( {
type : 'tool_group' as const ,
tools : [ ] as IndividualToolCallDisplay [ ] ,
borderTop : false ,
borderBottom : true ,
2026-02-17 18:41:43 -08:00
. . . appearance ,
2026-01-27 16:06:24 -08:00
} ) ;
}
return items ;
2026-02-17 18:41:43 -08:00
} , [
toolCalls ,
pushedToolCallIds ,
activeShellPtyId ,
isShellFocused ,
backgroundShells ,
] ) ;
2025-09-11 13:27:27 -07:00
2025-10-21 13:27:57 -07:00
const lastQueryRef = useRef < PartListUnion | null > ( null ) ;
const lastPromptIdRef = useRef < string | null > ( null ) ;
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-09-11 13:27:27 -07:00
const activePtyId = activeShellPtyId || activeToolPtyId ;
2025-11-19 15:49:39 -08:00
const prevActiveShellPtyIdRef = useRef < number | null > ( null ) ;
useEffect ( ( ) = > {
if (
turnCancelledRef . current &&
prevActiveShellPtyIdRef . current !== null &&
activeShellPtyId === null
) {
2026-01-13 14:15:04 -05:00
addItem ( { type : MessageType . INFO , text : 'Request cancelled.' } ) ;
2025-11-19 15:49:39 -08:00
setIsResponding ( false ) ;
}
prevActiveShellPtyIdRef . current = activeShellPtyId ;
2026-03-10 15:41:16 -03:00
} , [ activeShellPtyId , addItem , setIsResponding ] ) ;
2025-11-19 15:49:39 -08:00
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 ] ) ;
2026-01-13 23:03:19 -05:00
useEffect ( ( ) = > {
if ( ! isResponding ) {
setRetryStatus ( null ) ;
}
} , [ isResponding ] ) ;
2026-02-27 14:15:10 -05:00
const maybeAddSuppressedToolErrorNote = useCallback (
( userMessageTimestamp? : number ) = > {
if ( ! isLowErrorVerbosity ) {
return ;
}
if ( suppressedToolErrorCountRef . current === 0 ) {
return ;
}
if ( suppressedToolErrorNoteShownRef . current ) {
return ;
}
addItem (
{
type : MessageType . INFO ,
text : SUPPRESSED_TOOL_ERRORS_NOTE ,
} ,
userMessageTimestamp ,
) ;
suppressedToolErrorNoteShownRef . current = true ;
} ,
[ addItem , isLowErrorVerbosity ] ,
) ;
const maybeAddLowVerbosityFailureNote = useCallback (
( userMessageTimestamp? : number ) = > {
if ( ! isLowErrorVerbosity || config . getDebugMode ( ) ) {
return ;
}
2026-03-04 13:20:08 -08:00
if (
lowVerbosityFailureNoteShownRef . current ||
suppressedToolErrorNoteShownRef . current
) {
2026-02-27 14:15:10 -05:00
return ;
}
addItem (
{
type : MessageType . INFO ,
text : LOW_VERBOSITY_FAILURE_NOTE ,
} ,
userMessageTimestamp ,
) ;
lowVerbosityFailureNoteShownRef . current = true ;
} ,
[ addItem , config , isLowErrorVerbosity ] ,
) ;
2025-08-12 09:43:57 +05:30
const cancelOngoingRequest = useCallback ( ( ) = > {
2025-10-27 09:59:08 -07:00
if (
streamingState !== StreamingState . Responding &&
streamingState !== StreamingState . WaitingForConfirmation
) {
2025-08-12 09:43:57 +05:30
return ;
}
if ( turnCancelledRef . current ) {
return ;
}
turnCancelledRef . current = true ;
2025-10-27 09:59:08 -07:00
// A full cancellation means no tools have produced a final result yet.
// This determines if we show a generic "Request cancelled" message.
const isFullCancellation = ! toolCalls . some (
( tc ) = > tc . status === 'success' || tc . status === 'error' ,
) ;
// Ensure we have an abort controller, creating one if it doesn't exist.
if ( ! abortControllerRef . current ) {
abortControllerRef . current = new AbortController ( ) ;
}
// The order is important here.
// 1. Fire the signal to interrupt any active async operations.
abortControllerRef . current . abort ( ) ;
// 2. Call the imperative cancel to clear the queue of pending tools.
cancelAllToolCalls ( abortControllerRef . current . signal ) ;
2025-08-12 09:43:57 +05:30
if ( pendingHistoryItemRef . current ) {
2025-11-19 15:49:39 -08:00
const isShellCommand =
pendingHistoryItemRef . current . type === 'tool_group' &&
pendingHistoryItemRef . current . tools . some (
( t ) = > t . name === SHELL_COMMAND_NAME ,
) ;
// If it is a shell command, we update the status to Canceled and clear the output
// to avoid artifacts, then add it to history immediately.
if ( isShellCommand ) {
2026-02-10 00:10:15 +00:00
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
2025-11-19 15:49:39 -08:00
const toolGroup = pendingHistoryItemRef . current as HistoryItemToolGroup ;
const updatedTools = toolGroup . tools . map ( ( tool ) = > {
if ( tool . name === SHELL_COMMAND_NAME ) {
return {
. . . tool ,
2026-02-13 17:20:14 -05:00
status : CoreToolCallStatus.Cancelled ,
2025-11-19 15:49:39 -08:00
resultDisplay : tool.resultDisplay ,
} ;
}
return tool ;
} ) ;
2026-01-13 14:15:04 -05:00
addItem ( { . . . toolGroup , tools : updatedTools } as HistoryItemWithoutId ) ;
2025-11-19 15:49:39 -08:00
} else {
2026-01-13 14:15:04 -05:00
addItem ( pendingHistoryItemRef . current ) ;
2025-11-19 15:49:39 -08:00
}
2025-08-12 09:43:57 +05:30
}
setPendingHistoryItem ( null ) ;
2025-10-27 09:59:08 -07:00
// If it was a full cancellation, add the info message now.
// Otherwise, we let handleCompletedTools figure out the next step,
// which might involve sending partial results back to the model.
if ( isFullCancellation ) {
2025-11-19 15:49:39 -08:00
// If shell is active, we delay this message to ensure correct ordering
// (Shell item first, then Info message).
if ( ! activeShellPtyId ) {
2026-01-13 14:15:04 -05:00
addItem ( {
type : MessageType . INFO ,
text : 'Request cancelled.' ,
} ) ;
2025-11-19 15:49:39 -08:00
setIsResponding ( false ) ;
}
2025-10-27 09:59:08 -07:00
}
2025-11-19 11:11:36 +08:00
onCancelSubmit ( 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-10-27 09:59:08 -07:00
cancelAllToolCalls ,
toolCalls ,
2025-11-19 15:49:39 -08:00
activeShellPtyId ,
2026-03-10 15:41:16 -03:00
setIsResponding ,
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 ( ) ;
}
} ,
2025-10-27 09:59:08 -07:00
{
isActive :
streamingState === StreamingState . Responding ||
streamingState === StreamingState . WaitingForConfirmation ,
} ,
2025-08-12 14:05:49 -07:00
) ;
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 ( ) ;
await logger ? . logMessage ( MessageSenderType . USER , trimmedQuery ) ;
2025-05-05 20:48:34 +00:00
2025-10-17 23:00:27 +05:30
if ( ! shellModeActive ) {
// Handle UI-only commands first
const slashCommandResult = isSlashCommand ( trimmedQuery )
? await handleSlashCommand ( trimmedQuery )
: false ;
2025-07-07 16:45:44 -04:00
2025-10-17 23:00:27 +05:30
if ( slashCommandResult ) {
switch ( slashCommandResult . type ) {
case 'schedule_tool' : {
2026-03-10 12:24:54 -07:00
const { toolName , toolArgs , postSubmitPrompt } =
slashCommandResult ;
2025-10-17 23:00:27 +05:30
const toolCallRequest : ToolCallRequestInfo = {
callId : ` ${ toolName } - ${ Date . now ( ) } - ${ Math . random ( ) . toString ( 16 ) . slice ( 2 ) } ` ,
name : toolName ,
args : toolArgs ,
isClientInitiated : true ,
prompt_id ,
} ;
2026-01-16 11:55:15 -05:00
await scheduleToolCalls ( [ toolCallRequest ] , abortSignal ) ;
2026-03-10 12:24:54 -07:00
if ( postSubmitPrompt ) {
localQueryToSendToGemini = postSubmitPrompt ;
return {
queryToSend : localQueryToSendToGemini ,
shouldProceed : true ,
} ;
}
2025-10-17 23:00:27 +05:30
return { queryToSend : null , shouldProceed : false } ;
}
case 'submit_prompt' : {
localQueryToSendToGemini = slashCommandResult . content ;
2025-07-07 16:45:44 -04:00
2025-10-17 23:00:27 +05:30
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-07-22 00:34:55 -04:00
}
}
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 ) ) {
2026-01-23 15:50:45 +00:00
// Add user's turn before @ command processing for correct UI ordering.
addItem (
{ type : MessageType . USER , text : trimmedQuery } ,
userMessageTimestamp ,
) ;
2025-05-24 00:44:17 -07:00
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
2025-12-13 06:31:12 +05:30
if ( atCommandResult . error ) {
onDebugMessage ( atCommandResult . error ) ;
2025-05-24 00:44:17 -07:00
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 = > {
2026-01-13 23:03:19 -05:00
setRetryStatus ( null ) ;
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'
) {
2026-02-09 19:24:41 -08:00
// Flush any pending item before starting gemini content
2025-05-24 00:44:17 -07:00
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 ) = > ( {
2026-02-10 00:10:15 +00:00
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
2025-05-24 00:44:17 -07:00
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 ) ;
2026-02-18 12:53:06 -08:00
if ( beforeText . length > 0 ) {
addItem (
{
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
type : pendingHistoryItemRef . current ? . type as
| 'gemini'
| 'gemini_content' ,
text : beforeText ,
} ,
userMessageTimestamp ,
) ;
}
2025-05-24 00:44:17 -07:00
setPendingHistoryItem ( { type : 'gemini_content' , text : afterText } ) ;
newGeminiMessageBuffer = afterText ;
}
return newGeminiMessageBuffer ;
} ,
[ addItem , pendingHistoryItemRef , setPendingHistoryItem ] ,
) ;
2026-02-09 19:24:41 -08:00
const handleThoughtEvent = useCallback (
2026-03-06 20:20:27 -08:00
( eventValue : ThoughtSummary , _userMessageTimestamp : number ) = > {
2026-02-09 19:24:41 -08:00
setThought ( eventValue ) ;
if ( getInlineThinkingMode ( settings ) === 'full' ) {
2026-03-06 20:20:27 -08:00
addItem ( {
type : 'thinking' ,
thought : eventValue ,
} as HistoryItemThinking ) ;
2026-02-09 19:24:41 -08:00
}
} ,
[ addItem , settings , setThought ] ,
) ;
2025-05-24 00:44:17 -07:00
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 ) = >
2026-02-13 17:20:14 -05:00
tool . status === CoreToolCallStatus . Validating ||
tool . status === CoreToolCallStatus . Scheduled ||
tool . status === CoreToolCallStatus . AwaitingApproval ||
tool . status === CoreToolCallStatus . Executing
? { . . . tool , status : CoreToolCallStatus.Cancelled }
2025-05-24 00:44:17 -07:00
: tool ,
) ;
2026-02-13 17:20:14 -05:00
2025-05-24 00:44:17 -07:00
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
} ,
2026-03-10 15:41:16 -03:00
[
addItem ,
pendingHistoryItemRef ,
setPendingHistoryItem ,
setThought ,
setIsResponding ,
] ,
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
}
2026-02-27 14:15:10 -05:00
maybeAddSuppressedToolErrorNote ( userMessageTimestamp ) ;
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 ,
) ;
2026-02-27 14:15:10 -05:00
maybeAddLowVerbosityFailureNote ( 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
} ,
2026-02-27 14:15:10 -05:00
[
addItem ,
pendingHistoryItemRef ,
setPendingHistoryItem ,
config ,
setThought ,
maybeAddSuppressedToolErrorNote ,
maybeAddLowVerbosityFailureNote ,
] ,
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.' ,
2025-11-19 12:52:27 -08:00
[ FinishReason . IMAGE_PROHIBITED_CONTENT ] :
2025-11-21 12:19:34 -05:00
'Response stopped due to prohibited image content.' ,
[ FinishReason . NO_IMAGE ] :
'Response stopped because no image was generated.' ,
2025-07-22 06:57:11 +09:00
} ;
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 ) ;
}
2026-03-03 01:22:29 -08:00
const limit = tokenLimit ( config . getModel ( ) ) ;
const originalPercentage = Math . round (
( ( eventValue ? . originalTokenCount ? ? 0 ) / limit ) * 100 ,
) ;
const newPercentage = Math . round (
( ( eventValue ? . newTokenCount ? ? 0 ) / limit ) * 100 ,
) ;
addItem (
{
type : MessageType . INFO ,
text : ` Context compressed from ${ originalPercentage } % to ${ newPercentage } %. ` ,
secondaryText : ` Change threshold in /settings. ` ,
color : theme.status.warning ,
marginBottom : 1 ,
} as HistoryItemInfo ,
userMessageTimestamp ,
) ;
2025-09-17 13:12:06 -07:00
} ,
2026-03-03 01:22:29 -08:00
[ addItem , pendingHistoryItemRef , setPendingHistoryItem , config ] ,
2025-06-03 19:19:49 +00:00
) ;
2025-07-11 07:55:03 -07:00
const handleMaxSessionTurnsEvent = useCallback (
( ) = >
2026-01-13 14:15:04 -05:00
addItem ( {
type : 'info' ,
text :
` The session has reached the maximum number of turns: ${ config . getMaxSessionTurns ( ) } . ` +
` Please update this limit in your setting.json file. ` ,
} ) ,
2025-07-11 07:55:03 -07:00
[ addItem , config ] ,
) ;
2025-10-08 15:20:44 -07:00
const handleContextWindowWillOverflowEvent = useCallback (
( estimatedRequestTokenCount : number , remainingTokenCount : number ) = > {
2025-11-19 11:11:36 +08:00
onCancelSubmit ( true ) ;
2025-10-08 15:20:44 -07:00
2025-10-09 10:22:26 -07:00
const limit = tokenLimit ( config . getModel ( ) ) ;
2026-03-03 01:22:29 -08:00
const isMoreThan25PercentUsed =
2025-10-09 10:22:26 -07:00
limit > 0 && remainingTokenCount < limit * 0.75 ;
2026-03-03 01:22:29 -08:00
let text = ` Sending this message ( ${ estimatedRequestTokenCount } tokens) might exceed the context window limit ( ${ remainingTokenCount . toLocaleString ( ) } tokens left). ` ;
2025-10-09 10:22:26 -07:00
2026-03-03 01:22:29 -08:00
if ( isMoreThan25PercentUsed ) {
2025-10-09 10:22:26 -07:00
text +=
' Please try reducing the size of your message or use the `/compress` command to compress the chat history.' ;
}
2026-01-13 14:15:04 -05:00
addItem ( {
type : 'info' ,
text ,
} ) ;
2025-10-08 15:20:44 -07:00
} ,
2025-10-09 10:22:26 -07:00
[ addItem , onCancelSubmit , config ] ,
2025-10-08 15:20:44 -07:00
) ;
2025-11-13 19:11:06 -08:00
const handleChatModelEvent = useCallback (
( eventValue : string , userMessageTimestamp : number ) = > {
2026-01-15 09:26:10 -08:00
if ( ! settings . merged . ui . showModelInfoInChat ) {
2025-11-13 19:11:06 -08:00
return ;
}
if ( pendingHistoryItemRef . current ) {
addItem ( pendingHistoryItemRef . current , userMessageTimestamp ) ;
setPendingHistoryItem ( null ) ;
}
addItem (
{
type : 'model' ,
model : eventValue ,
} as HistoryItemModel ,
userMessageTimestamp ,
) ;
} ,
[ addItem , pendingHistoryItemRef , setPendingHistoryItem , settings ] ,
) ;
2026-01-04 18:58:34 -08:00
const handleAgentExecutionStoppedEvent = useCallback (
2026-01-23 17:14:30 -05:00
(
reason : string ,
userMessageTimestamp : number ,
systemMessage? : string ,
contextCleared? : boolean ,
) = > {
2026-01-04 18:58:34 -08:00
if ( pendingHistoryItemRef . current ) {
addItem ( pendingHistoryItemRef . current , userMessageTimestamp ) ;
setPendingHistoryItem ( null ) ;
}
addItem (
{
type : MessageType . INFO ,
2026-01-09 15:47:14 -05:00
text : ` Agent execution stopped: ${ systemMessage ? . trim ( ) || reason } ` ,
2026-01-04 18:58:34 -08:00
} ,
userMessageTimestamp ,
) ;
2026-02-27 14:15:10 -05:00
maybeAddLowVerbosityFailureNote ( userMessageTimestamp ) ;
2026-01-23 17:14:30 -05:00
if ( contextCleared ) {
addItem (
{
type : MessageType . INFO ,
text : 'Conversation context has been cleared.' ,
} ,
userMessageTimestamp ,
) ;
}
2026-01-04 18:58:34 -08:00
setIsResponding ( false ) ;
} ,
2026-02-27 14:15:10 -05:00
[
addItem ,
pendingHistoryItemRef ,
setPendingHistoryItem ,
setIsResponding ,
maybeAddLowVerbosityFailureNote ,
] ,
2026-01-04 18:58:34 -08:00
) ;
const handleAgentExecutionBlockedEvent = useCallback (
2026-01-23 17:14:30 -05:00
(
reason : string ,
userMessageTimestamp : number ,
systemMessage? : string ,
contextCleared? : boolean ,
) = > {
2026-01-04 18:58:34 -08:00
if ( pendingHistoryItemRef . current ) {
addItem ( pendingHistoryItemRef . current , userMessageTimestamp ) ;
setPendingHistoryItem ( null ) ;
}
addItem (
{
type : MessageType . WARNING ,
2026-01-09 15:47:14 -05:00
text : ` Agent execution blocked: ${ systemMessage ? . trim ( ) || reason } ` ,
2026-01-04 18:58:34 -08:00
} ,
userMessageTimestamp ,
) ;
2026-02-27 14:15:10 -05:00
maybeAddLowVerbosityFailureNote ( userMessageTimestamp ) ;
2026-01-23 17:14:30 -05:00
if ( contextCleared ) {
addItem (
{
type : MessageType . INFO ,
text : 'Conversation context has been cleared.' ,
} ,
userMessageTimestamp ,
) ;
}
2026-01-04 18:58:34 -08:00
} ,
2026-02-27 14:15:10 -05:00
[
addItem ,
pendingHistoryItemRef ,
setPendingHistoryItem ,
maybeAddLowVerbosityFailureNote ,
] ,
2026-01-04 18:58:34 -08: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 ) {
2026-02-09 19:24:41 -08:00
if (
event . type !== ServerGeminiEventType . Thought &&
thoughtRef . current !== null
) {
setThought ( null ) ;
}
2025-06-03 19:19:49 +00:00
switch ( event . type ) {
2025-06-15 11:19:05 -07:00
case ServerGeminiEventType . Thought :
2026-01-13 06:19:53 -08:00
setLastGeminiActivityTime ( Date . now ( ) ) ;
2026-02-09 19:24:41 -08:00
handleThoughtEvent ( event . value , userMessageTimestamp ) ;
2025-06-15 11:19:05 -07:00
break ;
2025-06-03 19:19:49 +00:00
case ServerGeminiEventType . Content :
2026-01-13 06:19:53 -08:00
setLastGeminiActivityTime ( Date . now ( ) ) ;
2025-06-03 19:19:49 +00:00
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 ;
2026-01-04 18:58:34 -08:00
case ServerGeminiEventType . AgentExecutionStopped :
handleAgentExecutionStoppedEvent (
event . value . reason ,
userMessageTimestamp ,
2026-01-09 15:47:14 -05:00
event . value . systemMessage ,
2026-01-23 17:14:30 -05:00
event . value . contextCleared ,
2026-01-04 18:58:34 -08:00
) ;
break ;
case ServerGeminiEventType . AgentExecutionBlocked :
handleAgentExecutionBlockedEvent (
event . value . reason ,
userMessageTimestamp ,
2026-01-09 15:47:14 -05:00
event . value . systemMessage ,
2026-01-23 17:14:30 -05:00
event . value . contextCleared ,
2026-01-04 18:58:34 -08:00
) ;
break ;
2025-06-03 19:19:49 +00:00
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-10-08 15:20:44 -07:00
case ServerGeminiEventType . ContextWindowWillOverflow :
handleContextWindowWillOverflowEvent (
event . value . estimatedRequestTokenCount ,
event . value . remainingTokenCount ,
) ;
break ;
2025-07-22 06:57:11 +09:00
case ServerGeminiEventType . Finished :
2025-12-12 17:43:43 -08:00
handleFinishedEvent ( event , userMessageTimestamp ) ;
2025-07-22 06:57:11 +09:00
break ;
2025-08-28 16:42:54 -07:00
case ServerGeminiEventType . Citation :
handleCitationEvent ( event . value , userMessageTimestamp ) ;
break ;
2025-11-13 19:11:06 -08:00
case ServerGeminiEventType . ModelInfo :
handleChatModelEvent ( 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 :
2025-10-09 18:04:08 -04:00
case ServerGeminiEventType . InvalidStream :
2025-09-03 22:00:16 -04:00
// 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 ) {
2026-01-19 17:22:15 -08:00
if ( pendingHistoryItemRef . current ) {
addItem ( pendingHistoryItemRef . current , userMessageTimestamp ) ;
setPendingHistoryItem ( null ) ;
}
2026-01-16 11:55:15 -05:00
await scheduleToolCalls ( toolCallRequests , signal ) ;
2025-06-01 14:16:24 -07:00
}
2025-05-24 00:44:17 -07:00
return StreamProcessingStatus . Completed ;
} ,
[
handleContentEvent ,
2026-02-09 19:24:41 -08:00
handleThoughtEvent ,
thoughtRef ,
2025-05-24 00:44:17 -07:00
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-10-08 15:20:44 -07:00
handleContextWindowWillOverflowEvent ,
2025-08-28 16:42:54 -07:00
handleCitationEvent ,
2025-11-13 19:11:06 -08:00
handleChatModelEvent ,
2026-01-04 18:58:34 -08:00
handleAgentExecutionStoppedEvent ,
handleAgentExecutionBlockedEvent ,
2026-01-19 17:22:15 -08:00
addItem ,
pendingHistoryItemRef ,
setPendingHistoryItem ,
2026-02-09 19:24:41 -08:00
setThought ,
2025-05-24 00:44:17 -07: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-10-27 19:16:44 -04:00
) = >
runInDevTraceSpan (
2026-02-26 18:26:16 -08:00
{
operation : options?.isContinuation
? GeminiCliOperation . SystemPrompt
: GeminiCliOperation . UserPrompt ,
} ,
2025-10-27 19:16:44 -04:00
async ( { metadata : spanMetadata } ) = > {
spanMetadata . input = query ;
2026-01-15 15:33:16 -05:00
2025-10-27 19:16:44 -04:00
if (
2026-03-10 15:41:16 -03:00
( isRespondingRef . current ||
streamingState === StreamingState . Responding ||
2025-10-27 19:16:44 -04:00
streamingState === StreamingState . WaitingForConfirmation ) &&
! options ? . isContinuation
)
return ;
2026-03-10 15:41:16 -03:00
const queryId = ` ${ Date . now ( ) } - ${ Math . random ( ) } ` ;
activeQueryIdRef . current = queryId ;
2025-07-09 13:55:56 -04:00
2025-10-27 19:16:44 -04:00
const userMessageTimestamp = Date . now ( ) ;
2025-05-14 22:14:15 +00:00
2025-10-27 19:16:44 -04:00
// Reset quota error flag when starting a new query (not a continuation)
if ( ! options ? . isContinuation ) {
setModelSwitchedFromQuotaError ( false ) ;
config . setQuotaErrorOccurred ( false ) ;
2026-03-06 19:14:44 -08:00
config . resetBillingTurnState (
settings . merged . billing ? . overageStrategy ,
) ;
2026-02-27 14:15:10 -05:00
suppressedToolErrorCountRef . current = 0 ;
suppressedToolErrorNoteShownRef . current = false ;
lowVerbosityFailureNoteShownRef . current = false ;
2025-10-27 19:16:44 -04:00
}
2025-04-19 19:45:42 +01:00
2025-10-27 19:16:44 -04:00
abortControllerRef . current = new AbortController ( ) ;
const abortSignal = abortControllerRef . current . signal ;
turnCancelledRef . current = false ;
2025-04-21 14:32:18 -04:00
2025-10-27 19:16:44 -04:00
if ( ! prompt_id ) {
prompt_id = config . getSessionId ( ) + '########' + getPromptCount ( ) ;
2025-09-22 14:31:06 -07:00
}
2025-10-27 19:16:44 -04:00
return promptIdContext . run ( prompt_id , async ( ) = > {
const { queryToSend , shouldProceed } = await prepareQueryForGemini (
query ,
userMessageTimestamp ,
abortSignal ,
prompt_id ! ,
) ;
2025-09-09 01:14:15 -04:00
2025-10-27 19:16:44 -04:00
if ( ! shouldProceed || queryToSend === null ) {
return ;
}
2025-10-21 13:27:57 -07:00
2025-10-27 19:16:44 -04:00
if ( ! options ? . isContinuation ) {
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 ,
) ,
) ;
}
startNewPrompt ( ) ;
setThought ( null ) ; // Reset thought when starting a new prompt
}
2025-09-09 01:14:15 -04:00
2025-10-27 19:16:44 -04:00
setIsResponding ( true ) ;
setInitError ( null ) ;
// Store query and prompt_id for potential retry on loop detection
lastQueryRef . current = queryToSend ;
lastPromptIdRef . current = prompt_id ! ;
try {
const stream = geminiClient . sendMessageStream (
queryToSend ,
abortSignal ,
prompt_id ! ,
2026-01-30 10:09:27 -08:00
undefined ,
false ,
query ,
2025-10-27 19:16:44 -04:00
) ;
const processingStatus = await processGeminiStreamEvents (
stream ,
userMessageTimestamp ,
abortSignal ,
) ;
if ( processingStatus === StreamProcessingStatus . UserCancelled ) {
return ;
}
2025-09-09 01:14:15 -04:00
2025-10-27 19:16:44 -04:00
if ( pendingHistoryItemRef . current ) {
addItem ( pendingHistoryItemRef . current , userMessageTimestamp ) ;
setPendingHistoryItem ( null ) ;
}
if ( loopDetectedRef . current ) {
loopDetectedRef . current = false ;
// Show the confirmation dialog to choose whether to disable loop detection
setLoopDetectionConfirmationRequest ( {
2026-03-10 15:41:16 -03:00
onComplete : async ( result : {
2025-10-27 19:16:44 -04:00
userSelection : 'disable' | 'keep' ;
} ) = > {
setLoopDetectionConfirmationRequest ( null ) ;
if ( result . userSelection === 'disable' ) {
config
. getGeminiClient ( )
. getLoopDetectionService ( )
. disableForSession ( ) ;
2026-01-13 14:15:04 -05:00
addItem ( {
type : 'info' ,
text : ` Loop detection has been disabled for this session. Retrying request... ` ,
} ) ;
2025-10-27 19:16:44 -04:00
if ( lastQueryRef . current && lastPromptIdRef . current ) {
2026-03-10 15:41:16 -03:00
await submitQuery (
2025-10-27 19:16:44 -04:00
lastQueryRef . current ,
{ isContinuation : true } ,
lastPromptIdRef . current ,
) ;
}
} else {
2026-01-13 14:15:04 -05:00
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. ` ,
} ) ;
2025-10-27 19:16:44 -04:00
}
} ,
} ) ;
}
} catch ( error : unknown ) {
spanMetadata . error = error ;
if ( error instanceof UnauthorizedError ) {
onAuthError ( 'Session expired or is unauthorized.' ) ;
2026-01-20 16:23:01 -08:00
} else if (
// Suppress ValidationRequiredError if it was marked as handled (e.g. user clicked change_auth or cancelled)
error instanceof ValidationRequiredError &&
error . userHandled
) {
// Error was handled by validation dialog, don't display again
2025-10-27 19:16:44 -04:00
} else if ( ! isNodeError ( error ) || error . name !== 'AbortError' ) {
2026-02-27 14:15:10 -05:00
maybeAddSuppressedToolErrorNote ( userMessageTimestamp ) ;
2025-10-27 19:16:44 -04:00
addItem (
{
type : MessageType . ERROR ,
text : parseAndFormatApiError (
getErrorMessage ( error ) || 'Unknown error' ,
config . getContentGeneratorConfig ( ) ? . authType ,
undefined ,
config . getModel ( ) ,
DEFAULT_GEMINI_FLASH_MODEL ,
) ,
} ,
userMessageTimestamp ,
) ;
2026-02-27 14:15:10 -05:00
maybeAddLowVerbosityFailureNote ( userMessageTimestamp ) ;
2025-10-27 19:16:44 -04:00
}
} finally {
if ( activeQueryIdRef . current === queryId ) {
setIsResponding ( false ) ;
}
}
} ) ;
} ,
) ,
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 ,
2026-02-09 19:24:41 -08:00
setThought ,
2026-02-27 14:15:10 -05:00
maybeAddSuppressedToolErrorNote ,
maybeAddLowVerbosityFailureNote ,
2026-03-06 19:14:44 -08:00
settings . merged . billing ? . overageStrategy ,
2026-03-10 15:41:16 -03:00
setIsResponding ,
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 ) = > {
2026-02-24 14:31:41 -05:00
if (
previousApprovalModeRef . current === ApprovalMode . PLAN &&
newApprovalMode !== ApprovalMode . PLAN &&
streamingState === StreamingState . Idle
) {
if ( geminiClient ) {
try {
await geminiClient . addHistory ( {
role : 'user' ,
parts : [
{
text : getPlanModeExitMessage ( newApprovalMode , true ) ,
} ,
] ,
} ) ;
} catch ( error ) {
onDebugMessage (
` Failed to notify model of Plan Mode exit: ${ getErrorMessage ( error ) } ` ,
) ;
addItem ( {
type : MessageType . ERROR ,
text : 'Failed to update the model about exiting Plan Mode. The model might be out of sync. Please consider restarting the session if you see unexpected behavior.' ,
} ) ;
}
}
}
previousApprovalModeRef . current = newApprovalMode ;
2025-09-14 20:20:21 -07:00
// 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 ) {
2026-02-13 11:14:35 +09:00
if ( call . correlationId ) {
2025-09-14 20:20:21 -07:00
try {
2026-02-13 11:14:35 +09:00
await config . getMessageBus ( ) . publish ( {
type : MessageBusType . TOOL_CONFIRMATION_RESPONSE ,
correlationId : call.correlationId ,
confirmed : true ,
requiresUserConfirmation : false ,
outcome : ToolConfirmationOutcome.ProceedOnce ,
} ) ;
2025-09-14 20:20:21 -07:00
} catch ( error ) {
2025-10-28 19:05:48 +00:00
debugLogger . warn (
2025-09-14 20:20:21 -07:00
` Failed to auto-approve tool call ${ call . request . callId } : ` ,
error ,
) ;
}
}
}
}
} ,
2026-02-24 14:31:41 -05:00
[ config , toolCalls , geminiClient , streamingState , addItem , onDebugMessage ] ,
2025-09-14 20:20:21 -07:00
) ;
2025-06-27 16:39:54 -07:00
const handleCompletedTools = useCallback (
async ( completedToolCallsFromScheduler : TrackedToolCall [ ] ) = > {
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 ) ,
) ;
2026-01-30 09:53:09 -08:00
// Handle backgrounded shell tools
completedAndReadyToSubmitTools . forEach ( ( t ) = > {
const isShell = t . request . name === 'run_shell_command' ;
// Access result from the tracked tool call response
const response = t . response as ToolResponseWithParts ;
const rawData = response ? . data ;
const data = isShellToolData ( rawData ) ? rawData : undefined ;
// Use data.pid for shell commands moved to the background.
const pid = data ? . pid ;
if ( isShell && pid ) {
2026-02-10 00:10:15 +00:00
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
2026-01-30 09:53:09 -08:00
const command = ( data ? . [ 'command' ] as string ) ? ? 'shell' ;
2026-02-10 00:10:15 +00:00
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
2026-01-30 09:53:09 -08:00
const initialOutput = ( data ? . [ 'initialOutput' ] as string ) ? ? '' ;
registerBackgroundShell ( pid , command , initialOutput ) ;
}
} ) ;
2025-06-22 01:35:36 -04:00
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 ,
) ;
2026-02-27 14:15:10 -05:00
if ( isLowErrorVerbosity ) {
// Low-mode suppression applies only to model-initiated tool failures.
suppressedToolErrorCountRef . current += geminiTools . filter (
( tc ) = > tc . status === CoreToolCallStatus . Error ,
) . length ;
}
2025-06-22 01:35:36 -04:00
if ( geminiTools . length === 0 ) {
return ;
}
2025-06-01 14:16:24 -07:00
2025-12-31 07:22:53 +08:00
// Check if any tool requested to stop execution immediately
const stopExecutionTool = geminiTools . find (
( tc ) = > tc . response . errorType === ToolErrorType . STOP_EXECUTION ,
) ;
if ( stopExecutionTool && stopExecutionTool . response . error ) {
2026-02-27 14:15:10 -05:00
maybeAddSuppressedToolErrorNote ( ) ;
2026-01-13 14:15:04 -05:00
addItem ( {
type : MessageType . INFO ,
text : ` Agent execution stopped: ${ stopExecutionTool . response . error . message } ` ,
} ) ;
2026-02-27 14:15:10 -05:00
maybeAddLowVerbosityFailureNote ( ) ;
2025-12-31 07:22:53 +08:00
setIsResponding ( false ) ;
const callIdsToMarkAsSubmitted = geminiTools . map (
( toolCall ) = > toolCall . request . callId ,
) ;
markToolsAsSubmitted ( callIdsToMarkAsSubmitted ) ;
return ;
}
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 (
2026-02-13 17:20:14 -05:00
( tc ) = > tc . status === CoreToolCallStatus . Cancelled ,
2025-06-08 11:14:45 -07:00
) ;
if ( allToolsCancelled ) {
2025-10-27 09:59:08 -07:00
// If the turn was cancelled via the imperative escape key flow,
// the cancellation message is added there. We check the ref to avoid duplication.
if ( ! turnCancelledRef . current ) {
2026-01-13 14:15:04 -05:00
addItem ( {
type : MessageType . INFO ,
text : 'Request cancelled.' ,
} ) ;
2025-10-27 09:59:08 -07:00
}
setIsResponding ( false ) ;
2025-06-08 11:14:45 -07:00
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-12-05 16:12:49 -08:00
// eslint-disable-next-line @typescript-eslint/no-floating-promises
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 ,
) ;
2026-02-18 14:05:50 -08:00
if ( consumeUserHint ) {
const userHint = consumeUserHint ( ) ;
if ( userHint && userHint . trim ( ) . length > 0 ) {
const hintText = userHint . trim ( ) ;
responsesToSend . unshift ( {
text : buildUserSteeringHintPrompt ( hintText ) ,
} ) ;
}
}
2025-06-22 01:35:36 -04:00
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-12-05 16:12:49 -08:00
// eslint-disable-next-line @typescript-eslint/no-floating-promises
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
} ,
[
submitQuery ,
markToolsAsSubmitted ,
geminiClient ,
performMemoryRefresh ,
2025-07-09 13:55:56 -04:00
modelSwitchedFromQuotaError ,
2025-10-27 09:59:08 -07:00
addItem ,
2026-01-30 09:53:09 -08:00
registerBackgroundShell ,
2026-02-18 14:05:50 -08:00
consumeUserHint ,
2026-02-27 14:15:10 -05:00
isLowErrorVerbosity ,
maybeAddSuppressedToolErrorNote ,
maybeAddLowVerbosityFailureNote ,
2026-03-10 15:41:16 -03:00
setIsResponding ,
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 (
( ) = >
2026-01-27 16:06:24 -08:00
[ pendingHistoryItem , . . . pendingToolGroupItems ] . filter (
( i ) : i is HistoryItemWithoutId = > i !== undefined && i !== null ,
2025-09-17 15:37:13 -07:00
) ,
2026-01-27 16:06:24 -08:00
[ pendingHistoryItem , pendingToolGroupItems ] ,
2025-09-17 15:37:13 -07:00
) ;
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 ) &&
2026-02-13 17:20:14 -05:00
toolCall . status === CoreToolCallStatus . AwaitingApproval ,
2025-06-11 15:33:09 -04:00
) ;
if ( restorableToolCalls . length > 0 ) {
2025-12-09 10:08:23 -05:00
if ( ! gitService ) {
onDebugMessage (
'Checkpointing is enabled but Git service is not available. Failed to create snapshot. Ensure Git is installed and working properly.' ,
) ;
2025-06-11 15:33:09 -04:00
return ;
}
2025-12-09 10:08:23 -05:00
const { checkpointsToWrite , errors } = await processRestorableToolCalls <
HistoryItem [ ]
> (
restorableToolCalls . map ( ( call ) = > call . request ) ,
gitService ,
geminiClient ,
history ,
) ;
2025-06-11 15:33:09 -04:00
2025-12-09 10:08:23 -05:00
if ( errors . length > 0 ) {
errors . forEach ( onDebugMessage ) ;
}
2025-06-11 15:33:09 -04:00
2025-12-09 10:08:23 -05:00
if ( checkpointsToWrite . size > 0 ) {
const checkpointDir = storage . getProjectTempCheckpointsDir ( ) ;
2025-06-11 15:33:09 -04:00
try {
2025-12-09 10:08:23 -05:00
await fs . mkdir ( checkpointDir , { recursive : true } ) ;
for ( const [ fileName , content ] of checkpointsToWrite ) {
const filePath = path . join ( checkpointDir , fileName ) ;
await fs . writeFile ( filePath , content ) ;
2025-08-22 08:29:52 -07:00
}
2025-06-11 15:33:09 -04:00
} catch ( error ) {
onDebugMessage (
2025-12-09 10:08:23 -05:00
` Failed to write checkpoint file: ${ getErrorMessage ( error ) } ` ,
2025-06-11 15:33:09 -04:00
) ;
}
}
}
} ;
2025-12-05 16:12:49 -08:00
// eslint-disable-next-line @typescript-eslint/no-floating-promises
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
2026-01-13 06:19:53 -08:00
const lastOutputTime = Math . max (
lastToolOutputTime ,
lastShellOutputTime ,
lastGeminiActivityTime ,
) ;
2025-11-21 12:19:34 -05: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-11-21 12:19:34 -05:00
lastOutputTime ,
2026-01-30 09:53:09 -08:00
backgroundShellCount ,
isBackgroundShellVisible ,
toggleBackgroundShell ,
backgroundCurrentShell ,
backgroundShells ,
dismissBackgroundShell ,
2026-01-13 23:03:19 -05:00
retryStatus ,
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
} ;