/** * @license * Copyright 2025 Vybestack LLC * SPDX-License-Identifier: Apache-2.0 */ import type React from 'react'; import { useEffect, useMemo, useState } from 'react'; import { useStdin, useStdout } from 'ink'; import { useResponsive } from '../../../hooks/useResponsive.js'; import { useBracketedPaste } from '../../../hooks/useBracketedPaste.js'; import { useConsoleMessages } from '../../../hooks/useConsoleMessages.js'; import { useExtensionAutoUpdate } from '../../../hooks/useExtensionAutoUpdate.js'; import { useCoreEventHandlers } from '../../../hooks/useCoreEventHandlers.js'; import { DEFAULT_HISTORY_MAX_BYTES, DEFAULT_HISTORY_MAX_ITEMS, } from '../../../../constants/historyLimits.js'; import { useRetractableHistory } from '../../../hooks/useHistoryManager.js'; import { useMemoryMonitor } from '../../../hooks/useMemoryMonitor.js'; import { type IContent, type IdeInfo, type MessageBus, type RecordingIntegration, type SessionRecordingService, type LockHandle, } from '@vybestack/llxprt-code-core'; import { useSessionStats } from '../../../contexts/SessionContext.js'; import { useFocus } from '../../../hooks/useFocus.js'; import type { AppState, AppAction } from '../../../reducers/appReducer.js'; import type { UpdateObject } from '../../../utils/updateCheck.js'; import { useRuntimeApi } from '../../../contexts/RuntimeContext.js'; import { useTodoContext } from '../../../contexts/TodoContext.js'; import { useRecordingInfrastructure } from './useRecordingInfrastructure.js'; import { useUpdateAndOAuthBridges } from './useUpdateAndOAuthBridges.js'; import { useSessionInitialization } from './useSessionInitialization.js'; import { useTokenMetricsTracking } from './useTokenMetricsTracking.js'; import { registerCleanup } from '../../../../utils/cleanup.js'; import type { Agent } from '@vybestack/llxprt-code-agents'; import type { MemoryTelemetryController } from '../../../hooks/memoryTrend/memoryTelemetry.js'; import type { LoadedSettings } from '../../../../config/settings.js'; import type { HistoryItem } from '../../../types.js'; import type { AgentClientSource, StreamRuntime, UiRuntime, } from '../../../cliUiRuntime.js'; export interface AppBootstrapProps { uiRuntime: UiRuntime; /** * The single interactive Agent threaded from the composition root. */ agent: Agent; settings: LoadedSettings; startupWarnings?: string[]; resumedHistory?: IContent[]; version: string; runtimeMessageBus?: MessageBus; appState: AppState; appDispatch: React.Dispatch; recordingIntegration?: RecordingIntegration; initialRecordingService?: SessionRecordingService; initialLockHandle?: LockHandle | null; /** P12: optional memory telemetry controller (perf+memory enabled only). */ memoryController?: MemoryTelemetryController; } export interface AppBootstrapResult { uiRuntime: UiRuntime; streamRuntime: StreamRuntime; /** * The single interactive Agent threaded from the composition root. */ agent: Agent; /** * Narrow boundary that resolves the live AgentClient for the streaming * path. Resolved once here so downstream hooks receive an explicit, * minimal surface instead of dereferencing the runtime object inline. */ agentClientSource: AgentClientSource; settings: LoadedSettings; runtime: ReturnType; isFocused: boolean; isNarrow: boolean; history: HistoryItem[]; addItem: ( item: Omit, baseTimestamp?: number, isResuming?: boolean, ) => number; removeItems: (ids: readonly number[]) => void; clearItems: () => void; loadHistory: (newHistory: HistoryItem[]) => void; llxprtMdFileCount: number; setLlxprtMdFileCount: (count: number) => void; coreMemoryFileCount: number; consoleMessages: ReturnType['consoleMessages']; handleNewMessage: ReturnType['handleNewMessage']; clearConsoleMessagesState: ReturnType< typeof useConsoleMessages >['clearConsoleMessages']; sessionStats: ReturnType['stats']; updateHistoryTokenCount: ReturnType< typeof useSessionStats >['updateHistoryTokenCount']; tokenMetrics: ReturnType['tokenMetrics']; todos: ReturnType['todos']; updateTodos: ReturnType['updateTodos']; recordingIntegrationRef: React.MutableRefObject; recordingSwapCallbacks: ReturnType< typeof useRecordingInfrastructure >['recordingSwapCallbacks']; idePromptAnswered: boolean; setIdePromptAnswered: React.Dispatch>; currentIDE: IdeInfo | undefined; shouldShowIdePrompt: boolean | null | undefined; updateInfo: UpdateObject | null; setUpdateInfo: React.Dispatch>; stdin: ReturnType['stdin']; setRawMode: ReturnType['setRawMode']; stdout: ReturnType['stdout']; nightly: boolean; runtimeMessageBus?: MessageBus; startupWarnings: string[]; resumedHistory?: IContent[]; recordingIntegration?: RecordingIntegration; } /** Initializes history, session, and IO primitives */ function useBootstrapHistory(props: AppBootstrapProps) { const { uiRuntime, agent, settings, resumedHistory } = props; const runtime = useRuntimeApi(); const isFocused = useFocus(); const { isNarrow } = useResponsive(); useBracketedPaste(); const [updateInfo, setUpdateInfo] = useState(null); const { stdout } = useStdout(); const { stdin, setRawMode } = useStdin(); const nightly = props.version.includes('nightly'); const historyLimits = useMemo( () => ({ maxItems: typeof settings.merged.ui.historyMaxItems === 'number' ? settings.merged.ui.historyMaxItems : DEFAULT_HISTORY_MAX_ITEMS, maxBytes: typeof settings.merged.ui.historyMaxBytes === 'number' ? settings.merged.ui.historyMaxBytes : DEFAULT_HISTORY_MAX_BYTES, }), [settings.merged.ui.historyMaxItems, settings.merged.ui.historyMaxBytes], ); const { history, addItem, removeItems, clearItems, loadHistory } = useRetractableHistory(historyLimits); const { llxprtMdFileCount, setLlxprtMdFileCount, coreMemoryFileCount, setCoreMemoryFileCount: _setCoreMemoryFileCount, } = useSessionInitialization({ uiRuntime, agent, addItem, loadHistory, resumedHistory, }); useMemoryMonitor({ addItem, memoryController: props.memoryController }); return { runtime, isFocused, isNarrow, updateInfo, setUpdateInfo, stdout, stdin, setRawMode, nightly, history, addItem, removeItems, clearItems, loadHistory, llxprtMdFileCount, setLlxprtMdFileCount, coreMemoryFileCount, }; } /** Initializes task-list state */ function useBootstrapTodo() { const { todos, updateTodos } = useTodoContext(); return { todos, updateTodos }; } /** Initializes recording, IDE prompt, messages, and token metrics */ function useBootstrapEvents( props: AppBootstrapProps, addItem: AppBootstrapResult['addItem'], setUpdateInfo: React.Dispatch>, runtime: ReturnType, ) { const { uiRuntime, settings, recordingIntegration, initialRecordingService, initialLockHandle, } = props; const { recordingIntegrationRef, recordingSwapCallbacks } = useRecordingInfrastructure( initialRecordingService, recordingIntegration, initialLockHandle, ); const [idePromptAnswered, setIdePromptAnswered] = useState(false); const currentIDE = uiRuntime.ide.getIdeClient()?.getCurrentIde(); useEffect(() => { const ideClient = uiRuntime.ide.getIdeClient(); if (ideClient === undefined) { return undefined; } registerCleanup(() => { void ideClient.disconnect(); }); return undefined; }, [uiRuntime]); const shouldShowIdePrompt = Boolean(currentIDE) && !uiRuntime.ide.getIdeMode() && settings.merged.hasSeenIdeIntegrationNudge !== true && !idePromptAnswered; useUpdateAndOAuthBridges({ addItem, setUpdateInfo, getCliOAuthManager: runtime.getCliOAuthManager, }); const { consoleMessages, handleNewMessage, clearConsoleMessages: clearConsoleMessagesState, } = useConsoleMessages(); useExtensionAutoUpdate({ settings, onConsoleMessage: handleNewMessage }); useCoreEventHandlers({ handleNewMessage, uiRuntime, recordingIntegrationRef, }); const { stats: sessionStats, updateHistoryTokenCount } = useSessionStats(); const { tokenMetrics } = useTokenMetricsTracking({ uiRuntime, updateHistoryTokenCount, recordingIntegrationRef, }); return { recordingIntegrationRef, recordingSwapCallbacks, idePromptAnswered, setIdePromptAnswered, currentIDE, shouldShowIdePrompt, consoleMessages, handleNewMessage, clearConsoleMessagesState, sessionStats, updateHistoryTokenCount, tokenMetrics, }; } export function useAppBootstrap(props: AppBootstrapProps): AppBootstrapResult { const { uiRuntime } = props; const streamRuntime: StreamRuntime = uiRuntime; const h = useBootstrapHistory(props); const t = useBootstrapTodo(); const e = useBootstrapEvents(props, h.addItem, h.setUpdateInfo, h.runtime); return { uiRuntime, streamRuntime, agent: props.agent, agentClientSource: streamRuntime.agentClientSource, settings: props.settings, runtimeMessageBus: props.runtimeMessageBus, startupWarnings: props.startupWarnings ?? [], resumedHistory: props.resumedHistory, recordingIntegration: props.recordingIntegration, nightly: h.nightly, runtime: h.runtime, isFocused: h.isFocused, isNarrow: h.isNarrow, history: h.history, addItem: h.addItem, removeItems: h.removeItems, clearItems: h.clearItems, loadHistory: h.loadHistory, llxprtMdFileCount: h.llxprtMdFileCount, setLlxprtMdFileCount: h.setLlxprtMdFileCount, coreMemoryFileCount: h.coreMemoryFileCount, updateInfo: h.updateInfo, setUpdateInfo: h.setUpdateInfo, stdin: h.stdin, setRawMode: h.setRawMode, stdout: h.stdout, todos: t.todos, updateTodos: t.updateTodos, recordingIntegrationRef: e.recordingIntegrationRef, recordingSwapCallbacks: e.recordingSwapCallbacks, idePromptAnswered: e.idePromptAnswered, setIdePromptAnswered: e.setIdePromptAnswered, currentIDE: e.currentIDE, shouldShowIdePrompt: e.shouldShowIdePrompt, consoleMessages: e.consoleMessages, handleNewMessage: e.handleNewMessage, clearConsoleMessagesState: e.clearConsoleMessagesState, sessionStats: e.sessionStats, updateHistoryTokenCount: e.updateHistoryTokenCount, tokenMetrics: e.tokenMetrics, }; }