Fix: Prevent UI tearing and improve display of long content

This commit introduces several changes to better manage terminal height and prevent UI tearing, especially when displaying long tool outputs or when the pending history item exceeds the available terminal height.

- Calculate and utilize available terminal height in `App.tsx`, `HistoryItemDisplay.tsx`, `ToolGroupMessage.tsx`, and `ToolMessage.tsx`.
- Refresh the static display area in `App.tsx` when a pending history item is too large, working around an Ink bug (see https://github.com/vadimdemedes/ink/pull/717).
- Truncate long tool output in `ToolMessage.tsx` and indicate the number of hidden lines.
- Refactor `App.tsx` to correctly measure and account for footer height.

Fixes https://b.corp.google.com/issues/414196943
This commit is contained in:
Taylor Mullen
2025-05-15 00:19:41 -07:00
committed by N. Taylor Mullen
parent 601a61ed31
commit 33743d347b
4 changed files with 231 additions and 142 deletions

View File

@@ -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>
);

View File

@@ -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 && (

View File

@@ -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>