diff options
Diffstat (limited to 'packages/cli/src/ui/App.tsx')
| -rw-r--r-- | packages/cli/src/ui/App.tsx | 304 |
1 files changed, 175 insertions, 129 deletions
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<string>(''); const [showHelp, setShowHelp] = useState<boolean>(false); const [themeError, setThemeError] = useState<string | null>(null); - + const [availableTerminalHeight, setAvailableTerminalHeight] = + useState<number>(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<DOMElement>(null); + const pendingHistoryItemRef = useRef<DOMElement>(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 ( <Box flexDirection="column" marginBottom={1} width="90%"> {/* @@ -219,146 +260,151 @@ export const App = ({ <Header /> <Tips /> </Box>, - ...history.map((h) => <HistoryItemDisplay key={h.id} item={h} />), + ...history.map((h) => <HistoryItemDisplay availableTerminalHeight={availableTerminalHeight} key={h.id} item={h} />), ]} > {(item) => item} </Static> {pendingHistoryItem && ( - <HistoryItemDisplay - // TODO(taehykim): It seems like references to ids aren't necessary in - // HistoryItemDisplay. Refactor later. Use a fake id for now. - item={{ ...pendingHistoryItem, id: 0 }} - /> + <Box ref={pendingHistoryItemRef}> + <HistoryItemDisplay + availableTerminalHeight={availableTerminalHeight} + // TODO(taehykim): It seems like references to ids aren't necessary in + // HistoryItemDisplay. Refactor later. Use a fake id for now. + item={{ ...pendingHistoryItem, id: 0 }} + /> + </Box> )} {showHelp && <Help commands={slashCommands} />} - {startupWarnings.length > 0 && ( - <Box - borderStyle="round" - borderColor={Colors.AccentYellow} - paddingX={1} - marginY={1} - flexDirection="column" - > - {startupWarnings.map((warning, index) => ( - <Text key={index} color={Colors.AccentYellow}> - {warning} - </Text> - ))} - </Box> - )} + <Box flexDirection="column" ref={footerRef}> + {startupWarnings.length > 0 && ( + <Box + borderStyle="round" + borderColor={Colors.AccentYellow} + paddingX={1} + marginY={1} + flexDirection="column" + > + {startupWarnings.map((warning, index) => ( + <Text key={index} color={Colors.AccentYellow}> + {warning} + </Text> + ))} + </Box> + )} - {isThemeDialogOpen ? ( - <Box flexDirection="column"> - {themeError && ( - <Box marginBottom={1}> - <Text color={Colors.AccentRed}>{themeError}</Text> - </Box> - )} - <ThemeDialog - onSelect={handleThemeSelect} - onHighlight={handleThemeHighlight} - settings={settings} - setQuery={setQuery} - /> - </Box> - ) : ( - <> - <LoadingIndicator - isLoading={streamingState === StreamingState.Responding} - currentLoadingPhrase={currentLoadingPhrase} - elapsedTime={elapsedTime} - /> - {isInputActive && ( - <> - <Box - marginTop={1} - display="flex" - justifyContent="space-between" - width="100%" - > - <Box> - <Text color={Colors.SubtleComment}>cwd: </Text> - <Text color={Colors.LightBlue}> - {shortenPath(config.getTargetDir(), 70)} - </Text> - </Box> + {isThemeDialogOpen ? ( + <Box flexDirection="column"> + {themeError && ( + <Box marginBottom={1}> + <Text color={Colors.AccentRed}>{themeError}</Text> </Box> - - <InputPrompt - query={query} - onChange={setQuery} - onChangeAndMoveCursor={onChangeAndMoveCursor} - editorState={editorState} - onSubmit={handleFinalSubmit} // Pass handleFinalSubmit directly - showSuggestions={completion.showSuggestions} - suggestions={completion.suggestions} - activeSuggestionIndex={completion.activeSuggestionIndex} - userMessages={userMessages} // Pass userMessages - navigateSuggestionUp={completion.navigateUp} - navigateSuggestionDown={completion.navigateDown} - resetCompletion={completion.resetCompletionState} - setEditorState={setEditorState} - onClearScreen={handleClearScreen} // Added onClearScreen prop - /> - {completion.showSuggestions && ( - <Box> - <SuggestionsDisplay - suggestions={completion.suggestions} - activeIndex={completion.activeSuggestionIndex} - isLoading={completion.isLoadingSuggestions} - width={suggestionsWidth} - scrollOffset={completion.visibleStartIndex} - userInput={query} - /> + )} + <ThemeDialog + onSelect={handleThemeSelect} + onHighlight={handleThemeHighlight} + settings={settings} + setQuery={setQuery} + /> + </Box> + ) : ( + <> + <LoadingIndicator + isLoading={streamingState === StreamingState.Responding} + currentLoadingPhrase={currentLoadingPhrase} + elapsedTime={elapsedTime} + /> + {isInputActive && ( + <> + <Box + marginTop={1} + display="flex" + justifyContent="space-between" + width="100%" + > + <Box> + <Text color={Colors.SubtleComment}>cwd: </Text> + <Text color={Colors.LightBlue}> + {shortenPath(config.getTargetDir(), 70)} + </Text> + </Box> </Box> - )} - </> - )} - </> - )} - {initError && streamingState !== StreamingState.Responding && ( - <Box - borderStyle="round" - borderColor={Colors.AccentRed} - paddingX={1} - marginBottom={1} - > - {history.find( - (item) => item.type === 'error' && item.text?.includes(initError), - )?.text ? ( - <Text color={Colors.AccentRed}> - { - history.find( - (item) => - item.type === 'error' && item.text?.includes(initError), - )?.text - } - </Text> - ) : ( - <> - <Text color={Colors.AccentRed}> - Initialization Error: {initError} - </Text> + <InputPrompt + query={query} + onChange={setQuery} + onChangeAndMoveCursor={onChangeAndMoveCursor} + editorState={editorState} + onSubmit={handleFinalSubmit} // Pass handleFinalSubmit directly + showSuggestions={completion.showSuggestions} + suggestions={completion.suggestions} + activeSuggestionIndex={completion.activeSuggestionIndex} + userMessages={userMessages} // Pass userMessages + navigateSuggestionUp={completion.navigateUp} + navigateSuggestionDown={completion.navigateDown} + resetCompletion={completion.resetCompletionState} + setEditorState={setEditorState} + onClearScreen={handleClearScreen} // Added onClearScreen prop + /> + {completion.showSuggestions && ( + <Box> + <SuggestionsDisplay + suggestions={completion.suggestions} + activeIndex={completion.activeSuggestionIndex} + isLoading={completion.isLoadingSuggestions} + width={suggestionsWidth} + scrollOffset={completion.visibleStartIndex} + userInput={query} + /> + </Box> + )} + </> + )} + </> + )} + + {initError && streamingState !== StreamingState.Responding && ( + <Box + borderStyle="round" + borderColor={Colors.AccentRed} + paddingX={1} + marginBottom={1} + > + {history.find( + (item) => item.type === 'error' && item.text?.includes(initError), + )?.text ? ( <Text color={Colors.AccentRed}> - {' '} - Please check API key and configuration. + { + history.find( + (item) => + item.type === 'error' && item.text?.includes(initError), + )?.text + } </Text> - </> - )} - </Box> - )} + ) : ( + <> + <Text color={Colors.AccentRed}> + Initialization Error: {initError} + </Text> + <Text color={Colors.AccentRed}> + {' '} + Please check API key and configuration. + </Text> + </> + )} + </Box> + )} - <Footer - config={config} - debugMode={config.getDebugMode()} - debugMessage={debugMessage} - cliVersion={cliVersion} - geminiMdFileCount={geminiMdFileCount} - /> - <ConsoleOutput /> + <Footer + config={config} + debugMode={config.getDebugMode()} + debugMessage={debugMessage} + cliVersion={cliVersion} + geminiMdFileCount={geminiMdFileCount} + /> + <ConsoleOutput /> + </Box> </Box> ); }; |
