Files
gemini-cli/packages/cli/src/ui/components/MainContent.tsx
T

271 lines
8.3 KiB
TypeScript
Raw Normal View History

/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { Box, Static } from 'ink';
import { HistoryItemDisplay } from './HistoryItemDisplay.js';
import { useUIState } from '../contexts/UIStateContext.js';
2026-03-28 21:33:38 +00:00
import { useSettings } from '../contexts/SettingsContext.js';
2025-11-11 07:50:11 -08:00
import { useAlternateBuffer } from '../hooks/useAlternateBuffer.js';
import {
SCROLL_TO_ITEM_END,
type VirtualizedListRef,
} from './shared/VirtualizedList.js';
2025-11-11 07:50:11 -08:00
import { ScrollableList } from './shared/ScrollableList.js';
import { useMemo, memo, useEffect, useRef } from 'react';
2025-11-11 07:50:11 -08:00
import { MAX_GEMINI_MESSAGE_LINES } from '../constants.js';
import { useConfirmingTool } from '../hooks/useConfirmingTool.js';
import { ToolConfirmationQueue } from './ToolConfirmationQueue.js';
2026-03-28 21:33:38 +00:00
import { isTopicTool } from './messages/TopicMessage.js';
2025-11-11 07:50:11 -08:00
const MemoizedHistoryItemDisplay = memo(HistoryItemDisplay);
// Limit Gemini messages to a very high number of lines to mitigate performance
// issues in the worst case if we somehow get an enormous response from Gemini.
// This threshold is arbitrary but should be high enough to never impact normal
// usage.
export const MainContent = () => {
const uiState = useUIState();
2025-11-11 07:50:11 -08:00
const isAlternateBuffer = useAlternateBuffer();
2025-11-03 13:41:58 -08:00
const confirmingTool = useConfirmingTool();
const showConfirmationQueue = confirmingTool !== null;
2026-03-02 21:04:31 +00:00
const confirmingToolCallId = confirmingTool?.tool.callId;
const scrollableListRef = useRef<VirtualizedListRef<unknown>>(null);
useEffect(() => {
if (showConfirmationQueue) {
scrollableListRef.current?.scrollToEnd();
}
2026-03-02 21:04:31 +00:00
}, [showConfirmationQueue, confirmingToolCallId]);
const {
pendingHistoryItems,
mainAreaWidth,
staticAreaMaxItemHeight,
availableTerminalHeight,
} = uiState;
const lastUserPromptIndex = useMemo(() => {
for (let i = uiState.history.length - 1; i >= 0; i--) {
const type = uiState.history[i].type;
if (type === 'user' || type === 'user_shell') {
return i;
}
}
return -1;
}, [uiState.history]);
2026-03-28 21:33:38 +00:00
const settings = useSettings();
const topicUpdateNarrationEnabled =
settings.merged.experimental?.topicUpdateNarration === true;
const suppressNarrationFlags = useMemo(() => {
const combinedHistory = [...uiState.history, ...pendingHistoryItems];
const flags = new Array<boolean>(combinedHistory.length).fill(false);
if (topicUpdateNarrationEnabled) {
let toolGroupInTurn = false;
for (let i = combinedHistory.length - 1; i >= 0; i--) {
const item = combinedHistory[i];
if (item.type === 'user' || item.type === 'user_shell') {
toolGroupInTurn = false;
} else if (item.type === 'tool_group') {
toolGroupInTurn = item.tools.some((t) => isTopicTool(t.name));
} else if (
(item.type === 'thinking' ||
item.type === 'gemini' ||
item.type === 'gemini_content') &&
toolGroupInTurn
) {
flags[i] = true;
}
}
}
return flags;
}, [uiState.history, pendingHistoryItems, topicUpdateNarrationEnabled]);
2026-03-06 20:20:27 -08:00
const augmentedHistory = useMemo(
() =>
2026-03-28 21:33:38 +00:00
uiState.history.map((item, i) => {
const prevType = i > 0 ? uiState.history[i - 1]?.type : undefined;
2026-03-06 20:20:27 -08:00
const isFirstThinking =
item.type === 'thinking' && prevType !== 'thinking';
const isFirstAfterThinking =
item.type !== 'thinking' && prevType === 'thinking';
return {
item,
2026-03-28 21:33:38 +00:00
isExpandable: i > lastUserPromptIndex,
2026-03-06 20:20:27 -08:00
isFirstThinking,
isFirstAfterThinking,
2026-03-28 21:33:38 +00:00
suppressNarration: suppressNarrationFlags[i] ?? false,
2026-03-06 20:20:27 -08:00
};
}),
2026-03-28 21:33:38 +00:00
[uiState.history, lastUserPromptIndex, suppressNarrationFlags],
2026-03-06 20:20:27 -08:00
);
const historyItems = useMemo(
() =>
augmentedHistory.map(
2026-03-28 21:33:38 +00:00
({
item,
isExpandable,
isFirstThinking,
isFirstAfterThinking,
suppressNarration,
}) => (
<MemoizedHistoryItemDisplay
terminalWidth={mainAreaWidth}
availableTerminalHeight={
uiState.constrainHeight || !isExpandable
? staticAreaMaxItemHeight
: undefined
}
availableTerminalHeightGemini={MAX_GEMINI_MESSAGE_LINES}
2026-03-06 20:20:27 -08:00
key={item.id}
item={item}
isPending={false}
commands={uiState.slashCommands}
isExpandable={isExpandable}
2026-03-06 20:20:27 -08:00
isFirstThinking={isFirstThinking}
isFirstAfterThinking={isFirstAfterThinking}
2026-03-28 21:33:38 +00:00
suppressNarration={suppressNarration}
/>
2026-03-06 20:20:27 -08:00
),
),
[
2026-03-06 20:20:27 -08:00
augmentedHistory,
mainAreaWidth,
staticAreaMaxItemHeight,
uiState.slashCommands,
uiState.constrainHeight,
],
);
2025-11-11 07:50:11 -08:00
const pendingItems = useMemo(
() => (
<Box flexDirection="column" key="pending-items-group">
2026-03-06 20:20:27 -08:00
{pendingHistoryItems.map((item, i) => {
const prevType =
i === 0
? uiState.history.at(-1)?.type
: pendingHistoryItems[i - 1]?.type;
const isFirstThinking =
item.type === 'thinking' && prevType !== 'thinking';
const isFirstAfterThinking =
item.type !== 'thinking' && prevType === 'thinking';
2026-03-28 21:33:38 +00:00
const suppressNarration =
suppressNarrationFlags[uiState.history.length + i] ?? false;
2026-03-06 20:20:27 -08:00
return (
<HistoryItemDisplay
key={`pending-${i}`}
2026-03-06 20:20:27 -08:00
availableTerminalHeight={
uiState.constrainHeight ? availableTerminalHeight : undefined
2026-03-06 20:20:27 -08:00
}
terminalWidth={mainAreaWidth}
item={{ ...item, id: -(i + 1) }}
2026-03-06 20:20:27 -08:00
isPending={true}
isExpandable={true}
isFirstThinking={isFirstThinking}
isFirstAfterThinking={isFirstAfterThinking}
2026-03-28 21:33:38 +00:00
suppressNarration={suppressNarration}
2026-03-06 20:20:27 -08:00
/>
);
})}
{showConfirmationQueue && confirmingTool && (
<ToolConfirmationQueue
key="confirmation-queue"
confirmingTool={confirmingTool}
/>
)}
</Box>
2025-11-11 07:50:11 -08:00
),
[
pendingHistoryItems,
uiState.constrainHeight,
availableTerminalHeight,
2025-11-11 07:50:11 -08:00
mainAreaWidth,
showConfirmationQueue,
confirmingTool,
2026-03-06 20:20:27 -08:00
uiState.history,
2026-03-28 21:33:38 +00:00
suppressNarrationFlags,
2025-11-11 07:50:11 -08:00
],
);
2025-11-03 13:41:58 -08:00
if (isAlternateBuffer) {
const virtualizedData = [
...augmentedHistory.map((ah) => ({ type: 'history' as const, ...ah })),
2025-11-11 07:50:11 -08:00
{ type: 'pending' as const },
];
2025-11-11 07:50:11 -08:00
const renderItem = ({
item,
}: {
item: (typeof virtualizedData)[number];
}) => {
if (item.type === 'history') {
2025-11-11 07:50:11 -08:00
return (
<MemoizedHistoryItemDisplay
2025-11-03 13:41:58 -08:00
terminalWidth={mainAreaWidth}
availableTerminalHeight={
uiState.constrainHeight || !item.isExpandable
? staticAreaMaxItemHeight
: undefined
}
2025-11-11 07:50:11 -08:00
availableTerminalHeightGemini={MAX_GEMINI_MESSAGE_LINES}
key={item.item.id}
item={item.item}
isPending={false}
commands={uiState.slashCommands}
isExpandable={item.isExpandable}
2026-03-06 20:20:27 -08:00
isFirstThinking={item.isFirstThinking}
isFirstAfterThinking={item.isFirstAfterThinking}
2026-03-28 21:33:38 +00:00
suppressNarration={item.suppressNarration}
2025-11-03 13:41:58 -08:00
/>
2025-11-11 07:50:11 -08:00
);
} else {
return pendingItems;
}
};
2025-11-03 13:41:58 -08:00
return (
2025-11-11 07:50:11 -08:00
<ScrollableList
ref={scrollableListRef}
hasFocus={!uiState.isEditorDialogOpen && !uiState.embeddedShellFocused}
width={uiState.terminalWidth}
2025-11-11 07:50:11 -08:00
data={virtualizedData}
renderItem={renderItem}
estimatedItemHeight={() => 100}
keyExtractor={(item, _index) => {
if (item.type === 'history') return item.item.id.toString();
return 'pending';
}}
initialScrollIndex={SCROLL_TO_ITEM_END}
initialScrollOffsetInIndex={SCROLL_TO_ITEM_END}
/>
2025-11-03 13:41:58 -08:00
);
}
const staticHistoryItems = historyItems.slice(0, lastUserPromptIndex + 1);
const lastResponseHistoryItems = historyItems.slice(lastUserPromptIndex + 1);
return (
<>
2025-11-11 07:50:11 -08:00
<Static
key={uiState.historyRemountKey}
items={[...staticHistoryItems, ...lastResponseHistoryItems]}
2025-11-11 07:50:11 -08:00
>
{(item) => item}
</Static>
2025-11-03 13:41:58 -08:00
{pendingItems}
</>
);
};