/** * @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'; import { useAppContext } from '../contexts/AppContext.js'; import { AppHeader } from './AppHeader.js'; 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'; 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(); const confirmingTool = useConfirmingTool(); const showConfirmationQueue = confirmingTool !== null; const confirmingToolCallId = confirmingTool?.tool.callId; const scrollableListRef = useRef>(null); useEffect(() => { if (showConfirmationQueue) { scrollableListRef.current?.scrollToEnd(); } }, [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]); const historyItems = useMemo( () => uiState.history.map((h, index) => { const isExpandable = index > lastUserPromptIndex; return ( ); }), [ uiState.history, mainAreaWidth, staticAreaMaxItemHeight, availableTerminalHeight, uiState.slashCommands, uiState.constrainHeight, lastUserPromptIndex, ], ); const staticHistoryItems = useMemo( () => historyItems.slice(0, lastUserPromptIndex + 1), [historyItems, lastUserPromptIndex], ); const lastResponseHistoryItems = useMemo( () => historyItems.slice(lastUserPromptIndex + 1), [historyItems, lastUserPromptIndex], ); const pendingItems = useMemo( () => ( {pendingHistoryItems.map((item, i) => ( ))} {showConfirmationQueue && confirmingTool && ( )} ), [ pendingHistoryItems, uiState.constrainHeight, availableTerminalHeight, mainAreaWidth, showConfirmationQueue, confirmingTool, ], ); const virtualizedData = useMemo( () => [ { type: 'header' as const }, ...uiState.history.map((item, index) => ({ type: 'history' as const, item, isExpandable: index > lastUserPromptIndex, })), { type: 'pending' as const }, ], [uiState.history, lastUserPromptIndex], ); const renderItem = useCallback( ({ item }: { item: (typeof virtualizedData)[number] }) => { if (item.type === 'header') { return ( ); } else if (item.type === 'history') { return ( ); } else { return pendingItems; } }, [ showHeaderDetails, version, mainAreaWidth, uiState.slashCommands, pendingItems, uiState.constrainHeight, staticAreaMaxItemHeight, availableTerminalHeight, ], ); if (isAlternateBuffer) { return ( 100} keyExtractor={(item, _index) => { if (item.type === 'header') return 'header'; if (item.type === 'history') return item.item.id.toString(); return 'pending'; }} initialScrollIndex={SCROLL_TO_ITEM_END} initialScrollOffsetInIndex={SCROLL_TO_ITEM_END} /> ); } return ( <> , ...staticHistoryItems, ...lastResponseHistoryItems, ]} > {(item) => item} {pendingItems} ); };