diff --git a/packages/cli/src/ui/components/HistoryItemDisplay.tsx b/packages/cli/src/ui/components/HistoryItemDisplay.tsx index 414b6dcbe4..513ad3a0cc 100644 --- a/packages/cli/src/ui/components/HistoryItemDisplay.tsx +++ b/packages/cli/src/ui/components/HistoryItemDisplay.tsx @@ -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 = ({ isToolGroupBoundary={isToolGroupBoundary} /> )} - {/* TODO: tool_group_display goes here */} + {itemForDisplay.type === 'tool_display_group' && ( + + )} {itemForDisplay.type === 'subagent' && ( = ({ + item, + isToolGroupBoundary, +}) => { + if (item.type !== 'tool_display_group') { + return null; + } + + const { tools, borderColor, borderDimColor, borderTop, borderBottom } = + item as HistoryItemToolDisplayGroup; + + return ( + + {tools.map((tool, index) => ( + + ))} + + ); +}; + +interface ToolDisplayMessageProps { + tool: ToolDisplayItem; +} + +const ToolDisplayMessage: React.FC = ({ 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 ( + + + Error: Tool display missing + + ); + } + + 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 ( + + + + {' '} + {name || tool.originalRequestName}{' '} + + {description && {description}} + {resultSummary && ( + → {resultSummary} + )} + + ); + } + + // Box format (full) + return ( + + + + + {' '} + {name || tool.originalRequestName}{' '} + + {description && {description}} + + {resultSummary && !result && ( + + → {resultSummary} + + )} + {result && ( + + + + )} + + ); +}; + +interface ToolResultDisplayContentProps { + content: ToolDisplayItem['result']; + summary?: string | null; +} + +const ToolResultDisplayContent: React.FC = ({ + content, + summary, +}) => { + if (!content) return null; + + switch (content.type) { + case 'text': + return {content.text}; + case 'diff': + // Simplified diff display for now + return ( + + {summary && {summary}} + + {`[Diff Display: ${content.beforeText.length} -> ${content.afterText.length} chars]`} + + + ); + case 'terminal': + return [Terminal Output]; + case 'agent': + return ( + [Subagent: {content.threadId}] + ); + default: + return null; + } +}; diff --git a/packages/cli/src/ui/hooks/useAgentStream.ts b/packages/cli/src/ui/hooks/useAgentStream.ts index 4fb4a9c94f..8f0fc61065 100644 --- a/packages/cli/src/ui/hooks/useAgentStream.ts +++ b/packages/cli/src/ui/hooks/useAgentStream.ts @@ -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, diff --git a/packages/cli/src/ui/types.ts b/packages/cli/src/ui/types.ts index 590b4e4ea8..d562bd76e1 100644 --- a/packages/cli/src/ui/types.ts +++ b/packages/cli/src/ui/types.ts @@ -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 & { diff --git a/packages/core/src/agent/event-translator.ts b/packages/core/src/agent/event-translator.ts index 654dea9d75..355a7f010b 100644 --- a/packages/core/src/agent/event-translator.ts +++ b/packages/core/src/agent/event-translator.ts @@ -236,6 +236,7 @@ export function translateEvent( requestId: event.value.callId, name: event.value.name, args: event.value.args, + display: event.value.display, }), ); break; @@ -281,7 +282,6 @@ export function translateEvent( ((x: never) => { throw new Error(`Unhandled event type: ${JSON.stringify(x)}`); })(event); - break; } return out; diff --git a/packages/core/src/core/geminiChat.ts b/packages/core/src/core/geminiChat.ts index c480c3800b..8c34fc1cb3 100644 --- a/packages/core/src/core/geminiChat.ts +++ b/packages/core/src/core/geminiChat.ts @@ -264,6 +264,10 @@ export class GeminiChat { ); } + get loopContext(): AgentLoopContext { + return this.context; + } + async initialize( resumedSessionData?: ResumedSessionData, kind: 'main' | 'subagent' = 'main', diff --git a/packages/core/src/core/turn.ts b/packages/core/src/core/turn.ts index 9c0e536c48..fc4c0f73f4 100644 --- a/packages/core/src/core/turn.ts +++ b/packages/core/src/core/turn.ts @@ -29,6 +29,7 @@ import { parseThought, type ThoughtSummary } from '../utils/thoughtUtils.js'; import type { ModelConfigKey } from '../services/modelConfigService.js'; import { getCitations } from '../utils/generateContentResponseUtilities.js'; import { LlmRole } from '../telemetry/types.js'; +import { populateToolDisplay } from '../agent/tool-display-utils.js'; import { type ToolCallRequestInfo, @@ -408,13 +409,36 @@ export class Turn { traceId?: string, ): ServerGeminiStreamEvent | null { const name = fnCall.name || 'undefined_tool_name'; - const args = fnCall.args || {}; + // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion + const args = (fnCall.args as Record) || {}; const callId = fnCall.id ?? `${name}_${Date.now()}_${this.callCounter++}`; + const tool = this.chat.loopContext.toolRegistry.getTool(name); + let display; + if (tool) { + let invocation; + try { + invocation = tool.build(args); + } catch { + // Ignore build errors for request display purposes + } + display = populateToolDisplay({ + name, + invocation, + displayName: tool.displayName, + }); + + // Fallback to static description if invocation failed or didn't provide one + if (!display.description) { + display.description = tool.description; + } + } + const toolCallRequest: ToolCallRequestInfo = { callId, name, args, + display, isClientInitiated: false, prompt_id: this.prompt_id, traceId, diff --git a/packages/core/src/scheduler/scheduler.ts b/packages/core/src/scheduler/scheduler.ts index fef22968e1..f6151b63d6 100644 --- a/packages/core/src/scheduler/scheduler.ts +++ b/packages/core/src/scheduler/scheduler.ts @@ -36,6 +36,7 @@ import { getToolSuggestion } from '../utils/tool-utils.js'; import { runInDevTraceSpan } from '../telemetry/trace.js'; import { logToolCall } from '../telemetry/loggers.js'; import { ToolCallEvent } from '../telemetry/types.js'; +import { populateToolDisplay } from '../agent/tool-display-utils.js'; import type { EditorType } from '../utils/editor.js'; import { MessageBusType, @@ -380,6 +381,16 @@ export class Scheduler { () => { try { const invocation = tool.build(request.args); + if (!request.display) { + request.display = populateToolDisplay({ + name: tool.name, + invocation, + displayName: tool.displayName, + }); + if (!request.display.description) { + request.display.description = tool.description; + } + } return { status: CoreToolCallStatus.Validating, request, diff --git a/packages/core/src/scheduler/state-manager.ts b/packages/core/src/scheduler/state-manager.ts index c524a139bd..6183be031c 100644 --- a/packages/core/src/scheduler/state-manager.ts +++ b/packages/core/src/scheduler/state-manager.ts @@ -23,6 +23,7 @@ import type { ToolConfirmationOutcome, ToolResultDisplay, AnyToolInvocation, + ToolDisplay, ToolCallConfirmationDetails, AnyDeclarativeTool, } from '../tools/tools.js'; @@ -172,10 +173,15 @@ export class SchedulerStateManager { const call = this.activeCalls.get(callId); if (!call || call.status === CoreToolCallStatus.Error) return; + const display: ToolDisplay = call.request.display + ? { ...call.request.display } + : { name: call.request.name }; + display.description = newInvocation.getDescription(); + this.activeCalls.set( callId, this.patchCall(call, { - request: { ...call.request, args: newArgs }, + request: { ...call.request, args: newArgs, display }, invocation: newInvocation, }), ); diff --git a/packages/core/src/scheduler/types.ts b/packages/core/src/scheduler/types.ts index 596fd1f8d1..3173b76f8d 100644 --- a/packages/core/src/scheduler/types.ts +++ b/packages/core/src/scheduler/types.ts @@ -37,6 +37,8 @@ export interface ToolCallRequestInfo { callId: string; name: string; args: Record; + /** Tool-controlled display information. */ + display?: ToolDisplay; /** * The original name and arguments of the tool requested by the model. * This is used for tail calls to ensure the final response and log retains