2025-09-06 01:39:02 -04:00
|
|
|
/**
|
|
|
|
|
* @license
|
|
|
|
|
* Copyright 2025 Google LLC
|
|
|
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import { Box, Static } from 'ink';
|
2025-09-23 15:44:33 -04:00
|
|
|
import { HistoryItemDisplay } from './HistoryItemDisplay.js';
|
2025-09-06 01:39:02 -04:00
|
|
|
import { useUIState } from '../contexts/UIStateContext.js';
|
2026-03-28 21:33:38 +00:00
|
|
|
import { useSettings } from '../contexts/SettingsContext.js';
|
2025-09-06 01:39:02 -04:00
|
|
|
import { useAppContext } from '../contexts/AppContext.js';
|
|
|
|
|
import { AppHeader } from './AppHeader.js';
|
2026-03-28 21:33:38 +00:00
|
|
|
|
2025-11-11 07:50:11 -08:00
|
|
|
import { useAlternateBuffer } from '../hooks/useAlternateBuffer.js';
|
2026-01-27 16:06:24 -08:00
|
|
|
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';
|
2026-01-27 16:06:24 -08:00
|
|
|
import { useMemo, memo, useCallback, useEffect, useRef } from 'react';
|
2025-11-11 07:50:11 -08:00
|
|
|
import { MAX_GEMINI_MESSAGE_LINES } from '../constants.js';
|
2026-01-27 16:06:24 -08:00
|
|
|
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);
|
|
|
|
|
const MemoizedAppHeader = memo(AppHeader);
|
2025-09-06 01:39:02 -04:00
|
|
|
|
2025-09-27 12:40:09 -07:00
|
|
|
// 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.
|
2025-09-06 01:39:02 -04:00
|
|
|
export const MainContent = () => {
|
|
|
|
|
const { version } = useAppContext();
|
|
|
|
|
const uiState = useUIState();
|
2025-11-11 07:50:11 -08:00
|
|
|
const isAlternateBuffer = useAlternateBuffer();
|
2025-11-03 13:41:58 -08:00
|
|
|
|
2026-01-27 16:06:24 -08:00
|
|
|
const confirmingTool = useConfirmingTool();
|
2026-02-11 19:46:58 -05:00
|
|
|
const showConfirmationQueue = confirmingTool !== null;
|
2026-03-02 21:04:31 +00:00
|
|
|
const confirmingToolCallId = confirmingTool?.tool.callId;
|
2026-01-27 16:06:24 -08:00
|
|
|
|
|
|
|
|
const scrollableListRef = useRef<VirtualizedListRef<unknown>>(null);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (showConfirmationQueue) {
|
|
|
|
|
scrollableListRef.current?.scrollToEnd();
|
|
|
|
|
}
|
2026-03-02 21:04:31 +00:00
|
|
|
}, [showConfirmationQueue, confirmingToolCallId]);
|
2026-01-27 16:06:24 -08:00
|
|
|
|
2025-09-06 01:39:02 -04:00
|
|
|
const {
|
|
|
|
|
pendingHistoryItems,
|
|
|
|
|
mainAreaWidth,
|
|
|
|
|
staticAreaMaxItemHeight,
|
2026-03-18 17:28:21 -04:00
|
|
|
availableTerminalHeight,
|
2026-02-12 14:25:24 -05:00
|
|
|
cleanUiDetailsVisible,
|
2025-09-06 01:39:02 -04:00
|
|
|
} = uiState;
|
2026-02-12 14:25:24 -05:00
|
|
|
const showHeaderDetails = cleanUiDetailsVisible;
|
2025-09-06 01:39:02 -04:00
|
|
|
|
2026-02-20 16:26:11 -08:00
|
|
|
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-01-12 11:53:04 -05:00
|
|
|
() =>
|
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,
|
|
|
|
|
}) => (
|
2026-02-20 16:26:11 -08:00
|
|
|
<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}
|
2026-02-20 16:26:11 -08:00
|
|
|
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-02-20 16:26:11 -08:00
|
|
|
/>
|
2026-03-06 20:20:27 -08:00
|
|
|
),
|
|
|
|
|
),
|
2026-01-12 11:53:04 -05:00
|
|
|
[
|
2026-03-06 20:20:27 -08:00
|
|
|
augmentedHistory,
|
2026-01-12 11:53:04 -05:00
|
|
|
mainAreaWidth,
|
|
|
|
|
staticAreaMaxItemHeight,
|
|
|