| |
| |
| |
| |
| |
|
|
| import { |
| useMemo, |
| useState, |
| useCallback, |
| useEffect, |
| useRef, |
| useLayoutEffect, |
| useContext, |
| } from 'react'; |
| import { |
| type DOMElement, |
| ResizeObserver, |
| useApp, |
| useStdout, |
| useStdin, |
| type AppProps, |
| AppContext as InkAppContext, |
| } from 'ink'; |
| import { App } from './App.js'; |
| import { AppContext } from './contexts/AppContext.js'; |
| import { UIStateContext, type UIState } from './contexts/UIStateContext.js'; |
| import { QuotaContext } from './contexts/QuotaContext.js'; |
| import { |
| UIActionsContext, |
| type UIActions, |
| } from './contexts/UIActionsContext.js'; |
| import { ConfigContext } from './contexts/ConfigContext.js'; |
| import { |
| type HistoryItem, |
| AuthState, |
| type ConfirmationRequest, |
| type PermissionConfirmationRequest, |
| type QuotaStats, |
| MessageType, |
| StreamingState, |
| type HistoryItemInfo, |
| } from './types.js'; |
| import { checkPermissions } from './hooks/atCommandProcessor.js'; |
| import { ToolActionsProvider } from './contexts/ToolActionsContext.js'; |
| import { MouseProvider } from './contexts/MouseContext.js'; |
| import { ScrollProvider } from './contexts/ScrollProvider.js'; |
| import { |
| type StartupWarning, |
| type Config, |
| type IdeInfo, |
| type IdeContext, |
| type UserTierId, |
| type GeminiUserTier, |
| type UserFeedbackPayload, |
| type HookSystemMessagePayload, |
| type AgentDefinition, |
| type ApprovalMode, |
| IdeClient, |
| ideContextStore, |
| getErrorMessage, |
| getAllGeminiMdFilenames, |
| AuthType, |
| clearCachedCredentialFile, |
| type ResumedSessionData, |
| recordExitFail, |
| ShellExecutionService, |
| saveApiKey, |
| debugLogger, |
| isValidEditorType, |
| coreEvents, |
| CoreEvent, |
| flattenMemory, |
| type MemoryChangedPayload, |
| writeToStdout, |
| disableMouseEvents, |
| enterAlternateScreen, |
| enableMouseEvents, |
| disableLineWrapping, |
| shouldEnterAlternateScreen, |
| startupProfiler, |
| SessionStartSource, |
| SessionEndReason, |
| generateSummary, |
| type ConsentRequestPayload, |
| type AgentsDiscoveredPayload, |
| ChangeAuthRequestedError, |
| ProjectIdRequiredError, |
| buildUserSteeringHintPrompt, |
| logBillingEvent, |
| ApiKeyUpdatedEvent, |
| LegacyAgentProtocol, |
| type InjectionSource, |
| } from '@google/gemini-cli-core'; |
| import { validateAuthMethod } from '../config/auth.js'; |
| import process from 'node:process'; |
| import { useHistory } from './hooks/useHistoryManager.js'; |
| import { useMemoryMonitor } from './hooks/useMemoryMonitor.js'; |
| import { useThemeCommand } from './hooks/useThemeCommand.js'; |
| import { useAuthCommand } from './auth/useAuth.js'; |
| import { useQuotaAndFallback } from './hooks/useQuotaAndFallback.js'; |
| import { useEditorSettings } from './hooks/useEditorSettings.js'; |
| import { useSettingsCommand } from './hooks/useSettingsCommand.js'; |
| import { useModelCommand } from './hooks/useModelCommand.js'; |
| import { useVoiceModelCommand } from './hooks/useVoiceModelCommand.js'; |
| import { useSlashCommandProcessor } from './hooks/slashCommandProcessor.js'; |
| import { useVimMode } from './contexts/VimModeContext.js'; |
| import { |
| useOverflowActions, |
| useOverflowState, |
| } from './contexts/OverflowContext.js'; |
| import { useErrorCount } from './hooks/useConsoleMessages.js'; |
| import { useTerminalSize } from './hooks/useTerminalSize.js'; |
| import { calculatePromptWidths } from './components/InputPrompt.js'; |
| import { calculateMainAreaWidth } from './utils/ui-sizing.js'; |
| import ansiEscapes from 'ansi-escapes'; |
| import { basename } from 'node:path'; |
| import { computeTerminalTitle } from '../utils/windowTitle.js'; |
| import { useTextBuffer } from './components/shared/text-buffer.js'; |
| import { useLogger } from './hooks/useLogger.js'; |
| import { useGeminiStream } from './hooks/useGeminiStream.js'; |
| import { useAgentStream } from './hooks/useAgentStream.js'; |
| import { type BackgroundTask } from './hooks/useExecutionLifecycle.js'; |
| import { useVim } from './hooks/vim.js'; |
| import { type LoadableSettingScope, SettingScope } from '../config/settings.js'; |
| import { type InitializationResult } from '../core/initializer.js'; |
| import { startAutoMemoryIfEnabled } from '../utils/autoMemory.js'; |
| import { useFocus } from './hooks/useFocus.js'; |
| import { useKeypress, type Key } from './hooks/useKeypress.js'; |
| import { KeypressPriority } from './contexts/KeypressContext.js'; |
| import { Command } from './key/keyMatchers.js'; |
| import { useLoadingIndicator } from './hooks/useLoadingIndicator.js'; |
| import { useShellInactivityStatus } from './hooks/useShellInactivityStatus.js'; |
| import { useFolderTrust } from './hooks/useFolderTrust.js'; |
| import { useIdeTrustListener } from './hooks/useIdeTrustListener.js'; |
| import { type IdeIntegrationNudgeResult } from './IdeIntegrationNudge.js'; |
| import { appEvents, AppEvent, TransientMessageType } from '../utils/events.js'; |
| import { type UpdateObject } from './utils/updateCheck.js'; |
| import { setUpdateHandler } from '../utils/handleAutoUpdate.js'; |
| import { |
| registerCleanup, |
| removeCleanup, |
| runExitCleanup, |
| } from '../utils/cleanup.js'; |
| import { relaunchApp } from '../utils/processUtils.js'; |
| import type { SessionInfo } from '../utils/sessionUtils.js'; |
| import { useMessageQueue } from './hooks/useMessageQueue.js'; |
| import { useMcpStatus } from './hooks/useMcpStatus.js'; |
| import { useApprovalModeIndicator } from './hooks/useApprovalModeIndicator.js'; |
| import { useSessionStats } from './contexts/SessionContext.js'; |
| import { useGitBranchName } from './hooks/useGitBranchName.js'; |
| import { |
| useConfirmUpdateRequests, |
| useExtensionUpdates, |
| } from './hooks/useExtensionUpdates.js'; |
| import { ShellFocusContext } from './contexts/ShellFocusContext.js'; |
| import { type ExtensionManager } from '../config/extension-manager.js'; |
| import { requestConsentInteractive } from '../config/extensions/consent.js'; |
| import { useSessionBrowser } from './hooks/useSessionBrowser.js'; |
| import { useSessionResume } from './hooks/useSessionResume.js'; |
| import { useIncludeDirsTrust } from './hooks/useIncludeDirsTrust.js'; |
| import { isWorkspaceTrusted } from '../config/trustedFolders.js'; |
| import { useSettings } from './contexts/SettingsContext.js'; |
| import { terminalCapabilityManager } from './utils/terminalCapabilityManager.js'; |
| import { useInputHistoryStore } from './hooks/useInputHistoryStore.js'; |
| import { useBanner } from './hooks/useBanner.js'; |
| import { useTerminalSetupPrompt } from './utils/terminalSetup.js'; |
| import { useHookDisplayState } from './hooks/useHookDisplayState.js'; |
| import { useBackgroundTaskManager } from './hooks/useBackgroundTaskManager.js'; |
| import { |
| WARNING_PROMPT_DURATION_MS, |
| QUEUE_ERROR_DISPLAY_DURATION_MS, |
| EXPAND_HINT_DURATION_MS, |
| } from './constants.js'; |
| import { NewAgentsChoice } from './components/NewAgentsNotification.js'; |
| import { isSlashCommand } from './utils/commandUtils.js'; |
| import { parseSlashCommand } from '../utils/commands.js'; |
| import { useTerminalTheme } from './hooks/useTerminalTheme.js'; |
| import { useTimedMessage } from './hooks/useTimedMessage.js'; |
| import { useIsHelpDismissKey } from './utils/shortcutsHelp.js'; |
| import { useSuspend } from './hooks/useSuspend.js'; |
| import { useRunEventNotifications } from './hooks/useRunEventNotifications.js'; |
| import { |
| isNotificationsEnabled, |
| getNotificationMethod, |
| } from '../utils/terminalNotifications.js'; |
| import { |
| getLastTurnToolCallIds, |
| isToolExecuting, |
| isToolAwaitingConfirmation, |
| getAllToolCalls, |
| } from './utils/historyUtils.js'; |
|
|
| interface AppContainerProps { |
| config: Config; |
| startupWarnings?: StartupWarning[]; |
| version: string; |
| initializationResult: InitializationResult; |
| resumedSessionData?: ResumedSessionData; |
| } |
|
|
| import { useRepeatedKeyPress } from './hooks/useRepeatedKeyPress.js'; |
| import { |
| useVisibilityToggle, |
| APPROVAL_MODE_REVEAL_DURATION_MS, |
| } from './hooks/useVisibilityToggle.js'; |
| import { useKeyMatchers } from './hooks/useKeyMatchers.js'; |
|
|
| import { InputContext } from './contexts/InputContext.js'; |
|
|
| |
| |
| |
| |
| const SHELL_WIDTH_FRACTION = 0.89; |
|
|
| |
| |
| |
| |
| const SHELL_HEIGHT_PADDING = 10; |
|
|
| export const AppContainer = (props: AppContainerProps) => { |
| const isHelpDismissKey = useIsHelpDismissKey(); |
| const keyMatchers = useKeyMatchers(); |
| const { config, initializationResult, resumedSessionData } = props; |
| const settings = useSettings(); |
| const { reset } = useOverflowActions()!; |
| const notificationsEnabled = isNotificationsEnabled(settings); |
| const notificationMethod = getNotificationMethod(settings); |
|
|
| const { setOptions, dumpCurrentFrame, startRecording, stopRecording } = |
| useContext(InkAppContext); |
| const recordingFilenameRef = useRef<string | null>(null); |
| const historyManager = useHistory({ |
| chatRecordingService: config.getGeminiClient()?.getChatRecordingService(), |
| }); |
|
|
| useMemoryMonitor(historyManager); |
| const isAlternateBuffer = config.getUseAlternateBuffer(); |
| const [mouseMode, setMouseMode] = useState(() => |
| config.getUseAlternateBuffer(), |
| ); |
|
|
| useEffect(() => { |
| setOptions({ |
| stickyHeadersInBackbuffer: mouseMode, |
| }); |
| if (mouseMode) { |
| enableMouseEvents(); |
| } else { |
| disableMouseEvents(); |
| } |
| }, [mouseMode, setOptions]); |
|
|
| const [corgiMode, setCorgiMode] = useState(false); |
| const [debugMessage, setDebugMessage] = useState<string>(''); |
| const [quittingMessages, setQuittingMessages] = useState< |
| HistoryItem[] | null |
| >(null); |
| const [showPrivacyNotice, setShowPrivacyNotice] = useState<boolean>(false); |
| const [themeError, setThemeError] = useState<string | null>( |
| initializationResult.themeError, |
| ); |
| const [isProcessing, setIsProcessing] = useState<boolean>(false); |
| const [embeddedShellFocused, setEmbeddedShellFocused] = useState(false); |
| const [showDebugProfiler, setShowDebugProfiler] = useState(false); |
| const [customDialog, setCustomDialog] = useState<React.ReactNode | null>( |
| null, |
| ); |
| const [copyModeEnabled, setCopyModeEnabled] = useState(false); |
| const [pendingRestorePrompt, setPendingRestorePrompt] = useState(false); |
| const toggleBackgroundTasksRef = useRef<() => void>(() => {}); |
| const isBackgroundTaskVisibleRef = useRef<boolean>(false); |
| const backgroundTasksRef = useRef<Map<number, BackgroundTask>>(new Map()); |
|
|
| const [adminSettingsChanged, setAdminSettingsChanged] = useState(false); |
|
|
| const [expandedTools, setExpandedTools] = useState<Set<string>>(new Set()); |
|
|
| const toggleExpansion = useCallback((callId: string) => { |
| setExpandedTools((prev) => { |
| const next = new Set(prev); |
| if (next.has(callId)) { |
| next.delete(callId); |
| } else { |
| next.add(callId); |
| } |
| return next; |
| }); |
| }, []); |
|
|
| const toggleAllExpansion = useCallback((callIds: string[]) => { |
| setExpandedTools((prev) => { |
| const next = new Set(prev); |
| const anyCollapsed = callIds.some((id) => !next.has(id)); |
|
|
| if (anyCollapsed) { |
| callIds.forEach((id) => next.add(id)); |
| } else { |
| callIds.forEach((id) => next.delete(id)); |
| } |
| return next; |
| }); |
| }, []); |
|
|
| const isExpanded = useCallback( |
| (callId: string) => expandedTools.has(callId), |
| [expandedTools], |
| ); |
|
|
| const [shellModeActive, setShellModeActive] = useState(false); |
| const [isVoiceModeEnabled, setVoiceModeEnabled] = useState(false); |
| const [modelSwitchedFromQuotaError, setModelSwitchedFromQuotaError] = |
| useState<boolean>(false); |
| const [historyRemountKey, setHistoryRemountKey] = useState(0); |
| const [settingsNonce, setSettingsNonce] = useState(0); |
| const activeHooks = useHookDisplayState(); |
| const [updateInfo, setUpdateInfo] = useState<UpdateObject | null>(null); |
| const [isTrustedFolder, setIsTrustedFolder] = useState<boolean | undefined>( |
| () => isWorkspaceTrusted(settings.merged).isTrusted, |
| ); |
|
|
| const [queueErrorMessage, setQueueErrorMessage] = useTimedMessage<string>( |
| QUEUE_ERROR_DISPLAY_DURATION_MS, |
| ); |
|
|
| const [newAgents, setNewAgents] = useState<AgentDefinition[] | null>(null); |
| const [constrainHeight, setConstrainHeight] = useState<boolean>(true); |
| const [expandHintTrigger, triggerExpandHint] = useTimedMessage<boolean>( |
| EXPAND_HINT_DURATION_MS, |
| ); |
| const showIsExpandableHint = Boolean(expandHintTrigger); |
| const overflowState = useOverflowState(); |
| const overflowingIdsSize = overflowState?.overflowingIds.size ?? 0; |
| const hasOverflowState = overflowingIdsSize > 0 || !constrainHeight; |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| useEffect(() => { |
| if (hasOverflowState) { |
| triggerExpandHint(true); |
| } |
| }, [hasOverflowState, overflowingIdsSize, triggerExpandHint]); |
|
|
| const [defaultBannerText, setDefaultBannerText] = useState(''); |
| const [warningBannerText, setWarningBannerText] = useState(''); |
| const [bannerVisible, setBannerVisible] = useState(true); |
|
|
| const bannerData = useMemo( |
| () => ({ |
| defaultText: defaultBannerText, |
| warningText: warningBannerText, |
| }), |
| [defaultBannerText, warningBannerText], |
| ); |
|
|
| const { bannerText } = useBanner(bannerData); |
|
|
| |
| const extensionManager = config.getExtensionLoader() as ExtensionManager; |
| |
| extensionManager.setRequestConsent((description) => |
| requestConsentInteractive(description, addConfirmUpdateExtensionRequest), |
| ); |
| extensionManager.setRequestSetting(); |
|
|
| const { addConfirmUpdateExtensionRequest, confirmUpdateExtensionRequests } = |
| useConfirmUpdateRequests(); |
| const { |
| extensionsUpdateState, |
| extensionsUpdateStateInternal, |
| dispatchExtensionStateUpdate, |
| } = useExtensionUpdates( |
| extensionManager, |
| historyManager.addItem, |
| config.getEnableExtensionReloading(), |
| ); |
|
|
| const [isPermissionsDialogOpen, setPermissionsDialogOpen] = useState(false); |
| const [permissionsDialogProps, setPermissionsDialogProps] = useState<{ |
| targetDirectory?: string; |
| } | null>(null); |
| const openPermissionsDialog = useCallback( |
| (props?: { targetDirectory?: string }) => { |
| setPermissionsDialogOpen(true); |
| setPermissionsDialogProps(props ?? null); |
| }, |
| [], |
| ); |
| const closePermissionsDialog = useCallback(() => { |
| setPermissionsDialogOpen(false); |
| setPermissionsDialogProps(null); |
| }, []); |
|
|
| const [isAgentConfigDialogOpen, setIsAgentConfigDialogOpen] = useState(false); |
| const [selectedAgentName, setSelectedAgentName] = useState< |
| string | undefined |
| >(); |
| const [selectedAgentDisplayName, setSelectedAgentDisplayName] = useState< |
| string | undefined |
| >(); |
| const [selectedAgentDefinition, setSelectedAgentDefinition] = useState< |
| AgentDefinition | undefined |
| >(); |
|
|
| const openAgentConfigDialog = useCallback( |
| (name: string, displayName: string, definition: AgentDefinition) => { |
| setSelectedAgentName(name); |
| setSelectedAgentDisplayName(displayName); |
| setSelectedAgentDefinition(definition); |
| setIsAgentConfigDialogOpen(true); |
| }, |
| [], |
| ); |
|
|
| const closeAgentConfigDialog = useCallback(() => { |
| setIsAgentConfigDialogOpen(false); |
| setSelectedAgentName(undefined); |
| setSelectedAgentDisplayName(undefined); |
| setSelectedAgentDefinition(undefined); |
| }, []); |
|
|
| const toggleDebugProfiler = useCallback( |
| () => setShowDebugProfiler((prev) => !prev), |
| [], |
| ); |
|
|
| const [currentModel, setCurrentModel] = useState(config.getModel()); |
|
|
| const [userTier, setUserTier] = useState<UserTierId | undefined>(undefined); |
| const [quotaStats, setQuotaStats] = useState<QuotaStats | undefined>(() => { |
| const remaining = config.getQuotaRemaining(); |
| const limit = config.getQuotaLimit(); |
| const resetTime = config.getQuotaResetTime(); |
| return remaining !== undefined || |
| limit !== undefined || |
| resetTime !== undefined |
| ? { remaining, limit, resetTime } |
| : undefined; |
| }); |
| const [paidTier, setPaidTier] = useState<GeminiUserTier | undefined>( |
| undefined, |
| ); |
|
|
| const [isConfigInitialized, setConfigInitialized] = useState(false); |
|
|
| const logger = useLogger(config); |
| const { inputHistory, addInput, initializeFromLogger } = |
| useInputHistoryStore(); |
|
|
| |
| const { columns: terminalWidth, rows: terminalHeight } = useTerminalSize(); |
| const { stdin, setRawMode } = useStdin(); |
| const { stdout } = useStdout(); |
| const app: AppProps = useApp(); |
|
|
| |
| const { stats: sessionStats } = useSessionStats(); |
| const branchName = useGitBranchName(config.getTargetDir()); |
|
|
| |
| |
| const rootUiRef = useRef<DOMElement>(null); |
| const lastTitleRef = useRef<string | null>(null); |
| const staticExtraHeight = 3; |
|
|
| useEffect(() => { |
| |
| (async () => { |
| |
| |
| if (!config.isInitialized()) { |
| await config.initialize(); |
| } |
| setConfigInitialized(true); |
| startupProfiler.flush(config); |
|
|
| startAutoMemoryIfEnabled(config); |
|
|
| const sessionStartSource = resumedSessionData |
| ? SessionStartSource.Resume |
| : SessionStartSource.Startup; |
| const result = await config |
| .getHookSystem() |
| ?.fireSessionStartEvent(sessionStartSource); |
|
|
| if (result) { |
| const additionalContext = result.getAdditionalContext(); |
| const geminiClient = config.getGeminiClient(); |
| if (additionalContext && geminiClient) { |
| await geminiClient.addHistory({ |
| role: 'user', |
| parts: [ |
| { text: `<hook_context>${additionalContext}</hook_context>` }, |
| ], |
| }); |
| } |
| } |
|
|
| |
| generateSummary(config).catch((e) => { |
| debugLogger.warn('Background summary generation failed:', e); |
| }); |
| })(); |
| const cleanupFn = async () => { |
| |
| disableMouseEvents(); |
|
|
| |
| await Promise.all( |
| Array.from(backgroundTasksRef.current.keys()).map((pid) => |
| ShellExecutionService.kill(pid), |
| ), |
| ); |
|
|
| const ideClient = await IdeClient.getInstance(); |
| await ideClient.disconnect(); |
|
|
| |
| await config?.getHookSystem()?.fireSessionEndEvent(SessionEndReason.Exit); |
| }; |
| registerCleanup(cleanupFn); |
|
|
| return () => { |
| removeCleanup(cleanupFn); |
| cleanupFn().catch((e: unknown) => |
| debugLogger.error('Error during cleanup:', e), |
| ); |
| }; |
| }, [config, resumedSessionData]); |
|
|
| useEffect( |
| () => setUpdateHandler(historyManager.addItem, setUpdateInfo), |
| [historyManager.addItem], |
| ); |
|
|
| |
| useEffect(() => { |
| const handleModelChanged = () => { |
| setCurrentModel(config.getModel()); |
| }; |
|
|
| const handleQuotaChanged = (payload: { |
| remaining: number | undefined; |
| limit: number | undefined; |
| resetTime?: string; |
| }) => { |
| setQuotaStats({ |
| remaining: payload.remaining, |
| limit: payload.limit, |
| resetTime: payload.resetTime, |
| }); |
| }; |
|
|
| coreEvents.on(CoreEvent.ModelChanged, handleModelChanged); |
| coreEvents.on(CoreEvent.QuotaChanged, handleQuotaChanged); |
| return () => { |
| coreEvents.off(CoreEvent.ModelChanged, handleModelChanged); |
| coreEvents.off(CoreEvent.QuotaChanged, handleQuotaChanged); |
| }; |
| }, [config]); |
|
|
| useEffect(() => { |
| const handleSettingsChanged = () => { |
| setSettingsNonce((prev) => prev + 1); |
| }; |
|
|
| const handleAdminSettingsChanged = () => { |
| setAdminSettingsChanged(true); |
| }; |
|
|
| const handleAgentsDiscovered = (payload: AgentsDiscoveredPayload) => { |
| setNewAgents(payload.agents); |
| }; |
|
|
| coreEvents.on(CoreEvent.SettingsChanged, handleSettingsChanged); |
| coreEvents.on(CoreEvent.AdminSettingsChanged, handleAdminSettingsChanged); |
| coreEvents.on(CoreEvent.AgentsDiscovered, handleAgentsDiscovered); |
| return () => { |
| coreEvents.off(CoreEvent.SettingsChanged, handleSettingsChanged); |
| coreEvents.off( |
| CoreEvent.AdminSettingsChanged, |
| handleAdminSettingsChanged, |
| ); |
| coreEvents.off(CoreEvent.AgentsDiscovered, handleAgentsDiscovered); |
| }; |
| }, [settings]); |
|
|
| const { errorCount, clearErrorCount } = useErrorCount(); |
|
|
| const mainAreaWidth = calculateMainAreaWidth(terminalWidth, config); |
| |
| const { inputWidth, suggestionsWidth } = useMemo(() => { |
| const { inputWidth, suggestionsWidth } = |
| calculatePromptWidths(mainAreaWidth); |
| return { inputWidth, suggestionsWidth }; |
| }, [mainAreaWidth]); |
|
|
| const staticAreaMaxItemHeight = Math.max(terminalHeight * 4, 100); |
|
|
| const getPreferredEditor = useCallback(() => { |
| const val = settings.merged.general.preferredEditor; |
| return isValidEditorType(val) ? val : undefined; |
| }, [settings.merged.general.preferredEditor]); |
|
|
| const buffer = useTextBuffer({ |
| initialText: '', |
| viewport: { height: 10, width: inputWidth }, |
| stdin, |
| setRawMode, |
| escapePastedPaths: true, |
| shellModeActive, |
| getPreferredEditor, |
| }); |
| const bufferRef = useRef(buffer); |
| useEffect(() => { |
| bufferRef.current = buffer; |
| }, [buffer]); |
|
|
| const stableSetText = useCallback((text: string) => { |
| bufferRef.current.setText(text); |
| }, []); |
|
|
| |
| useEffect(() => { |
| |
| initializeFromLogger(logger); |
| }, [logger, initializeFromLogger]); |
|
|
| |
| useTerminalSetupPrompt({ |
| addConfirmUpdateExtensionRequest, |
| addItem: historyManager.addItem, |
| }); |
|
|
| const refreshStatic = useCallback(() => { |
| if (!isAlternateBuffer && !config.getUseTerminalBuffer()) { |
| stdout.write(ansiEscapes.clearTerminal); |
| setHistoryRemountKey((prev) => prev + 1); |
| } |
| }, [setHistoryRemountKey, isAlternateBuffer, stdout, config]); |
|
|
| const shouldUseAlternateScreen = shouldEnterAlternateScreen( |
| isAlternateBuffer, |
| config.getScreenReader(), |
| ); |
|
|
| const handleEditorClose = useCallback(() => { |
| if (shouldUseAlternateScreen) { |
| |
| |
| enterAlternateScreen(); |
| enableMouseEvents(); |
| disableLineWrapping(); |
| app.rerender(); |
| } else if (config.getUseTerminalBuffer()) { |
| app.rerender(); |
| } |
| terminalCapabilityManager.enableSupportedModes(); |
| refreshStatic(); |
| }, [refreshStatic, shouldUseAlternateScreen, app, config]); |
|
|
| const [editorError, setEditorError] = useState<string | null>(null); |
| const { |
| isEditorDialogOpen, |
| openEditorDialog, |
| handleEditorSelect, |
| exitEditorDialog, |
| } = useEditorSettings(settings, setEditorError, historyManager.addItem); |
|
|
| useEffect(() => { |
| coreEvents.on(CoreEvent.ExternalEditorClosed, handleEditorClose); |
| coreEvents.on(CoreEvent.RequestEditorSelection, openEditorDialog); |
| return () => { |
| coreEvents.off(CoreEvent.ExternalEditorClosed, handleEditorClose); |
| coreEvents.off(CoreEvent.RequestEditorSelection, openEditorDialog); |
| }; |
| }, [handleEditorClose, openEditorDialog]); |
|
|
| useEffect(() => { |
| if ( |
| !(settings.merged.ui.hideBanner || config.getScreenReader()) && |
| bannerVisible && |
| bannerText |
| ) { |
| |
| |
| refreshStatic(); |
| } |
| }, [bannerVisible, bannerText, settings, config, refreshStatic]); |
|
|
| const { isSettingsDialogOpen, openSettingsDialog, closeSettingsDialog } = |
| useSettingsCommand(); |
|
|
| const { |
| isThemeDialogOpen, |
| openThemeDialog, |
| closeThemeDialog, |
| handleThemeSelect, |
| handleThemeHighlight, |
| } = useThemeCommand( |
| settings, |
| setThemeError, |
| historyManager.addItem, |
| initializationResult.themeError, |
| refreshStatic, |
| ); |
| |
| useTerminalTheme(handleThemeSelect, config, refreshStatic); |
| const { |
| authState, |
| setAuthState, |
| authError, |
| onAuthError, |
| apiKeyDefaultValue, |
| reloadApiKey, |
| accountSuspensionInfo, |
| setAccountSuspensionInfo, |
| } = useAuthCommand( |
| settings, |
| config, |
| initializationResult.authError, |
| initializationResult.accountSuspensionInfo, |
| ); |
| const [authContext, setAuthContext] = useState<{ requiresRestart?: boolean }>( |
| {}, |
| ); |
|
|
| useEffect(() => { |
| if (authState === AuthState.Authenticated && authContext.requiresRestart) { |
| setAuthState(AuthState.AwaitingLoginRestart); |
| setAuthContext({}); |
| } |
| }, [authState, authContext, setAuthState]); |
|
|
| const { |
| proQuotaRequest, |
| handleProQuotaChoice, |
| validationRequest, |
| handleValidationChoice, |
| |
| overageMenuRequest, |
| handleOverageMenuChoice, |
| emptyWalletRequest, |
| handleEmptyWalletChoice, |
| } = useQuotaAndFallback({ |
| config, |
| historyManager, |
| userTier, |
| paidTier, |
| settings, |
| setModelSwitchedFromQuotaError, |
| onShowAuthSelection: () => setAuthState(AuthState.Updating), |
| errorVerbosity: settings.merged.ui.errorVerbosity, |
| }); |
|
|
| |
| const isAuthDialogOpen = authState === AuthState.Updating; |
| |
| const isAuthenticating = |
| authState === AuthState.Unauthenticated && |
| settings.merged.security.auth.selectedType !== AuthType.USE_GEMINI; |
|
|
| |
| const isGeminiClientInitialized = config.getGeminiClient()?.isInitialized(); |
|
|
| const { loadHistoryForResume, isResuming } = useSessionResume({ |
| config, |
| historyManager, |
| refreshStatic, |
| isGeminiClientInitialized, |
| setQuittingMessages, |
| resumedSessionData, |
| isAuthenticating, |
| }); |
| const { |
| isSessionBrowserOpen, |
| openSessionBrowser, |
| closeSessionBrowser, |
| handleResumeSession, |
| handleDeleteSession: handleDeleteSessionSync, |
| } = useSessionBrowser(config, loadHistoryForResume); |
| |
| const handleDeleteSession = useCallback( |
| async (session: SessionInfo): Promise<void> => { |
| await handleDeleteSessionSync(session); |
| }, |
| [handleDeleteSessionSync], |
| ); |
|
|
| |
| const handleAuthSelect = useCallback( |
| async (authType: AuthType | undefined, scope: LoadableSettingScope) => { |
| if (authType) { |
| const previousAuthType = |
| config.getContentGeneratorConfig()?.authType ?? 'unknown'; |
| if (authType === AuthType.LOGIN_WITH_GOOGLE) { |
| setAuthContext({ requiresRestart: true }); |
| } else { |
| setAuthContext({}); |
| } |
| await clearCachedCredentialFile(); |
| settings.setValue(scope, 'security.auth.selectedType', authType); |
|
|
| try { |
| config.setRemoteAdminSettings(undefined); |
| await config.refreshAuth(authType); |
| setAuthState(AuthState.Authenticated); |
| logBillingEvent( |
| config, |
| new ApiKeyUpdatedEvent(previousAuthType, authType), |
| ); |
| } catch (e) { |
| if (e instanceof ChangeAuthRequestedError) { |
| return; |
| } |
| if (e instanceof ProjectIdRequiredError) { |
| |
| |
| onAuthError(getErrorMessage(e)); |
| return; |
| } |
| onAuthError( |
| `Failed to authenticate: ${e instanceof Error ? e.message : String(e)}`, |
| ); |
| return; |
| } |
|
|
| if ( |
| authType === AuthType.LOGIN_WITH_GOOGLE && |
| config.isBrowserLaunchSuppressed() |
| ) { |
| writeToStdout(` |
| ---------------------------------------------------------------- |
| Logging in with Google... Restarting Gemini CLI to continue. |
| ---------------------------------------------------------------- |
| `); |
| await relaunchApp(); |
| } |
| } |
| setAuthState(AuthState.Authenticated); |
| }, |
| [settings, config, setAuthState, onAuthError, setAuthContext], |
| ); |
|
|
| const handleApiKeySubmit = useCallback( |
| async (apiKey: string) => { |
| try { |
| onAuthError(null); |
| if (!apiKey.trim()) { |
| onAuthError('API key cannot be empty or whitespace only.'); |
| return; |
| } |
|
|
| await saveApiKey(apiKey); |
| await reloadApiKey(); |
| await config.refreshAuth(AuthType.USE_GEMINI); |
| setAuthState(AuthState.Authenticated); |
| } catch (e) { |
| onAuthError( |
| `Failed to save API key: ${e instanceof Error ? e.message : String(e)}`, |
| ); |
| } |
| }, |
| [setAuthState, onAuthError, reloadApiKey, config], |
| ); |
|
|
| const handleApiKeyCancel = useCallback(() => { |
| |
| setAuthState(AuthState.Updating); |
| }, [setAuthState]); |
|
|
| |
| useEffect(() => { |
| |
| if (authState === AuthState.Authenticated) { |
| setUserTier(config.getUserTier()); |
| setPaidTier(config.getUserPaidTier()); |
| } |
| }, [config, authState]); |
|
|
| |
| useEffect(() => { |
| if ( |
| settings.merged.security.auth.enforcedType && |
| settings.merged.security.auth.selectedType && |
| settings.merged.security.auth.enforcedType !== |
| settings.merged.security.auth.selectedType |
| ) { |
| onAuthError( |
| `Authentication is enforced to be ${settings.merged.security.auth.enforcedType}, but you are currently using ${settings.merged.security.auth.selectedType}.`, |
| ); |
| } else if ( |
| settings.merged.security.auth.selectedType && |
| !settings.merged.security.auth.useExternal |
| ) { |
| |
| |
| |
| if (settings.merged.security.auth.selectedType === AuthType.USE_GEMINI) { |
| return; |
| } |
|
|
| const authMethod = settings.merged.security.auth.selectedType; |
| void (async () => { |
| try { |
| const error = await validateAuthMethod(authMethod); |
| if ( |
| error && |
| authMethod === settings.merged.security.auth.selectedType |
| ) { |
| onAuthError(error); |
| } |
| } catch (e) { |
| if (authMethod === settings.merged.security.auth.selectedType) { |
| onAuthError(getErrorMessage(e)); |
| } |
| } |
| })(); |
| } |
| }, [ |
| settings.merged.security.auth.selectedType, |
| settings.merged.security.auth.enforcedType, |
| settings.merged.security.auth.useExternal, |
| onAuthError, |
| ]); |
|
|
| const { isModelDialogOpen, openModelDialog, closeModelDialog } = |
| useModelCommand(); |
|
|
| const { |
| isVoiceModelDialogOpen, |
| openVoiceModelDialog, |
| closeVoiceModelDialog, |
| } = useVoiceModelCommand(); |
|
|
| const { toggleVimEnabled } = useVimMode(); |
|
|
| const setIsBackgroundTaskListOpenRef = useRef<(open: boolean) => void>( |
| () => {}, |
| ); |
| const [shortcutsHelpVisible, setShortcutsHelpVisible] = useState(false); |
|
|
| const { |
| cleanUiDetailsVisible, |
| setCleanUiDetailsVisible, |
| toggleCleanUiDetailsVisible, |
| revealCleanUiDetailsTemporarily, |
| } = useVisibilityToggle(); |
|
|
| const slashCommandActions = useMemo( |
| () => ({ |
| openAuthDialog: () => setAuthState(AuthState.Updating), |
| openThemeDialog, |
| openEditorDialog, |
| openPrivacyNotice: () => setShowPrivacyNotice(true), |
| openSettingsDialog, |
| openSessionBrowser, |
| openModelDialog, |
| openVoiceModelDialog, |
| openAgentConfigDialog, |
| openPermissionsDialog, |
| quit: (messages: HistoryItem[]) => { |
| closeThemeDialog(); |
| setQuittingMessages(messages); |
| setTimeout(async () => { |
| await runExitCleanup(); |
| process.exit(0); |
| }, 100); |
| }, |
| setDebugMessage, |
| toggleCorgiMode: () => setCorgiMode((prev) => !prev), |
| toggleVoiceMode: () => setVoiceModeEnabled((prev) => !prev), |
| toggleDebugProfiler, |
| dispatchExtensionStateUpdate, |
| addConfirmUpdateExtensionRequest, |
| toggleBackgroundTasks: () => { |
| toggleBackgroundTasksRef.current(); |
| if (!isBackgroundTaskVisibleRef.current) { |
| setEmbeddedShellFocused(true); |
| if (backgroundTasksRef.current.size > 1) { |
| setIsBackgroundTaskListOpenRef.current(true); |
| } else { |
| setIsBackgroundTaskListOpenRef.current(false); |
| } |
| } |
| }, |
| toggleShortcutsHelp: () => setShortcutsHelpVisible((visible) => !visible), |
| setText: stableSetText, |
| }), |
| [ |
| setAuthState, |
| openThemeDialog, |
| closeThemeDialog, |
| openEditorDialog, |
| openSettingsDialog, |
| openSessionBrowser, |
| openModelDialog, |
| openVoiceModelDialog, |
| openAgentConfigDialog, |
| setQuittingMessages, |
| setDebugMessage, |
| setShowPrivacyNotice, |
| setCorgiMode, |
| dispatchExtensionStateUpdate, |
| openPermissionsDialog, |
| addConfirmUpdateExtensionRequest, |
| toggleDebugProfiler, |
| setShortcutsHelpVisible, |
| stableSetText, |
| ], |
| ); |
|
|
| const { |
| handleSlashCommand, |
| slashCommands, |
| pendingHistoryItems: pendingSlashCommandHistoryItems, |
| commandContext, |
| confirmationRequest: commandConfirmationRequest, |
| } = useSlashCommandProcessor( |
| config, |
| settings, |
| historyManager.addItem, |
| historyManager.clearItems, |
| historyManager.loadHistory, |
| refreshStatic, |
| toggleVimEnabled, |
| setIsProcessing, |
| slashCommandActions, |
| extensionsUpdateStateInternal, |
| isConfigInitialized, |
| setBannerVisible, |
| setCustomDialog, |
| ); |
|
|
| const [authConsentRequest, setAuthConsentRequest] = |
| useState<ConfirmationRequest | null>(null); |
| const [permissionConfirmationRequest, setPermissionConfirmationRequest] = |
| useState<PermissionConfirmationRequest | null>(null); |
|
|
| useEffect(() => { |
| const handleConsentRequest = (payload: ConsentRequestPayload) => { |
| setAuthConsentRequest({ |
| prompt: payload.prompt, |
| onConfirm: (confirmed: boolean) => { |
| setAuthConsentRequest(null); |
| payload.onConfirm(confirmed); |
| }, |
| }); |
| }; |
|
|
| coreEvents.on(CoreEvent.ConsentRequest, handleConsentRequest); |
| return () => { |
| coreEvents.off(CoreEvent.ConsentRequest, handleConsentRequest); |
| }; |
| }, []); |
|
|
| const performMemoryRefresh = useCallback(async () => { |
| historyManager.addItem( |
| { |
| type: MessageType.INFO, |
| text: 'Refreshing hierarchical memory (GEMINI.md or other context files)...', |
| }, |
| Date.now(), |
| ); |
| try { |
| await config.getMemoryContextManager()?.refresh(); |
| config.updateSystemInstructionIfInitialized(); |
| const flattenedMemory = flattenMemory(config.getUserMemory()); |
| const fileCount = config.getGeminiMdFileCount(); |
|
|
| historyManager.addItem( |
| { |
| type: MessageType.INFO, |
| text: `Memory reloaded successfully. ${ |
| flattenedMemory.length > 0 |
| ? `Loaded ${flattenedMemory.length} characters from ${fileCount} file(s)` |
| : 'No memory content found' |
| }`, |
| }, |
| Date.now(), |
| ); |
| if (config.getDebugMode()) { |
| debugLogger.log( |
| `[DEBUG] Refreshed memory content in config: ${flattenedMemory.substring( |
| 0, |
| 200, |
| )}...`, |
| ); |
| } |
| } catch (error) { |
| const errorMessage = getErrorMessage(error); |
| historyManager.addItem( |
| { |
| type: MessageType.ERROR, |
| text: `Error refreshing memory: ${errorMessage}`, |
| }, |
| Date.now(), |
| ); |
| debugLogger.warn('Error refreshing memory:', error); |
| } |
| }, [config, historyManager]); |
|
|
| const cancelHandlerRef = useRef< |
| (shouldRestorePrompt?: boolean, clearBuffer?: boolean) => void |
| >(() => {}); |
|
|
| const onCancelSubmit = useCallback( |
| (shouldRestorePrompt?: boolean, clearBuffer: boolean = false) => { |
| if (shouldRestorePrompt) { |
| setPendingRestorePrompt(true); |
| } else { |
| setPendingRestorePrompt(false); |
| cancelHandlerRef.current(false, clearBuffer); |
| } |
| }, |
| [], |
| ); |
|
|
| useEffect(() => { |
| if (pendingRestorePrompt) { |
| const lastHistoryUserMsg = historyManager.history.findLast( |
| (h) => h.type === 'user', |
| ); |
| const lastUserMsg = inputHistory.at(-1); |
|
|
| if ( |
| !lastHistoryUserMsg || |
| (typeof lastHistoryUserMsg.text === 'string' && |
| lastHistoryUserMsg.text === lastUserMsg) |
| ) { |
| cancelHandlerRef.current(true); |
| setPendingRestorePrompt(false); |
| } |
| } |
| }, [pendingRestorePrompt, inputHistory, historyManager.history]); |
|
|
| const pendingHintsRef = useRef<string[]>([]); |
| const [pendingHintCount, setPendingHintCount] = useState(0); |
|
|
| const consumePendingHints = useCallback(() => { |
| if (pendingHintsRef.current.length === 0) { |
| return null; |
| } |
| const hint = pendingHintsRef.current.join('\n'); |
| pendingHintsRef.current = []; |
| setPendingHintCount(0); |
| return hint; |
| }, []); |
|
|
| useEffect(() => { |
| const hintListener = (text: string, source: InjectionSource) => { |
| if (source !== 'user_steering' && source !== 'background_completion') { |
| return; |
| } |
| pendingHintsRef.current.push(text); |
| setPendingHintCount((prev) => prev + 1); |
| }; |
| config.injectionService.onInjection(hintListener); |
| return () => { |
| config.injectionService.offInjection(hintListener); |
| }; |
| }, [config]); |
|
|
| const streamAgent = useMemo( |
| () => |
| config?.getAgentSessionInteractiveEnabled() |
| ? new LegacyAgentProtocol({ config, getPreferredEditor }) |
| : undefined, |
| [config, getPreferredEditor], |
| ); |
|
|
| const activeStream = streamAgent |
| ? |
| useAgentStream({ |
| agent: streamAgent, |
| addItem: historyManager.addItem, |
| onCancelSubmit, |
| isShellFocused: embeddedShellFocused, |
| logger, |
| }) |
| : |
| useGeminiStream( |
| config.getGeminiClient(), |
| historyManager.history, |
| historyManager.addItem, |
| config, |
| settings, |
| setDebugMessage, |
| handleSlashCommand, |
| shellModeActive, |
| getPreferredEditor, |
| onAuthError, |
| performMemoryRefresh, |
| modelSwitchedFromQuotaError, |
| setModelSwitchedFromQuotaError, |
| onCancelSubmit, |
| setEmbeddedShellFocused, |
| terminalWidth, |
| terminalHeight, |
| embeddedShellFocused, |
| consumePendingHints, |
| ); |
|
|
| const { |
| streamingState, |
| submitQuery, |
| initError, |
| pendingHistoryItems: pendingGeminiHistoryItems, |
| thought, |
| cancelOngoingRequest, |
| pendingToolCalls, |
| handleApprovalModeChange, |
| activePtyId, |
| loopDetectionConfirmationRequest, |
| lastOutputTime, |
| backgroundTaskCount, |
| isBackgroundTaskVisible, |
| toggleBackgroundTasks, |
| backgroundCurrentExecution, |
| backgroundTasks, |
| dismissBackgroundTask, |
| retryStatus, |
| } = activeStream; |
|
|
| const pendingHistoryItems = useMemo( |
| () => [...pendingSlashCommandHistoryItems, ...pendingGeminiHistoryItems], |
| [pendingSlashCommandHistoryItems, pendingGeminiHistoryItems], |
| ); |
|
|
| toggleBackgroundTasksRef.current = toggleBackgroundTasks; |
| isBackgroundTaskVisibleRef.current = isBackgroundTaskVisible; |
| backgroundTasksRef.current = backgroundTasks; |
|
|
| const { |
| activeBackgroundTaskPid, |
| setIsBackgroundTaskListOpen, |
| isBackgroundTaskListOpen, |
| setActiveBackgroundTaskPid, |
| backgroundTaskHeight, |
| } = useBackgroundTaskManager({ |
| backgroundTasks, |
| backgroundTaskCount, |
| isBackgroundTaskVisible, |
| activePtyId, |
| embeddedShellFocused, |
| setEmbeddedShellFocused, |
| terminalHeight, |
| }); |
|
|
| setIsBackgroundTaskListOpenRef.current = setIsBackgroundTaskListOpen; |
|
|
| const lastOutputTimeRef = useRef(0); |
|
|
| useEffect(() => { |
| lastOutputTimeRef.current = lastOutputTime; |
| }, [lastOutputTime]); |
|
|
| const { shouldShowFocusHint, inactivityStatus } = useShellInactivityStatus({ |
| activePtyId, |
| lastOutputTime, |
| streamingState, |
| pendingToolCalls, |
| embeddedShellFocused, |
| isInteractiveShellEnabled: config.isInteractiveShellEnabled(), |
| }); |
|
|
| const shouldShowActionRequiredTitle = inactivityStatus === 'action_required'; |
| const shouldShowSilentWorkingTitle = inactivityStatus === 'silent_working'; |
|
|
| const handleApprovalModeChangeWithUiReveal = useCallback( |
| (mode: ApprovalMode) => { |
| void handleApprovalModeChange(mode); |
| if (!cleanUiDetailsVisible) { |
| revealCleanUiDetailsTemporarily(APPROVAL_MODE_REVEAL_DURATION_MS); |
| } |
| }, |
| [ |
| handleApprovalModeChange, |
| cleanUiDetailsVisible, |
| revealCleanUiDetailsTemporarily, |
| ], |
| ); |
|
|
| const { isMcpReady } = useMcpStatus(config); |
|
|
| const isCompressing = useMemo( |
| () => |
| pendingHistoryItems.some( |
| (item) => |
| item.type === MessageType.COMPRESSION && item.compression.isPending, |
| ), |
| [pendingHistoryItems], |
| ); |
|
|
| const { |
| messageQueue, |
| addMessage, |
| clearQueue, |
| getQueuedMessagesText, |
| popAllMessages, |
| } = useMessageQueue({ |
| isConfigInitialized, |
| streamingState, |
| submitQuery, |
| isMcpReady, |
| isCompressing, |
| }); |
|
|
| cancelHandlerRef.current = useCallback( |
| (shouldRestorePrompt: boolean = true, clearBuffer: boolean = false) => { |
| if (!clearBuffer && isToolAwaitingConfirmation(pendingHistoryItems)) { |
| return; |
| } |
|
|
| |
| if (!shouldRestorePrompt) { |
| |
| if (clearBuffer) { |
| buffer.setText(''); |
| } |
| |
| return; |
| } |
|
|
| |
| const lastUserMessage = inputHistory.at(-1); |
| let textToSet = lastUserMessage || ''; |
|
|
| const queuedText = getQueuedMessagesText(); |
| if (queuedText) { |
| textToSet = textToSet ? `${textToSet}\n\n${queuedText}` : queuedText; |
| clearQueue(); |
| } |
|
|
| if (textToSet) { |
| buffer.setText(textToSet); |
| } |
| }, |
| [ |
| buffer, |
| inputHistory, |
| getQueuedMessagesText, |
| clearQueue, |
| pendingHistoryItems, |
| ], |
| ); |
|
|
| const handleHintSubmit = useCallback( |
| (hint: string) => { |
| const trimmed = hint.trim(); |
| if (!trimmed) { |
| return; |
| } |
| config.injectionService.addInjection(trimmed, 'user_steering'); |
| |
| historyManager.addItem({ |
| type: 'hint', |
| text: trimmed, |
| }); |
| }, |
| [config, historyManager], |
| ); |
|
|
| const handleFinalSubmit = useCallback( |
| async (submittedValue: string) => { |
| reset(); |
| |
| triggerExpandHint(null); |
| if (!constrainHeight) { |
| setConstrainHeight(true); |
| if (!isAlternateBuffer) { |
| refreshStatic(); |
| } |
| } |
|
|
| const isSlash = isSlashCommand(submittedValue.trim()); |
| const isIdle = streamingState === StreamingState.Idle; |
| const isAgentRunning = |
| streamingState === StreamingState.Responding || |
| isToolExecuting(pendingHistoryItems); |
|
|
| if (isSlash && isAgentRunning) { |
| const { commandToExecute } = parseSlashCommand( |
| submittedValue, |
| slashCommands ?? [], |
| ); |
| if (commandToExecute?.isSafeConcurrent) { |
| void handleSlashCommand(submittedValue); |
| addInput(submittedValue); |
| return; |
| } |
| } |
|
|
| if (config.isModelSteeringEnabled() && isAgentRunning && !isSlash) { |
| handleHintSubmit(submittedValue); |
| addInput(submittedValue); |
| return; |
| } |
|
|
| const isMcpOrConfigReady = isConfigInitialized && isMcpReady; |
| if ( |
| (isSlash && isConfigInitialized) || |
| (!isCompressing && isIdle && isMcpOrConfigReady) |
| ) { |
| if (!isSlash) { |
| const permissions = await checkPermissions(submittedValue, config); |
| if (permissions.length > 0) { |
| setPermissionConfirmationRequest({ |
| files: permissions, |
| onComplete: (result) => { |
| setPermissionConfirmationRequest(null); |
| if (result.allowed) { |
| permissions.forEach((p) => |
| config.getWorkspaceContext().addReadOnlyPath(p), |
| ); |
| } |
| void submitQuery(submittedValue); |
| }, |
| }); |
| addInput(submittedValue); |
| return; |
| } |
| } |
| void submitQuery(submittedValue); |
| } else { |
| |
| if ( |
| isIdle && |
| !isCompressing && |
| !isMcpOrConfigReady && |
| messageQueue.length === 0 |
| ) { |
| coreEvents.emitFeedback( |
| 'info', |
| !isConfigInitialized |
| ? 'Initializing... Prompts will be queued.' |
| : 'Waiting for MCP servers to initialize... Slash commands are still available and prompts will be queued.', |
| ); |
| } |
| addMessage(submittedValue); |
| } |
| addInput(submittedValue); |
| }, |
| [ |
| addMessage, |
| addInput, |
| submitQuery, |
| handleSlashCommand, |
| slashCommands, |
| isMcpReady, |
| streamingState, |
| isCompressing, |
| messageQueue.length, |
| pendingHistoryItems, |
| config, |
| constrainHeight, |
| setConstrainHeight, |
| isAlternateBuffer, |
| refreshStatic, |
| reset, |
| handleHintSubmit, |
| isConfigInitialized, |
| triggerExpandHint, |
| ], |
| ); |
|
|
| const handleClearScreen = useCallback(() => { |
| reset(); |
| |
| triggerExpandHint(null); |
| historyManager.clearItems(); |
| clearErrorCount(); |
| refreshStatic(); |
| }, [ |
| historyManager, |
| clearErrorCount, |
| refreshStatic, |
| reset, |
| triggerExpandHint, |
| ]); |
|
|
| const { handleInput: vimHandleInput } = useVim(buffer, handleFinalSubmit); |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| const isInputActive = |
| !initError && |
| !isProcessing && |
| !isResuming && |
| (streamingState === StreamingState.Idle || |
| streamingState === StreamingState.Responding || |
| streamingState === StreamingState.WaitingForConfirmation) && |
| !proQuotaRequest; |
|
|
| const observerRef = useRef<ResizeObserver | null>(null); |
|
|
| useEffect( |
| () => () => { |
| observerRef.current?.disconnect(); |
| }, |
| [], |
| ); |
|
|
| const [controlsHeight, setControlsHeight] = useState(0); |
| const [lastNonCopyControlsHeight, setLastNonCopyControlsHeight] = useState(0); |
|
|
| useLayoutEffect(() => { |
| if (!copyModeEnabled && controlsHeight > 0) { |
| setLastNonCopyControlsHeight(controlsHeight); |
| } |
| }, [copyModeEnabled, controlsHeight]); |
|
|
| const stableControlsHeight = |
| copyModeEnabled && lastNonCopyControlsHeight > 0 |
| ? lastNonCopyControlsHeight |
| : controlsHeight; |
|
|
| const mainControlsRef = useCallback((node: DOMElement | null) => { |
| if (observerRef.current) { |
| observerRef.current.disconnect(); |
| observerRef.current = null; |
| } |
|
|
| if (node) { |
| const observer = new ResizeObserver((entries) => { |
| const entry = entries[0]; |
| if (entry) { |
| const roundedHeight = Math.round(entry.contentRect.height); |
| setControlsHeight((prev) => |
| roundedHeight !== prev ? roundedHeight : prev, |
| ); |
| } |
| }); |
| observer.observe(node); |
| observerRef.current = observer; |
| } |
| }, []); |
|
|
| |
| const availableTerminalHeight = Math.max( |
| 0, |
| terminalHeight - stableControlsHeight - backgroundTaskHeight - 1, |
| ); |
|
|
| config.setShellExecutionConfig({ |
| terminalWidth: Math.floor(terminalWidth * SHELL_WIDTH_FRACTION), |
| terminalHeight: Math.max( |
| Math.floor(availableTerminalHeight - SHELL_HEIGHT_PADDING), |
| 1, |
| ), |
| pager: settings.merged.tools.shell.pager, |
| showColor: settings.merged.tools.shell.showColor, |
| sanitizationConfig: config.sanitizationConfig, |
| sandboxManager: config.sandboxManager, |
| }); |
|
|
| const { isFocused, hasReceivedFocusEvent } = useFocus(); |
|
|
| |
| const contextFileNames = useMemo(() => { |
| const fromSettings = settings.merged.context.fileName; |
| return fromSettings |
| ? Array.isArray(fromSettings) |
| ? fromSettings |
| : [fromSettings] |
| : getAllGeminiMdFilenames(); |
| }, [settings.merged.context.fileName]); |
| |
| const initialPrompt = useMemo(() => config.getQuestion(), [config]); |
| const initialPromptSubmitted = useRef(false); |
| const geminiClient = config.getGeminiClient(); |
|
|
| useEffect(() => { |
| if ( |
| initialPrompt && |
| isConfigInitialized && |
| !initialPromptSubmitted.current && |
| !isAuthenticating && |
| !isAuthDialogOpen && |
| !isThemeDialogOpen && |
| !isEditorDialogOpen && |
| !showPrivacyNotice && |
| geminiClient?.isInitialized?.() |
| ) { |
| void handleFinalSubmit(initialPrompt); |
| initialPromptSubmitted.current = true; |
| } |
| }, [ |
| initialPrompt, |
| isConfigInitialized, |
| handleFinalSubmit, |
| isAuthenticating, |
| isAuthDialogOpen, |
| isThemeDialogOpen, |
| isEditorDialogOpen, |
| showPrivacyNotice, |
| geminiClient, |
| ]); |
|
|
| const [idePromptAnswered, setIdePromptAnswered] = useState(false); |
| const [currentIDE, setCurrentIDE] = useState<IdeInfo | null>(null); |
|
|
| useEffect(() => { |
| const getIde = async () => { |
| const ideClient = await IdeClient.getInstance(); |
| const currentIde = ideClient.getCurrentIde(); |
| setCurrentIDE(currentIde || null); |
| }; |
| |
| getIde(); |
| }, []); |
| const shouldShowIdePrompt = Boolean( |
| currentIDE && |
| !config.getIdeMode() && |
| !settings.merged.ide.hasSeenNudge && |
| !idePromptAnswered, |
| ); |
|
|
| const [showErrorDetails, setShowErrorDetails] = useState<boolean>(false); |
| const [showFullTodos, setShowFullTodos] = useState<boolean>(false); |
| const [renderMarkdown, setRenderMarkdown] = useState<boolean>(true); |
|
|
| const handleExitRepeat = useCallback( |
| (count: number) => { |
| if (count > 2) { |
| recordExitFail(config); |
| } |
| if (count > 1) { |
| void handleSlashCommand('/quit', undefined, undefined, false); |
| } |
| }, |
| [config, handleSlashCommand], |
| ); |
|
|
| const { pressCount: ctrlCPressCount, handlePress: handleCtrlCPress } = |
| useRepeatedKeyPress({ |
| windowMs: WARNING_PROMPT_DURATION_MS, |
| onRepeat: handleExitRepeat, |
| }); |
|
|
| const { pressCount: ctrlDPressCount, handlePress: handleCtrlDPress } = |
| useRepeatedKeyPress({ |
| windowMs: WARNING_PROMPT_DURATION_MS, |
| onRepeat: handleExitRepeat, |
| }); |
|
|
| const [ideContextState, setIdeContextState] = useState< |
| IdeContext | undefined |
| >(); |
| const [showEscapePrompt, setShowEscapePrompt] = useState(false); |
| const [showIdeRestartPrompt, setShowIdeRestartPrompt] = useState(false); |
|
|
| const [transientMessage, showTransientMessage] = useTimedMessage<{ |
| text: string; |
| type: TransientMessageType; |
| }>(WARNING_PROMPT_DURATION_MS); |
|
|
| const { |
| isFolderTrustDialogOpen, |
| discoveryResults: folderDiscoveryResults, |
| handleFolderTrustSelect, |
| isRestarting, |
| } = useFolderTrust(settings, setIsTrustedFolder, historyManager.addItem); |
|
|
| const policyUpdateConfirmationRequest = |
| config.getPolicyUpdateConfirmationRequest(); |
| const [isPolicyUpdateDialogOpen, setIsPolicyUpdateDialogOpen] = useState( |
| !!policyUpdateConfirmationRequest, |
| ); |
| const { |
| needsRestart: ideNeedsRestart, |
| restartReason: ideTrustRestartReason, |
| } = useIdeTrustListener(); |
| useIncludeDirsTrust(config, isTrustedFolder, historyManager, setCustomDialog); |
|
|
| const tabFocusTimeoutRef = useRef<NodeJS.Timeout | null>(null); |
|
|
| useEffect(() => { |
| const handleTransientMessage = (payload: { |
| message: string; |
| type: TransientMessageType; |
| }) => { |
| showTransientMessage({ text: payload.message, type: payload.type }); |
| }; |
|
|
| const handleSelectionWarning = () => { |
| showTransientMessage({ |
| text: 'Press Ctrl-S to enter selection mode to copy text.', |
| type: TransientMessageType.Warning, |
| }); |
| }; |
| const handlePasteTimeout = () => { |
| showTransientMessage({ |
| text: 'Paste Timed out. Possibly due to slow connection.', |
| type: TransientMessageType.Warning, |
| }); |
| }; |
|
|
| appEvents.on(AppEvent.TransientMessage, handleTransientMessage); |
| appEvents.on(AppEvent.SelectionWarning, handleSelectionWarning); |
| appEvents.on(AppEvent.PasteTimeout, handlePasteTimeout); |
|
|
| return () => { |
| appEvents.off(AppEvent.TransientMessage, handleTransientMessage); |
| appEvents.off(AppEvent.SelectionWarning, handleSelectionWarning); |
| appEvents.off(AppEvent.PasteTimeout, handlePasteTimeout); |
| if (tabFocusTimeoutRef.current) { |
| clearTimeout(tabFocusTimeoutRef.current); |
| } |
| }; |
| }, [showTransientMessage]); |
|
|
| const handleWarning = useCallback( |
| (message: string) => { |
| showTransientMessage({ |
| text: message, |
| type: TransientMessageType.Warning, |
| }); |
| }, |
| [showTransientMessage], |
| ); |
|
|
| const { handleSuspend } = useSuspend({ |
| handleWarning, |
| setRawMode, |
| shouldUseAlternateScreen, |
| }); |
|
|
| useEffect(() => { |
| if (ideNeedsRestart) { |
| |
| setShowIdeRestartPrompt(true); |
| } |
| }, [ideNeedsRestart]); |
|
|
| useEffect(() => { |
| const unsubscribe = ideContextStore.subscribe(setIdeContextState); |
| setIdeContextState(ideContextStore.get()); |
| return unsubscribe; |
| }, []); |
|
|
| useEffect(() => { |
| const openDebugConsole = () => { |
| setShowErrorDetails(true); |
| setConstrainHeight(false); |
| }; |
| appEvents.on(AppEvent.OpenDebugConsole, openDebugConsole); |
|
|
| return () => { |
| appEvents.off(AppEvent.OpenDebugConsole, openDebugConsole); |
| }; |
| }, [config]); |
|
|
| const handleEscapePromptChange = useCallback((showPrompt: boolean) => { |
| setShowEscapePrompt(showPrompt); |
| }, []); |
|
|
| const handleIdePromptComplete = useCallback( |
| (result: IdeIntegrationNudgeResult) => { |
| if (result.userSelection === 'yes') { |
| |
| handleSlashCommand('/ide install'); |
| settings.setValue(SettingScope.User, 'ide.hasSeenNudge', true); |
| } else if (result.userSelection === 'dismiss') { |
| settings.setValue(SettingScope.User, 'ide.hasSeenNudge', true); |
| } |
| setIdePromptAnswered(true); |
| }, |
| [handleSlashCommand, settings], |
| ); |
|
|
| const handleGlobalKeypress = useCallback( |
| (key: Key): boolean => { |
| |
| if (settings.merged.general.debugKeystrokeLogging) { |
| debugLogger.log('[DEBUG] Keystroke:', JSON.stringify(key)); |
| } |
|
|
| if (shortcutsHelpVisible && isHelpDismissKey(key)) { |
| setShortcutsHelpVisible(false); |
| } |
|
|
| if (keyMatchers[Command.TOGGLE_MOUSE_MODE](key)) { |
| setMouseMode((prev) => !prev); |
| if (mouseMode && !isAlternateBuffer) { |
| appEvents.emit(AppEvent.ScrollToBottom); |
| } |
| return true; |
| } |
|
|
| if (isAlternateBuffer && keyMatchers[Command.TOGGLE_COPY_MODE](key)) { |
| setCopyModeEnabled(true); |
| disableMouseEvents(); |
| return true; |
| } |
|
|
| if (keyMatchers[Command.QUIT](key)) { |
| |
| |
| void cancelOngoingRequest?.(); |
|
|
| handleCtrlCPress(); |
| return true; |
| } else if (keyMatchers[Command.EXIT](key)) { |
| |
| if (bufferRef.current.text.length > 0) { |
| return false; |
| } |
| handleCtrlDPress(); |
| return true; |
| } else if (keyMatchers[Command.SUSPEND_APP](key)) { |
| handleSuspend(); |
| } else if (keyMatchers[Command.DUMP_FRAME](key)) { |
| const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); |
| const filename = `snapshot-${timestamp}.json`; |
| if (dumpCurrentFrame) { |
| dumpCurrentFrame(filename); |
| debugLogger.log(`Dumped frame to: ${filename}`); |
| } |
| return true; |
| } else if (keyMatchers[Command.START_RECORDING](key)) { |
| const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); |
| const filename = `recording-${timestamp}.json`; |
| if (startRecording) { |
| startRecording(filename); |
| recordingFilenameRef.current = filename; |
| debugLogger.log(`Started recording to: ${filename}`); |
| } |
| return true; |
| } else if (keyMatchers[Command.STOP_RECORDING](key)) { |
| if (stopRecording) { |
| stopRecording(); |
| debugLogger.log( |
| `Stopped recording, saved to: ${recordingFilenameRef.current ?? 'unknown'}`, |
| ); |
| recordingFilenameRef.current = null; |
| } |
| return true; |
| } else if ( |
| keyMatchers[Command.TOGGLE_COPY_MODE](key) && |
| !isAlternateBuffer |
| ) { |
| showTransientMessage({ |
| text: 'Use Ctrl+O to expand and collapse blocks of content.', |
| type: TransientMessageType.Warning, |
| }); |
| return true; |
| } |
|
|
| const toggleLastTurnTools = () => { |
| triggerExpandHint(true); |
|
|
| const targetToolCallIds = getLastTurnToolCallIds( |
| historyManager.history, |
| pendingHistoryItems, |
| ); |
|
|
| if (targetToolCallIds.length > 0) { |
| toggleAllExpansion(targetToolCallIds); |
| } |
| }; |
|
|
| let enteringConstrainHeightMode = false; |
| if (!constrainHeight) { |
| enteringConstrainHeightMode = true; |
| setConstrainHeight(true); |
| if (keyMatchers[Command.SHOW_MORE_LINES](key)) { |
| toggleLastTurnTools(); |
| } |
| if (!isAlternateBuffer) { |
| refreshStatic(); |
| } |
| } |
|
|
| if (keyMatchers[Command.SHOW_ERROR_DETAILS](key)) { |
| if (settings.merged.general.devtools) { |
| void (async () => { |
| const { toggleDevToolsPanel } = await import( |
| '../utils/devtoolsService.js' |
| ); |
| await toggleDevToolsPanel( |
| config, |
| showErrorDetails, |
| () => setShowErrorDetails((prev) => !prev), |
| () => setShowErrorDetails(true), |
| ); |
| })(); |
| } else { |
| setShowErrorDetails((prev) => !prev); |
| } |
| return true; |
| } else if (keyMatchers[Command.SHOW_FULL_TODOS](key)) { |
| setShowFullTodos((prev) => !prev); |
| return true; |
| } else if (keyMatchers[Command.TOGGLE_MARKDOWN](key)) { |
| setRenderMarkdown((prev) => { |
| const newValue = !prev; |
| |
| refreshStatic(); |
| return newValue; |
| }); |
| return true; |
| } else if ( |
| keyMatchers[Command.SHOW_IDE_CONTEXT_DETAIL](key) && |
| config.getIdeMode() && |
| ideContextState |
| ) { |
| |
| handleSlashCommand('/ide status'); |
| return true; |
| } else if ( |
| keyMatchers[Command.SHOW_MORE_LINES](key) && |
| !enteringConstrainHeightMode |
| ) { |
| setConstrainHeight(false); |
| toggleLastTurnTools(); |
| refreshStatic(); |
| return true; |
| } else if ( |
| (keyMatchers[Command.FOCUS_SHELL_INPUT](key) || |
| keyMatchers[Command.UNFOCUS_BACKGROUND_SHELL_LIST](key)) && |
| (activePtyId || (isBackgroundTaskVisible && backgroundTasks.size > 0)) |
| ) { |
| if (embeddedShellFocused) { |
| const capturedTime = lastOutputTimeRef.current; |
| if (tabFocusTimeoutRef.current) |
| clearTimeout(tabFocusTimeoutRef.current); |
| tabFocusTimeoutRef.current = setTimeout(() => { |
| if (lastOutputTimeRef.current === capturedTime) { |
| setEmbeddedShellFocused(false); |
| } else { |
| showTransientMessage({ |
| text: 'Use Shift+Tab to unfocus', |
| type: TransientMessageType.Warning, |
| }); |
| } |
| }, 150); |
| return false; |
| } |
|
|
| const isIdle = Date.now() - lastOutputTimeRef.current >= 100; |
|
|
| if (isIdle && !activePtyId && !isBackgroundTaskVisible) { |
| if (tabFocusTimeoutRef.current) |
| clearTimeout(tabFocusTimeoutRef.current); |
| toggleBackgroundTasks(); |
| setEmbeddedShellFocused(true); |
| if (backgroundTasks.size > 1) setIsBackgroundTaskListOpen(true); |
| return true; |
| } |
|
|
| setEmbeddedShellFocused(true); |
| return true; |
| } else if ( |
| keyMatchers[Command.UNFOCUS_SHELL_INPUT](key) || |
| keyMatchers[Command.UNFOCUS_BACKGROUND_SHELL](key) |
| ) { |
| if (embeddedShellFocused) { |
| setEmbeddedShellFocused(false); |
| return true; |
| } |
| return false; |
| } else if (keyMatchers[Command.TOGGLE_BACKGROUND_SHELL](key)) { |
| if (activePtyId) { |
| backgroundCurrentExecution(); |
| |
| } else { |
| toggleBackgroundTasks(); |
| |
| if (!isBackgroundTaskVisible && backgroundTasks.size > 0) { |
| setEmbeddedShellFocused(true); |
| if (backgroundTasks.size > 1) { |
| setIsBackgroundTaskListOpen(true); |
| } |
| } else { |
| setEmbeddedShellFocused(false); |
| } |
| } |
| return true; |
| } else if (keyMatchers[Command.TOGGLE_BACKGROUND_SHELL_LIST](key)) { |
| if (backgroundTasks.size > 0 && isBackgroundTaskVisible) { |
| if (!embeddedShellFocused) { |
| setEmbeddedShellFocused(true); |
| } |
| setIsBackgroundTaskListOpen(true); |
| } |
| return true; |
| } |
| return false; |
| }, |
| [ |
| constrainHeight, |
| setConstrainHeight, |
| setShowErrorDetails, |
| config, |
| ideContextState, |
| handleCtrlCPress, |
| handleCtrlDPress, |
| handleSlashCommand, |
| cancelOngoingRequest, |
| activePtyId, |
| handleSuspend, |
| embeddedShellFocused, |
| settings.merged.general.debugKeystrokeLogging, |
| refreshStatic, |
| setCopyModeEnabled, |
| tabFocusTimeoutRef, |
| isAlternateBuffer, |
| shortcutsHelpVisible, |
| backgroundCurrentExecution, |
| toggleBackgroundTasks, |
| backgroundTasks, |
| isBackgroundTaskVisible, |
| setIsBackgroundTaskListOpen, |
| lastOutputTimeRef, |
| showTransientMessage, |
| settings.merged.general.devtools, |
| showErrorDetails, |
| triggerExpandHint, |
| keyMatchers, |
| isHelpDismissKey, |
| historyManager.history, |
| pendingHistoryItems, |
| toggleAllExpansion, |
| dumpCurrentFrame, |
| startRecording, |
| stopRecording, |
| mouseMode, |
| ], |
| ); |
|
|
| useKeypress(handleGlobalKeypress, { isActive: true, priority: true }); |
|
|
| useKeypress( |
| (key: Key) => { |
| if ( |
| keyMatchers[Command.SCROLL_UP](key) || |
| keyMatchers[Command.SCROLL_DOWN](key) || |
| keyMatchers[Command.PAGE_UP](key) || |
| keyMatchers[Command.PAGE_DOWN](key) || |
| keyMatchers[Command.SCROLL_HOME](key) || |
| keyMatchers[Command.SCROLL_END](key) |
| ) { |
| return false; |
| } |
|
|
| setCopyModeEnabled(false); |
| if (mouseMode) { |
| enableMouseEvents(); |
| } |
| return true; |
| }, |
| { |
| isActive: copyModeEnabled, |
| |
| |
| priority: KeypressPriority.Critical, |
| }, |
| ); |
|
|
| useEffect(() => { |
| |
| if (settings.merged.ui.hideWindowTitle) return; |
|
|
| const paddedTitle = computeTerminalTitle({ |
| streamingState, |
| thoughtSubject: thought?.subject, |
| isConfirming: |
| !!commandConfirmationRequest || shouldShowActionRequiredTitle, |
| isSilentWorking: shouldShowSilentWorkingTitle, |
| folderName: basename(config.getTargetDir()), |
| showThoughts: !!settings.merged.ui.showStatusInTitle, |
| useDynamicTitle: settings.merged.ui.dynamicWindowTitle, |
| }); |
|
|
| |
| if (lastTitleRef.current !== paddedTitle) { |
| lastTitleRef.current = paddedTitle; |
| stdout.write(`\x1b]0;${paddedTitle}\x07`); |
| } |
| |
| }, [ |
| streamingState, |
| thought, |
| commandConfirmationRequest, |
| shouldShowActionRequiredTitle, |
| shouldShowSilentWorkingTitle, |
| settings.merged.ui.showStatusInTitle, |
| settings.merged.ui.dynamicWindowTitle, |
| settings.merged.ui.hideWindowTitle, |
| config, |
| stdout, |
| ]); |
|
|
| useEffect(() => { |
| const handleUserFeedback = (payload: UserFeedbackPayload) => { |
| let type: MessageType; |
| switch (payload.severity) { |
| case 'error': |
| type = MessageType.ERROR; |
| break; |
| case 'warning': |
| type = MessageType.WARNING; |
| break; |
| case 'info': |
| type = MessageType.INFO; |
| break; |
| default: |
| throw new Error( |
| `Unexpected severity for user feedback: ${payload.severity}`, |
| ); |
| } |
|
|
| historyManager.addItem( |
| { |
| type, |
| text: payload.message, |
| }, |
| Date.now(), |
| ); |
|
|
| |
| if (payload.error) { |
| debugLogger.warn( |
| `[Feedback Details for "${payload.message}"]`, |
| payload.error, |
| ); |
| } |
| }; |
|
|
| const handleHookSystemMessage = (payload: HookSystemMessagePayload) => { |
| historyManager.addItem( |
| { |
| type: MessageType.INFO, |
| text: payload.message, |
| source: payload.hookName, |
| } as HistoryItemInfo, |
| Date.now(), |
| ); |
| }; |
|
|
| coreEvents.on(CoreEvent.UserFeedback, handleUserFeedback); |
| coreEvents.on(CoreEvent.HookSystemMessage, handleHookSystemMessage); |
|
|
| |
| |
| coreEvents.drainBacklogs(); |
|
|
| return () => { |
| coreEvents.off(CoreEvent.UserFeedback, handleUserFeedback); |
| coreEvents.off(CoreEvent.HookSystemMessage, handleHookSystemMessage); |
| }; |
| }, [historyManager]); |
|
|
| const nightly = props.version.includes('nightly'); |
|
|
| const isAwaitingLoginRestart = authState === AuthState.AwaitingLoginRestart; |
| const loginRestartMessage = |
| settings.merged.security.auth.selectedType === AuthType.USE_VERTEX_AI |
| ? 'Authenticating to Vertex AI in Cloud Shell requires a restart to apply project settings.' |
| : undefined; |
|
|
| const dialogsVisible = |
| shouldShowIdePrompt || |
| isFolderTrustDialogOpen || |
| isPolicyUpdateDialogOpen || |
| adminSettingsChanged || |
| !!commandConfirmationRequest || |
| !!authConsentRequest || |
| !!permissionConfirmationRequest || |
| !!customDialog || |
| confirmUpdateExtensionRequests.length > 0 || |
| !!loopDetectionConfirmationRequest || |
| isThemeDialogOpen || |
| isSettingsDialogOpen || |
| isModelDialogOpen || |
| isVoiceModelDialogOpen || |
| isAgentConfigDialogOpen || |
| isPermissionsDialogOpen || |
| isAuthenticating || |
| isAuthDialogOpen || |
| isEditorDialogOpen || |
| showPrivacyNotice || |
| showIdeRestartPrompt || |
| !!proQuotaRequest || |
| !!validationRequest || |
| !!overageMenuRequest || |
| !!emptyWalletRequest || |
| isSessionBrowserOpen || |
| authState === AuthState.AwaitingApiKeyInput || |
| isAwaitingLoginRestart || |
| !!newAgents; |
|
|
| const hasPendingToolConfirmation = useMemo( |
| () => isToolAwaitingConfirmation(pendingHistoryItems), |
| [pendingHistoryItems], |
| ); |
|
|
| const hasConfirmUpdateExtensionRequests = |
| confirmUpdateExtensionRequests.length > 0; |
| const hasLoopDetectionConfirmationRequest = |
| !!loopDetectionConfirmationRequest; |
|
|
| const hasPendingActionRequired = |
| hasPendingToolConfirmation || |
| !!commandConfirmationRequest || |
| !!authConsentRequest || |
| hasConfirmUpdateExtensionRequests || |
| hasLoopDetectionConfirmationRequest || |
| !!proQuotaRequest || |
| !!validationRequest || |
| !!overageMenuRequest || |
| !!emptyWalletRequest || |
| !!customDialog; |
|
|
| const loadingPhrases = settings.merged.ui.loadingPhrases; |
| const showStatusTips = loadingPhrases === 'tips' || loadingPhrases === 'all'; |
| const showStatusWit = loadingPhrases === 'witty' || loadingPhrases === 'all'; |
|
|
| const showLoadingIndicator = |
| (!embeddedShellFocused || isBackgroundTaskVisible) && |
| streamingState === StreamingState.Responding && |
| !hasPendingActionRequired; |
|
|
| let estimatedStatusLength = 0; |
| if (activeHooks.length > 0 && settings.merged.hooksConfig.notifications) { |
| const hookLabel = |
| activeHooks.length > 1 ? 'Executing Hooks' : 'Executing Hook'; |
| const hookNames = activeHooks |
| .map( |
| (h) => |
| h.name + |
| (h.index && h.total && h.total > 1 ? ` (${h.index}/${h.total})` : ''), |
| ) |
| .join(', '); |
| estimatedStatusLength = hookLabel.length + hookNames.length + 10; |
| } else if (showLoadingIndicator) { |
| const thoughtText = thought?.subject || 'Waiting for model...'; |
| estimatedStatusLength = thoughtText.length + 25; |
| } else if (hasPendingActionRequired) { |
| estimatedStatusLength = 35; |
| } |
|
|
| const maxLength = terminalWidth - estimatedStatusLength - 5; |
|
|
| const { elapsedTime, currentLoadingPhrase, currentTip, currentWittyPhrase } = |
| useLoadingIndicator({ |
| streamingState, |
| shouldShowFocusHint, |
| retryStatus, |
| showTips: showStatusTips, |
| showWit: showStatusWit, |
| customWittyPhrases: settings.merged.ui.customWittyPhrases, |
| errorVerbosity: settings.merged.ui.errorVerbosity, |
| maxLength, |
| }); |
|
|
| const allowPlanMode = |
| config.isPlanEnabled() && |
| streamingState === StreamingState.Idle && |
| !hasPendingActionRequired; |
|
|
| const showApprovalModeIndicator = useApprovalModeIndicator({ |
| config, |
| addItem: historyManager.addItem, |
| onApprovalModeChange: handleApprovalModeChangeWithUiReveal, |
| isActive: !embeddedShellFocused, |
| allowPlanMode, |
| }); |
|
|
| useRunEventNotifications({ |
| notificationsEnabled, |
| notificationMethod, |
| isFocused, |
| hasReceivedFocusEvent, |
| streamingState, |
| hasPendingActionRequired, |
| pendingHistoryItems, |
| commandConfirmationRequest, |
| authConsentRequest, |
| permissionConfirmationRequest, |
| hasConfirmUpdateExtensionRequests, |
| hasLoopDetectionConfirmationRequest, |
| }); |
|
|
| const isPassiveShortcutsHelpState = |
| isInputActive && |
| streamingState === StreamingState.Idle && |
| !hasPendingActionRequired; |
|
|
| useEffect(() => { |
| if (shortcutsHelpVisible && !isPassiveShortcutsHelpState) { |
| setShortcutsHelpVisible(false); |
| } |
| }, [ |
| shortcutsHelpVisible, |
| isPassiveShortcutsHelpState, |
| setShortcutsHelpVisible, |
| ]); |
|
|
| useEffect(() => { |
| if ( |
| !isConfigInitialized || |
| !config.isModelSteeringEnabled() || |
| streamingState !== StreamingState.Idle || |
| !isMcpReady || |
| isToolAwaitingConfirmation(pendingHistoryItems) |
| ) { |
| return; |
| } |
|
|
| const pendingHint = consumePendingHints(); |
| if (!pendingHint) { |
| return; |
| } |
|
|
| void submitQuery([{ text: buildUserSteeringHintPrompt(pendingHint) }]); |
| }, [ |
| config, |
| historyManager, |
| isConfigInitialized, |
| isMcpReady, |
| streamingState, |
| submitQuery, |
| consumePendingHints, |
| pendingHistoryItems, |
| pendingHintCount, |
| ]); |
|
|
| const allToolCalls = useMemo( |
| () => getAllToolCalls(pendingHistoryItems), |
| [pendingHistoryItems], |
| ); |
|
|
| const [geminiMdFileCount, setGeminiMdFileCount] = useState<number>( |
| config.getGeminiMdFileCount(), |
| ); |
| useEffect(() => { |
| const handleMemoryChanged = (result: MemoryChangedPayload) => { |
| setGeminiMdFileCount(result.fileCount); |
| }; |
| coreEvents.on(CoreEvent.MemoryChanged, handleMemoryChanged); |
| return () => { |
| coreEvents.off(CoreEvent.MemoryChanged, handleMemoryChanged); |
| }; |
| }, []); |
|
|
| useEffect(() => { |
| let isMounted = true; |
|
|
| const fetchBannerTexts = async () => { |
| const [defaultBanner, warningBanner] = await Promise.all([ |
| config.getBannerTextNoCapacityIssues(), |
| config.getBannerTextCapacityIssues(), |
| ]); |
|
|
| if (isMounted) { |
| setDefaultBannerText(defaultBanner); |
| setWarningBannerText(warningBanner); |
| setBannerVisible(true); |
| } |
| }; |
| |
| fetchBannerTexts(); |
|
|
| return () => { |
| isMounted = false; |
| }; |
| }, [config, refreshStatic]); |
|
|
| const inputState = useMemo( |
| () => ({ |
| buffer, |
| userMessages: inputHistory, |
| shellModeActive, |
| showEscapePrompt, |
| copyModeEnabled, |
| inputWidth, |
| suggestionsWidth, |
| }), |
| [ |
| buffer, |
| inputHistory, |
| shellModeActive, |
| showEscapePrompt, |
| copyModeEnabled, |
| inputWidth, |
| suggestionsWidth, |
| ], |
| ); |
|
|
| const quotaState = useMemo( |
| () => ({ |
| userTier, |
| stats: quotaStats, |
| proQuotaRequest, |
| validationRequest, |
| |
| overageMenuRequest, |
| emptyWalletRequest, |
| }), |
| [ |
| userTier, |
| quotaStats, |
| proQuotaRequest, |
| validationRequest, |
| overageMenuRequest, |
| emptyWalletRequest, |
| ], |
| ); |
|
|
| const uiState: UIState = useMemo( |
| () => ({ |
| history: historyManager.history, |
| historyManager, |
| isThemeDialogOpen, |
|
|
| themeError, |
| isAuthenticating, |
| isConfigInitialized, |
| authError, |
| accountSuspensionInfo, |
| isAuthDialogOpen, |
| isAwaitingApiKeyInput: authState === AuthState.AwaitingApiKeyInput, |
| isAwaitingLoginRestart, |
| loginRestartMessage, |
| apiKeyDefaultValue, |
| editorError, |
| isEditorDialogOpen, |
| showPrivacyNotice, |
| mouseMode, |
| corgiMode, |
| debugMessage, |
| quittingMessages, |
| isSettingsDialogOpen, |
| isSessionBrowserOpen, |
| isModelDialogOpen, |
| isVoiceModelDialogOpen, |
| isAgentConfigDialogOpen, |
| selectedAgentName, |
| selectedAgentDisplayName, |
| selectedAgentDefinition, |
| isPermissionsDialogOpen, |
| permissionsDialogProps, |
| slashCommands, |
| pendingSlashCommandHistoryItems, |
| commandContext, |
| commandConfirmationRequest, |
| authConsentRequest, |
| confirmUpdateExtensionRequests, |
| loopDetectionConfirmationRequest, |
| permissionConfirmationRequest, |
| geminiMdFileCount, |
| streamingState, |
| initError, |
| pendingGeminiHistoryItems, |
| thought, |
| isInputActive, |
| isVoiceModeEnabled, |
| isResuming, |
| shouldShowIdePrompt, |
| isFolderTrustDialogOpen: isFolderTrustDialogOpen ?? false, |
| folderDiscoveryResults, |
| isPolicyUpdateDialogOpen, |
| policyUpdateConfirmationRequest, |
| isTrustedFolder, |
| constrainHeight, |
| showErrorDetails, |
| showFullTodos, |
| ideContextState, |
| renderMarkdown, |
| ctrlCPressedOnce: ctrlCPressCount >= 1, |
| ctrlDPressedOnce: ctrlDPressCount >= 1, |
| shortcutsHelpVisible, |
| cleanUiDetailsVisible, |
| isFocused, |
| elapsedTime, |
| currentLoadingPhrase, |
| currentTip, |
| currentWittyPhrase, |
| historyRemountKey, |
| activeHooks, |
| messageQueue, |
| queueErrorMessage, |
| showApprovalModeIndicator, |
| allowPlanMode, |
| currentModel, |
| contextFileNames, |
| errorCount, |
| availableTerminalHeight, |
| stableControlsHeight, |
| mainAreaWidth, |
| staticAreaMaxItemHeight, |
| staticExtraHeight, |
| dialogsVisible, |
| pendingHistoryItems, |
| nightly, |
| branchName, |
| sessionStats, |
| terminalWidth, |
| terminalHeight, |
| mainControlsRef, |
| rootUiRef, |
| currentIDE, |
| updateInfo, |
| showIdeRestartPrompt, |
| ideTrustRestartReason, |
| isRestarting, |
| extensionsUpdateState, |
| activePtyId, |
| backgroundTaskCount, |
| isBackgroundTaskVisible, |
| embeddedShellFocused, |
| showDebugProfiler, |
| customDialog, |
| transientMessage, |
| bannerData, |
| bannerVisible, |
| terminalBackgroundColor: config.getTerminalBackground(), |
| settingsNonce, |
| backgroundTasks, |
| activeBackgroundTaskPid, |
| backgroundTaskHeight, |
| isBackgroundTaskListOpen, |
| adminSettingsChanged, |
| newAgents, |
| showIsExpandableHint, |
| hintMode: |
| config.isModelSteeringEnabled() && isToolExecuting(pendingHistoryItems), |
| hintBuffer: '', |
| }), |
| [ |
| isThemeDialogOpen, |
|
|
| themeError, |
| isAuthenticating, |
| isConfigInitialized, |
| authError, |
| accountSuspensionInfo, |
| isAuthDialogOpen, |
| editorError, |
| isEditorDialogOpen, |
| showPrivacyNotice, |
| mouseMode, |
| corgiMode, |
| debugMessage, |
| quittingMessages, |
| isSettingsDialogOpen, |
| isSessionBrowserOpen, |
| isModelDialogOpen, |
| isVoiceModelDialogOpen, |
| isAgentConfigDialogOpen, |
| selectedAgentName, |
| selectedAgentDisplayName, |
| selectedAgentDefinition, |
| isPermissionsDialogOpen, |
| permissionsDialogProps, |
| slashCommands, |
| pendingSlashCommandHistoryItems, |
| commandContext, |
| commandConfirmationRequest, |
| authConsentRequest, |
| confirmUpdateExtensionRequests, |
| loopDetectionConfirmationRequest, |
| permissionConfirmationRequest, |
| geminiMdFileCount, |
| streamingState, |
| initError, |
| pendingGeminiHistoryItems, |
| thought, |
| isInputActive, |
| isVoiceModeEnabled, |
| isResuming, |
| shouldShowIdePrompt, |
| isFolderTrustDialogOpen, |
| folderDiscoveryResults, |
| isPolicyUpdateDialogOpen, |
| policyUpdateConfirmationRequest, |
| isTrustedFolder, |
| constrainHeight, |
| showErrorDetails, |
| showFullTodos, |
| ideContextState, |
| renderMarkdown, |
| ctrlCPressCount, |
| ctrlDPressCount, |
| shortcutsHelpVisible, |
| cleanUiDetailsVisible, |
| isFocused, |
| elapsedTime, |
| currentLoadingPhrase, |
| currentTip, |
| currentWittyPhrase, |
| historyRemountKey, |
| activeHooks, |
| messageQueue, |
| queueErrorMessage, |
| showApprovalModeIndicator, |
| allowPlanMode, |
| contextFileNames, |
| errorCount, |
| availableTerminalHeight, |
| stableControlsHeight, |
| mainAreaWidth, |
| staticAreaMaxItemHeight, |
| staticExtraHeight, |
| dialogsVisible, |
| pendingHistoryItems, |
| nightly, |
| branchName, |
| sessionStats, |
| terminalWidth, |
| terminalHeight, |
| mainControlsRef, |
| rootUiRef, |
| currentIDE, |
| updateInfo, |
| showIdeRestartPrompt, |
| ideTrustRestartReason, |
| isRestarting, |
| currentModel, |
| extensionsUpdateState, |
| activePtyId, |
| backgroundTaskCount, |
| isBackgroundTaskVisible, |
| historyManager, |
| embeddedShellFocused, |
| showDebugProfiler, |
| customDialog, |
| apiKeyDefaultValue, |
| authState, |
| isAwaitingLoginRestart, |
| loginRestartMessage, |
| transientMessage, |
| bannerData, |
| bannerVisible, |
| config, |
| settingsNonce, |
| backgroundTaskHeight, |
| isBackgroundTaskListOpen, |
| activeBackgroundTaskPid, |
| backgroundTasks, |
| adminSettingsChanged, |
| newAgents, |
| showIsExpandableHint, |
| ], |
| ); |
|
|
| const exitPrivacyNotice = useCallback( |
| () => setShowPrivacyNotice(false), |
| [setShowPrivacyNotice], |
| ); |
|
|
| const uiActions: UIActions = useMemo( |
| () => ({ |
| handleThemeSelect, |
| closeThemeDialog, |
| handleThemeHighlight, |
| handleAuthSelect, |
| setAuthState, |
| onAuthError, |
| handleEditorSelect, |
| exitEditorDialog, |
| exitPrivacyNotice, |
| closeSettingsDialog, |
| closeModelDialog, |
| openVoiceModelDialog, |
| closeVoiceModelDialog, |
| openAgentConfigDialog, |
| closeAgentConfigDialog, |
| openPermissionsDialog, |
| closePermissionsDialog, |
| setShellModeActive, |
| vimHandleInput, |
| handleIdePromptComplete, |
| handleFolderTrustSelect, |
| setIsPolicyUpdateDialogOpen, |
| setConstrainHeight, |
| onEscapePromptChange: handleEscapePromptChange, |
| refreshStatic, |
| handleFinalSubmit, |
| handleClearScreen, |
| handleProQuotaChoice, |
| handleValidationChoice, |
| |
| handleOverageMenuChoice, |
| handleEmptyWalletChoice, |
| openSessionBrowser, |
| closeSessionBrowser, |
| handleResumeSession, |
| handleDeleteSession, |
| setQueueErrorMessage, |
| addMessage, |
| popAllMessages, |
| handleApiKeySubmit, |
| handleApiKeyCancel, |
| setBannerVisible, |
| setShortcutsHelpVisible, |
| setCleanUiDetailsVisible, |
| toggleCleanUiDetailsVisible, |
| revealCleanUiDetailsTemporarily, |
| handleWarning, |
| setEmbeddedShellFocused, |
| dismissBackgroundTask, |
| setActiveBackgroundTaskPid, |
| setIsBackgroundTaskListOpen, |
| setAuthContext, |
| dismissLoginRestart: () => { |
| setAuthContext({}); |
| setAuthState(AuthState.Updating); |
| }, |
| onHintInput: () => {}, |
| onHintBackspace: () => {}, |
| onHintClear: () => {}, |
| onHintSubmit: () => {}, |
| handleRestart: async () => { |
| if (process.send) { |
| const remoteSettings = config.getRemoteAdminSettings(); |
| if (remoteSettings) { |
| process.send({ |
| type: 'admin-settings-update', |
| settings: remoteSettings, |
| }); |
| } |
| } |
| await relaunchApp(); |
| }, |
| handleNewAgentsSelect: async (choice: NewAgentsChoice) => { |
| if (newAgents && choice === NewAgentsChoice.ACKNOWLEDGE) { |
| const registry = config.getAgentRegistry(); |
| try { |
| await Promise.all( |
| newAgents.map((agent) => registry.acknowledgeAgent(agent)), |
| ); |
| } catch (error) { |
| debugLogger.error('Failed to acknowledge agents:', error); |
| historyManager.addItem( |
| { |
| type: MessageType.ERROR, |
| text: `Failed to acknowledge agents: ${getErrorMessage(error)}`, |
| }, |
| Date.now(), |
| ); |
| } |
| } |
| setNewAgents(null); |
| }, |
| getPreferredEditor, |
| clearAccountSuspension: () => { |
| setAccountSuspensionInfo(null); |
| setAuthState(AuthState.Updating); |
| }, |
| setVoiceModeEnabled: (value: boolean) => { |
| setVoiceModeEnabled(value); |
| }, |
| }), |
| [ |
| handleThemeSelect, |
| closeThemeDialog, |
| handleThemeHighlight, |
| handleAuthSelect, |
| setAuthState, |
| onAuthError, |
| handleEditorSelect, |
| exitEditorDialog, |
| exitPrivacyNotice, |
| closeSettingsDialog, |
| closeModelDialog, |
| openVoiceModelDialog, |
| closeVoiceModelDialog, |
| openAgentConfigDialog, |
| closeAgentConfigDialog, |
| openPermissionsDialog, |
| closePermissionsDialog, |
| setShellModeActive, |
| vimHandleInput, |
| handleIdePromptComplete, |
| handleFolderTrustSelect, |
| setIsPolicyUpdateDialogOpen, |
| setConstrainHeight, |
| handleEscapePromptChange, |
| refreshStatic, |
| handleFinalSubmit, |
| handleClearScreen, |
| handleProQuotaChoice, |
| handleValidationChoice, |
| handleOverageMenuChoice, |
| handleEmptyWalletChoice, |
| openSessionBrowser, |
| closeSessionBrowser, |
| handleResumeSession, |
| handleDeleteSession, |
| setQueueErrorMessage, |
| addMessage, |
| popAllMessages, |
| handleApiKeySubmit, |
| handleApiKeyCancel, |
| setBannerVisible, |
| setShortcutsHelpVisible, |
| setCleanUiDetailsVisible, |
| toggleCleanUiDetailsVisible, |
| revealCleanUiDetailsTemporarily, |
| handleWarning, |
| setEmbeddedShellFocused, |
| dismissBackgroundTask, |
| setActiveBackgroundTaskPid, |
| setIsBackgroundTaskListOpen, |
| setAuthContext, |
| setAccountSuspensionInfo, |
| newAgents, |
| config, |
| historyManager, |
| getPreferredEditor, |
| setVoiceModeEnabled, |
| ], |
| ); |
|
|
| return ( |
| <UIStateContext.Provider value={uiState}> |
| <QuotaContext.Provider value={quotaState}> |
| <InputContext.Provider value={inputState}> |
| <UIActionsContext.Provider value={uiActions}> |
| <ConfigContext.Provider value={config}> |
| <AppContext.Provider |
| value={{ |
| version: props.version, |
| startupWarnings: props.startupWarnings || [], |
| }} |
| > |
| <ToolActionsProvider |
| config={config} |
| toolCalls={allToolCalls} |
| isExpanded={isExpanded} |
| toggleExpansion={toggleExpansion} |
| toggleAllExpansion={toggleAllExpansion} |
| > |
| <ShellFocusContext.Provider value={isFocused}> |
| <MouseProvider mouseEventsEnabled={mouseMode}> |
| <ScrollProvider> |
| <App /> |
| </ScrollProvider> |
| </MouseProvider> |
| </ShellFocusContext.Provider> |
| </ToolActionsProvider> |
| </AppContext.Provider> |
| </ConfigContext.Provider> |
| </UIActionsContext.Provider> |
| </InputContext.Provider> |
| </QuotaContext.Provider> |
| </UIStateContext.Provider> |
| ); |
| }; |
|
|