/** * @license * Copyright 2025 Vybestack LLC * SPDX-License-Identifier: Apache-2.0 */ import React from 'react'; import { Box } from 'ink'; import type { MessageBus, IdeContext, ThoughtSummary, ApprovalMode, } from '@vybestack/llxprt-code-core'; import type { StreamingState, HistoryItem, ConsoleMessageItem, } from '../types.js'; import type { SlashCommandRuntime, UiRuntime } from '../cliUiRuntime.js'; import type { QueuedSubmission } from '../hooks/agentStream/types.js'; import type { LoadedSettings } from '../../config/settings.js'; import type { UpdateObject } from '../utils/updateCheck.js'; import type { UIState } from '../contexts/UIStateContext.js'; import type { UIActions } from '../contexts/UIActionsContext.js'; import { OverflowProvider } from '../contexts/OverflowContext.js'; import { getCliRuntimeContext } from '@vybestack/llxprt-code-providers/runtime.js'; import { themeManager } from '../themes/theme-manager.js'; import type { SlashCommand } from '../commands/types.js'; import { AppHeader } from '../components/AppHeader.js'; import { HistoryItemDisplay } from '../components/HistoryItemDisplay.js'; import { ShowMoreLines } from '../components/ShowMoreLines.js'; import { Notifications } from '../components/Notifications.js'; import { TodoPanel } from '../components/TodoPanel.js'; import { QueuedMessagesPanel } from '../components/QueuedMessagesPanel.js'; import { Footer } from '../components/Footer.js'; import { DialogManager } from '../components/DialogManager.js'; import { BucketAuthConfirmation } from '../components/BucketAuthConfirmation.js'; import { InlineContent } from './InlineContent.js'; export type { ScrollableMainContentItem } from './scrollableMainContent.js'; export { renderScrollableMainContentItem, keyExtractorScrollableMainContentItem, estimateScrollableMainContentItemHeight, } from './scrollableMainContent.js'; import type { ScrollableMainContentItem } from './scrollableMainContent.js'; export function hasActiveDialog(uiState: UIState): boolean { const dialogFlags = [ uiState.showWorkspaceMigrationDialog, uiState.shouldShowIdePrompt, uiState.isFolderTrustDialogOpen, uiState.isWelcomeDialogOpen, uiState.isPermissionsDialogOpen, Boolean(uiState.confirmationRequest), uiState.isThemeDialogOpen, uiState.isSettingsDialogOpen, uiState.isAuthDialogOpen, uiState.isOAuthCodeDialogOpen, uiState.isEditorDialogOpen, uiState.isProviderDialogOpen, uiState.isLoadProfileDialogOpen, uiState.isCreateProfileDialogOpen, uiState.isProfileListDialogOpen, uiState.isProfileDetailDialogOpen, uiState.isProfileEditorDialogOpen, uiState.isToolsDialogOpen, uiState.isLoggingDialogOpen, uiState.isSubagentDialogOpen, uiState.isModelsDialogOpen, uiState.isSessionBrowserDialogOpen, uiState.isModelConfigDialogOpen, uiState.isPoliciesDialogOpen, uiState.showPrivacyNotice, ]; return dialogFlags.some(Boolean); } export interface LayoutSettings { showTodoPanelSetting: boolean; hideContextSummary: boolean; hideFooter: boolean; showMemoryUsage: boolean; disableLoadingPhrases: boolean; currentThemeName: string; isNarrow: boolean; useAlternateBuffer: boolean; debugConsoleMaxHeight: number; staticAreaMaxItemHeight: number; effectiveAvailableHeight: number; } export function useLayoutSettings( config: UiRuntime, settings: LoadedSettings, availableTerminalHeight: number, terminalHeight: number, constrainHeight: boolean, uiAvailableTerminalHeight: number, isNarrow: boolean, ): LayoutSettings { const showTodoPanelSetting = settings.merged.ui.showTodoPanel ?? true; const hideContextSummary = settings.merged.ui.hideContextSummary ?? false; const hideFooter = settings.merged.ui.hideFooter ?? false; const showMemoryUsage = config.app.getDebugMode() || (settings.merged.ui.showMemoryUsage ?? false); const disableLoadingPhrases = config.app.getAccessibility().disableLoadingPhrases === true || config.app.getScreenReader(); const currentThemeName = themeManager.getActiveTheme().name; const useAlternateBuffer = settings.merged.ui.useAlternateBuffer === true && !config.app.getScreenReader(); const debugConsoleMaxHeight = Math.floor(Math.max(terminalHeight * 0.2, 5)); const staticAreaMaxItemHeight = Math.max(terminalHeight * 4, 100); const effectiveAvailableHeight = constrainHeight ? uiAvailableTerminalHeight : availableTerminalHeight; return { showTodoPanelSetting, hideContextSummary, hideFooter, showMemoryUsage, disableLoadingPhrases, currentThemeName, isNarrow, useAlternateBuffer, debugConsoleMaxHeight, staticAreaMaxItemHeight, effectiveAvailableHeight, }; } function useHistoryItemDisplayProps( config: SlashCommandRuntime, mainAreaWidth: number, showTodoPanelSetting: boolean, slashCommands: readonly SlashCommand[] | undefined, activeShellPtyId: number | null, embeddedShellFocused: boolean, ) { return { terminalWidth: mainAreaWidth, config, slashCommands, showTodoPanel: showTodoPanelSetting, activeShellPtyId, embeddedShellFocused, }; } export function useListItems( headerElement: React.ReactElement, pendingElement: React.ReactElement, history: HistoryItem[], config: SlashCommandRuntime, mainAreaWidth: number, staticAreaMaxItemHeight: number, slashCommands: readonly SlashCommand[] | undefined, showTodoPanelSetting: boolean, activeShellPtyId: number | null, embeddedShellFocused: boolean, ): ScrollableMainContentItem[] { const base = useHistoryItemDisplayProps( config, mainAreaWidth, showTodoPanelSetting, slashCommands, activeShellPtyId, embeddedShellFocused, ); return React.useMemo( () => [ { key: 'header', estimatedHeight: 100, element: {headerElement}, }, ...history.map((h) => ({ key: `history-${h.id}`, estimatedHeight: 100, element: ( ), })), { key: 'pending', estimatedHeight: 100, element: pendingElement, }, ], [headerElement, history, base, staticAreaMaxItemHeight, pendingElement], ); } export function useStaticItems( config: SlashCommandRuntime, settings: LoadedSettings, version: string, nightly: boolean, terminalWidth: number, history: HistoryItem[], mainAreaWidth: number, staticAreaMaxItemHeight: number, slashCommands: readonly SlashCommand[] | undefined, showTodoPanelSetting: boolean, activeShellPtyId: number | null, embeddedShellFocused: boolean, ): React.ReactElement[] { const base = useHistoryItemDisplayProps( config, mainAreaWidth, showTodoPanelSetting, slashCommands, activeShellPtyId, embeddedShellFocused, ); return React.useMemo(() => { if (process.env.LLXPRT_CODE_SUPPRESS_STATIC_HEADER === 'true') { return history.map((h) => ( )); } return [ , ...history.map((h) => ( )), ]; }, [ config, settings, version, nightly, terminalWidth, history, base, staticAreaMaxItemHeight, ]); } export function usePendingItems( uiState: UIState, config: SlashCommandRuntime, mainAreaWidth: number, constrainHeight: boolean, effectiveAvailableHeight: number, slashCommands: readonly SlashCommand[] | undefined, showTodoPanelSetting: boolean, activeShellPtyId: number | null, embeddedShellFocused: boolean, ): React.ReactElement[] { const base = useHistoryItemDisplayProps( config, mainAreaWidth, showTodoPanelSetting, slashCommands, activeShellPtyId, embeddedShellFocused, ); return React.useMemo( () => uiState.pendingHistoryItems.map((item, i) => ( )), [ uiState.pendingHistoryItems, base, constrainHeight, effectiveAvailableHeight, uiState.isEditorDialogOpen, ], ); } export function usePendingElement( uiState: UIState, config: SlashCommandRuntime, mainAreaWidth: number, constrainHeight: boolean, effectiveAvailableHeight: number, slashCommands: readonly SlashCommand[] | undefined, showTodoPanelSetting: boolean, activeShellPtyId: number | null, embeddedShellFocused: boolean, ): React.ReactElement { const pendingItems = usePendingItems( uiState, config, mainAreaWidth, constrainHeight, effectiveAvailableHeight, slashCommands, showTodoPanelSetting, activeShellPtyId, embeddedShellFocused, ); return React.useMemo( () => ( {pendingItems} ), [uiState.pendingHistoryItemRef, pendingItems, constrainHeight], ); } export function useScrollableContent( config: SlashCommandRuntime, settings: LoadedSettings, version: string, nightly: boolean, terminalWidth: number, mainAreaWidth: number, staticAreaMaxItemHeight: number, constrainHeight: boolean, effectiveAvailableHeight: number, showTodoPanelSetting: boolean, uiState: UIState, slashCommands: readonly SlashCommand[] | undefined, activeShellPtyId: number | null, embeddedShellFocused: boolean, ) { const headerElement = React.useMemo( () => ( ), [config, settings, version, nightly, terminalWidth], ); const pendingElement = usePendingElement( uiState, config, mainAreaWidth, constrainHeight, effectiveAvailableHeight, slashCommands, showTodoPanelSetting, activeShellPtyId, embeddedShellFocused, ); const listItems = useListItems( headerElement, pendingElement, uiState.history, config, mainAreaWidth, staticAreaMaxItemHeight, slashCommands, showTodoPanelSetting, activeShellPtyId, embeddedShellFocused, ); const staticItems = useStaticItems( config, settings, version, nightly, terminalWidth, uiState.history, mainAreaWidth, staticAreaMaxItemHeight, slashCommands, showTodoPanelSetting, activeShellPtyId, embeddedShellFocused, ); const pendingItems = usePendingItems( uiState, config, mainAreaWidth, constrainHeight, effectiveAvailableHeight, slashCommands, showTodoPanelSetting, activeShellPtyId, embeddedShellFocused, ); return { listItems, staticItems, pendingItems }; } export interface FooterProps { config: SlashCommandRuntime; settings: LoadedSettings; hideFooter: boolean; showMemoryUsage: boolean; currentThemeName: string; nightly: boolean; vimModeEnabled: boolean; vimMode: string | undefined; currentModel: string; currentModelLabel?: string; contextLimit: number | undefined; branchName: string | undefined; debugMessage: string; errorCount: number; showErrorDetails: boolean; historyTokenCount: number; tokenMetrics: { tokensPerMinute: number; throttleWaitTimeMs: number; sessionTokenTotal: number; }; } export function FooterSection(props: FooterProps) { const { config, settings, hideFooter, showMemoryUsage, currentThemeName, nightly, vimModeEnabled, vimMode, currentModel, currentModelLabel, contextLimit, branchName, debugMessage, errorCount, showErrorDetails, historyTokenCount, tokenMetrics, } = props; if (hideFooter) { return null; } return (