Merge pull request #2595 from QwenLM/fix/1367-tool-display-unification

feat(webui): unify remaining tool display labels
This commit is contained in:
tanzhenxin 2026-04-05 14:33:18 +08:00 committed by GitHub
commit 3fa3a1773e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 264 additions and 49 deletions

View file

@ -33,6 +33,11 @@ export const getToolCallComponent = (kind: string): FC<BaseToolCallProps> => {
// 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':

View file

@ -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(
<ChatViewer messages={[createToolCallMessage('read_many_files')]} />,
);
expect(html).toContain('ReadToolCall');
});
it('routes list_directory to ReadToolCall', () => {
const html = renderToStaticMarkup(
<ChatViewer messages={[createToolCallMessage('list_directory')]} />,
);
expect(html).toContain('ReadToolCall');
});
});

View file

@ -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;

View file

@ -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<BaseToolCallProps> = ({
}) => {
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<BaseToolCallProps> = ({
if (errors.length > 0) {
return (
<ToolCallCard icon="🔧">
<ToolCallRow label={getDisplayLabel()}>
<ToolCallRow label={displayLabel}>
<div>{operationText}</div>
</ToolCallRow>
<ToolCallRow label="Error">
@ -76,7 +60,7 @@ export const GenericToolCall: FC<BaseToolCallProps> = ({
return (
<ToolCallCard icon="🔧">
<ToolCallRow label={getDisplayLabel()}>
<ToolCallRow label={displayLabel}>
<div>{operationText}</div>
</ToolCallRow>
<ToolCallRow label="Output">
@ -95,7 +79,7 @@ export const GenericToolCall: FC<BaseToolCallProps> = ({
: 'success';
return (
<ToolCallContainer
label={getDisplayLabel()}
label={displayLabel}
status={statusFlag}
toolCallId={toolCallId}
isFirst={isFirst}
@ -114,7 +98,7 @@ export const GenericToolCall: FC<BaseToolCallProps> = ({
: 'success';
return (
<ToolCallContainer
label={getDisplayLabel()}
label={displayLabel}
status={statusFlag}
toolCallId={toolCallId}
isFirst={isFirst}
@ -133,7 +117,7 @@ export const GenericToolCall: FC<BaseToolCallProps> = ({
: 'success';
return (
<ToolCallContainer
label={getDisplayLabel()}
label={displayLabel}
status={statusFlag}
toolCallId={toolCallId}
isFirst={isFirst}

View file

@ -19,6 +19,7 @@ import type {
BaseToolCallProps,
ToolCallContainerProps,
} from './shared/index.js';
import { getToolDisplayLabel } from './labelUtils.js';
/**
* Simple container for Read tool calls
@ -65,7 +66,7 @@ export const ReadToolCall: FC<BaseToolCallProps> = ({
isFirst,
isLast,
}) => {
const { content, locations, toolCallId } = toolCall;
const { kind, title, content, locations, toolCallId } = toolCall;
const platform = usePlatform();
const openedDiffsRef = useRef<Map<string, string>>(new Map());
const [isExpanded, setIsExpanded] = useState(false);
@ -136,13 +137,14 @@ export const ReadToolCall: FC<BaseToolCallProps> = ({
// 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 (
<ReadToolCallContainer
label="Read"
label={displayLabel}
className="read-tool-call-error"
status="error"
toolCallId={toolCallId}
@ -170,7 +172,7 @@ export const ReadToolCall: FC<BaseToolCallProps> = ({
textOutputs.length > 0 ? textOutputs.join('\n') : 'Read operation failed';
return (
<ReadToolCallContainer
label="Read"
label={displayLabel}
className="read-tool-call-error"
status="error"
toolCallId={toolCallId}
@ -196,7 +198,7 @@ export const ReadToolCall: FC<BaseToolCallProps> = ({
const path = diffs[0]?.path || locations?.[0]?.path || '';
return (
<ReadToolCallContainer
label="Read"
label={displayLabel}
className="read-tool-call-success"
status={containerStatus}
toolCallId={toolCallId}
@ -226,7 +228,7 @@ export const ReadToolCall: FC<BaseToolCallProps> = ({
return (
<ReadToolCallContainer
label="Read"
label={displayLabel}
className="read-tool-call-success"
status={containerStatus}
toolCallId={toolCallId}
@ -293,7 +295,7 @@ export const ReadToolCall: FC<BaseToolCallProps> = ({
return (
<ReadToolCallContainer
label="Read"
label={displayLabel}
className="read-tool-call-success"
status={containerStatus}
toolCallId={toolCallId}

View file

@ -15,6 +15,7 @@ import {
} from './shared/index.js';
import type { BaseToolCallProps, ContainerStatus } from './shared/index.js';
import { FileLink } from '../layout/FileLink.js';
import { getToolDisplayLabel } from './labelUtils.js';
/**
* Collapsible output component for search results
@ -87,22 +88,6 @@ const LocationsListLocal: FC<{
</div>
);
/**
* 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<BaseToolCallProps> = ({
}) => {
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,
);

View file

@ -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<BaseToolCallProps & { variant: ShellVariant }> = ({
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);

View file

@ -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<BaseToolCallProps> = ({
}
const entries = parsePlanEntries(textOutputs);
const label = safeTitle(toolCall.title) || 'TodoWrite';
const label = getToolDisplayLabel({
kind: toolCall.kind,
title: safeTitle(toolCall.title),
});
return (
<PlanToolCallContainer

View file

@ -15,6 +15,7 @@ import {
mapToolStatusToContainerStatus,
} from './shared/index.js';
import type { BaseToolCallProps } from './shared/index.js';
import { getToolDisplayLabel } from './labelUtils.js';
type WebVariant = 'fetch' | 'search';
@ -121,7 +122,7 @@ const WebFetchToolCallImpl: FC<BaseToolCallProps & { variant: WebVariant }> = ({
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);

View file

@ -0,0 +1,76 @@
/**
* @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',
);
expect(getToolDisplayLabel({ kind: 'switch_mode' })).toBe('ExitPlanMode');
});
});

View file

@ -0,0 +1,111 @@
/**
* @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':
case 'switch_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;
}
};