Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 106 additions & 0 deletions docs/plans/2026-03-22-agent-tool-display-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# Agent Tool Display Implementation Plan

> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

**Goal:** Add a dedicated VSCode/web UI display for Agent tool executions so subagent progress, summaries, and failures render from structured `rawOutput` instead of falling back to the generic tool card.

**Architecture:** Preserve ACP `rawOutput` through the VSCode session/update pipeline into `ToolCallData`, then let the shared web UI router detect `task_execution` payloads and render a dedicated `AgentToolCall` component. Keep the change shared in `packages/webui` so VSCode and `ChatViewer` stay aligned.

**Tech Stack:** TypeScript, React, Vitest, shared `@qwen-code/webui` tool-call components.

### Task 1: Lock in the failing data-flow behavior

**Files:**

- Modify: `packages/vscode-ide-companion/src/services/qwenSessionUpdateHandler.test.ts`
- Create: `packages/vscode-ide-companion/src/webview/hooks/useToolCalls.test.tsx`

**Step 1: Write the failing tests**

- Add a session handler test asserting `tool_call_update` forwards `rawOutput` when ACP sends a `task_execution` payload.
- Add a hook test asserting `useToolCalls` stores and updates `rawOutput` for an agent tool call.

**Step 2: Run test to verify it fails**

Run: `npm test --workspace=packages/vscode-ide-companion -- --run qwenSessionUpdateHandler.test.ts useToolCalls.test.tsx`

Expected: failures because `rawOutput` is not preserved in the current handler/hook pipeline.

### Task 2: Lock in the failing renderer behavior

**Files:**

- Create: `packages/vscode-ide-companion/src/webview/components/messages/toolcalls/index.test.tsx`

**Step 1: Write the failing test**

- Render the routed tool call with `kind: 'other'` plus `rawOutput.type === 'task_execution'`.
- Assert the task description, active child tool, summary, and failure reason render from a dedicated agent display instead of generic text output.

**Step 2: Run test to verify it fails**

Run: `npm test --workspace=packages/vscode-ide-companion -- --run packages/vscode-ide-companion/src/webview/components/messages/toolcalls/index.test.tsx`

Expected: failure because the router only keys off `kind` and no dedicated agent component exists.

### Task 3: Preserve structured agent output end-to-end

**Files:**

- Modify: `packages/vscode-ide-companion/src/types/chatTypes.ts`
- Modify: `packages/vscode-ide-companion/src/services/qwenSessionUpdateHandler.ts`
- Modify: `packages/vscode-ide-companion/src/webview/hooks/useToolCalls.ts`
- Modify: `packages/webui/src/components/toolcalls/shared/types.ts`

**Step 1: Implement the minimal data model changes**

- Add optional `rawOutput` to the VSCode session/webview tool-call types.
- Forward `rawOutput` in `QwenSessionUpdateHandler`.
- Store/merge `rawOutput` in `useToolCalls`.
- Expose `rawOutput` in shared web UI tool-call data types.

**Step 2: Run the focused tests**

Run: `npm test --workspace=packages/vscode-ide-companion -- --run qwenSessionUpdateHandler.test.ts useToolCalls.test.tsx`

Expected: pass.

### Task 4: Add the shared agent tool-call UI

**Files:**

- Create: `packages/webui/src/components/toolcalls/AgentToolCall.tsx`
- Modify: `packages/webui/src/components/toolcalls/index.ts`
- Modify: `packages/vscode-ide-companion/src/webview/components/messages/toolcalls/index.tsx`
- Modify: `packages/webui/src/components/ChatViewer/ChatViewer.tsx`

**Step 1: Implement the minimal renderer**

- Add a guard for `rawOutput.type === 'task_execution'`.
- Render task description as the header.
- Show agent name + status, currently running child tools, completion summary, and failure/cancel reason.
- Keep the layout compatible with multiple parallel agent cards by rendering each tool call independently.

**Step 2: Run the focused renderer test**

Run: `npm test --workspace=packages/vscode-ide-companion -- --run packages/vscode-ide-companion/src/webview/components/messages/toolcalls/index.test.tsx`

Expected: pass.

### Task 5: Verify the integrated surface

**Files:**

- Modify: `packages/webui/src/index.ts`

**Step 1: Export the new shared component if needed**

- Re-export any new component/types needed by VSCode or `ChatViewer`.

**Step 2: Run package verification**

Run: `npm test --workspace=packages/vscode-ide-companion -- --run qwenSessionUpdateHandler.test.ts useToolCalls.test.tsx packages/vscode-ide-companion/src/webview/components/messages/toolcalls/index.test.tsx`
Run: `npm run check-types --workspace=packages/vscode-ide-companion`
Run: `npm run typecheck --workspace=packages/webui`

Expected: all targeted tests and typechecks pass.
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,48 @@ describe('QwenSessionUpdateHandler', () => {
locations: undefined,
});
});

it('forwards rawOutput for structured agent execution updates', () => {
const rawOutput = {
type: 'task_execution',
subagentName: 'Explore',
taskDescription: 'Explore auth logic',
taskPrompt: 'Inspect auth flow implementation',
status: 'running',
toolCalls: [
{
callId: 'child-1',
name: 'read',
status: 'executing',
},
],
};

const toolCallUpdate = {
sessionId: 'test-session',
update: {
sessionUpdate: 'tool_call_update',
toolCallId: 'call-agent',
kind: 'other',
title: 'Launch agent',
status: 'in_progress',
rawOutput,
},
} as SessionNotification;

handler.handleSessionUpdate(toolCallUpdate);

expect(mockCallbacks.onToolCall).toHaveBeenCalledWith({
toolCallId: 'call-agent',
kind: 'other',
title: 'Launch agent',
status: 'in_progress',
rawInput: undefined,
rawOutput,
content: undefined,
locations: undefined,
});
});
});

describe('plan handling', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ export class QwenSessionUpdateHandler {
title: (update.title as string) || undefined,
status: (update.status as string) || undefined,
rawInput: update.rawInput,
rawOutput: (update as { rawOutput?: unknown }).rawOutput,
content: update.content as
| Array<Record<string, unknown>>
| undefined,
Expand All @@ -134,6 +135,7 @@ export class QwenSessionUpdateHandler {
title: (update.title as string) || undefined,
status: (update.status as string) || undefined,
rawInput: update.rawInput,
rawOutput: (update as { rawOutput?: unknown }).rawOutput,
content: update.content as
| Array<Record<string, unknown>>
| undefined,
Expand Down
2 changes: 2 additions & 0 deletions packages/vscode-ide-companion/src/types/chatTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export interface ToolCallUpdateData {
title?: string;
status?: string;
rawInput?: unknown;
rawOutput?: unknown;
content?: Array<Record<string, unknown>>;
locations?: Array<{ path: string; line?: number | null }>;
timestamp?: number;
Expand Down Expand Up @@ -87,6 +88,7 @@ export interface ToolCallUpdate {
title?: string;
status?: 'pending' | 'in_progress' | 'completed' | 'failed';
rawInput?: unknown;
rawOutput?: unknown;
content?: Array<{
type: 'content' | 'diff';
content?: {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/

/** @vitest-environment jsdom */

import { act } from 'react';
import { createRoot, type Root } from 'react-dom/client';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { ToolCallRouter } from './index.js';

vi.mock('@qwen-code/webui', async () => {
const React = await vi.importActual<typeof import('react')>('react');

const renderLabel = (label: string) =>
function MockTool({
toolCall,
}: {
toolCall: {
title?: string;
rawOutput?: {
taskDescription?: string;
terminateReason?: string;
};
};
}) {
return React.createElement(
'div',
undefined,
`${label}:${toolCall.rawOutput?.taskDescription || toolCall.title || ''}:${toolCall.rawOutput?.terminateReason || ''}`,
);
};

return {
shouldShowToolCall: () => true,
isAgentExecutionToolCall: (toolCall: { rawOutput?: { type?: string } }) =>
toolCall.rawOutput?.type === 'task_execution',
GenericToolCall: renderLabel('generic'),
ThinkToolCall: renderLabel('think'),
SaveMemoryToolCall: renderLabel('memory'),
EditToolCall: renderLabel('edit'),
WriteToolCall: renderLabel('write'),
SearchToolCall: renderLabel('search'),
UpdatedPlanToolCall: renderLabel('plan'),
ShellToolCall: renderLabel('shell'),
ReadToolCall: renderLabel('read'),
WebFetchToolCall: renderLabel('web'),
AgentToolCall: renderLabel('agent'),
};
});

describe('ToolCallRouter agent execution rendering', () => {
let container: HTMLDivElement | null = null;
let root: Root | null = null;

beforeEach(() => {
(
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
).IS_REACT_ACT_ENVIRONMENT = true;
container = document.createElement('div');
document.body.appendChild(container);
root = createRoot(container);
});

afterEach(() => {
if (root) {
act(() => {
root?.unmount();
});
root = null;
}
if (container) {
container.remove();
container = null;
}
});

it('renders a dedicated view for structured agent progress and summary', () => {
act(() => {
root?.render(
<ToolCallRouter
toolCall={
{
toolCallId: 'agent-1',
kind: 'other',
title: 'Launch agent',
status: 'completed',
rawOutput: {
type: 'task_execution',
subagentName: 'Explore',
taskDescription: 'Explore auth logic',
taskPrompt: 'Inspect auth flow implementation',
status: 'completed',
toolCalls: [
{
callId: 'child-1',
name: 'read',
status: 'success',
},
{
callId: 'child-2',
name: 'grep',
status: 'success',
},
],
executionSummary: {
totalToolCalls: 2,
totalTokens: 1234,
totalDurationMs: 2200,
},
},
} as never
}
/>,
);
});

expect(container?.textContent).toContain('agent:Explore auth logic');
});

it('renders the agent failure reason from structured rawOutput', () => {
act(() => {
root?.render(
<ToolCallRouter
toolCall={
{
toolCallId: 'agent-2',
kind: 'other',
title: 'Launch agent',
status: 'failed',
rawOutput: {
type: 'task_execution',
subagentName: 'Explore',
taskDescription: 'Explore auth logic',
taskPrompt: 'Inspect auth flow implementation',
status: 'failed',
terminateReason: 'Subagent crashed',
},
} as never
}
/>,
);
});

expect(container?.textContent).toContain(
'agent:Explore auth logic:Subagent crashed',
);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import type { FC } from 'react';
import {
shouldShowToolCall,
// All ToolCall components from webui
AgentToolCall,
isAgentExecutionToolCall,
GenericToolCall,
ThinkToolCall,
SaveMemoryToolCall,
Expand All @@ -22,13 +24,19 @@ import {
ReadToolCall,
WebFetchToolCall,
} from '@qwen-code/webui';
import type { BaseToolCallProps } from '@qwen-code/webui';
import type { BaseToolCallProps, ToolCallData } from '@qwen-code/webui';

/**
* Factory function that returns the appropriate tool call component based on kind
*/
export const getToolCallComponent = (kind: string): FC<BaseToolCallProps> => {
const normalizedKind = kind.toLowerCase();
export const getToolCallComponent = (
toolCall: ToolCallData,
): FC<BaseToolCallProps> => {
if (isAgentExecutionToolCall(toolCall)) {
return AgentToolCall;
}

const normalizedKind = toolCall.kind.toLowerCase();

// Route to specialized components
switch (normalizedKind) {
Expand Down Expand Up @@ -91,7 +99,7 @@ export const ToolCallRouter: React.FC<BaseToolCallProps> = ({ toolCall }) => {
}

// Get the appropriate component for this kind
const Component = getToolCallComponent(toolCall.kind);
const Component = getToolCallComponent(toolCall);

// Render the specialized component
return <Component toolCall={toolCall} />;
Expand Down
Loading
Loading