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 4c97f146c..a9b6e8ad0 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 4955dcea1..4dc00fc9c 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 e897c5f08..6ec67e1a7 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 414ce6723..d35db05be 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 000000000..19b2efabf --- /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 000000000..079c450bd --- /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; + } +};