2025-09-06 01:39:02 -04:00
|
|
|
/**
|
|
|
|
|
* @license
|
2026-02-09 21:53:10 -05:00
|
|
|
* Copyright 2026 Google LLC
|
2025-09-06 01:39:02 -04:00
|
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
|
|
|
*/
|
|
|
|
|
|
2026-02-13 17:20:14 -05:00
|
|
|
import {
|
|
|
|
|
ApprovalMode,
|
2026-02-17 07:16:37 -08:00
|
|
|
checkExhaustive,
|
2026-02-13 17:20:14 -05:00
|
|
|
CoreToolCallStatus,
|
2026-03-23 19:30:48 -07:00
|
|
|
isUserVisibleHook,
|
2026-02-13 17:20:14 -05:00
|
|
|
} from '@google/gemini-cli-core';
|
2026-03-23 19:30:48 -07:00
|
|
|
import { Box, Text, useIsScreenReaderEnabled } from 'ink';
|
|
|
|
|
import { useState, useEffect, useMemo } from 'react';
|
|
|
|
|
import { useConfig } from '../contexts/ConfigContext.js';
|
|
|
|
|
import { useSettings } from '../contexts/SettingsContext.js';
|
|
|
|
|
import { useUIState } from '../contexts/UIStateContext.js';
|
|
|
|
|
import { useUIActions } from '../contexts/UIActionsContext.js';
|
|
|
|
|
import { useVimMode } from '../contexts/VimModeContext.js';
|
|
|
|
|
import { useAlternateBuffer } from '../hooks/useAlternateBuffer.js';
|
|
|
|
|
import { useTerminalSize } from '../hooks/useTerminalSize.js';
|
|
|
|
|
import { isNarrowWidth } from '../utils/isNarrowWidth.js';
|
|
|
|
|
import { isContextUsageHigh } from '../utils/contextUsage.js';
|
|
|
|
|
import { theme } from '../semantic-colors.js';
|
|
|
|
|
import { GENERIC_WORKING_LABEL } from '../textConstants.js';
|
|
|
|
|
import { INTERACTIVE_SHELL_WAITING_PHRASE } from '../hooks/usePhraseCycler.js';
|
|
|
|
|
import { StreamingState, type HistoryItemToolGroup } from '../types.js';
|
2025-09-06 01:39:02 -04:00
|
|
|
import { LoadingIndicator } from './LoadingIndicator.js';
|
2026-03-23 19:30:48 -07:00
|
|
|
import { ContextUsageDisplay } from './ContextUsageDisplay.js';
|
2026-01-06 15:52:12 -05:00
|
|
|
import { StatusDisplay } from './StatusDisplay.js';
|
2026-03-23 19:30:48 -07:00
|
|
|
import { HorizontalLine } from './shared/HorizontalLine.js';
|
2026-02-10 08:39:28 -05:00
|
|
|
import { ToastDisplay, shouldShowToast } from './ToastDisplay.js';
|
2026-01-21 10:19:47 -05:00
|
|
|
import { ApprovalModeIndicator } from './ApprovalModeIndicator.js';
|
2025-09-06 01:39:02 -04:00
|
|
|
import { ShellModeIndicator } from './ShellModeIndicator.js';
|
|
|
|
|
import { DetailedMessagesDisplay } from './DetailedMessagesDisplay.js';
|
2025-10-16 11:23:36 -07:00
|
|
|
import { RawMarkdownIndicator } from './RawMarkdownIndicator.js';
|
2026-02-06 11:33:39 -08:00
|
|
|
import { ShortcutsHelp } from './ShortcutsHelp.js';
|
2025-10-09 19:27:20 -07:00
|
|
|
import { InputPrompt } from './InputPrompt.js';
|
2025-09-26 21:27:00 -04:00
|
|
|
import { Footer } from './Footer.js';
|
2025-09-06 01:39:02 -04:00
|
|
|
import { ShowMoreLines } from './ShowMoreLines.js';
|
2025-09-18 11:54:09 -07:00
|
|
|
import { QueuedMessageDisplay } from './QueuedMessageDisplay.js';
|
2025-09-06 01:39:02 -04:00
|
|
|
import { OverflowProvider } from '../contexts/OverflowContext.js';
|
2026-03-23 19:30:48 -07:00
|
|
|
import { ConfigInitDisplay } from './ConfigInitDisplay.js';
|
2025-10-19 05:22:01 -07:00
|
|
|
import { TodoTray } from './messages/Todo.js';
|
2025-09-06 01:39:02 -04:00
|
|
|
|
2026-01-23 20:32:35 -05:00
|
|
|
export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
|
2025-09-06 01:39:02 -04:00
|
|
|
const uiState = useUIState();
|
|
|
|
|
const uiActions = useUIActions();
|
2026-03-23 19:30:48 -07:00
|
|
|
const settings = useSettings();
|
|
|
|
|
const config = useConfig();
|
2026-01-29 23:31:47 -08:00
|
|
|
const { vimEnabled, vimMode } = useVimMode();
|
2026-03-23 19:30:48 -07:00
|
|
|
const isScreenReaderEnabled = useIsScreenReaderEnabled();
|
|
|
|
|
const { columns: terminalWidth } = useTerminalSize();
|
2025-09-06 01:39:02 -04:00
|
|
|
const isNarrow = isNarrowWidth(terminalWidth);
|
|
|
|
|
const debugConsoleMaxHeight = Math.floor(Math.max(terminalWidth * 0.2, 5));
|
2025-11-11 07:50:11 -08:00
|
|
|
const [suggestionsVisible, setSuggestionsVisible] = useState(false);
|
2025-09-06 01:39:02 -04:00
|
|
|
|
2025-11-11 07:50:11 -08:00
|
|
|
const isAlternateBuffer = useAlternateBuffer();
|
2026-03-23 19:30:48 -07:00
|
|
|
const showApprovalModeIndicator = uiState.showApprovalModeIndicator;
|
|
|
|
|
const loadingPhrases = settings.merged.ui.loadingPhrases;
|
|
|
|
|
const showTips = loadingPhrases === 'tips' || loadingPhrases === 'all';
|
|
|
|
|
const showWit = loadingPhrases === 'witty' || loadingPhrases === 'all';
|
|
|
|
|
|
2026-02-12 14:25:24 -05:00
|
|
|
const showUiDetails = uiState.cleanUiDetailsVisible;
|
2025-11-11 07:50:11 -08:00
|
|
|
const suggestionsPosition = isAlternateBuffer ? 'above' : 'below';
|
|
|
|
|
const hideContextSummary =
|
|
|
|
|
suggestionsVisible && suggestionsPosition === 'above';
|
2026-02-12 11:35:40 -05:00
|
|
|
|
|
|
|
|
const hasPendingToolConfirmation = useMemo(
|
|
|
|
|
() =>
|
|
|
|
|
(uiState.pendingHistoryItems ?? [])
|
|
|
|
|
.filter(
|
|
|
|
|
(item): item is HistoryItemToolGroup => item.type === 'tool_group',
|
|
|
|
|
)
|
|
|
|
|
.some((item) =>
|
2026-02-13 17:20:14 -05:00
|
|
|
item.tools.some(
|
|
|
|
|
(tool) => tool.status === CoreToolCallStatus.AwaitingApproval,
|
|
|
|
|
),
|
2026-02-12 11:35:40 -05:00
|
|
|
),
|
|
|
|
|
[uiState.pendingHistoryItems],
|
2026-02-06 11:33:39 -08:00
|
|
|
);
|
2026-02-12 11:35:40 -05:00
|
|
|
|
2026-02-06 11:33:39 -08:00
|
|
|
const hasPendingActionRequired =
|
|
|
|
|
hasPendingToolConfirmation ||
|
|
|
|
|
Boolean(uiState.commandConfirmationRequest) ||
|
|
|
|
|
Boolean(uiState.authConsentRequest) ||
|
|
|
|
|
(uiState.confirmUpdateExtensionRequests?.length ?? 0) > 0 ||
|
|
|
|
|
Boolean(uiState.loopDetectionConfirmationRequest) ||
|
2026-02-09 21:53:10 -05:00
|
|
|
Boolean(uiState.quota.proQuotaRequest) ||
|
|
|
|
|
Boolean(uiState.quota.validationRequest) ||
|
2026-02-06 11:33:39 -08:00
|
|
|
Boolean(uiState.customDialog);
|
2026-03-23 19:30:48 -07:00
|
|
|
|
2026-02-12 11:35:40 -05:00
|
|
|
const isPassiveShortcutsHelpState =
|
|
|
|
|
uiState.isInputActive &&
|
|
|
|
|
uiState.streamingState === StreamingState.Idle &&
|
|
|
|
|
!hasPendingActionRequired;
|
|
|
|
|
|
|
|
|
|
const { setShortcutsHelpVisible } = uiActions;
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (uiState.shortcutsHelpVisible && !isPassiveShortcutsHelpState) {
|
|
|
|
|
setShortcutsHelpVisible(false);
|
|
|
|
|
}
|
|
|
|
|
}, [
|
|
|
|
|
uiState.shortcutsHelpVisible,
|
|
|
|
|
isPassiveShortcutsHelpState,
|
|
|
|
|
setShortcutsHelpVisible,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
const showShortcutsHelp =
|
|
|
|
|
uiState.shortcutsHelpVisible &&
|
|
|
|
|
uiState.streamingState === StreamingState.Idle &&
|
|
|
|
|
!hasPendingActionRequired;
|
2026-03-23 19:30:48 -07:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Use the setting if provided, otherwise default to true for the new UX.
|
|
|
|
|
* This allows tests to override the collapse behavior.
|
|
|
|
|
*/
|
|
|
|
|
const shouldCollapseDuringApproval =
|
|
|
|
|
settings.merged.ui.collapseDrawerDuringApproval !== false;
|
|
|
|
|
|
|
|
|
|
if (hasPendingActionRequired && shouldCollapseDuringApproval) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-10 08:39:28 -05:00
|
|
|
const hasToast = shouldShowToast(uiState);
|
2026-02-06 11:33:39 -08:00
|
|
|
const showLoadingIndicator =
|
|
|
|
|
(!uiState.embeddedShellFocused || uiState.isBackgroundShellVisible) &&
|
|
|
|
|
uiState.streamingState === StreamingState.Responding &&
|
|
|
|
|
!hasPendingActionRequired;
|
2026-03-23 19:30:48 -07:00
|
|
|
|
2026-02-12 14:25:24 -05:00
|
|
|
const hideUiDetailsForSuggestions =
|
|
|
|
|
suggestionsVisible && suggestionsPosition === 'above';
|
|
|
|
|
const showApprovalIndicator =
|
|
|
|
|
!uiState.shellModeActive && !hideUiDetailsForSuggestions;
|
2026-02-06 11:33:39 -08:00
|
|
|
const showRawMarkdownIndicator = !uiState.renderMarkdown;
|
2026-03-23 19:30:48 -07:00
|
|
|
|
2026-02-17 07:16:37 -08:00
|
|
|
let modeBleedThrough: { text: string; color: string } | null = null;
|
|
|
|
|
switch (showApprovalModeIndicator) {
|
|
|
|
|
case ApprovalMode.YOLO:
|
|
|
|
|
modeBleedThrough = { text: 'YOLO', color: theme.status.error };
|
|
|
|
|
break;
|
|
|
|
|
case ApprovalMode.PLAN:
|
|
|
|
|
modeBleedThrough = { text: 'plan', color: theme.status.success };
|
|
|
|
|
break;
|
|
|
|
|
case ApprovalMode.AUTO_EDIT:
|
|
|
|
|
modeBleedThrough = { text: 'auto edit', color: theme.status.warning };
|
|
|
|
|
break;
|
|
|
|
|
case ApprovalMode.DEFAULT:
|
|
|
|
|
modeBleedThrough = null;
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
checkExhaustive(showApprovalModeIndicator);
|
|
|
|
|
modeBleedThrough = null;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-12 14:25:24 -05:00
|
|
|
const hideMinimalModeHintWhileBusy =
|
|
|
|
|
!showUiDetails && (showLoadingIndicator || hasPendingActionRequired);
|
2026-02-27 07:34:49 -08:00
|
|
|
|
2026-03-23 19:30:48 -07:00
|
|
|
// Universal Content Objects
|
|
|
|
|
const modeContentObj = hideMinimalModeHintWhileBusy ? null : modeBleedThrough;
|
2026-02-27 07:34:49 -08:00
|
|
|
|
2026-03-23 19:30:48 -07:00
|
|
|
const allHooks = uiState.activeHooks;
|
|
|
|
|
const hasAnyHooks = allHooks.length > 0;
|
|
|
|
|
const userVisibleHooks = allHooks.filter((h) => isUserVisibleHook(h.source));
|
|
|
|
|
const hasUserVisibleHooks = userVisibleHooks.length > 0;
|
2026-02-27 07:34:49 -08:00
|
|
|
|
2026-03-10 14:29:29 -07:00
|
|
|
const shouldReserveSpaceForShortcutsHint =
|
2026-03-23 15:42:30 -04:00
|
|
|
settings.merged.ui.showShortcutsHint &&
|
2026-03-23 19:30:48 -07:00
|
|
|
!hideUiDetailsForSuggestions &&
|
2026-03-23 15:42:30 -04:00
|
|
|
!hasPendingActionRequired;
|
2025-09-06 01:39:02 -04:00
|
|
|
|
2026-03-23 19:30:48 -07:00
|
|
|
const isInteractiveShellWaiting = uiState.currentLoadingPhrase?.includes(
|
|
|
|
|
INTERACTIVE_SHELL_WAITING_PHRASE,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Calculate the estimated length of the status message to avoid collisions
|
|
|
|
|
* with the tips area.
|
|
|
|
|
*/
|
|
|
|
|
let estimatedStatusLength = 0;
|
|
|
|
|
if (hasAnyHooks) {
|
|
|
|
|
if (hasUserVisibleHooks) {
|
|
|
|
|
const hookLabel =
|
|
|
|
|
userVisibleHooks.length > 1 ? 'Executing Hooks' : 'Executing Hook';
|
|
|
|
|
const hookNames = userVisibleHooks
|
|
|
|
|
.map(
|
|
|
|
|
(h) =>
|
|
|
|
|
h.name +
|
|
|
|
|
(h.index && h.total && h.total > 1
|
|
|
|
|
? ` (${h.index}/${h.total})`
|
|
|
|
|
: ''),
|
|
|
|
|
)
|
|
|
|
|
.join(', ');
|
|
|
|
|
estimatedStatusLength = hookLabel.length + hookNames.length + 10;
|
|
|
|
|
} else {
|
|
|
|
|
estimatedStatusLength = GENERIC_WORKING_LABEL.length + 10;
|
|
|
|
|
}
|
|
|
|
|
} else if (showLoadingIndicator) {
|
|
|
|
|
const thoughtText = uiState.thought?.subject || GENERIC_WORKING_LABEL;
|
|
|
|
|
const inlineWittyLength =
|
|
|
|
|
showWit && uiState.currentWittyPhrase
|
|
|
|
|
? uiState.currentWittyPhrase.length + 1
|
|
|
|
|
: 0;
|
|
|
|
|
estimatedStatusLength = thoughtText.length + 25 + inlineWittyLength;
|
|
|
|
|
} else if (hasPendingActionRequired) {
|
|
|
|
|
estimatedStatusLength = 20;
|
|
|
|
|
} else if (hasToast) {
|
|
|
|
|
estimatedStatusLength = 40;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Determine the ambient text (tip) to display.
|
|
|
|
|
*/
|
|
|
|
|
const tipContentStr = (() => {
|
|
|
|
|
// 1. Proactive Tip (Priority)
|
|
|
|
|
if (
|
|
|
|
|
showTips &&
|
|
|
|
|
uiState.currentTip &&
|
|
|
|
|
!(
|
|
|
|
|
isInteractiveShellWaiting &&
|
|
|
|
|
uiState.currentTip === INTERACTIVE_SHELL_WAITING_PHRASE
|
|
|
|
|
)
|
|
|
|
|
) {
|
|
|
|
|
if (
|
|
|
|
|
estimatedStatusLength + uiState.currentTip.length + 10 <=
|
|
|
|
|
terminalWidth
|
|
|
|
|
) {
|
|
|
|
|
return uiState.currentTip;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 2. Shortcut Hint (Fallback)
|
|
|
|
|
if (
|
|
|
|
|
settings.merged.ui.showShortcutsHint &&
|
|
|
|
|
!hideUiDetailsForSuggestions &&
|
|
|
|
|
!hasPendingActionRequired &&
|
|
|
|
|
uiState.buffer.text.length === 0
|
|
|
|
|
) {
|
|
|
|
|
return showUiDetails ? '? for shortcuts' : 'press tab twice for more';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return undefined;
|
|
|
|
|
})();
|
|
|
|
|
|
|
|
|
|
const tipLength = tipContentStr?.length || 0;
|
|
|
|
|
const willCollideTip = estimatedStatusLength + tipLength + 5 > terminalWidth;
|
|
|
|
|
|
|
|
|
|
const showTipLine =
|
|
|
|
|
!hasPendingActionRequired && tipContentStr && !willCollideTip && !isNarrow;
|
|
|
|
|
|
|
|
|
|
// Mini Mode VIP Flags (Pure Content Triggers)
|
|
|
|
|
const miniMode_ShowApprovalMode =
|
|
|
|
|
Boolean(modeContentObj) && !hideUiDetailsForSuggestions;
|
|
|
|
|
const miniMode_ShowToast = hasToast;
|
|
|
|
|
const miniMode_ShowShortcuts = shouldReserveSpaceForShortcutsHint;
|
|
|
|
|
const miniMode_ShowStatus = showLoadingIndicator || hasAnyHooks;
|
|
|
|
|
const miniMode_ShowTip = showTipLine;
|
|
|
|
|
const miniMode_ShowContext = isContextUsageHigh(
|
|
|
|
|
uiState.sessionStats.lastPromptTokenCount,
|
|
|
|
|
uiState.currentModel,
|
|
|
|
|
settings.merged.model?.compressionThreshold,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Composite Mini Mode Triggers
|
|
|
|
|
const showRow1_MiniMode =
|
|
|
|
|
miniMode_ShowToast ||
|
|
|
|
|
miniMode_ShowStatus ||
|
|
|
|
|
miniMode_ShowShortcuts ||
|
|
|
|
|
miniMode_ShowTip;
|
|
|
|
|
|
|
|
|
|
const showRow2_MiniMode = miniMode_ShowApprovalMode || miniMode_ShowContext;
|
|
|
|
|
|
|
|
|
|
// Final Display Rules (Stable Footer Architecture)
|
|
|
|
|
const showRow1 = showUiDetails || showRow1_MiniMode;
|
|
|
|
|
const showRow2 = showUiDetails || showRow2_MiniMode;
|
|
|
|
|
|
|
|
|
|
const showMinimalBleedThroughRow = !showUiDetails && showRow2_MiniMode;
|
|
|
|
|
|
|
|
|
|
const renderTipNode = () => {
|
|
|
|
|
if (!tipContentStr) return null;
|
|
|
|
|
|
|
|
|
|
const isShortcutHint =
|
|
|
|
|
tipContentStr === '? for shortcuts' ||
|
|
|
|
|
tipContentStr === 'press tab twice for more';
|
|
|
|
|
const color =
|
|
|
|
|
isShortcutHint && uiState.shortcutsHelpVisible
|
|
|
|
|
? theme.text.accent
|
|
|
|
|
: theme.text.secondary;
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Box flexDirection="row" justifyContent="flex-end">
|
|
|
|
|
<Text
|
|
|
|
|
color={color}
|
|
|
|
|
wrap="truncate-end"
|
|
|
|
|
italic={
|
|
|
|
|
!isShortcutHint && tipContentStr === uiState.currentWittyPhrase
|
|
|
|
|
}
|
|
|
|
|
>
|
|
|
|
|
{tipContentStr === uiState.currentTip
|
|
|
|
|
? `Tip: ${tipContentStr}`
|
|
|
|
|
: tipContentStr}
|
|
|
|
|
</Text>
|
|
|
|
|
</Box>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const renderStatusNode = () => {
|
|
|
|
|
const allHooks = uiState.activeHooks;
|
|
|
|
|
if (allHooks.length === 0 && !showLoadingIndicator) return null;
|
|
|
|
|
|
|
|
|
|
if (allHooks.length > 0) {
|
|
|
|
|
const userVisibleHooks = allHooks.filter((h) =>
|
|
|
|
|
isUserVisibleHook(h.source),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let hookText = GENERIC_WORKING_LABEL;
|
|
|
|
|
if (userVisibleHooks.length > 0) {
|
|
|
|
|
const label =
|
|
|
|
|
userVisibleHooks.length > 1 ? 'Executing Hooks' : 'Executing Hook';
|
|
|
|
|
const displayNames = userVisibleHooks.map((h) => {
|
|
|
|
|
let name = h.name;
|
|
|
|
|
if (h.index && h.total && h.total > 1) {
|
|
|
|
|
name += ` (${h.index}/${h.total})`;
|
|
|
|
|
}
|
|
|
|
|
return name;
|
|
|
|
|
});
|
|
|
|
|
hookText = `${label}: ${displayNames.join(', ')}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<LoadingIndicator
|
|
|
|
|
inline
|
|
|
|
|
showTips={showTips}
|
|
|
|
|
showWit={showWit}
|
|
|
|
|
errorVerbosity={settings.merged.ui.errorVerbosity}
|
|
|
|
|
currentLoadingPhrase={hookText}
|
|
|
|
|
elapsedTime={uiState.elapsedTime}
|
|
|
|
|
forceRealStatusOnly={false}
|
|
|
|
|
wittyPhrase={uiState.currentWittyPhrase}
|
2026-01-26 19:59:20 +03:00
|
|
|
/>
|
2026-03-23 19:30:48 -07:00
|
|
|
);
|
|
|
|
|
}
|
2025-09-08 16:37:36 -07:00
|
|
|
|
2026-03-23 19:30:48 -07:00
|
|
|
return (
|
|
|
|
|
<LoadingIndicator
|
|
|
|
|
inline
|
|
|
|
|
showTips={showTips}
|
|
|
|
|
showWit={showWit}
|
|
|
|
|
errorVerbosity={settings.merged.ui.errorVerbosity}
|
|
|
|
|
thought={uiState.thought}
|
|
|
|
|
elapsedTime={uiState.elapsedTime}
|
|
|
|
|
forceRealStatusOnly={false}
|
|
|
|
|
wittyPhrase={uiState.currentWittyPhrase}
|
|
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const statusNode = renderStatusNode();
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Renders the minimal metadata row content shown when UI details are hidden.
|
|
|
|
|
*/
|
|
|
|
|
const renderMinimalMetaRowContent = () => (
|
|
|
|
|
<Box flexDirection="row" columnGap={1}>
|
|
|
|
|
{renderStatusNode()}
|
|
|
|
|
{showMinimalBleedThroughRow && (
|
|
|
|
|
<Box>
|
|
|
|
|
{miniMode_ShowApprovalMode && modeContentObj && (
|
|
|
|
|
<Text color={modeContentObj.color}>● {modeContentObj.text}</Text>
|
|
|
|
|
)}
|
|
|
|
|
</Box>
|
2026-02-12 14:25:24 -05:00
|
|
|
)}
|
2026-03-23 19:30:48 -07:00
|
|
|
</Box>
|
|
|
|
|
);
|
2025-09-06 01:39:02 -04:00
|
|
|
|
2026-03-23 19:30:48 -07:00
|
|
|
const renderStatusRow = () => {
|
|
|
|
|
// Mini Mode Height Reservation (The "Anti-Jitter" line)
|
|
|
|
|
if (!showUiDetails && !showRow1_MiniMode && !showRow2_MiniMode) {
|
|
|
|
|
return <Box height={1} />;
|
|
|
|
|
}
|
2025-10-21 15:40:45 -07:00
|
|
|
|
2026-03-23 19:30:48 -07:00
|
|
|
return (
|
|
|
|
|
<Box flexDirection="column" width="100%">
|
|
|
|
|
{/* Row 1: multipurpose status (thinking, hooks, wit, tips) */}
|
|
|
|
|
{showRow1 && (
|
2026-02-06 11:33:39 -08:00
|
|
|
<Box
|
2026-03-23 19:30:48 -07:00
|
|
|
width="100%"
|
2026-02-06 11:33:39 -08:00
|
|
|
flexDirection="row"
|
2026-03-23 19:30:48 -07:00
|
|
|
alignItems="center"
|
2026-02-12 14:25:24 -05:00
|
|
|
justifyContent="space-between"
|
2026-03-23 19:30:48 -07:00
|
|
|
minHeight={1}
|
2026-02-06 11:33:39 -08:00
|
|
|
>
|
2026-03-23 19:30:48 -07:00
|
|
|
<Box flexDirection="row" flexGrow={1} flexShrink={1}>
|
|
|
|
|
{!showUiDetails && showRow1_MiniMode ? (
|
|
|
|
|
renderMinimalMetaRowContent()
|
|
|
|
|
) : isInteractiveShellWaiting ? (
|
|
|
|
|
<Box width="100%" marginLeft={1}>
|
|
|
|
|
<Text color={theme.status.warning}>
|
|
|
|
|
! Shell awaiting input (Tab to focus)
|
|
|
|
|
</Text>
|
|
|
|
|
</Box>
|
|
|
|
|
) : (
|
2026-02-10 08:39:28 -05:00
|
|
|
<Box
|
2026-03-23 19:30:48 -07:00
|
|
|
flexDirection="row"
|
|
|
|
|
alignItems={isNarrow ? 'flex-start' : 'center'}
|
|
|
|
|
flexGrow={1}
|
|
|
|
|
flexShrink={0}
|
|
|
|
|
marginLeft={1}
|
2026-02-10 08:39:28 -05:00
|
|
|
>
|
2026-03-23 19:30:48 -07:00
|
|
|
{statusNode}
|
2026-02-10 08:39:28 -05:00
|
|
|
</Box>
|
2026-02-12 14:25:24 -05:00
|
|
|
)}
|
|
|
|
|
</Box>
|
2026-03-23 19:30:48 -07:00
|
|
|
|
|
|
|
|
<Box flexShrink={0} marginLeft={2} marginRight={isNarrow ? 0 : 1}>
|
|
|
|
|
{!isNarrow && showTipLine && renderTipNode()}
|
|
|
|
|
</Box>
|
2026-02-06 11:33:39 -08:00
|
|
|
</Box>
|
2026-02-12 14:25:24 -05:00
|
|
|
)}
|
2026-03-23 19:30:48 -07:00
|
|
|
|
|
|
|
|
{/* Internal Separator Line */}
|
|
|
|
|
{showRow1 &&
|
|
|
|
|
showRow2 &&
|
|
|
|
|
(showUiDetails || (showRow1_MiniMode && showRow2_MiniMode)) && (
|
|
|
|
|
<Box width="100%">
|
|
|
|
|
<HorizontalLine dim />
|
|
|
|
|
</Box>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Row 2: Mode and Context Summary */}
|
|
|
|
|
{showRow2 && (
|
2026-02-06 11:33:39 -08:00
|
|
|
<Box
|
2026-02-12 14:25:24 -05:00
|
|
|
width="100%"
|
|
|
|
|
flexDirection={isNarrow ? 'column' : 'row'}
|
|
|
|
|
alignItems={isNarrow ? 'flex-start' : 'center'}
|
2026-03-23 19:30:48 -07:00
|
|
|
justifyContent="space-between"
|
2026-02-06 11:33:39 -08:00
|
|
|
>
|
2026-03-23 19:30:48 -07:00
|
|
|
<Box flexDirection="row" alignItems="center" marginLeft={1}>
|
|
|
|
|
{showUiDetails ? (
|
|
|
|
|
<>
|
2026-02-13 15:02:39 -05:00
|
|
|
{showApprovalIndicator && (
|
|
|
|
|
<ApprovalModeIndicator
|
|
|
|
|
approvalMode={showApprovalModeIndicator}
|
2026-02-17 12:36:59 -05:00
|
|
|
allowPlanMode={uiState.allowPlanMode}
|
2026-02-13 15:02:39 -05:00
|
|
|
/>
|
|
|
|
|
)}
|
2026-03-23 19:30:48 -07:00
|
|
|
{uiState.shellModeActive && (
|
|
|
|
|
<Box
|
|
|
|
|
marginLeft={showApprovalIndicator && !isNarrow ? 1 : 0}
|
|
|
|
|
marginTop={showApprovalIndicator && isNarrow ? 1 : 0}
|
|
|
|
|
>
|
|
|
|
|
<ShellModeIndicator />
|
|
|
|
|
</Box>
|
2026-02-13 15:02:39 -05:00
|
|
|
)}
|
2026-03-23 19:30:48 -07:00
|
|
|
{showRawMarkdownIndicator && (
|
|
|
|
|
<Box
|
|
|
|
|
marginLeft={
|
|
|
|
|
(showApprovalIndicator || uiState.shellModeActive) &&
|
|
|
|
|
!isNarrow
|
|
|
|
|
? 1
|
|
|
|
|
: 0
|
|
|
|
|
}
|
|
|
|
|
marginTop={
|
|
|
|
|
(showApprovalIndicator || uiState.shellModeActive) &&
|
|
|
|
|
isNarrow
|
|
|
|
|
? 1
|
|
|
|
|
: 0
|
|
|
|
|
}
|
|
|
|
|
>
|
|
|
|
|
<RawMarkdownIndicator />
|
|
|
|
|
</Box>
|
|
|
|
|
)}
|
|
|
|
|
</>
|
|
|
|
|
) : (
|
|
|
|
|
miniMode_ShowApprovalMode &&
|
|
|
|
|
modeContentObj && (
|
|
|
|
|
<Text color={modeContentObj.color}>
|
|
|
|
|
● {modeContentObj.text}
|
|
|
|
|
</Text>
|
|
|
|
|
)
|
2026-02-12 14:25:24 -05:00
|
|
|
)}
|
|
|
|
|
</Box>
|
|
|
|
|
<Box
|
|
|
|
|
marginTop={isNarrow ? 1 : 0}
|
2026-03-23 19:30:48 -07:00
|
|
|
flexDirection="row"
|
|
|
|
|
alignItems="center"
|
|
|
|
|
marginLeft={isNarrow ? 1 : 0}
|
2026-02-12 14:25:24 -05:00
|
|
|
>
|
2026-03-23 19:30:48 -07:00
|
|
|
{(showUiDetails || miniMode_ShowContext) && (
|
2026-02-12 14:25:24 -05:00
|
|
|
<StatusDisplay hideContextSummary={hideContextSummary} />
|
|
|
|
|
)}
|
2026-03-23 19:30:48 -07:00
|
|
|
{miniMode_ShowContext && !showUiDetails && (
|
|
|
|
|
<Box marginLeft={1}>
|
|
|
|
|
<ContextUsageDisplay
|
|
|
|
|
promptTokenCount={uiState.sessionStats.lastPromptTokenCount}
|
|
|
|
|
model={
|
|
|
|
|
typeof uiState.currentModel === 'string'
|
|
|
|
|
? uiState.currentModel
|
|
|
|
|
: undefined
|
|
|
|
|
}
|
|
|
|
|
terminalWidth={uiState.terminalWidth}
|
|
|
|
|
/>
|
|
|
|
|
</Box>
|
|
|
|
|
)}
|
2026-02-12 14:25:24 -05:00
|
|
|
</Box>
|
2026-02-06 11:33:39 -08:00
|
|
|
</Box>
|
2026-02-12 14:25:24 -05:00
|
|
|
)}
|
2025-09-06 01:39:02 -04:00
|
|
|
</Box>
|
2026-03-23 19:30:48 -07:00
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Box
|
|
|
|
|
flexDirection="column"
|
|
|
|
|
width={uiState.terminalWidth}
|
|
|
|
|
flexGrow={0}
|
|
|
|
|
flexShrink={0}
|
|
|
|
|
>
|
|
|
|
|
{(!uiState.slashCommands ||
|
|
|
|
|
!uiState.isConfigInitialized ||
|
|
|
|
|
uiState.isResuming) && (
|
|
|
|
|
<ConfigInitDisplay
|
|
|
|
|
message={uiState.isResuming ? 'Resuming session...' : undefined}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{showUiDetails && (
|
|
|
|
|
<QueuedMessageDisplay messageQueue={uiState.messageQueue} />
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{showUiDetails && <TodoTray />}
|
|
|
|
|
|
|
|
|
|
{showShortcutsHelp && <ShortcutsHelp />}
|
|
|
|
|
|
|
|
|
|
{(showUiDetails || miniMode_ShowToast) && (
|
|
|
|
|
<Box minHeight={1} marginLeft={isNarrow ? 0 : 1}>
|
|
|
|
|
<ToastDisplay />
|
|
|
|
|
</Box>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
<Box width="100%" flexDirection="column">
|
|
|
|
|
{renderStatusRow()}
|
|
|
|
|
</Box>
|
2025-09-06 01:39:02 -04:00
|
|
|
|
2026-02-12 14:25:24 -05:00
|
|
|
{showUiDetails && uiState.showErrorDetails && (
|
2025-09-06 01:39:02 -04:00
|
|
|
<OverflowProvider>
|
|
|
|
|
<Box flexDirection="column">
|
|
|
|
|
<DetailedMessagesDisplay
|
|
|
|
|
maxHeight={
|
|
|
|
|
uiState.constrainHeight ? debugConsoleMaxHeight : undefined
|
|
|
|
|
}
|
2026-01-26 15:23:54 -08:00
|
|
|
width={uiState.terminalWidth}
|
2025-11-11 07:50:11 -08:00
|
|
|
hasFocus={uiState.showErrorDetails}
|
2025-09-06 01:39:02 -04:00
|
|
|
/>
|
|
|
|
|
<ShowMoreLines constrainHeight={uiState.constrainHeight} />
|
|
|
|
|
</Box>
|
|
|
|
|
</OverflowProvider>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{uiState.isInputActive && (
|
|
|
|
|
<InputPrompt
|
|
|
|
|
buffer={uiState.buffer}
|
|
|
|
|
inputWidth={uiState.inputWidth}
|
|
|
|
|
suggestionsWidth={uiState.suggestionsWidth}
|
|
|
|
|
onSubmit={uiActions.handleFinalSubmit}
|
|
|
|
|
userMessages={uiState.userMessages}
|
2025-11-18 12:01:16 -05:00
|
|
|
setBannerVisible={uiActions.setBannerVisible}
|
2025-09-06 01:39:02 -04:00
|
|
|
onClearScreen={uiActions.handleClearScreen}
|
|
|
|
|
config={config}
|
2025-10-14 18:15:57 -07:00
|
|
|
slashCommands={uiState.slashCommands || []}
|
2025-09-06 01:39:02 -04:00
|
|
|
commandContext={uiState.commandContext}
|
|
|
|
|
shellModeActive={uiState.shellModeActive}
|
|
|
|
|
setShellModeActive={uiActions.setShellModeActive}
|
2026-01-21 10:19:47 -05:00
|
|
|
approvalMode={showApprovalModeIndicator}
|
2025-09-06 01:39:02 -04:00
|
|
|
onEscapePromptChange={uiActions.onEscapePromptChange}
|
2026-01-23 20:32:35 -05:00
|
|
|
focus={isFocused}
|
2025-09-06 01:39:02 -04:00
|
|
|
vimHandleInput={uiActions.vimHandleInput}
|
2025-09-20 10:59:37 -07:00
|
|
|
isEmbeddedShellFocused={uiState.embeddedShellFocused}
|
2025-10-16 17:04:13 -07:00
|
|
|
popAllMessages={uiActions.popAllMessages}
|
2025-09-06 01:39:02 -04:00
|
|
|
placeholder={
|
|
|
|
|
vimEnabled
|
2026-01-29 23:31:47 -08:00
|
|
|
? vimMode === 'INSERT'
|
|
|
|
|
? " Press 'Esc' for NORMAL mode."
|
|
|
|
|
: " Press 'i' for INSERT mode."
|
2025-12-19 23:37:45 +05:30
|
|
|
: uiState.shellModeActive
|
|
|
|
|
? ' Type your shell command'
|
|
|
|
|
: ' Type your message or @path/to/file'
|
2025-09-06 01:39:02 -04:00
|
|
|
}
|
2025-10-15 22:32:50 +05:30
|
|
|
setQueueErrorMessage={uiActions.setQueueErrorMessage}
|
|
|
|
|
streamingState={uiState.streamingState}
|
2025-11-11 07:50:11 -08:00
|
|
|
suggestionsPosition={suggestionsPosition}
|
|
|
|
|
onSuggestionsVisibilityChange={setSuggestionsVisible}
|
2025-09-06 01:39:02 -04:00
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
|
2026-02-12 14:25:24 -05:00
|
|
|
{showUiDetails &&
|
|
|
|
|
!settings.merged.ui.hideFooter &&
|
|
|
|
|
!isScreenReaderEnabled && <Footer />}
|
2025-09-06 01:39:02 -04:00
|
|
|
</Box>
|
|
|
|
|
);
|
|
|
|
|
};
|