diff options
| author | Taylor Mullen <[email protected]> | 2025-04-25 17:11:08 -0700 |
|---|---|---|
| committer | N. Taylor Mullen <[email protected]> | 2025-04-26 16:08:05 -0700 |
| commit | 5be89befeff9c4d4f3ab9f508f030bc153fdd06b (patch) | |
| tree | 9d4f679a0e7292132cab04fdd3c24062fcd66ce8 /packages/cli/src/ui/components/messages | |
| parent | aa65a4a1fc3f51589c7633217f9d3c8bd0141abb (diff) | |
feat: Fix flickering in iTerm + scrolling + performance issues.
- Refactors history display using Ink's <Static> component to prevent flickering and improve performance by rendering completed items statically.
- Introduces ConsolePatcher component to capture and display console.log, console.warn, and console.error output within the Ink UI, addressing native handling issues.
- Introduce a new content splitting mechanism to work better for static items. Basically when content gets too long we will now split content into multiple blocks for Gemini messages to ensure that we can statically cache larger pieces of history.
Fixes:
- https://b.corp.google.com/issues/411450097
- https://b.corp.google.com/issues/412716309
Diffstat (limited to 'packages/cli/src/ui/components/messages')
6 files changed, 58 insertions, 9 deletions
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"> |
