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:
Michael Bleigh
2026-04-12 11:46:18 -07:00
parent 410e675837
commit 45eababfd8
10 changed files with 262 additions and 12 deletions
+1 -1
View File
@@ -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;
+4
View File
@@ -264,6 +264,10 @@ export class GeminiChat {
);
}
get loopContext(): AgentLoopContext {
return this.context;
}
async initialize(
resumedSessionData?: ResumedSessionData,
kind: 'main' | 'subagent' = 'main',
+25 -1
View File
@@ -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<string, unknown>) || {};
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,
+11
View File
@@ -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,
+7 -1
View File
@@ -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,
}),
);
+2
View File
@@ -37,6 +37,8 @@ export interface ToolCallRequestInfo {
callId: string;
name: string;
args: Record<string, unknown>;
/** 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