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

323 lines
9.6 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';
import { useAppContext } from '../contexts/AppContext.js';
import { AppHeader } from './AppHeader.js';
2026-03-28 21:33:38 +00:00
import { useAlternateBuffer } from '../hooks/useAlternateBuffer.js';
import {
SCROLL_TO_ITEM_END,
type VirtualizedListRef,
} from './shared/VirtualizedList.js';
import { ScrollableList } from './shared/ScrollableList.js';
import { useMemo, memo, useCallback, useEffect, useRef } from 'react';
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';
const MemoizedHistoryItemDisplay = memo(HistoryItemDisplay);
const MemoizedAppHeader = memo(AppHeader);
// 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 { version } = useAppContext();
const uiState = useUIState();
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,
cleanUiDetailsVisible,
} = uiState;
const showHeaderDetails = cleanUiDetailsVisible;
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]);
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;
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,
isFirstThinking,
isFirstAfterThinking,
2026-03-28 21:33:38 +00:00
suppressNarration: suppressNarrationFlags[i] ?? false,
};
}),
2026-03-28 21:33:38 +00:00
[uiState.history, lastUserPromptIndex, suppressNarrationFlags],
);
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}
key={item.id}
item={item}
isPending={false}
commands={uiState.slashCommands}
isExpandable={isExpandable}
isFirstThinking={isFirstThinking}
isFirstAfterThinking={isFirstAfterThinking}
2026-03-28 21:33:38 +00:00
suppressNarration={suppressNarration}
/>
),
),
[
augmentedHistory,
mainAreaWidth,
staticAreaMaxItemHeight,