summaryrefslogtreecommitdiff
path: root/packages/cli/src/ui/components
diff options
context:
space:
mode:
Diffstat (limited to 'packages/cli/src/ui/components')
-rw-r--r--packages/cli/src/ui/components/ConsolePatcher.tsx85
-rw-r--r--packages/cli/src/ui/components/HistoryDisplay.tsx43
-rw-r--r--packages/cli/src/ui/components/HistoryItemDisplay.tsx44
-rw-r--r--packages/cli/src/ui/components/InputPrompt.tsx7
-rw-r--r--packages/cli/src/ui/components/messages/DiffRenderer.tsx7
-rw-r--r--packages/cli/src/ui/components/messages/ErrorMessage.tsx2
-rw-r--r--packages/cli/src/ui/components/messages/GeminiMessageContent.tsx33
-rw-r--r--packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx5
-rw-r--r--packages/cli/src/ui/components/messages/ToolGroupMessage.tsx16
-rw-r--r--packages/cli/src/ui/components/messages/ToolMessage.tsx4
10 files changed, 193 insertions, 53 deletions
diff --git a/packages/cli/src/ui/components/ConsolePatcher.tsx b/packages/cli/src/ui/components/ConsolePatcher.tsx
new file mode 100644
index 00000000..7070fbe4
--- /dev/null
+++ b/packages/cli/src/ui/components/ConsolePatcher.tsx
@@ -0,0 +1,85 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React, { useState, useEffect, Key } from 'react';
+import { Box, Text } from 'ink';
+import util from 'util';
+
+interface ConsoleMessage {
+ id: Key;
+ type: 'log' | 'warn' | 'error';
+ content: string;
+}
+
+// Using a module-level counter for unique IDs.
+// This ensures IDs are unique across messages.
+let messageIdCounter = 0;
+
+export const ConsoleOutput: React.FC = () => {
+ const [messages, setMessages] = useState<ConsoleMessage[]>([]);
+
+ useEffect(() => {
+ const originalConsoleLog = console.log;
+ const originalConsoleWarn = console.warn;
+ const originalConsoleError = console.error;
+
+ const formatArgs = (args: unknown[]): string => util.format(...args);
+ const addMessage = (type: 'log' | 'warn' | 'error', args: unknown[]) => {
+ setMessages((prevMessages) => [
+ ...prevMessages,
+ {
+ id: `console-msg-${messageIdCounter++}`,
+ type,
+ content: formatArgs(args),
+ },
+ ]);
+ };
+
+ // It's patching time
+ console.log = (...args: unknown[]) => addMessage('log', args);
+ console.warn = (...args: unknown[]) => addMessage('warn', args);
+ console.error = (...args: unknown[]) => addMessage('error', args);
+
+ return () => {
+ console.log = originalConsoleLog;
+ console.warn = originalConsoleWarn;
+ console.error = originalConsoleError;
+ };
+ }, []);
+
+ return (
+ <Box flexDirection="column">
+ {messages.map((msg) => {
+ const textProps: { color?: string } = {};
+ let prefix = '';
+
+ switch (msg.type) {
+ case 'warn':
+ textProps.color = 'yellow';
+ prefix = 'WARN: ';
+ break;
+ case 'error':
+ textProps.color = 'red';
+ prefix = 'ERROR: ';
+ break;
+ case 'log':
+ default:
+ prefix = 'LOG: ';
+ break;
+ }
+
+ return (
+ <Box key={msg.id}>
+ <Text {...textProps}>
+ {prefix}
+ {msg.content}
+ </Text>
+ </Box>
+ );
+ })}
+ </Box>
+ );
+};
diff --git a/packages/cli/src/ui/components/HistoryDisplay.tsx b/packages/cli/src/ui/components/HistoryDisplay.tsx
deleted file mode 100644
index 914b84c2..00000000
--- a/packages/cli/src/ui/components/HistoryDisplay.tsx
+++ /dev/null
@@ -1,43 +0,0 @@
-/**
- * @license
- * Copyright 2025 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import React from 'react';
-import { Box } from 'ink';
-import type { HistoryItem } from '../types.js';
-import { UserMessage } from './messages/UserMessage.js';
-import { GeminiMessage } from './messages/GeminiMessage.js';
-import { InfoMessage } from './messages/InfoMessage.js';
-import { ErrorMessage } from './messages/ErrorMessage.js';
-import { ToolGroupMessage } from './messages/ToolGroupMessage.js';
-import { PartListUnion } from '@google/genai';
-
-interface HistoryDisplayProps {
- history: HistoryItem[];
- onSubmit: (value: PartListUnion) => void;
-}
-
-export const HistoryDisplay: React.FC<HistoryDisplayProps> = ({
- history,
- onSubmit,
-}) => (
- // No grouping logic needed here anymore
- <Box flexDirection="column">
- {history.map((item) => (
- <Box key={item.id} marginBottom={1}>
- {/* Render standard message types */}
- {item.type === 'user' && <UserMessage text={item.text} />}
- {item.type === 'gemini' && <GeminiMessage text={item.text} />}
- {item.type === 'info' && <InfoMessage text={item.text} />}
- {item.type === 'error' && <ErrorMessage text={item.text} />}
-
- {/* Render the tool group component */}
- {item.type === 'tool_group' && (
- <ToolGroupMessage toolCalls={item.tools} onSubmit={onSubmit} />
- )}
- </Box>
- ))}
- </Box>
-);
diff --git a/packages/cli/src/ui/components/HistoryItemDisplay.tsx b/packages/cli/src/ui/components/HistoryItemDisplay.tsx
new file mode 100644
index 00000000..5fb9b32f
--- /dev/null
+++ b/packages/cli/src/ui/components/HistoryItemDisplay.tsx
@@ -0,0 +1,44 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React from 'react';
+import type { HistoryItem } from '../types.js';
+import { UserMessage } from './messages/UserMessage.js';
+import { GeminiMessage } from './messages/GeminiMessage.js';
+import { InfoMessage } from './messages/InfoMessage.js';
+import { ErrorMessage } from './messages/ErrorMessage.js';
+import { ToolGroupMessage } from './messages/ToolGroupMessage.js';
+import { PartListUnion } from '@google/genai';
+import { GeminiMessageContent } from './messages/GeminiMessageContent.js';
+import { Box } from 'ink';
+
+interface HistoryItemDisplayProps {
+ item: HistoryItem;
+ onSubmit: (value: PartListUnion) => void;
+}
+
+export const HistoryItemDisplay: React.FC<HistoryItemDisplayProps> = ({
+ item,
+ onSubmit,
+}) => (
+ <Box flexDirection="column" key={item.id}>
+ {/* Render standard message types */}
+ {item.type === 'user' && <UserMessage text={item.text} />}
+ {item.type === 'gemini' && <GeminiMessage text={item.text} />}
+ {item.type === 'gemini_content' && (
+ <GeminiMessageContent text={item.text} />
+ )}
+ {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}
+ onSubmit={onSubmit}
+ />
+ )}
+ </Box>
+);
diff --git a/packages/cli/src/ui/components/InputPrompt.tsx b/packages/cli/src/ui/components/InputPrompt.tsx
index b5c3cc7d..153b4701 100644
--- a/packages/cli/src/ui/components/InputPrompt.tsx
+++ b/packages/cli/src/ui/components/InputPrompt.tsx
@@ -14,7 +14,12 @@ interface InputPromptProps {
}
export const InputPrompt: React.FC<InputPromptProps> = ({ onSubmit }) => {
- const [value, setValue] = React.useState('');
+ const [value, setValue] = React.useState(
+ "I'd like to update my web fetch tool to be a little smarter about the content it fetches from web pages. Instead of returning the entire HTML to the LLM I was extract the body text and other important information to reduce the amount of tokens we need to use.",
+ );
+ // const [value, setValue] = React.useState('Add "Hello World" to the top of README.md');
+ // const [value, setValue] = React.useState('show me "Hello World" in as many langauges as you can think of');
+
const { isFocused } = useFocus({ autoFocus: true });
useInput(
diff --git a/packages/cli/src/ui/components/messages/DiffRenderer.tsx b/packages/cli/src/ui/components/messages/DiffRenderer.tsx
index eb3133c3..4d196e6d 100644
--- a/packages/cli/src/ui/components/messages/DiffRenderer.tsx
+++ b/packages/cli/src/ui/components/messages/DiffRenderer.tsx
@@ -7,6 +7,7 @@
import React from 'react';
import { Box, Text } from 'ink';
import { Colors } from '../../colors.js';
+import crypto from 'crypto';
interface DiffLine {
type: 'add' | 'del' | 'context' | 'hunk' | 'other';
@@ -94,6 +95,7 @@ const DEFAULT_TAB_WIDTH = 4; // Spaces per tab for normalization
export const DiffRenderer: React.FC<DiffRendererProps> = ({
diffContent,
+ filename,
tabWidth = DEFAULT_TAB_WIDTH,
}) => {
if (!diffContent || typeof diffContent !== 'string') {
@@ -137,8 +139,11 @@ export const DiffRenderer: React.FC<DiffRendererProps> = ({
}
// --- End Modification ---
+ const key = filename
+ ? `diff-box-${filename}`
+ : `diff-box-${crypto.createHash('sha1').update(diffContent).digest('hex')}`;
return (
- <Box flexDirection="column">
+ <Box flexDirection="column" key={key}>
{/* Iterate over the lines that should be displayed (already normalized) */}
{displayableLines.map((line, index) => {
const key = `diff-line-${index}`;
diff --git a/packages/cli/src/ui/components/messages/ErrorMessage.tsx b/packages/cli/src/ui/components/messages/ErrorMessage.tsx
index 22d82465..edbea435 100644
--- a/packages/cli/src/ui/components/messages/ErrorMessage.tsx
+++ b/packages/cli/src/ui/components/messages/ErrorMessage.tsx
@@ -17,7 +17,7 @@ export const ErrorMessage: React.FC<ErrorMessageProps> = ({ text }) => {
const prefixWidth = prefix.length;
return (
- <Box flexDirection="row">
+ <Box flexDirection="row" marginBottom={1}>
<Box width={prefixWidth}>
<Text color={Colors.AccentRed}>{prefix}</Text>
</Box>
diff --git a/packages/cli/src/ui/components/messages/GeminiMessageContent.tsx b/packages/cli/src/ui/components/messages/GeminiMessageContent.tsx
new file mode 100644
index 00000000..fb025231
--- /dev/null
+++ b/packages/cli/src/ui/components/messages/GeminiMessageContent.tsx
@@ -0,0 +1,33 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React from 'react';
+import { Box } from 'ink';
+import { MarkdownRenderer } from '../../utils/MarkdownRenderer.js';
+
+interface GeminiMessageContentProps {
+ text: string;
+}
+
+/*
+ * Gemini message content is a semi-hacked component. The intention is to represent a partial
+ * of GeminiMessage and is only used when a response gets too long. In that instance messages
+ * are split into multiple GeminiMessageContent's to enable the root <Static> component in
+ * App.tsx to be as performant as humanly possible.
+ */
+export const GeminiMessageContent: React.FC<GeminiMessageContentProps> = ({
+ text,
+}) => {
+ const originalPrefix = '✦ ';
+ const prefixWidth = originalPrefix.length;
+ const renderedBlocks = MarkdownRenderer.render(text);
+
+ return (
+ <Box flexDirection="column" paddingLeft={prefixWidth}>
+ {renderedBlocks}
+ </Box>
+ );
+};
diff --git a/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx b/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx
index 748c7d1c..7099537e 100644
--- a/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx
+++ b/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx
@@ -8,7 +8,6 @@ import React from 'react';
import { Box, Text, useInput } from 'ink';
import { PartListUnion } from '@google/genai';
import { DiffRenderer } from './DiffRenderer.js';
-import { UI_WIDTH } from '../../constants.js';
import { Colors } from '../../colors.js';
import {
ToolCallConfirmationDetails,
@@ -88,7 +87,7 @@ export const ToolConfirmationMessage: React.FC<
value: ToolConfirmationOutcome.ProceedOnce,
},
{
- label: `Yes, allow always for ${executionProps.rootCommand} ...`,
+ label: `Yes, allow always "${executionProps.rootCommand} ..."`,
value: ToolConfirmationOutcome.ProceedAlways,
},
{ label: 'No (esc)', value: ToolConfirmationOutcome.Cancel },
@@ -96,7 +95,7 @@ export const ToolConfirmationMessage: React.FC<
}
return (
- <Box flexDirection="column" padding={1} minWidth={UI_WIDTH}>
+ <Box flexDirection="column" padding={1} minWidth="90%">
{/* Body Content (Diff Renderer or Command Info) */}
{/* No separate context display here anymore for edits */}
<Box flexGrow={1} flexShrink={1} overflow="hidden" marginBottom={1}>
diff --git a/packages/cli/src/ui/components/messages/ToolGroupMessage.tsx b/packages/cli/src/ui/components/messages/ToolGroupMessage.tsx
index 0675411f..2d4982c2 100644
--- a/packages/cli/src/ui/components/messages/ToolGroupMessage.tsx
+++ b/packages/cli/src/ui/components/messages/ToolGroupMessage.tsx
@@ -13,12 +13,14 @@ import { ToolConfirmationMessage } from './ToolConfirmationMessage.js';
import { Colors } from '../../colors.js';
interface ToolGroupMessageProps {
+ groupId: number;
toolCalls: IndividualToolCallDisplay[];
onSubmit: (value: PartListUnion) => void;
}
// Main component renders the border and maps the tools using ToolMessage
export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
+ groupId,
toolCalls,
onSubmit,
}) => {
@@ -29,13 +31,23 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
return (
<Box
+ key={groupId}
flexDirection="column"
borderStyle="round"
+ /*
+ This width constraint is highly important and protects us from an Ink rendering bug.
+ Since the ToolGroup can typically change rendering states frequently, it can cause
+ Ink to render the border of the box incorrectly and span multiple lines and even
+ cause tearing.
+ */
+ width="100%"
+ marginLeft={1}
borderDimColor={hasPending}
borderColor={borderColor}
+ marginBottom={1}
>
{toolCalls.map((tool) => (
- <React.Fragment key={tool.callId}>
+ <Box key={groupId + '-' + tool.callId} flexDirection="column">
<ToolMessage
key={tool.callId} // Use callId as the key
callId={tool.callId} // Pass callId
@@ -52,7 +64,7 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
onSubmit={onSubmit}
></ToolConfirmationMessage>
)}
- </React.Fragment>
+ </Box>
))}
{/* Optional: Add padding below the last item if needed,
though ToolMessage already has some vertical space implicitly */}
diff --git a/packages/cli/src/ui/components/messages/ToolMessage.tsx b/packages/cli/src/ui/components/messages/ToolMessage.tsx
index ab590f53..4d5fca37 100644
--- a/packages/cli/src/ui/components/messages/ToolMessage.tsx
+++ b/packages/cli/src/ui/components/messages/ToolMessage.tsx
@@ -54,8 +54,8 @@ export const ToolMessage: React.FC<IndividualToolCallDisplay> = ({
</Box>
</Box>
{hasResult && (
- <Box paddingLeft={statusIndicatorWidth}>
- <Box flexShrink={1} flexDirection="row">
+ <Box paddingLeft={statusIndicatorWidth} width="100%">
+ <Box flexDirection="row">
{/* Use default text color (white) or gray instead of dimColor */}
{typeof resultDisplay === 'string' && (
<Box flexDirection="column">