From 2490a03fe41fe61fd0bac6400475068b73edc3ff Mon Sep 17 00:00:00 2001 From: yiliang114 <1204183885@qq.com> Date: Mon, 23 Mar 2026 00:47:19 +0800 Subject: [PATCH 1/2] feat(webui): unify remaining tool display labels Refs #1367 --- .../components/messages/toolcalls/index.tsx | 5 + .../components/toolcalls/GenericToolCall.tsx | 30 ++--- .../src/components/toolcalls/ReadToolCall.tsx | 14 ++- .../components/toolcalls/SearchToolCall.tsx | 19 +-- .../components/toolcalls/ShellToolCall.tsx | 3 +- .../toolcalls/UpdatedPlanToolCall.tsx | 6 +- .../components/toolcalls/WebFetchToolCall.tsx | 3 +- .../components/toolcalls/labelUtils.test.ts | 75 ++++++++++++ .../src/components/toolcalls/labelUtils.ts | 110 ++++++++++++++++++ 9 files changed, 216 insertions(+), 49 deletions(-) create mode 100644 packages/webui/src/components/toolcalls/labelUtils.test.ts create mode 100644 packages/webui/src/components/toolcalls/labelUtils.ts diff --git a/packages/vscode-ide-companion/src/webview/components/messages/toolcalls/index.tsx b/packages/vscode-ide-companion/src/webview/components/messages/toolcalls/index.tsx index 4c97f146c6..a9b6e8ad08 100644 --- a/packages/vscode-ide-companion/src/webview/components/messages/toolcalls/index.tsx +++ b/packages/vscode-ide-companion/src/webview/components/messages/toolcalls/index.tsx @@ -33,6 +33,11 @@ export const getToolCallComponent = (kind: string): FC => { // Route to specialized components switch (normalizedKind) { case 'read': + case 'read_file': + case 'read_many_files': + case 'readmanyfiles': + case 'list_directory': + case 'listfiles': return ReadToolCall; case 'write': diff --git a/packages/webui/src/components/toolcalls/GenericToolCall.tsx b/packages/webui/src/components/toolcalls/GenericToolCall.tsx index 4955dcea1d..4dc00fc9c0 100644 --- a/packages/webui/src/components/toolcalls/GenericToolCall.tsx +++ b/packages/webui/src/components/toolcalls/GenericToolCall.tsx @@ -16,6 +16,7 @@ import { groupContent, } from './shared/index.js'; import type { BaseToolCallProps } from './shared/index.js'; +import { getToolDisplayLabel } from './labelUtils.js'; /** * Generic tool call component that can display any tool call type @@ -29,24 +30,7 @@ export const GenericToolCall: FC = ({ }) => { const { kind, title, content, locations, toolCallId } = toolCall; const operationText = safeTitle(title); - - /** - * Map tool call kind to appropriate display name - */ - const getDisplayLabel = (): string => { - const normalizedKind = kind.toLowerCase(); - if (normalizedKind === 'task') { - return 'Task'; - } else if (normalizedKind === 'web_fetch') { - return 'WebFetch'; - } else if (normalizedKind === 'web_search') { - return 'WebSearch'; - } else if (normalizedKind === 'exit_plan_mode') { - return 'ExitPlanMode'; - } else { - return kind; // fallback to original kind if not mapped - } - }; + const displayLabel = getToolDisplayLabel({ kind, title }); // Group content by type const { textOutputs, errors } = groupContent(content); @@ -55,7 +39,7 @@ export const GenericToolCall: FC = ({ if (errors.length > 0) { return ( - +
{operationText}
@@ -76,7 +60,7 @@ export const GenericToolCall: FC = ({ return ( - +
{operationText}
@@ -95,7 +79,7 @@ export const GenericToolCall: FC = ({ : 'success'; return ( = ({ : 'success'; return ( = ({ : 'success'; return ( = ({ isFirst, isLast, }) => { - const { content, locations, toolCallId } = toolCall; + const { kind, title, content, locations, toolCallId } = toolCall; const platform = usePlatform(); const openedDiffsRef = useRef>(new Map()); const [isExpanded, setIsExpanded] = useState(false); @@ -136,13 +137,14 @@ export const ReadToolCall: FC = ({ // Compute container status based on toolCall.status const containerStatus = mapToolStatusToContainerStatus(toolCall.status); + const displayLabel = getToolDisplayLabel({ kind, title }); // Error case: show error from content if (errors.length > 0) { const path = locations?.[0]?.path || ''; return ( = ({ textOutputs.length > 0 ? textOutputs.join('\n') : 'Read operation failed'; return ( = ({ const path = diffs[0]?.path || locations?.[0]?.path || ''; return ( = ({ return ( = ({ return ( ); -/** - * Map tool call kind to appropriate display name - */ -const getDisplayLabel = (kind: string): string => { - const normalizedKind = kind.toLowerCase(); - if (normalizedKind === 'grep' || normalizedKind === 'grep_search') { - return 'Grep'; - } else if (normalizedKind === 'glob') { - return 'Glob'; - } else if (normalizedKind === 'web_search') { - return 'WebSearch'; - } else { - return 'Search'; - } -}; - /** * Specialized component for Search tool calls * Optimized for displaying search operations and results @@ -114,7 +99,7 @@ export const SearchToolCall: FC = ({ }) => { const { kind, title, content, locations } = toolCall; const queryText = safeTitle(title); - const displayLabel = getDisplayLabel(kind); + const displayLabel = getToolDisplayLabel({ kind, title }); const containerStatus: ContainerStatus = mapToolStatusToContainerStatus( toolCall.status, ); diff --git a/packages/webui/src/components/toolcalls/ShellToolCall.tsx b/packages/webui/src/components/toolcalls/ShellToolCall.tsx index e897c5f08e..6ec67e1a75 100644 --- a/packages/webui/src/components/toolcalls/ShellToolCall.tsx +++ b/packages/webui/src/components/toolcalls/ShellToolCall.tsx @@ -19,6 +19,7 @@ import type { BaseToolCallProps, ToolCallContainerProps, } from './shared/index.js'; +import { getToolDisplayLabel } from './labelUtils.js'; import './ShellToolCall.css'; @@ -129,7 +130,7 @@ const ShellToolCallImpl: FC = ({ const Container = variant === 'execute' ? ExecuteToolCallContainer : ToolCallContainer; - const label = variant === 'execute' ? 'Execute' : 'Bash'; + const label = getToolDisplayLabel({ kind: toolCall.kind, title }); // Group content by type const { textOutputs, errors } = groupContent(content); diff --git a/packages/webui/src/components/toolcalls/UpdatedPlanToolCall.tsx b/packages/webui/src/components/toolcalls/UpdatedPlanToolCall.tsx index 414ce67233..d35db05bee 100644 --- a/packages/webui/src/components/toolcalls/UpdatedPlanToolCall.tsx +++ b/packages/webui/src/components/toolcalls/UpdatedPlanToolCall.tsx @@ -16,6 +16,7 @@ import type { PlanEntryStatus, } from './shared/index.js'; import { CheckboxDisplay } from './CheckboxDisplay.js'; +import { getToolDisplayLabel } from './labelUtils.js'; /** * Custom container for UpdatedPlanToolCall with specific styling @@ -141,7 +142,10 @@ export const UpdatedPlanToolCall: FC = ({ } const entries = parsePlanEntries(textOutputs); - const label = safeTitle(toolCall.title) || 'TodoWrite'; + const label = getToolDisplayLabel({ + kind: toolCall.kind, + title: safeTitle(toolCall.title), + }); return ( = ({ const { title, content, rawInput, toolCallId } = toolCall; const webTarget = getWebTarget(variant, title, rawInput); - const label = variant === 'fetch' ? 'Web Fetch' : 'Web Search'; + const label = getToolDisplayLabel({ kind: toolCall.kind, title }); // Group content by type const { textOutputs, errors } = groupContent(content); diff --git a/packages/webui/src/components/toolcalls/labelUtils.test.ts b/packages/webui/src/components/toolcalls/labelUtils.test.ts new file mode 100644 index 0000000000..19b2efabf0 --- /dev/null +++ b/packages/webui/src/components/toolcalls/labelUtils.test.ts @@ -0,0 +1,75 @@ +/** + * @license + * Copyright 2025 Qwen Team + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, expect, it } from 'vitest'; +import { getToolDisplayLabel } from './labelUtils.js'; + +describe('getToolDisplayLabel', () => { + it('unifies shell tool variants to Shell', () => { + expect(getToolDisplayLabel({ kind: 'execute' })).toBe('Shell'); + expect(getToolDisplayLabel({ kind: 'bash' })).toBe('Shell'); + expect(getToolDisplayLabel({ kind: 'command' })).toBe('Shell'); + }); + + it('uses core names for web fetch and web search', () => { + expect(getToolDisplayLabel({ kind: 'web_fetch' })).toBe('WebFetch'); + expect(getToolDisplayLabel({ kind: 'web_search' })).toBe('WebSearch'); + }); + + it('normalizes todo write labels even when older titles are still present', () => { + expect( + getToolDisplayLabel({ kind: 'todo_write', title: 'Updated Plan' }), + ).toBe('TodoWrite'); + expect( + getToolDisplayLabel({ kind: 'update_todos', title: 'Update Todos' }), + ).toBe('TodoWrite'); + expect( + getToolDisplayLabel({ kind: 'updated_plan', title: 'Updated Plan' }), + ).toBe('TodoWrite'); + }); + + it('uses core names for read-family tools by kind', () => { + expect(getToolDisplayLabel({ kind: 'read_many_files' })).toBe( + 'ReadManyFiles', + ); + expect(getToolDisplayLabel({ kind: 'list_directory' })).toBe('ListFiles'); + }); + + it('derives read-family tool names from the title when kind is normalized', () => { + expect( + getToolDisplayLabel({ + kind: 'read', + title: 'ReadFile packages/webui/src/index.ts', + }), + ).toBe('ReadFile'); + expect( + getToolDisplayLabel({ + kind: 'read', + title: 'ReadManyFiles packages/webui/src packages/core/src', + }), + ).toBe('ReadManyFiles'); + expect( + getToolDisplayLabel({ + kind: 'read', + title: 'ListFiles packages/webui/src/components', + }), + ).toBe('ListFiles'); + expect( + getToolDisplayLabel({ + kind: 'read', + title: 'Skill open-source-flow', + }), + ).toBe('Skill'); + }); + + it('capitalizes generic label mappings that still fall through generic rendering', () => { + expect(getToolDisplayLabel({ kind: 'task' })).toBe('Task'); + expect(getToolDisplayLabel({ kind: 'skill' })).toBe('Skill'); + expect(getToolDisplayLabel({ kind: 'exit_plan_mode' })).toBe( + 'ExitPlanMode', + ); + }); +}); diff --git a/packages/webui/src/components/toolcalls/labelUtils.ts b/packages/webui/src/components/toolcalls/labelUtils.ts new file mode 100644 index 0000000000..079c450bd4 --- /dev/null +++ b/packages/webui/src/components/toolcalls/labelUtils.ts @@ -0,0 +1,110 @@ +/** + * @license + * Copyright 2025 Qwen Team + * SPDX-License-Identifier: Apache-2.0 + */ + +const normalizeValue = (value: unknown): string => + typeof value === 'string' ? value.trim().toLowerCase() : ''; + +const startsWithAny = (value: string, prefixes: string[]): boolean => + prefixes.some((prefix) => value.startsWith(prefix)); + +const getReadLikeLabelFromTitle = (title: unknown): string | null => { + const normalizedTitle = normalizeValue(title); + + if (startsWithAny(normalizedTitle, ['readmanyfiles', 'read many files'])) { + return 'ReadManyFiles'; + } + + if ( + startsWithAny(normalizedTitle, [ + 'listfiles', + 'list files', + 'list directory', + ]) + ) { + return 'ListFiles'; + } + + if (startsWithAny(normalizedTitle, ['readfile', 'read file'])) { + return 'ReadFile'; + } + + if (startsWithAny(normalizedTitle, ['skill'])) { + return 'Skill'; + } + + return null; +}; + +export const getToolDisplayLabel = ({ + kind, + title, +}: { + kind: string; + title?: unknown; +}): string => { + const normalizedKind = normalizeValue(kind); + + switch (normalizedKind) { + case 'execute': + case 'bash': + case 'command': + case 'shell': + case 'run_shell_command': + return 'Shell'; + case 'todo_write': + case 'todowrite': + case 'update_todos': + case 'updated_plan': + case 'updatedplan': + return 'TodoWrite'; + case 'web_fetch': + case 'webfetch': + case 'fetch': + return 'WebFetch'; + case 'web_search': + case 'websearch': + return 'WebSearch'; + case 'grep': + case 'grep_search': + return 'Grep'; + case 'glob': + return 'Glob'; + case 'search': + case 'find': + return 'Search'; + case 'write': + case 'write_file': + case 'writefile': + return 'WriteFile'; + case 'read_many_files': + case 'readmanyfiles': + return 'ReadManyFiles'; + case 'list_directory': + case 'listfiles': + case 'ls': + return 'ListFiles'; + case 'read_file': + case 'readfile': + return 'ReadFile'; + case 'save_memory': + case 'savememory': + case 'memory': + return 'SaveMemory'; + case 'exit_plan_mode': + return 'ExitPlanMode'; + case 'task': + return 'Task'; + case 'skill': + return 'Skill'; + case 'think': + case 'thinking': + return 'Think'; + case 'read': + return getReadLikeLabelFromTitle(title) ?? 'Read'; + default: + return kind; + } +}; From 401ec89ffea9a3bd4d62ee0c3e412b79ce3b0a7c Mon Sep 17 00:00:00 2001 From: yiliang114 <1204183885@qq.com> Date: Sat, 4 Apr 2026 14:10:03 +0800 Subject: [PATCH 2/2] fix(webui): handle switch_mode and read-family routing --- .../components/ChatViewer/ChatViewer.test.tsx | 41 +++++++++++++++++++ .../src/components/ChatViewer/ChatViewer.tsx | 5 +++ .../components/toolcalls/labelUtils.test.ts | 1 + .../src/components/toolcalls/labelUtils.ts | 1 + 4 files changed, 48 insertions(+) create mode 100644 packages/webui/src/components/ChatViewer/ChatViewer.test.tsx diff --git a/packages/webui/src/components/ChatViewer/ChatViewer.test.tsx b/packages/webui/src/components/ChatViewer/ChatViewer.test.tsx new file mode 100644 index 0000000000..65ac7006ec --- /dev/null +++ b/packages/webui/src/components/ChatViewer/ChatViewer.test.tsx @@ -0,0 +1,41 @@ +/** + * @license + * Copyright 2025 Qwen Team + * SPDX-License-Identifier: Apache-2.0 + */ + +// eslint-disable-next-line import/no-internal-modules +import { renderToStaticMarkup } from 'react-dom/server'; +import { describe, expect, it } from 'vitest'; +import { ChatViewer, type ChatMessageData } from './ChatViewer.js'; + +const createToolCallMessage = (kind: string): ChatMessageData => ({ + uuid: `${kind}-1`, + timestamp: '2026-03-22T16:48:35.000Z', + type: 'tool_call', + toolCall: { + toolCallId: `${kind}-tool-call`, + kind, + title: kind, + status: 'completed', + locations: [{ path: 'src/index.ts' }, { path: 'src/App.tsx' }], + }, +}); + +describe('ChatViewer tool routing', () => { + it('routes read_many_files to ReadToolCall', () => { + const html = renderToStaticMarkup( + , + ); + + expect(html).toContain('ReadToolCall'); + }); + + it('routes list_directory to ReadToolCall', () => { + const html = renderToStaticMarkup( + , + ); + + expect(html).toContain('ReadToolCall'); + }); +}); diff --git a/packages/webui/src/components/ChatViewer/ChatViewer.tsx b/packages/webui/src/components/ChatViewer/ChatViewer.tsx index ff3d7d8729..121f31158a 100644 --- a/packages/webui/src/components/ChatViewer/ChatViewer.tsx +++ b/packages/webui/src/components/ChatViewer/ChatViewer.tsx @@ -151,6 +151,11 @@ function getToolCallComponent(kind: string) { switch (normalizedKind) { case 'read': + case 'read_file': + case 'read_many_files': + case 'readmanyfiles': + case 'list_directory': + case 'listfiles': return ReadToolCall; case 'write': return WriteToolCall; diff --git a/packages/webui/src/components/toolcalls/labelUtils.test.ts b/packages/webui/src/components/toolcalls/labelUtils.test.ts index 19b2efabf0..4247b53a21 100644 --- a/packages/webui/src/components/toolcalls/labelUtils.test.ts +++ b/packages/webui/src/components/toolcalls/labelUtils.test.ts @@ -71,5 +71,6 @@ describe('getToolDisplayLabel', () => { expect(getToolDisplayLabel({ kind: 'exit_plan_mode' })).toBe( 'ExitPlanMode', ); + expect(getToolDisplayLabel({ kind: 'switch_mode' })).toBe('ExitPlanMode'); }); }); diff --git a/packages/webui/src/components/toolcalls/labelUtils.ts b/packages/webui/src/components/toolcalls/labelUtils.ts index 079c450bd4..dff7ea7455 100644 --- a/packages/webui/src/components/toolcalls/labelUtils.ts +++ b/packages/webui/src/components/toolcalls/labelUtils.ts @@ -94,6 +94,7 @@ export const getToolDisplayLabel = ({ case 'memory': return 'SaveMemory'; case 'exit_plan_mode': + case 'switch_mode': return 'ExitPlanMode'; case 'task': return 'Task';