/** * @license * Copyright 2025 Google LLC * SPDX-License-Identifier: Apache-2.0 */ import { useCallback, useEffect, useMemo, useState, useRef } from 'react'; import { Box, DOMElement, measureElement, Static, Text, useStdin, useStdout, } from 'ink'; import { StreamingState, type HistoryItem, MessageType } from './types.js'; import { useTerminalSize } from './hooks/useTerminalSize.js'; import { useGeminiStream } from './hooks/useGeminiStream.js'; import { useLoadingIndicator } from './hooks/useLoadingIndicator.js'; import { useThemeCommand } from './hooks/useThemeCommand.js'; import { useAuthCommand } from './hooks/useAuthCommand.js'; import { useFolderTrust } from './hooks/useFolderTrust.js'; import { useEditorSettings } from './hooks/useEditorSettings.js'; import { useSlashCommandProcessor } from './hooks/slashCommandProcessor.js'; import { useAutoAcceptIndicator } from './hooks/useAutoAcceptIndicator.js'; import { useConsoleMessages } from './hooks/useConsoleMessages.js'; import { Header } from './components/Header.js'; import { LoadingIndicator } from './components/LoadingIndicator.js'; import { AutoAcceptIndicator } from './components/AutoAcceptIndicator.js'; import { ShellModeIndicator } from './components/ShellModeIndicator.js'; import { InputPrompt } from './components/InputPrompt.js'; import { Footer } from './components/Footer.js'; import { ThemeDialog } from './components/ThemeDialog.js'; import { AuthDialog } from './components/AuthDialog.js'; import { AuthInProgress } from './components/AuthInProgress.js'; import { EditorSettingsDialog } from './components/EditorSettingsDialog.js'; import { FolderTrustDialog } from './components/FolderTrustDialog.js'; import { ShellConfirmationDialog } from './components/ShellConfirmationDialog.js'; import { RadioButtonSelect } from './components/shared/RadioButtonSelect.js'; import { Colors } from './colors.js'; import { loadHierarchicalGeminiMemory } from '../config/config.js'; import { LoadedSettings, SettingScope } from '../config/settings.js'; import { Tips } from './components/Tips.js'; import { ConsolePatcher } from './utils/ConsolePatcher.js'; import { registerCleanup } from '../utils/cleanup.js'; import { DetailedMessagesDisplay } from './components/DetailedMessagesDisplay.js'; import { HistoryItemDisplay } from './components/HistoryItemDisplay.js'; import { ContextSummaryDisplay } from './components/ContextSummaryDisplay.js'; import { useHistory } from './hooks/useHistoryManager.js'; import process from 'node:process'; import { getErrorMessage, type Config, getAllGeminiMdFilenames, ApprovalMode, isEditorAvailable, EditorType, FlashFallbackEvent, logFlashFallback, AuthType, type IdeContext, ideContext, } from '@google/gemini-cli-core'; import { IdeIntegrationNudge, IdeIntegrationNudgeResult, } from './IdeIntegrationNudge.js'; import { validateAuthMethod } from '../config/auth.js'; import { useLogger } from './hooks/useLogger.js'; import { StreamingContext } from './contexts/StreamingContext.js'; import { SessionStatsProvider, useSessionStats, } from './contexts/SessionContext.js'; import { useGitBranchName } from './hooks/useGitBranchName.js'; import { useFocus } from './hooks/useFocus.js'; import { useBracketedPaste } from './hooks/useBracketedPaste.js'; import { useTextBuffer } from './components/shared/text-buffer.js'; import { useVimMode, VimModeProvider } from './contexts/VimModeContext.js'; import { useVim } from './hooks/vim.js'; import { useKeypress, Key } from './hooks/useKeypress.js'; import { useKittyKeyboardProtocol } from './hooks/useKittyKeyboardProtocol.js'; import { keyMatchers, Command } from './keyMatchers.js'; import * as fs from 'fs'; import { UpdateNotification } from './components/UpdateNotification.js'; import { isProQuotaExceededError, isGenericQuotaExceededError, UserTierId, } from '@google/gemini-cli-core'; import { UpdateObject } from './utils/updateCheck.js'; import ansiEscapes from 'ansi-escapes'; import { OverflowProvider } from './contexts/OverflowContext.js'; import { ShowMoreLines } from './components/ShowMoreLines.js'; import { PrivacyNotice } from './privacy/PrivacyNotice.js'; import { useSettingsCommand } from './hooks/useSettingsCommand.js'; import { SettingsDialog } from './components/SettingsDialog.js'; import { setUpdateHandler } from '../utils/handleAutoUpdate.js'; import { appEvents, AppEvent } from '../utils/events.js'; import { isNarrowWidth } from './utils/isNarrowWidth.js'; const CTRL_EXIT_PROMPT_DURATION_MS = 1000; interface AppProps { config: Config; settings: LoadedSettings; startupWarnings?: string[]; version: string; } export const AppWrapper = (props: AppProps) => ( ); const App = ({ config, settings, startupWarnings = [], version }: AppProps) => { const isFocused = useFocus(); useBracketedPaste(); const [updateInfo, setUpdateInfo] = useState(null); const { stdout } = useStdout(); const nightly = version.includes('nightly'); const { history, addItem, clearItems, loadHistory } = useHistory(); const [idePromptAnswered, setIdePromptAnswered] = useState(false); const currentIDE = config.getIdeClient().getCurrentIde(); useEffect(() => { registerCleanup(() => config.getIdeClient().disconnect()); }, [config]); const shouldShowIdePrompt = currentIDE && !config.getIdeMode() && !settings.merged.hasSeenIdeIntegrationNudge && !idePromptAnswered; useEffect(() => { const cleanup = setUpdateHandler(addItem, setUpdateInfo); return cleanup; }, [addItem]); const { consoleMessages, handleNewMessage, clearConsoleMessages: clearConsoleMessagesState, } = useConsoleMessages(); useEffect(() => { const consolePatcher = new ConsolePatcher({ onNewMessage: handleNewMessage, debugMode: config.getDebugMode(), }); consolePatcher.patch(); registerCleanup(consolePatcher.cleanup); }, [handleNewMessage, config]); const { stats: sessionStats } = useSessionStats(); const [staticNeedsRefresh, setStaticNeedsRefresh] = useState(false); const [staticKey, setStaticKey] = useState(0); const refreshStatic = useCallback(() => { stdout.write(ansiEscapes.clearTerminal); setStaticKey((prev) => prev + 1); }, [setStaticKey, stdout]); const [geminiMdFileCount, setGeminiMdFileCount] = useState(0); const [debugMessage, setDebugMessage] = useState(''); const [themeError, setThemeError] = useState(null); const [authError, setAuthError] = useState(null); const [editorError, setEditorError] = useState(null); const [footerHeight, setFooterHeight] = useState(0); const [corgiMode, setCorgiMode] = useState(false); const [currentModel, setCurrentModel] = useState(config.getModel()); const [shellModeActive, setShellModeActive] = useState(false); const [showErrorDetails, setShowErrorDetails] = useState(false); const [showToolDescriptions, setShowToolDescriptions] = useState(false); const [ctrlCPressedOnce, setCtrlCPressedOnce] = useState(false); const [quittingMessages, setQuittingMessages] = useState< HistoryItem[] | null >(null); const ctrlCTimerRef = useRef(null); const [ctrlDPressedOnce, setCtrlDPressedOnce] = useState(false); const ctrlDTimerRef = useRef(null); const [constrainHeight, setConstrainHeight] = useState(true); const [showPrivacyNotice, setShowPrivacyNotice] = useState(false); const [modelSwitchedFromQuotaError, setModelSwitchedFromQuotaError] = useState(false); const [userTier, setUserTier] = useState(undefined); const [ideContextState, setIdeContextState] = useState< IdeContext | undefined >(); const [showEscapePrompt, setShowEscapePrompt] = useState(false); const [isProcessing, setIsProcessing] = useState(false); useEffect(() => { const unsubscribe = ideContext.subscribeToIdeContext(setIdeContextState); // Set the initial value setIdeContextState(ideContext.getIdeContext()); return unsubscribe; }, []); useEffect(() => { const openDebugConsole = () => { setShowErrorDetails(true); setConstrainHeight(false); // Make sure the user sees the full message. }; appEvents.on(AppEvent.OpenDebugConsole, openDebugConsole); const logErrorHandler = (errorMessage: unknown) => { handleNewMessage({ type: 'error', content: String(errorMessage), count: 1, }); }; appEvents.on(AppEvent.LogError, logErrorHandler); return () => { appEvents.off(AppEvent.OpenDebugConsole, openDebugConsole); appEvents.off(AppEvent.LogError, logErrorHandler); }; }, [handleNewMessage]); const openPrivacyNotice = useCallback(() => { setShowPrivacyNotice(true); }, []); const handleEscapePromptChange = useCallback((showPrompt: boolean) => { setShowEscapePrompt(showPrompt); }, []); const initialPromptSubmitted = useRef(false); const errorCount = useMemo( () => consoleMessages .filter((msg) => msg.type === 'error') .reduce((total, msg) => total + msg.count, 0), [consoleMessages], ); const { isThemeDialogOpen, openThemeDialog, handleThemeSelect, handleThemeHighlight, } = useThemeCommand(settings, setThemeError, addItem); const { isSettingsDialogOpen, openSettingsDialog, closeSettingsDialog } = useSettingsCommand(); const { isFolderTrustDialogOpen, handleFolderTrustSelect } = useFolderTrust( settings, config, ); const { isAuthDialogOpen, openAuthDialog, handleAuthSelect, isAuthenticating, cancelAuthentication, } = useAuthCommand(settings, setAuthError, config); useEffect(() => { if (settings.merged.selectedAuthType && !settings.merged.useExternalAuth) { const error = validateAuthMethod(settings.merged.selectedAuthType); if (error) { setAuthError(error); openAuthDialog(); } } }, [ settings.merged.selectedAuthType, settings.merged.useExternalAuth, openAuthDialog, setAuthError, ]); // Sync user tier from config when authentication changes useEffect(() => { // Only sync when not currently authenticating if (!isAuthenticating) { setUserTier(config.getGeminiClient()?.getUserTier()); } }, [config, isAuthenticating]); const { isEditorDialogOpen, openEditorDialog, handleEditorSelect, exitEditorDialog, } = useEditorSettings(settings, setEditorError, addItem); const toggleCorgiMode = useCallback(() => { setCorgiMode((prev) => !prev); }, []); const performMemoryRefresh = useCallback(async () => { addItem( { type: MessageType.INFO, text: 'Refreshing hierarchical memory (GEMINI.md or other context files)...', }, Date.now(), ); try { const { memoryContent, fileCount } = await loadHierarchicalGeminiMemory( process.cwd(), settings.merged.loadMemoryFromIncludeDirectories ? config.getWorkspaceContext().getDirectories() : [], config.getDebugMode(), config.getFileService(), settings.merged, config.getExtensionContextFilePaths(), settings.merged.memoryImportFormat || 'tree', // Use setting or default to 'tree' config.getFileFilteringOptions(), ); config.setUserMemory(memoryContent); config.setGeminiMdFileCount(fileCount); setGeminiMdFileCount(fileCount); addItem( { type: MessageType.INFO, text: `Memory refreshed successfully. ${memoryContent.length > 0 ? `Loaded ${memoryContent.length} characters from ${fileCount} file(s).` : 'No memory content found.'}`, }, Date.now(), ); if (config.getDebugMode()) { console.log( `[DEBUG] Refreshed memory content in config: ${memoryContent.substring(0, 200)}...`, ); } } catch (error) { const errorMessage = getErrorMessage(error); addItem( { type: MessageType.ERROR, text: `Error refreshing memory: ${errorMessage}`, }, Date.now(), ); console.error('Error refreshing memory:', error); } }, [config, addItem, settings.merged]); // Watch for model changes (e.g., from Flash fallback) useEffect(() => { const checkModelChange = () => { const configModel = config.getModel(); if (configModel !== currentModel) { setCurrentModel(configModel); } }; // Check immediately and then periodically checkModelChange(); const interval = setInterval(checkModelChange, 1000); // Check every second return () => clearInterval(interval); }, [config, currentModel]); // Set up Flash fallback handler useEffect(() => { const flashFallbackHandler = async ( currentModel: string, fallbackModel: string, error?: unknown, ): Promise => { let message: string; if ( config.getContentGeneratorConfig().authType === AuthType.LOGIN_WITH_GOOGLE ) { // Use actual user tier if available; otherwise, default to FREE tier behavior (safe default) const isPaidTier = userTier === UserTierId.LEGACY || userTier === UserTierId.STANDARD; // Check if this is a Pro quota exceeded error if (error && isProQuotaExceededError(error)) { if (isPaidTier) { message = `⚡ You have reached your daily ${currentModel} quota limit. ⚡ Automatically switching from ${currentModel} to ${fallbackModel} for the remainder of this session. ⚡ To continue accessing the ${currentModel} model today, consider using /auth to switch to using a paid API key from AI Studio at https://aistudio.google.com/apikey`; } else { message = `⚡ You have reached your daily ${currentModel} quota limit. ⚡ Automatically switching from ${currentModel} to ${fallbackModel} for the remainder of this session. ⚡ To increase your limits, upgrade to a Gemini Code Assist Standard or Enterprise plan with higher limits at https://goo.gle/set-up-gemini-code-assist ⚡ Or you can utilize a Gemini API Key. See: https://goo.gle/gemini-cli-docs-auth#gemini-api-key ⚡ You can switch authentication methods by typing /auth`; } } else if (error && isGenericQuotaExceededError(error)) { if (isPaidTier) { message = `⚡ You have reached your daily quota limit. ⚡ Automatically switching from ${currentModel} to ${fallbackModel} for the remainder of this session. ⚡ To continue accessing the ${currentModel} model today, consider using /auth to switch to using a paid API key from AI Studio at https://aistudio.google.com/apikey`; } else { message = `⚡ You have reached your daily quota limit. ⚡ Automatically switching from ${currentModel} to ${fallbackModel} for the remainder of this session. ⚡ To increase your limits, upgrade to a Gemini Code Assist Standard or Enterprise plan with higher limits at https://goo.gle/set-up-gemini-code-assist ⚡ Or you can utilize a Gemini API Key. See: https://goo.gle/gemini-cli-docs-auth#gemini-api-key ⚡ You can switch authentication methods by typing /auth`; } } else { if (isPaidTier) { // Default fallback message for other cases (like consecutive 429s) message = `⚡ Automatically switching from ${currentModel} to ${fallbackModel} for faster responses for the remainder of this session. ⚡ Possible reasons for this are that you have received multiple consecutive capacity errors or you have reached your daily ${currentModel} quota limit ⚡ To continue accessing the ${currentModel} model today, consider using /auth to switch to using a paid API key from AI Studio at https://aistudio.google.com/apikey`; } else { // Default fallback message for other cases (like consecutive 429s) message = `⚡ Automatically switching from ${currentModel} to ${fallbackModel} for faster responses for the remainder of this session. ⚡ Possible reasons for this are that you have received multiple consecutive capacity errors or you have reached your daily ${currentModel} quota limit ⚡ To increase your limits, upgrade to a Gemini Code Assist Standard or Enterprise plan with higher limits at https://goo.gle/set-up-gemini-code-assist ⚡ Or you can utilize a Gemini API Key. See: https://goo.gle/gemini-cli-docs-auth#gemini-api-key ⚡ You can switch authentication methods by typing /auth`; } } // Add message to UI history addItem( { type: MessageType.INFO, text: message, }, Date.now(), ); // Set the flag to prevent tool continuation setModelSwitchedFromQuotaError(true); // Set global quota error flag to prevent Flash model calls config.setQuotaErrorOccurred(true); } // Switch model for future use but return false to stop current retry config.setModel(fallbackModel); config.setFallbackMode(true); logFlashFallback( config, new FlashFallbackEvent(config.getContentGeneratorConfig().authType!), ); return false; // Don't continue with current prompt }; config.setFlashFallbackHandler(flashFallbackHandler); }, [config, addItem, userTier]); // Terminal and UI setup const { rows: terminalHeight, columns: terminalWidth } = useTerminalSize(); const isNarrow = isNarrowWidth(terminalWidth); const { stdin, setRawMode } = useStdin(); const isInitialMount = useRef(true); const widthFraction = 0.9; const inputWidth = Math.max( 20, Math.floor(terminalWidth * widthFraction) - 3, ); const suggestionsWidth = Math.max(20, Math.floor(terminalWidth * 0.8)); // Utility callbacks const isValidPath = useCallback((filePath: string): boolean => { try { return fs.existsSync(filePath) && fs.statSync(filePath).isFile(); } catch (_e) { return false; } }, []); const getPreferredEditor = useCallback(() => { const editorType = settings.merged.preferredEditor; const isValidEditor = isEditorAvailable(editorType); if (!isValidEditor) { openEditorDialog(); return; } return editorType as EditorType; }, [settings, openEditorDialog]); const onAuthError = useCallback(() => { setAuthError('reauth required'); openAuthDialog(); }, [openAuthDialog, setAuthError]); // Core hooks and processors const { vimEnabled: vimModeEnabled, vimMode, toggleVimEnabled, } = useVimMode(); const { handleSlashCommand, slashCommands, pendingHistoryItems: pendingSlashCommandHistoryItems, commandContext, shellConfirmationRequest, confirmationRequest, } = useSlashCommandProcessor( config, settings, addItem, clearItems, loadHistory, refreshStatic, setDebugMessage, openThemeDialog, openAuthDialog, openEditorDialog, toggleCorgiMode, setQuittingMessages, openPrivacyNotice, openSettingsDialog, toggleVimEnabled, setIsProcessing, setGeminiMdFileCount, ); const buffer = useTextBuffer({ initialText: '', viewport: { height: 10, width: inputWidth }, stdin, setRawMode, isValidPath, shellModeActive, }); const [userMessages, setUserMessages] = useState([]); const handleUserCancel = useCallback(() => { const lastUserMessage = userMessages.at(-1); if (lastUserMessage) { buffer.setText(lastUserMessage); } }, [buffer, userMessages]); const { streamingState, submitQuery, initError, pendingHistoryItems: pendingGeminiHistoryItems, thought, cancelOngoingRequest, } = useGeminiStream( config.getGeminiClient(), history, addItem, config, setDebugMessage, handleSlashCommand, shellModeActive, getPreferredEditor, onAuthError, performMemoryRefresh, modelSwitchedFromQuotaError, setModelSwitchedFromQuotaError, refreshStatic, handleUserCancel, ); // Input handling const handleFinalSubmit = useCallback( (submittedValue: string) => { const trimmedValue = submittedValue.trim(); if (trimmedValue.length > 0) { submitQuery(trimmedValue); } }, [submitQuery], ); const handleIdePromptComplete = useCallback( (result: IdeIntegrationNudgeResult) => { if (result.userSelection === 'yes') { if (result.isExtensionPreInstalled) { handleSlashCommand('/ide enable'); } else { handleSlashCommand('/ide install'); } settings.setValue( SettingScope.User, 'hasSeenIdeIntegrationNudge', true, ); } else if (result.userSelection === 'dismiss') { settings.setValue( SettingScope.User, 'hasSeenIdeIntegrationNudge', true, ); } setIdePromptAnswered(true); }, [handleSlashCommand, settings], ); const { handleInput: vimHandleInput } = useVim(buffer, handleFinalSubmit); const pendingHistoryItems = [...pendingSlashCommandHistoryItems]; pendingHistoryItems.push(...pendingGeminiHistoryItems); const { elapsedTime, currentLoadingPhrase } = useLoadingIndicator(streamingState); const showAutoAcceptIndicator = useAutoAcceptIndicator({ config }); const kittyProtocolStatus = useKittyKeyboardProtocol(); const handleExit = useCallback( ( pressedOnce: boolean, setPressedOnce: (value: boolean) => void, timerRef: React.MutableRefObject, ) => { if (pressedOnce) { if (timerRef.current) { clearTimeout(timerRef.current); } // Directly invoke the central command handler. handleSlashCommand('/quit'); } else { setPressedOnce(true); timerRef.current = setTimeout(() => { setPressedOnce(false); timerRef.current = null; }, CTRL_EXIT_PROMPT_DURATION_MS); } }, [handleSlashCommand], ); const handleGlobalKeypress = useCallback( (key: Key) => { let enteringConstrainHeightMode = false; if (!constrainHeight) { enteringConstrainHeightMode = true; setConstrainHeight(true); } if (keyMatchers[Command.SHOW_ERROR_DETAILS](key)) { setShowErrorDetails((prev) => !prev); } else if (keyMatchers[Command.TOGGLE_TOOL_DESCRIPTIONS](key)) { const newValue = !showToolDescriptions; setShowToolDescriptions(newValue); const mcpServers = config.getMcpServers(); if (Object.keys(mcpServers || {}).length > 0) { handleSlashCommand(newValue ? '/mcp desc' : '/mcp nodesc'); } } else if ( keyMatchers[Command.TOGGLE_IDE_CONTEXT_DETAIL](key) && config.getIdeMode() && ideContextState ) { // Show IDE status when in IDE mode and context is available. handleSlashCommand('/ide status'); } else if (keyMatchers[Command.QUIT](key)) { // When authenticating, let AuthInProgress component handle Ctrl+C. if (isAuthenticating) { return; } if (!ctrlCPressedOnce) { cancelOngoingRequest?.(); } handleExit(ctrlCPressedOnce, setCtrlCPressedOnce, ctrlCTimerRef); } else if (keyMatchers[Command.EXIT](key)) { if (buffer.text.length > 0) { return; } handleExit(ctrlDPressedOnce, setCtrlDPressedOnce, ctrlDTimerRef); } else if ( keyMatchers[Command.SHOW_MORE_LINES](key) && !enteringConstrainHeightMode ) { setConstrainHeight(false); } }, [ constrainHeight, setConstrainHeight, setShowErrorDetails, showToolDescriptions, setShowToolDescriptions, config, ideContextState, handleExit, ctrlCPressedOnce, setCtrlCPressedOnce, ctrlCTimerRef, buffer.text.length, ctrlDPressedOnce, setCtrlDPressedOnce, ctrlDTimerRef, handleSlashCommand, isAuthenticating, cancelOngoingRequest, ], ); useKeypress(handleGlobalKeypress, { isActive: true, kittyProtocolEnabled: kittyProtocolStatus.enabled, config, }); useEffect(() => { if (config) { setGeminiMdFileCount(config.getGeminiMdFileCount()); } }, [config, config.getGeminiMdFileCount]); const logger = useLogger(); useEffect(() => { const fetchUserMessages = async () => { const pastMessagesRaw = (await logger?.getPreviousUserMessages()) || []; // Newest first const currentSessionUserMessages = history .filter( (item): item is HistoryItem & { type: 'user'; text: string } => item.type === 'user' && typeof item.text === 'string' && item.text.trim() !== '', ) .map((item) => item.text) .reverse(); // Newest first, to match pastMessagesRaw sorting // Combine, with current session messages being more recent const combinedMessages = [ ...currentSessionUserMessages, ...pastMessagesRaw, ]; // Deduplicate consecutive identical messages from the combined list (still newest first) const deduplicatedMessages: string[] = []; if (combinedMessages.length > 0) { deduplicatedMessages.push(combinedMessages[0]); // Add the newest one unconditionally for (let i = 1; i < combinedMessages.length; i++) { if (combinedMessages[i] !== combinedMessages[i - 1]) { deduplicatedMessages.push(combinedMessages[i]); } } } // Reverse to oldest first for useInputHistory setUserMessages(deduplicatedMessages.reverse()); }; fetchUserMessages(); }, [history, logger]); const isInputActive = streamingState === StreamingState.Idle && !initError && !isProcessing; const handleClearScreen = useCallback(() => { clearItems(); clearConsoleMessagesState(); console.clear(); refreshStatic(); }, [clearItems, clearConsoleMessagesState, refreshStatic]); const mainControlsRef = useRef(null); const pendingHistoryItemRef = useRef(null); useEffect(() => { if (mainControlsRef.current) { const fullFooterMeasurement = measureElement(mainControlsRef.current); setFooterHeight(fullFooterMeasurement.height); } }, [terminalHeight, consoleMessages, showErrorDetails]); const staticExtraHeight = /* margins and padding */ 3; const availableTerminalHeight = useMemo( () => terminalHeight - footerHeight - staticExtraHeight, [terminalHeight, footerHeight], ); useEffect(() => { // skip refreshing Static during first mount if (isInitialMount.current) { isInitialMount.current = false; return; } // debounce so it doesn't fire up too often during resize const handler = setTimeout(() => { setStaticNeedsRefresh(false); refreshStatic(); }, 300); return () => { clearTimeout(handler); }; }, [terminalWidth, terminalHeight, refreshStatic]); useEffect(() => { if (streamingState === StreamingState.Idle && staticNeedsRefresh) { setStaticNeedsRefresh(false); refreshStatic(); } }, [streamingState, refreshStatic, staticNeedsRefresh]); const filteredConsoleMessages = useMemo(() => { if (config.getDebugMode()) { return consoleMessages; } return consoleMessages.filter((msg) => msg.type !== 'debug'); }, [consoleMessages, config]); const branchName = useGitBranchName(config.getTargetDir()); const contextFileNames = useMemo(() => { const fromSettings = settings.merged.contextFileName; if (fromSettings) { return Array.isArray(fromSettings) ? fromSettings : [fromSettings]; } return getAllGeminiMdFilenames(); }, [settings.merged.contextFileName]); const initialPrompt = useMemo(() => config.getQuestion(), [config]); const geminiClient = config.getGeminiClient(); useEffect(() => { if ( initialPrompt && !initialPromptSubmitted.current && !isAuthenticating && !isAuthDialogOpen && !isThemeDialogOpen && !isEditorDialogOpen && !showPrivacyNotice && geminiClient?.isInitialized?.() ) { submitQuery(initialPrompt); initialPromptSubmitted.current = true; } }, [ initialPrompt, submitQuery, isAuthenticating, isAuthDialogOpen, isThemeDialogOpen, isEditorDialogOpen, showPrivacyNotice, geminiClient, ]); if (quittingMessages) { return ( {quittingMessages.map((item) => ( ))} ); } const mainAreaWidth = Math.floor(terminalWidth * 0.9); const debugConsoleMaxHeight = Math.floor(Math.max(terminalHeight * 0.2, 5)); // Arbitrary threshold to ensure that items in the static area are large // enough but not too large to make the terminal hard to use. const staticAreaMaxItemHeight = Math.max(terminalHeight * 4, 100); const placeholder = vimModeEnabled ? " Press 'i' for INSERT mode and 'Esc' for NORMAL mode." : ' Type your message or @path/to/file'; return ( {/* * The Static component is an Ink intrinsic in which there can only be 1 per application. * Because of this restriction we're hacking it slightly by having a 'header' item here to * ensure that it's statically rendered. * * Background on the Static Item: Anything in the Static component is written a single time * to the console. Think of it like doing a console.log and then never using ANSI codes to * clear that content ever again. Effectively it has a moving frame that every time new static * content is set it'll flush content to the terminal and move the area which it's "clearing" * down a notch. Without Static the area which gets erased and redrawn continuously grows. */} {!settings.merged.hideBanner && (
)} {!settings.merged.hideTips && } , ...history.map((h) => ( )), ]} > {(item) => item} {pendingHistoryItems.map((item, i) => ( ))} {/* Move UpdateNotification to render update notification above input area */} {updateInfo && } {startupWarnings.length > 0 && ( {startupWarnings.map((warning, index) => ( {warning} ))} )} {shouldShowIdePrompt && currentIDE ? ( ) : isFolderTrustDialogOpen ? ( ) : shellConfirmationRequest ? ( ) : confirmationRequest ? ( {confirmationRequest.prompt} { confirmationRequest.onConfirm(value); }} /> ) : isThemeDialogOpen ? ( {themeError && ( {themeError} )} ) : isSettingsDialogOpen ? ( closeSettingsDialog()} onRestartRequest={() => process.exit(0)} /> ) : isAuthenticating ? ( <> { setAuthError('Authentication timed out. Please try again.'); cancelAuthentication(); openAuthDialog(); }} /> {showErrorDetails && ( )} ) : isAuthDialogOpen ? ( ) : isEditorDialogOpen ? ( {editorError && ( {editorError} )} ) : showPrivacyNotice ? ( setShowPrivacyNotice(false)} config={config} /> ) : ( <> {process.env.GEMINI_SYSTEM_MD && ( |⌐■_■| )} {ctrlCPressedOnce ? ( Press Ctrl+C again to exit. ) : ctrlDPressedOnce ? ( Press Ctrl+D again to exit. ) : showEscapePrompt ? ( Press Esc again to clear. ) : ( )} {showAutoAcceptIndicator !== ApprovalMode.DEFAULT && !shellModeActive && ( )} {shellModeActive && } {showErrorDetails && ( )} {isInputActive && ( )} )} {initError && streamingState !== StreamingState.Responding && ( {history.find( (item) => item.type === 'error' && item.text?.includes(initError), )?.text ? ( { history.find( (item) => item.type === 'error' && item.text?.includes(initError), )?.text } ) : ( <> Initialization Error: {initError} {' '} Please check API key and configuration. )} )}