mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-08-14 19:24:54 +00:00
Merge pull request #2595 from QwenLM/fix/1367-tool-display-unification
feat(webui): unify remaining tool display labels
This commit is contained in:
commit
3fa3a1773e
11 changed files with 264 additions and 49 deletions
|
|
@ -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':
|
||||
|
|
|
|||
41
packages/webui/src/components/ChatViewer/ChatViewer.test.tsx
Normal file
41
packages/webui/src/components/ChatViewer/ChatViewer.test.tsx
Normal 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');
|
||||
});
|
||||
});
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
76
packages/webui/src/components/toolcalls/labelUtils.test.ts
Normal file
76
packages/webui/src/components/toolcalls/labelUtils.test.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
111
packages/webui/src/components/toolcalls/labelUtils.ts
Normal file
111
packages/webui/src/components/toolcalls/labelUtils.ts
Normal 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;
|
||||
}
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue