summaryrefslogtreecommitdiff
path: root/packages/cli/src
diff options
context:
space:
mode:
Diffstat (limited to 'packages/cli/src')
-rw-r--r--packages/cli/src/ui/App.tsx304
-rw-r--r--packages/cli/src/ui/components/HistoryItemDisplay.tsx8
-rw-r--r--packages/cli/src/ui/components/messages/ToolGroupMessage.tsx12
-rw-r--r--packages/cli/src/ui/components/messages/ToolMessage.tsx45
4 files changed, 229 insertions, 140 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>
);
};
diff --git a/packages/cli/src/ui/components/HistoryItemDisplay.tsx b/packages/cli/src/ui/components/HistoryItemDisplay.tsx
index 0c724feb..bd35d335 100644
--- a/packages/cli/src/ui/components/HistoryItemDisplay.tsx
+++ b/packages/cli/src/ui/components/HistoryItemDisplay.tsx
@@ -16,10 +16,12 @@ import { Box } from 'ink';
interface HistoryItemDisplayProps {
item: HistoryItem;
+ availableTerminalHeight: number;
}
export const HistoryItemDisplay: React.FC<HistoryItemDisplayProps> = ({
item,
+ availableTerminalHeight,
}) => (
<Box flexDirection="column" key={item.id}>
{/* Render standard message types */}
@@ -31,7 +33,11 @@ export const HistoryItemDisplay: React.FC<HistoryItemDisplayProps> = ({
{item.type === 'info' && <InfoMessage text={item.text} />}
{item.type === 'error' && <ErrorMessage text={item.text} />}
{item.type === 'tool_group' && (
- <ToolGroupMessage toolCalls={item.tools} groupId={item.id} />
+ <ToolGroupMessage
+ toolCalls={item.tools}
+ groupId={item.id}
+ availableTerminalHeight={availableTerminalHeight}
+ />
)}
</Box>
);
diff --git a/packages/cli/src/ui/components/messages/ToolGroupMessage.tsx b/packages/cli/src/ui/components/messages/ToolGroupMessage.tsx
index 35408114..33460405 100644
--- a/packages/cli/src/ui/components/messages/ToolGroupMessage.tsx
+++ b/packages/cli/src/ui/components/messages/ToolGroupMessage.tsx
@@ -14,18 +14,23 @@ import { Colors } from '../../colors.js';
interface ToolGroupMessageProps {
groupId: number;
toolCalls: IndividualToolCallDisplay[];
+ availableTerminalHeight: number;
}
// Main component renders the border and maps the tools using ToolMessage
export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
groupId,
toolCalls,
+ availableTerminalHeight,
}) => {
const hasPending = !toolCalls.every(
(t) => t.status === ToolCallStatus.Success,
);
const borderColor = hasPending ? Colors.AccentYellow : Colors.SubtleComment;
+ const staticHeight = /* border */ 2 + /* marginBottom */ 1;
+ availableTerminalHeight -= staticHeight;
+
return (
<Box
key={groupId}
@@ -46,13 +51,14 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
{toolCalls.map((tool) => (
<Box key={groupId + '-' + tool.callId} flexDirection="column">
<ToolMessage
- key={tool.callId} // Use callId as the key
- callId={tool.callId} // Pass callId
+ key={tool.callId}
+ callId={tool.callId}
name={tool.name}
description={tool.description}
resultDisplay={tool.resultDisplay}
status={tool.status}
- confirmationDetails={tool.confirmationDetails} // Pass confirmationDetails
+ confirmationDetails={tool.confirmationDetails}
+ availableTerminalHeight={availableTerminalHeight}
/>
{tool.status === ToolCallStatus.Confirming &&
tool.confirmationDetails && (
diff --git a/packages/cli/src/ui/components/messages/ToolMessage.tsx b/packages/cli/src/ui/components/messages/ToolMessage.tsx
index 3b58c052..220578de 100644
--- a/packages/cli/src/ui/components/messages/ToolMessage.tsx
+++ b/packages/cli/src/ui/components/messages/ToolMessage.tsx
@@ -12,14 +12,38 @@ import { DiffRenderer } from './DiffRenderer.js';
import { Colors } from '../../colors.js';
import { MarkdownDisplay } from '../../utils/MarkdownDisplay.js';
-export const ToolMessage: React.FC<IndividualToolCallDisplay> = ({
+export interface ToolMessageProps extends IndividualToolCallDisplay {
+ availableTerminalHeight: number;
+}
+
+export const ToolMessage: React.FC<ToolMessageProps> = ({
name,
description,
resultDisplay,
status,
+ availableTerminalHeight,
}) => {
const statusIndicatorWidth = 3;
const hasResult = resultDisplay && resultDisplay.toString().trim().length > 0;
+ const staticHeight = /* Header */ 1;
+ availableTerminalHeight -= staticHeight;
+
+ let displayableResult = resultDisplay;
+ let hiddenLines = 0;
+
+ // Truncate the overall string content if it's too long.
+ // MarkdownRenderer will handle specific truncation for code blocks within this content.
+ if (typeof resultDisplay === 'string' && resultDisplay.length > 0) {
+ const lines = resultDisplay.split('\n');
+ // Estimate available height for this specific tool message content area
+ // This is a rough estimate; ideally, we'd have a more precise measurement.
+ const contentHeightEstimate = availableTerminalHeight - 5; // Subtracting lines for tool name, status, padding etc.
+ if (lines.length > contentHeightEstimate && contentHeightEstimate > 0) {
+ displayableResult = lines.slice(0, contentHeightEstimate).join('\n');
+ hiddenLines = lines.length - contentHeightEstimate;
+ }
+ }
+
return (
<Box paddingX={1} paddingY={0} flexDirection="column">
<Box minHeight={1}>
@@ -56,15 +80,22 @@ export const ToolMessage: React.FC<IndividualToolCallDisplay> = ({
</Box>
{hasResult && (
<Box paddingLeft={statusIndicatorWidth} width="100%">
- <Box flexDirection="row">
- {/* Use default text color (white) or gray instead of dimColor */}
- {typeof resultDisplay === 'string' && (
+ <Box flexDirection="column">
+ {typeof displayableResult === 'string' && (
<Box flexDirection="column">
- <MarkdownDisplay text={resultDisplay} />
+ <MarkdownDisplay text={displayableResult} />
</Box>
)}
- {typeof resultDisplay === 'object' && (
- <DiffRenderer diffContent={resultDisplay.fileDiff} />
+ {typeof displayableResult === 'object' && (
+ <DiffRenderer diffContent={displayableResult.fileDiff} />
+ )}
+ {hiddenLines > 0 && (
+ <Box>
+ <Text color={Colors.SubtleComment}>
+ ... {hiddenLines} more line{hiddenLines === 1 ? '' : 's'}{' '}
+ hidden ...
+ </Text>
+ </Box>
)}
</Box>
</Box>