Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -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':
Expand Down
41 changes: 41 additions & 0 deletions packages/webui/src/components/ChatViewer/ChatViewer.test.tsx
Original file line number Diff line number Diff line change
@@ -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');
});
});
5 changes: 5 additions & 0 deletions packages/webui/src/components/ChatViewer/ChatViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
30 changes: 7 additions & 23 deletions packages/webui/src/components/toolcalls/GenericToolCall.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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);
Expand All @@ -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">
Expand All @@ -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">
Expand All @@ -95,7 +79,7 @@ export const GenericToolCall: FC<BaseToolCallProps> = ({
: 'success';
return (
<ToolCallContainer
label={getDisplayLabel()}
label={displayLabel}
status={statusFlag}
toolCallId={toolCallId}
isFirst={isFirst}
Expand All @@ -114,7 +98,7 @@ export const GenericToolCall: FC<BaseToolCallProps> = ({
: 'success';
return (
<ToolCallContainer
label={getDisplayLabel()}
label={displayLabel}
status={statusFlag}
toolCallId={toolCallId}
isFirst={isFirst}
Expand All @@ -133,7 +117,7 @@ export const GenericToolCall: FC<BaseToolCallProps> = ({
: 'success';
return (
<ToolCallContainer
label={getDisplayLabel()}
label={displayLabel}
status={statusFlag}
toolCallId={toolCallId}
isFirst={isFirst}
Expand Down
14 changes: 8 additions & 6 deletions packages/webui/src/components/toolcalls/ReadToolCall.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import type {
BaseToolCallProps,
ToolCallContainerProps,
} from './shared/index.js';
import { getToolDisplayLabel } from './labelUtils.js';

/**
* Simple container for Read tool calls
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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}
Expand Down Expand Up @@ -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}
Expand All @@ -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}
Expand Down Expand Up @@ -226,7 +228,7 @@ export const ReadToolCall: FC<BaseToolCallProps> = ({

return (
<ReadToolCallContainer
label="Read"
label={displayLabel}
className="read-tool-call-success"
status={containerStatus}
toolCallId={toolCallId}
Expand Down Expand Up @@ -293,7 +295,7 @@ export const ReadToolCall: FC<BaseToolCallProps> = ({

return (
<ReadToolCallContainer
label="Read"
label={displayLabel}
className="read-tool-call-success"
status={containerStatus}
toolCallId={toolCallId}
Expand Down
19 changes: 2 additions & 17 deletions packages/webui/src/components/toolcalls/SearchToolCall.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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,
);
Expand Down
3 changes: 2 additions & 1 deletion packages/webui/src/components/toolcalls/ShellToolCall.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import type {
BaseToolCallProps,
ToolCallContainerProps,
} from './shared/index.js';
import { getToolDisplayLabel } from './labelUtils.js';

import './ShellToolCall.css';

Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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);
Expand Down
76 changes: 76 additions & 0 deletions packages/webui/src/components/toolcalls/labelUtils.test.ts
Original file line number Diff line number Diff line change
@@ -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');
});
});
Loading
Loading