/** * @license * Copyright 2025 Vybestack LLC * SPDX-License-Identifier: Apache-2.0 */ import type React from 'react'; import { useCallback, useMemo, useRef } from 'react'; import { useAgentStream } from '../../../hooks/agentStream/index.js'; import type { OperationLifecycleRegistry } from '../../../hooks/agentStream/operationLifecycle.js'; import { useAutoAcceptIndicator } from '../../../hooks/useAutoAcceptIndicator.js'; import { useLoadingIndicator } from '../../../hooks/useLoadingIndicator.js'; import { useSlashCommandProcessor } from '../../../hooks/slashCommandProcessor.js'; import { useTerminalSize } from '../../../hooks/useTerminalSize.js'; import { useVimMode } from '../../../contexts/VimModeContext.js'; import { useVim } from '../../../hooks/vim.js'; import { useTextBuffer } from '../../../components/shared/text-buffer.js'; import { useInputHistoryStore } from '../../../hooks/useInputHistoryStore.js'; import { shouldClearTodos } from '../../../hooks/useTodoPausePreserver.js'; import { StreamingState, type HistoryItem } from '../../../types.js'; import { submitOAuthCode } from '../../../oauth-submission.js'; import { getPendingOAuthProvider } from '../../../oauthGlobalState.js'; import type { EditorType } from '@vybestack/llxprt-code-core'; import { isEditorAvailable } from '@vybestack/llxprt-code-core'; import { SettingScope } from '../../../../config/settings.js'; import type { AppState, AppAction } from '../../../reducers/appReducer.js'; import type { IdeIntegrationNudgeResult } from '../../../IdeIntegrationNudge.js'; import { useSlashCommandActions } from './useSlashCommandActions.js'; import { useExitHandling } from './useExitHandling.js'; import { useInputHandling } from './useInputHandling.js'; import { useShellFocusAutoReset } from './useShellFocusAutoReset.js'; import { useSteer } from './useSteer.js'; import * as fs from 'fs'; import type { AppBootstrapResult } from './useAppBootstrap.js'; import type { AppDialogsResult } from './useAppDialogs.js'; import type { SlashCommandRuntime, UiSubagentManager, } from '../../../cliUiRuntime.js'; export interface AppInputParams { // From bootstrap streamRuntime: AppBootstrapResult['streamRuntime']; slashCommandRuntime: SlashCommandRuntime; agent: AppBootstrapResult['agent']; settings: AppBootstrapResult['settings']; runtime: AppBootstrapResult['runtime']; subagentManager?: UiSubagentManager; history: AppBootstrapResult['history']; addItem: (item: Omit, baseTimestamp?: number) => number; removeItems: (ids: readonly number[]) => void; clearItems: AppBootstrapResult['clearItems']; loadHistory: AppBootstrapResult['loadHistory']; todos: AppBootstrapResult['todos']; updateTodos: AppBootstrapResult['updateTodos']; recordingIntegrationRef: AppBootstrapResult['recordingIntegrationRef']; recordingSwapCallbacks: AppBootstrapResult['recordingSwapCallbacks']; recordingIntegration: AppBootstrapResult['recordingIntegration']; runtimeMessageBus: AppBootstrapResult['runtimeMessageBus']; stdin: AppBootstrapResult['stdin']; setRawMode: AppBootstrapResult['setRawMode']; stdout: AppBootstrapResult['stdout']; setIdePromptAnswered: AppBootstrapResult['setIdePromptAnswered']; setLlxprtMdFileCount: AppBootstrapResult['setLlxprtMdFileCount']; // From dialogs openAuthDialog: AppDialogsResult['openAuthDialog']; openThemeDialog: AppDialogsResult['openThemeDialog']; openEditorDialog: AppDialogsResult['openEditorDialog']; openPrivacyNotice: AppDialogsResult['openPrivacyNotice']; openSettingsDialog: AppDialogsResult['openSettingsDialog']; openLoggingDialog: AppDialogsResult['openLoggingDialog']; openSubagentDialog: AppDialogsResult['openSubagentDialog']; openModelsDialog: AppDialogsResult['openModelsDialog']; openPermissionsDialog: AppDialogsResult['openPermissionsDialog']; openPoliciesDialog: AppDialogsResult['openPoliciesDialog']; openProviderDialog: AppDialogsResult['openProviderDialog']; openLoadProfileDialog: AppDialogsResult['openLoadProfileDialog']; openCreateProfileDialog: AppDialogsResult['openCreateProfileDialog']; openProfileListDialog: AppDialogsResult['openProfileListDialog']; viewProfileDetail: AppDialogsResult['viewProfileDetail']; openProfileEditor: AppDialogsResult['openProfileEditor']; openSessionBrowserDialog: AppDialogsResult['openSessionBrowserDialog']; setDebugMessage: AppDialogsResult['setDebugMessage']; toggleCorgiMode: AppDialogsResult['toggleCorgiMode']; toggleDebugProfiler: AppDialogsResult['toggleDebugProfiler']; dispatchExtensionStateUpdate: AppDialogsResult['dispatchExtensionStateUpdate']; addConfirmUpdateExtensionRequest: AppDialogsResult['addConfirmUpdateExtensionRequest']; welcomeActions: AppDialogsResult['welcomeActions']; extensionsUpdateState: AppDialogsResult['extensionsUpdateState']; setIsProcessing: AppDialogsResult['setIsProcessing']; setEmbeddedShellFocused: AppDialogsResult['setEmbeddedShellFocused']; embeddedShellFocused: AppDialogsResult['embeddedShellFocused']; setAuthError: AppDialogsResult['setAuthError']; shellModeActive: AppDialogsResult['shellModeActive']; isProcessing: AppDialogsResult['isProcessing']; performMemoryRefresh: AppDialogsResult['performMemoryRefresh']; handleExternalEditorOpen: AppDialogsResult['handleExternalEditorOpen']; refreshStatic: AppDialogsResult['refreshStatic']; // Direct appState: AppState; appDispatch: React.Dispatch; /** P12: optional perf operation lifecycle registry (perf enabled only). */ operationLifecycle?: OperationLifecycleRegistry; } function useInputCoreCallbacks(p: AppInputParams) { const { settings, openEditorDialog, setAuthError, appDispatch } = p; const { rows: terminalHeight, columns: terminalWidth } = useTerminalSize(); const inputWidth = Math.max(20, Math.floor(terminalWidth * 0.9) - 6); const suggestionsWidth = Math.max(60, Math.floor(terminalWidth * 0.8)); const isValidPath = useCallback((filePath: string): boolean => { try { return fs.existsSync(filePath) && fs.statSync(filePath).isFile(); } catch { return false; } }, []); const getPreferredEditor = useCallback(() => { const editorType = settings.merged.ui.preferredEditor; if (!isEditorAvailable(editorType)) { openEditorDialog(); return undefined; } return editorType as EditorType; }, [settings, openEditorDialog]); const onAuthError = useCallback(() => { setAuthError('reauth required'); appDispatch({ type: 'SET_NEEDS_RELOGIN', payload: true }); }, [setAuthError, appDispatch]); const handleAuthTimeout = useCallback(() => { setAuthError('Authentication timed out. Please try again.'); }, [setAuthError]); return { terminalHeight, terminalWidth, inputWidth, suggestionsWidth, isValidPath, getPreferredEditor, onAuthError, handleAuthTimeout, }; } function useSlashActions( p: AppInputParams, quitHandler: (messages: HistoryItem[]) => void, ) { return useSlashCommandActions({ openAuthDialog: p.openAuthDialog, openThemeDialog: p.openThemeDialog, openEditorDialog: p.openEditorDialog, openPrivacyNotice: p.openPrivacyNotice, openSettingsDialog: p.openSettingsDialog, openLoggingDialog: p.openLoggingDialog, openSubagentDialog: p.openSubagentDialog, openModelsDialog: p.openModelsDialog, openPermissionsDialog: p.openPermissionsDialog, openPoliciesDialog: p.openPoliciesDialog, openProviderDialog: p.openProviderDialog, openLoadProfileDialog: p.openLoadProfileDialog, openCreateProfileDialog: p.openCreateProfileDialog, openProfileListDialog: p.openProfileListDialog, viewProfileDetail: p.viewProfileDetail, openProfileEditor: p.openProfileEditor, quitHandler, setDebugMessage: p.setDebugMessage, toggleCorgiMode: p.toggleCorgiMode, toggleDebugProfiler: p.toggleDebugProfiler, dispatchExtensionStateUpdate: p.dispatchExtensionStateUpdate, addConfirmUpdateExtensionRequest: p.addConfirmUpdateExtensionRequest, welcomeActions: p.welcomeActions as { resetAndReopen: () => void }, openSessionBrowserDialog: p.openSessionBrowserDialog, }); } function useSlashCommandSetup( p: AppInputParams, quitHandler: (messages: HistoryItem[]) => void, toggleVimEnabled: () => Promise, ) { const { agent, settings, addItem, clearItems, loadHistory, todos, updateTodos, recordingIntegrationRef, recordingSwapCallbacks, extensionsUpdateState, setIsProcessing, setLlxprtMdFileCount, refreshStatic, } = p; const slashCommandProcessorActions = useSlashActions(p, quitHandler); const todoContextForCommands = useMemo( () => ({ todos, updateTodos, refreshTodos: () => {} }), [todos, updateTodos], ); return useSlashCommandProcessor( p.slashCommandRuntime, agent, settings, addItem, clearItems, loadHistory, refreshStatic, toggleVimEnabled, setIsProcessing, setLlxprtMdFileCount, slashCommandProcessorActions, extensionsUpdateState, true, todoContextForCommands, recordingIntegrationRef.current ?? undefined, recordingSwapCallbacks, ); } function useInputCoreProcessors(p: AppInputParams) { const { vimEnabled: vimModeEnabled, vimMode, toggleVimEnabled, } = useVimMode(); const setQuittingMessagesRef = useRef< ((messages: HistoryItem[]) => void) | null >(null); const quitHandler = useCallback((messages: HistoryItem[]) => { if (setQuittingMessagesRef.current) setQuittingMessagesRef.current(messages); }, []); const slashResult = useSlashCommandSetup(p, quitHandler, toggleVimEnabled); const exitResult = useExitHandling({ handleSlashCommand: slashResult.handleSlashCommand, config: p.streamRuntime.hooks, }); setQuittingMessagesRef.current = exitResult.setQuittingMessages; return { vimModeEnabled, vimMode, toggleVimEnabled, setQuittingMessagesRef, ...slashResult, ...exitResult, }; } function useInputCore(p: AppInputParams) { const cb = useInputCoreCallbacks(p); const proc = useInputCoreProcessors(p); return { ...cb, ...proc }; } function useInputBuffer( p: AppInputParams, core: ReturnType, ) { const { stdin, setRawMode, appDispatch, runtime } = p; const { shellModeActive } = p; const viewport = useMemo( () => ({ height: 10, width: core.inputWidth }), [core.inputWidth], ); const buffer = useTextBuffer({ initialText: '', viewport, stdin, setRawMode, isValidPath: core.isValidPath, shellModeActive, }); const inputHistoryStore = useInputHistoryStore(); const lastSubmittedPromptRef = useRef(''); const handleOAuthCodeDialogClose = useCallback(() => { appDispatch({ type: 'CLOSE_DIALOG', payload: 'oauthCode' }); }, [appDispatch]); const handleOAuthCodeSubmit = useCallback( async (code: string) => { submitOAuthCode( { getOAuthManager: () => runtime.getCliOAuthManager(), getActiveProvider: getPendingOAuthProvider, }, code, ); }, [runtime], ); const handleUserCancel = useCallback( (shouldRestorePrompt?: boolean) => { if (shouldRestorePrompt === true) { const last = lastSubmittedPromptRef.current; if (last != null) buffer.setText(last); } else buffer.setText(''); }, [buffer], ); return { buffer, viewport, inputHistoryStore, lastSubmittedPromptRef, handleOAuthCodeDialogClose, handleOAuthCodeSubmit, handleUserCancel, }; } function useInputStreamSetup( p: AppInputParams, core: ReturnType, ) { const { streamRuntime, settings, history, addItem, removeItems, recordingIntegration, runtimeMessageBus, stdout, setEmbeddedShellFocused, performMemoryRefresh, handleExternalEditorOpen, refreshStatic, } = p; const { handleSlashCommand, setDebugMessage, shellModeActive } = { ...core, ...p, }; const bufferSetup = useInputBuffer(p, core); const { handleUserCancel } = bufferSetup; const agentStreamResult = useAgentStream( p.agent, history, addItem, streamRuntime, settings, setDebugMessage, handleSlashCommand, shellModeActive, core.getPreferredEditor, core.onAuthError, performMemoryRefresh, refreshStatic, handleUserCancel, setEmbeddedShellFocused, stdout.columns, stdout.rows, handleExternalEditorOpen, recordingIntegration, runtimeMessageBus, p.subagentManager, removeItems, p.operationLifecycle, ); return { ...bufferSetup, agentStreamResult }; } function useInputStreamWiring( p: AppInputParams, core: ReturnType, setup: ReturnType, ) { const { todos, updateTodos, embeddedShellFocused, setEmbeddedShellFocused } = p; const { buffer, inputHistoryStore, lastSubmittedPromptRef, agentStreamResult, } = setup; const { submitQuery } = agentStreamResult; const pendingHistoryItems = useMemo( () => [ ...(core.pendingHistoryItems as HistoryItem[]), ...agentStreamResult.pendingHistoryItems, ], [core.pendingHistoryItems, agentStreamResult.pendingHistoryItems], ); const activeShellPtyId = agentStreamResult.activeShellPtyId; useShellFocusAutoReset({ pendingHistoryItems, embeddedShellFocused, setEmbeddedShellFocused, }); const { handleFinalSubmit } = useInputHandling({ buffer, inputHistoryStore, submitQuery, pendingHistoryItems, lastSubmittedPromptRef, needsRelogin: p.appState.needsRelogin, appDispatch: p.appDispatch, }); const handleUserInputSubmit = useCallback( (submittedValue: string) => { if (shouldClearTodos(todos)) { updateTodos([]); } handleFinalSubmit(submittedValue); }, [todos, updateTodos, handleFinalSubmit], ); const handleSteer = useSteer( p.agent, agentStreamResult.streamingState, agentStreamResult.sanitizeContent, ); const { activeShellPtyId: _ptyIdFromStream, pendingHistoryItems: _pendingFromStream, queuedSubmissions, ...streamRest } = agentStreamResult; return { handleFinalSubmit, handleUserInputSubmit, handleSteer, pendingHistoryItems, activeShellPtyId, queuedSubmissions, ...streamRest, }; } function useInputStream( p: AppInputParams, core: ReturnType, ) { const setup = useInputStreamSetup(p, core); const wiring = useInputStreamWiring(p, core, setup); const { agentStreamResult: _agentStreamResult, ...setupRest } = setup; return { ...setupRest, ...wiring }; } function useInputFinish( p: AppInputParams, core: ReturnType, stream: ReturnType, ) { const { settings, setIdePromptAnswered, isProcessing } = p; const { handleSlashCommand, vimModeEnabled, vimMode, toggleVimEnabled } = core; const { buffer, handleFinalSubmit, streamingState, initError, slashCommands, } = { ...core, ...stream }; const handleIdePromptComplete = useCallback( (result: IdeIntegrationNudgeResult) => { if (result.userSelection === 'yes') { if (result.isExtensionPreInstalled) { void handleSlashCommand('/ide enable'); } else { void handleSlashCommand('/ide install'); } settings.setValue( SettingScope.User, 'hasSeenIdeIntegrationNudge', true, ); } else if (result.userSelection === 'dismiss') { settings.setValue( SettingScope.User, 'hasSeenIdeIntegrationNudge', true, ); } setIdePromptAnswered(true); }, [handleSlashCommand, settings, setIdePromptAnswered], ); const { handleInput: vimHandleInput } = useVim(buffer, handleFinalSubmit); const { elapsedTime, currentLoadingPhrase } = useLoadingIndicator( streamingState, settings.merged.ui.wittyPhraseStyle ?? settings.merged.wittyPhraseStyle ?? 'default', settings.merged.ui.customWittyPhrases ?? settings.merged.customWittyPhrases, stream.activeShellPtyId != null && !p.embeddedShellFocused, stream.lastOutputTime, ); const showAutoAcceptIndicator = useAutoAcceptIndicator({ agent: p.agent, addItem: p.addItem, }); const handleSettingsRestart = useCallback(() => { void handleSlashCommand('/quit'); }, [handleSlashCommand]); const isStreamingIdleOrResponding = streamingState === StreamingState.Idle || streamingState === StreamingState.Responding; const isInputActive = isStreamingIdleOrResponding && !initError && !isProcessing && !!slashCommands; return { handleIdePromptComplete, vimHandleInput, vimModeEnabled, vimMode, toggleVimEnabled, elapsedTime, currentLoadingPhrase, showAutoAcceptIndicator, handleSettingsRestart, isInputActive, }; } export function useAppInput(params: AppInputParams) { const core = useInputCore(params); const stream = useInputStream(params, core); const finish = useInputFinish(params, core, stream); return { ...core, ...stream, ...finish }; } export type AppInputResult = ReturnType;