mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-06-01 19:03:42 +00:00
feat(cli): refactor tool rendering to declarative ToolDisplay system
This change completes the transition of the interactive agent session (`useAgentStream`) to a declarative-first tool rendering system. Key changes: - Reverted experimental `ToolDisplay` logic from legacy UI components (`DenseToolMessage`, etc.) to establish a clean baseline. - Introduced `HistoryItemToolDisplayGroup` and `ToolGroupDisplay` component in CLI. - Added `display` property to `ToolCallRequestInfo` to carry declarative UI info natively. - Populated tool request display information at the source (`Turn.ts` and `Scheduler.ts`) using dynamic descriptions from tool invocations. - Updated `useAgentStream` to emit the new history item type, providing a standalone rendering path for interactive sessions. - Ensured tool descriptions are updated when arguments are modified during confirmation.
This commit is contained in:
@@ -14,6 +14,7 @@ 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 { ToolGroupDisplay } from './messages/ToolGroupDisplay.js';
|
||||
import { GeminiMessageContent } from './messages/GeminiMessageContent.js';
|
||||
import { CompressionMessage } from './messages/CompressionMessage.js';
|
||||
import { WarningMessage } from './messages/WarningMessage.js';
|
||||
@@ -208,7 +209,12 @@ export const HistoryItemDisplay: React.FC<HistoryItemDisplayProps> = ({
|
||||
isToolGroupBoundary={isToolGroupBoundary}
|
||||
/>
|
||||
)}
|
||||
{/* TODO: tool_group_display goes here */}
|
||||
{itemForDisplay.type === 'tool_display_group' && (
|
||||
<ToolGroupDisplay
|
||||
item={itemForDisplay}
|
||||
isToolGroupBoundary={isToolGroupBoundary}
|
||||
/>
|
||||
)}
|
||||
{itemForDisplay.type === 'subagent' && (
|
||||
<SubagentHistoryMessage
|
||||
item={itemForDisplay}
|
||||
|
||||
176
packages/cli/src/ui/components/messages/ToolGroupDisplay.tsx
Normal file
176
packages/cli/src/ui/components/messages/ToolGroupDisplay.tsx
Normal file
@@ -0,0 +1,176 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import type {
|
||||
HistoryItem,
|
||||
HistoryItemWithoutId,
|
||||
HistoryItemToolDisplayGroup,
|
||||
ToolDisplayItem,
|
||||
} from '../../types.js';
|
||||
import { theme } from '../../semantic-colors.js';
|
||||
import { ToolStatusIndicator } from './ToolShared.js';
|
||||
import { useSettings } from '../../contexts/SettingsContext.js';
|
||||
|
||||
interface ToolGroupDisplayProps {
|
||||
item: HistoryItem | HistoryItemWithoutId;
|
||||
isToolGroupBoundary?: boolean;
|
||||
}
|
||||
|
||||
export const ToolGroupDisplay: React.FC<ToolGroupDisplayProps> = ({
|
||||
item,
|
||||
isToolGroupBoundary,
|
||||
}) => {
|
||||
if (item.type !== 'tool_display_group') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { tools, borderColor, borderDimColor, borderTop, borderBottom } =
|
||||
item as HistoryItemToolDisplayGroup;
|
||||
|
||||
return (
|
||||
<Box
|
||||
flexDirection="column"
|
||||
borderStyle="round"
|
||||
borderColor={borderColor}
|
||||
borderDimColor={borderDimColor}
|
||||
borderTop={borderTop}
|
||||
borderBottom={borderBottom}
|
||||
borderLeft={!isToolGroupBoundary}
|
||||
borderRight={!isToolGroupBoundary}
|
||||
marginTop={borderTop ? 1 : 0}
|
||||
marginBottom={borderBottom ? 1 : 0}
|
||||
paddingX={1}
|
||||
>
|
||||
{tools.map((tool, index) => (
|
||||
<ToolDisplayMessage key={index} tool={tool} />
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
interface ToolDisplayMessageProps {
|
||||
tool: ToolDisplayItem;
|
||||
}
|
||||
|
||||
const ToolDisplayMessage: React.FC<ToolDisplayMessageProps> = ({ tool }) => {
|
||||
const settings = useSettings();
|
||||
const isCompactModeEnabled = settings.merged.ui?.compactToolOutput === true;
|
||||
|
||||
// Since ToolDisplayItem is ToolDisplay & { status, ... }, we check for identifying properties
|
||||
// of ToolDisplay. If name or description is missing and there's no result, it might be "empty".
|
||||
// But per instructions, if display is missing (which we now interpret as the ToolDisplay part being effectively empty/null), show error.
|
||||
if (!tool.name && !tool.description && !tool.result && !tool.resultSummary) {
|
||||
return (
|
||||
<Box paddingLeft={2}>
|
||||
<ToolStatusIndicator
|
||||
status={tool.status}
|
||||
name={tool.originalRequestName || 'unknown'}
|
||||
/>
|
||||
<Text color={theme.status.error}> Error: Tool display missing</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
const {
|
||||
status,
|
||||
format: preferredFormat,
|
||||
name,
|
||||
description,
|
||||
resultSummary,
|
||||
result,
|
||||
} = tool;
|
||||
const format = preferredFormat || 'auto';
|
||||
|
||||
if (format === 'hidden') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isCompact =
|
||||
format === 'compact' || (format === 'auto' && isCompactModeEnabled);
|
||||
|
||||
if (isCompact) {
|
||||
return (
|
||||
<Box paddingLeft={2} flexDirection="row" flexWrap="wrap">
|
||||
<ToolStatusIndicator
|
||||
status={status}
|
||||
name={name || tool.originalRequestName || ''}
|
||||
/>
|
||||
<Text bold color={theme.text.primary}>
|
||||
{' '}
|
||||
{name || tool.originalRequestName}{' '}
|
||||
</Text>
|
||||
{description && <Text color={theme.text.secondary}>{description}</Text>}
|
||||
{resultSummary && (
|
||||
<Text color={theme.text.accent}> → {resultSummary}</Text>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
// Box format (full)
|
||||
return (
|
||||
<Box flexDirection="column" paddingLeft={2} marginBottom={1}>
|
||||
<Box flexDirection="row">
|
||||
<ToolStatusIndicator
|
||||
status={status}
|
||||
name={name || tool.originalRequestName || ''}
|
||||
/>
|
||||
<Text bold color={theme.text.primary}>
|
||||
{' '}
|
||||
{name || tool.originalRequestName}{' '}
|
||||
</Text>
|
||||
{description && <Text color={theme.text.secondary}>{description}</Text>}
|
||||
</Box>
|
||||
{resultSummary && !result && (
|
||||
<Box paddingLeft={2}>
|
||||
<Text color={theme.text.accent}>→ {resultSummary}</Text>
|
||||
</Box>
|
||||
)}
|
||||
{result && (
|
||||
<Box paddingLeft={2} marginTop={0}>
|
||||
<ToolResultDisplayContent content={result} summary={resultSummary} />
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
interface ToolResultDisplayContentProps {
|
||||
content: ToolDisplayItem['result'];
|
||||
summary?: string | null;
|
||||
}
|
||||
|
||||
const ToolResultDisplayContent: React.FC<ToolResultDisplayContentProps> = ({
|
||||
content,
|
||||
summary,
|
||||
}) => {
|
||||
if (!content) return null;
|
||||
|
||||
switch (content.type) {
|
||||
case 'text':
|
||||
return <Text color={theme.text.secondary}>{content.text}</Text>;
|
||||
case 'diff':
|
||||
// Simplified diff display for now
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
{summary && <Text color={theme.text.accent}>{summary}</Text>}
|
||||
<Text color={theme.text.secondary}>
|
||||
{`[Diff Display: ${content.beforeText.length} -> ${content.afterText.length} chars]`}
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
case 'terminal':
|
||||
return <Text color={theme.text.secondary}>[Terminal Output]</Text>;
|
||||
case 'agent':
|
||||
return (
|
||||
<Text color={theme.text.secondary}>[Subagent: {content.threadId}]</Text>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
@@ -26,7 +26,7 @@ import type {
|
||||
HistoryItemWithoutId,
|
||||
LoopDetectionConfirmationRequest,
|
||||
IndividualToolCallDisplay,
|
||||
HistoryItemToolGroup,
|
||||
HistoryItemToolDisplayGroup,
|
||||
} from '../types.js';
|
||||
import { StreamingState, MessageType } from '../types.js';
|
||||
import { findLastSafeSplitPoint } from '../utils/markdownUtilities.js';
|
||||
@@ -415,9 +415,15 @@ export const useAgentStream = ({
|
||||
backgroundTasks,
|
||||
);
|
||||
|
||||
const historyItem: HistoryItemToolGroup = {
|
||||
type: 'tool_group',
|
||||
tools: toolsToPush,
|
||||
const historyItem: HistoryItemToolDisplayGroup = {
|
||||
type: 'tool_display_group',
|
||||
tools: toolsToPush.map((tc) => ({
|
||||
name: tc.name,
|
||||
description: tc.description,
|
||||
...tc.display,
|
||||
status: tc.status,
|
||||
originalRequestName: tc.originalRequestName,
|
||||
})),
|
||||
borderTop: isFirstToolInGroupRef.current,
|
||||
borderBottom: isLastInBatch,
|
||||
...appearance,
|
||||
@@ -456,8 +462,14 @@ export const useAgentStream = ({
|
||||
|
||||
if (remainingTools.length > 0) {
|
||||
items.push({
|
||||
type: 'tool_group',
|
||||
tools: remainingTools,
|
||||
type: 'tool_display_group',
|
||||
tools: remainingTools.map((tc) => ({
|
||||
name: tc.name,
|
||||
description: tc.description,
|
||||
...tc.display,
|
||||
status: tc.status,
|
||||
originalRequestName: tc.originalRequestName,
|
||||
})),
|
||||
borderTop: pushedToolCallIds.size === 0,
|
||||
borderBottom: false,
|
||||
...appearance,
|
||||
@@ -486,7 +498,7 @@ export const useAgentStream = ({
|
||||
(anyVisibleInHistory || anyVisibleInPending)
|
||||
) {
|
||||
items.push({
|
||||
type: 'tool_group' as const,
|
||||
type: 'tool_display_group',
|
||||
tools: [],
|
||||
borderTop: false,
|
||||
borderBottom: true,
|
||||
|
||||
@@ -260,9 +260,18 @@ export type HistoryItemToolGroup = HistoryItemBase & {
|
||||
borderDimColor?: boolean;
|
||||
};
|
||||
|
||||
export type ToolDisplayItem = ToolDisplay & {
|
||||
status: CoreToolCallStatus;
|
||||
originalRequestName?: string;
|
||||
};
|
||||
|
||||
export type HistoryItemToolDisplayGroup = HistoryItemBase & {
|
||||
type: 'tool_display_group';
|
||||
tools: ToolDisplay[];
|
||||
tools: ToolDisplayItem[];
|
||||
borderTop?: boolean;
|
||||
borderBottom?: boolean;
|
||||
borderColor?: string;
|
||||
borderDimColor?: boolean;
|
||||
};
|
||||
|
||||
export type HistoryItemUserShell = HistoryItemBase & {
|
||||
|
||||
Reference in New Issue
Block a user