diff --git a/packages/cli/src/ui/App.tsx b/packages/cli/src/ui/App.tsx index 21d6a730..b6275491 100644 --- a/packages/cli/src/ui/App.tsx +++ b/packages/cli/src/ui/App.tsx @@ -4,8 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { useCallback, useEffect, useMemo, useState } from 'react'; -import { Box, Static, Text, useStdout } from 'ink'; +import { useCallback, useEffect, useMemo, useState, useRef } from 'react'; +import { Box, DOMElement, measureElement, Static, Text, useStdout } from 'ink'; import { StreamingState, type HistoryItem } from './types.js'; import { useGeminiStream } from './hooks/useGeminiStream.js'; import { useLoadingIndicator } from './hooks/useLoadingIndicator.js'; @@ -46,6 +46,7 @@ export const App = ({ startupWarnings = [], }: AppProps) => { const { history, addItem, clearItems } = useHistory(); + const [staticNeedsRefresh, setStaticNeedsRefresh] = useState(false); const [staticKey, setStaticKey] = useState(0); const refreshStatic = useCallback(() => { setStaticKey((prev) => prev + 1); @@ -55,7 +56,8 @@ export const App = ({ const [debugMessage, setDebugMessage] = useState(''); const [showHelp, setShowHelp] = useState(false); const [themeError, setThemeError] = useState(null); - + const [availableTerminalHeight, setAvailableTerminalHeight] = + useState(0); const { isThemeDialogOpen, openThemeDialog, @@ -193,12 +195,51 @@ export const App = ({ // --- Render Logic --- - // Get terminal width + // Get terminal dimensions + const { stdout } = useStdout(); const terminalWidth = stdout?.columns ?? 80; + const terminalHeight = stdout?.rows ?? 24; + const footerRef = useRef(null); + const pendingHistoryItemRef = useRef(null); + // Calculate width for suggestions, leave some padding const suggestionsWidth = Math.max(60, Math.floor(terminalWidth * 0.8)); + useEffect(() => { + const staticExtraHeight = /* margins and padding */ 3; + const fullFooterMeasurement = measureElement(footerRef.current!); + const fullFooterHeight = fullFooterMeasurement.height; + + setAvailableTerminalHeight( + terminalHeight - fullFooterHeight - staticExtraHeight, + ); + }, [terminalHeight]); + + useEffect(() => { + if (!pendingHistoryItem) { + return; + } + + const pendingItemDimensions = measureElement( + pendingHistoryItemRef.current!, + ); + + // If our pending history item happens to exceed the terminal height we will most likely need to refresh + // our static collection to ensure no duplication or tearing. This is currently working around a core bug + // in Ink which we have a PR out to fix: https://github.com/vadimdemedes/ink/pull/717 + if (pendingItemDimensions.height > availableTerminalHeight) { + setStaticNeedsRefresh(true); + } + }, [pendingHistoryItem, availableTerminalHeight, streamingState]); + + useEffect(() => { + if (streamingState === StreamingState.Idle && staticNeedsRefresh) { + setStaticNeedsRefresh(false); + refreshStatic(); + } + }, [streamingState, refreshStatic, staticNeedsRefresh]); + return ( {/* @@ -219,146 +260,151 @@ export const App = ({
, - ...history.map((h) => ), + ...history.map((h) => ), ]} > {(item) => item} {pendingHistoryItem && ( - + + + )} {showHelp && } - {startupWarnings.length > 0 && ( - - {startupWarnings.map((warning, index) => ( - - {warning} - - ))} - - )} + + {startupWarnings.length > 0 && ( + + {startupWarnings.map((warning, index) => ( + + {warning} + + ))} + + )} - {isThemeDialogOpen ? ( - - {themeError && ( - - {themeError} - - )} - - - ) : ( - <> - - {isInputActive && ( - <> - - - cwd: - - {shortenPath(config.getTargetDir(), 70)} - - + {isThemeDialogOpen ? ( + + {themeError && ( + + {themeError} - - - {completion.showSuggestions && ( - - + )} + + + ) : ( + <> + + {isInputActive && ( + <> + + + cwd: + + {shortenPath(config.getTargetDir(), 70)} + + - )} - - )} - - )} - {initError && streamingState !== StreamingState.Responding && ( - - {history.find( - (item) => item.type === 'error' && item.text?.includes(initError), - )?.text ? ( - - { - history.find( - (item) => - item.type === 'error' && item.text?.includes(initError), - )?.text - } - - ) : ( - <> - - Initialization Error: {initError} - - - {' '} - Please check API key and configuration. - - - )} - - )} + + {completion.showSuggestions && ( + + + + )} + + )} + + )} -