/**
* @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 (
);
}
export interface MainControlsProps {
config: SlashCommandRuntime;
settings: LoadedSettings;
startupWarnings: string[];
updateInfo: UpdateObject | null;
history: HistoryItem[];
inputWidth: number;
isTodoPanelCollapsed: boolean;
isQueuedMessagesPanelCollapsed: boolean;
queuedSubmissions: readonly QueuedSubmission[];
showTodoPanelSetting: boolean;
dialogsVisible: boolean;
hideContextSummary: boolean;
hideFooter: boolean;
showMemoryUsage: boolean;
currentThemeName: string;
nightly: boolean;
constrainHeight: boolean;
debugConsoleMaxHeight: number;
effectiveAvailableHeight: number;
disableLoadingPhrases: boolean;
streamingState: StreamingState;
thought: ThoughtSummary | null;
currentLoadingPhrase: string | undefined;
elapsedTime: number;
isNarrow: boolean;
ctrlCPressedOnce: boolean;
ctrlDPressedOnce: boolean;
showEscapePrompt: boolean;
ideContextState: IdeContext | undefined;
llxprtMdFileCount: number;
coreMemoryFileCount: number;
contextFileNames: string[];
showToolDescriptions: boolean;
showAutoAcceptIndicator: ApprovalMode;
shellModeActive: boolean;
showErrorDetails: boolean;
consoleMessages: ConsoleMessageItem[];
isInputActive: boolean;
vimModeEnabled: boolean;
vimMode: string | undefined;
currentModel: string;
currentModelLabel?: string;
contextLimit: number | undefined;
branchName: string | undefined;
debugMessage: string;
errorCount: number;
historyTokenCount: number;
tokenMetrics: {
tokensPerMinute: number;
throttleWaitTimeMs: number;
sessionTokenTotal: number;
};
uiActions: UIActions;
terminalWidth: number;
onSuggestionsVisibilityChange: (visible: boolean) => void;
}
export function MainControls(props: MainControlsProps) {
const { dialogsVisible, hideFooter } = props;
return (
<>
{dialogsVisible ? (
) : (
)}
>
);
}
function NotificationsSection(props: MainControlsProps) {
return (
);
}
function TodoPanelSection({
showTodoPanelSetting,
inputWidth,
isTodoPanelCollapsed,
}: {
showTodoPanelSetting: boolean;
inputWidth: number;
isTodoPanelCollapsed: boolean;
}) {
if (!showTodoPanelSetting) {
return null;
}
return ;
}
function QueuedMessagesPanelSection({
inputWidth,
isQueuedMessagesPanelCollapsed,
queuedSubmissions,
}: {
inputWidth: number;
isQueuedMessagesPanelCollapsed: boolean;
queuedSubmissions: readonly QueuedSubmission[];
}) {
if (queuedSubmissions.length === 0) {
return null;
}
return (
);
}
function BucketAuthSection({ dialogsVisible }: { dialogsVisible: boolean }) {
return (
);
}
export interface QuittingDisplayProps {
constrainHeight: boolean;
effectiveAvailableHeight: number;
terminalWidth: number;
quittingMessages: HistoryItem[];
config: SlashCommandRuntime;
slashCommands: readonly SlashCommand[] | undefined;
showTodoPanelSetting: boolean;
}
export function QuittingDisplay(props: QuittingDisplayProps) {
const {
constrainHeight,
effectiveAvailableHeight,
terminalWidth,
quittingMessages,
config,
slashCommands,
showTodoPanelSetting,
} = props;
return (
{quittingMessages.map((item) => (
))}
);
}