mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-21 01:07:46 +00:00
refactor(webview): 重构工具调用显示逻辑
- 新增多个工具调用组件,分别处理不同类型的工具调用 - 优化工具调用卡片的样式和布局 - 添加加载状态和随机加载消息 - 重构 App 组件,支持新的工具调用显示逻辑
This commit is contained in:
@@ -2,188 +2,37 @@
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Main ToolCall component - uses factory pattern to route to specialized components
|
||||
*
|
||||
* This file serves as the public API for tool call rendering.
|
||||
* It re-exports the router and types from the toolcalls module.
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import { ToolCallRouter } from './toolcalls/index.js';
|
||||
|
||||
export interface ToolCallContent {
|
||||
type: 'content' | 'diff';
|
||||
// For content type
|
||||
content?: {
|
||||
type: string;
|
||||
text?: string;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
// For diff type
|
||||
path?: string;
|
||||
oldText?: string | null;
|
||||
newText?: string;
|
||||
}
|
||||
// Re-export types from the toolcalls module for backward compatibility
|
||||
export type {
|
||||
ToolCallData,
|
||||
BaseToolCallProps as ToolCallProps,
|
||||
} from './toolcalls/shared/types.js';
|
||||
|
||||
export interface ToolCallData {
|
||||
toolCallId: string;
|
||||
kind: string;
|
||||
title: string;
|
||||
status: 'pending' | 'in_progress' | 'completed' | 'failed';
|
||||
rawInput?: string | object;
|
||||
content?: ToolCallContent[];
|
||||
locations?: Array<{
|
||||
path: string;
|
||||
line?: number | null;
|
||||
}>;
|
||||
}
|
||||
// Re-export the content type for external use
|
||||
export type { ToolCallContent } from './toolcalls/shared/types.js';
|
||||
|
||||
export interface ToolCallProps {
|
||||
toolCall: ToolCallData;
|
||||
}
|
||||
|
||||
const StatusTag: React.FC<{ status: string }> = ({ status }) => {
|
||||
const getStatusInfo = () => {
|
||||
switch (status) {
|
||||
case 'pending':
|
||||
return { className: 'status-pending', text: 'Pending', icon: '⏳' };
|
||||
case 'in_progress':
|
||||
return {
|
||||
className: 'status-in-progress',
|
||||
text: 'In Progress',
|
||||
icon: '🔄',
|
||||
};
|
||||
case 'completed':
|
||||
return { className: 'status-completed', text: 'Completed', icon: '✓' };
|
||||
case 'failed':
|
||||
return { className: 'status-failed', text: 'Failed', icon: '✗' };
|
||||
default:
|
||||
return { className: 'status-unknown', text: status, icon: '•' };
|
||||
}
|
||||
};
|
||||
|
||||
const { className, text, icon } = getStatusInfo();
|
||||
return (
|
||||
<span className={`tool-call-status ${className}`}>
|
||||
<span className="status-icon">{icon}</span>
|
||||
{text}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
const ContentView: React.FC<{ content: ToolCallContent }> = ({ content }) => {
|
||||
// Handle diff type
|
||||
if (content.type === 'diff') {
|
||||
const fileName =
|
||||
content.path?.split(/[/\\]/).pop() || content.path || 'Unknown file';
|
||||
const oldText = content.oldText || '';
|
||||
const newText = content.newText || '';
|
||||
|
||||
return (
|
||||
<div className="tool-call-diff">
|
||||
<div className="diff-header">
|
||||
<span className="diff-icon">📝</span>
|
||||
<span className="diff-filename">{fileName}</span>
|
||||
</div>
|
||||
<div className="diff-content">
|
||||
<div className="diff-side">
|
||||
<div className="diff-side-label">Before</div>
|
||||
<pre className="diff-code">{oldText || '(empty)'}</pre>
|
||||
</div>
|
||||
<div className="diff-arrow">→</div>
|
||||
<div className="diff-side">
|
||||
<div className="diff-side-label">After</div>
|
||||
<pre className="diff-code">{newText || '(empty)'}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Handle content type with text
|
||||
if (content.type === 'content' && content.content?.text) {
|
||||
return (
|
||||
<div className="tool-call-content">
|
||||
<div className="content-text">{content.content.text}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const getKindDisplayName = (kind: string): { name: string; icon: string } => {
|
||||
const kindMap: Record<string, { name: string; icon: string }> = {
|
||||
edit: { name: 'File Edit', icon: '✏️' },
|
||||
read: { name: 'File Read', icon: '📖' },
|
||||
execute: { name: 'Shell Command', icon: '⚡' },
|
||||
fetch: { name: 'Web Fetch', icon: '🌐' },
|
||||
delete: { name: 'Delete', icon: '🗑️' },
|
||||
move: { name: 'Move/Rename', icon: '📦' },
|
||||
search: { name: 'Search', icon: '🔍' },
|
||||
think: { name: 'Thinking', icon: '💭' },
|
||||
other: { name: 'Other', icon: '🔧' },
|
||||
};
|
||||
|
||||
return kindMap[kind] || { name: kind, icon: '🔧' };
|
||||
};
|
||||
|
||||
const formatRawInput = (rawInput: string | object | undefined): string => {
|
||||
if (rawInput === undefined) {
|
||||
return '';
|
||||
}
|
||||
if (typeof rawInput === 'string') {
|
||||
return rawInput;
|
||||
}
|
||||
return JSON.stringify(rawInput, null, 2);
|
||||
};
|
||||
|
||||
export const ToolCall: React.FC<ToolCallProps> = ({ toolCall }) => {
|
||||
const { kind, title, status, rawInput, content, locations, toolCallId } =
|
||||
toolCall;
|
||||
const kindInfo: { name: string; icon: string } = getKindDisplayName(kind);
|
||||
|
||||
return (
|
||||
<div className="tool-call-card">
|
||||
<div className="tool-call-header">
|
||||
<span className="tool-call-kind-icon">{kindInfo.icon}</span>
|
||||
<span className="tool-call-title">{title || kindInfo.name}</span>
|
||||
<StatusTag status={status} />
|
||||
</div>
|
||||
|
||||
{/* Show raw input if available */}
|
||||
{rawInput !== undefined && rawInput !== null ? (
|
||||
<div className="tool-call-raw-input">
|
||||
<div className="raw-input-label">Input</div>
|
||||
<pre className="raw-input-content">{formatRawInput(rawInput)}</pre>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{/* Show locations if available */}
|
||||
{locations && locations.length > 0 && (
|
||||
<div className="tool-call-locations">
|
||||
<div className="locations-label">Files</div>
|
||||
{locations.map((location, index) => (
|
||||
<div key={index} className="location-item">
|
||||
<span className="location-icon">📄</span>
|
||||
<span className="location-path">{location.path}</span>
|
||||
{location.line !== null && location.line !== undefined && (
|
||||
<span className="location-line">:{location.line}</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Show content if available */}
|
||||
{content && content.length > 0 && (
|
||||
<div className="tool-call-content-list">
|
||||
{content.map((item, index) => (
|
||||
<ContentView key={index} content={item} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="tool-call-footer">
|
||||
<span className="tool-call-id">
|
||||
ID: {toolCallId.substring(0, 8)}...
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
/**
|
||||
* Main ToolCall component
|
||||
* Routes to specialized components based on the tool call kind
|
||||
*
|
||||
* Supported kinds:
|
||||
* - read: File reading operations
|
||||
* - write/edit: File writing and editing operations
|
||||
* - execute/bash/command: Command execution
|
||||
* - search/grep/glob/find: Search operations
|
||||
* - think/thinking: AI reasoning
|
||||
* - All others: Generic display
|
||||
*/
|
||||
export const ToolCall: React.FC<{
|
||||
toolCall: import('./toolcalls/shared/types.js').ToolCallData;
|
||||
}> = ({ toolCall }) => <ToolCallRouter toolCall={toolCall} />;
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Execute tool call component - specialized for command execution operations
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import type { BaseToolCallProps } from './shared/types.js';
|
||||
import {
|
||||
ToolCallCard,
|
||||
ToolCallRow,
|
||||
StatusIndicator,
|
||||
CodeBlock,
|
||||
} from './shared/LayoutComponents.js';
|
||||
import { formatValue, safeTitle, groupContent } from './shared/utils.js';
|
||||
|
||||
/**
|
||||
* Specialized component for Execute tool calls
|
||||
* Optimized for displaying command execution with stdout/stderr
|
||||
*/
|
||||
export const ExecuteToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
|
||||
const { title, status, rawInput, content } = toolCall;
|
||||
const titleText = safeTitle(title);
|
||||
|
||||
// Group content by type
|
||||
const { textOutputs, errors, otherData } = groupContent(content);
|
||||
|
||||
return (
|
||||
<ToolCallCard icon="⚡">
|
||||
{/* Title row */}
|
||||
<ToolCallRow label="Execute">
|
||||
<StatusIndicator status={status} text={titleText} />
|
||||
</ToolCallRow>
|
||||
|
||||
{/* Command */}
|
||||
{rawInput && (
|
||||
<ToolCallRow label="Command">
|
||||
<CodeBlock>{formatValue(rawInput)}</CodeBlock>
|
||||
</ToolCallRow>
|
||||
)}
|
||||
|
||||
{/* Standard output */}
|
||||
{textOutputs.length > 0 && (
|
||||
<ToolCallRow label="Output">
|
||||
<CodeBlock>{textOutputs.join('\n')}</CodeBlock>
|
||||
</ToolCallRow>
|
||||
)}
|
||||
|
||||
{/* Standard error / Errors */}
|
||||
{errors.length > 0 && (
|
||||
<ToolCallRow label="Error">
|
||||
<div style={{ color: '#c74e39' }}>
|
||||
<CodeBlock>{errors.join('\n')}</CodeBlock>
|
||||
</div>
|
||||
</ToolCallRow>
|
||||
)}
|
||||
|
||||
{/* Exit code or other execution details */}
|
||||
{otherData.length > 0 && (
|
||||
<ToolCallRow label="Details">
|
||||
<CodeBlock>
|
||||
{otherData.map((data: unknown) => formatValue(data)).join('\n\n')}
|
||||
</CodeBlock>
|
||||
</ToolCallRow>
|
||||
)}
|
||||
</ToolCallCard>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Generic tool call component - handles all tool call types as fallback
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import type { BaseToolCallProps } from './shared/types.js';
|
||||
import {
|
||||
ToolCallCard,
|
||||
ToolCallRow,
|
||||
StatusIndicator,
|
||||
CodeBlock,
|
||||
LocationsList,
|
||||
DiffDisplay,
|
||||
} from './shared/LayoutComponents.js';
|
||||
import {
|
||||
formatValue,
|
||||
safeTitle,
|
||||
getKindIcon,
|
||||
groupContent,
|
||||
} from './shared/utils.js';
|
||||
|
||||
/**
|
||||
* Generic tool call component that can display any tool call type
|
||||
* Used as fallback for unknown tool call kinds
|
||||
*/
|
||||
export const GenericToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
|
||||
const { kind, title, status, rawInput, content, locations } = toolCall;
|
||||
const kindIcon = getKindIcon(kind);
|
||||
const titleText = safeTitle(title);
|
||||
|
||||
// Group content by type
|
||||
const { textOutputs, errors, diffs, otherData } = groupContent(content);
|
||||
|
||||
return (
|
||||
<ToolCallCard icon={kindIcon}>
|
||||
{/* Title row */}
|
||||
<ToolCallRow label="Tool">
|
||||
<StatusIndicator status={status} text={titleText} />
|
||||
</ToolCallRow>
|
||||
|
||||
{/* Input row */}
|
||||
{rawInput && (
|
||||
<ToolCallRow label="Input">
|
||||
<CodeBlock>{formatValue(rawInput)}</CodeBlock>
|
||||
</ToolCallRow>
|
||||
)}
|
||||
|
||||
{/* Locations row */}
|
||||
{locations && locations.length > 0 && (
|
||||
<ToolCallRow label="Files">
|
||||
<LocationsList locations={locations} />
|
||||
</ToolCallRow>
|
||||
)}
|
||||
|
||||
{/* Output row - combined text outputs */}
|
||||
{textOutputs.length > 0 && (
|
||||
<ToolCallRow label="Output">
|
||||
<CodeBlock>{textOutputs.join('\n')}</CodeBlock>
|
||||
</ToolCallRow>
|
||||
)}
|
||||
|
||||
{/* Error row - combined errors */}
|
||||
{errors.length > 0 && (
|
||||
<ToolCallRow label="Error">
|
||||
<div style={{ color: '#c74e39' }}>{errors.join('\n')}</div>
|
||||
</ToolCallRow>
|
||||
)}
|
||||
|
||||
{/* Diff rows */}
|
||||
{diffs.map(
|
||||
(item: import('./shared/types.js').ToolCallContent, idx: number) => (
|
||||
<ToolCallRow key={`diff-${idx}`} label="Diff">
|
||||
<DiffDisplay
|
||||
path={item.path}
|
||||
oldText={item.oldText}
|
||||
newText={item.newText}
|
||||
/>
|
||||
</ToolCallRow>
|
||||
),
|
||||
)}
|
||||
|
||||
{/* Other data rows */}
|
||||
{otherData.length > 0 && (
|
||||
<ToolCallRow label="Data">
|
||||
<CodeBlock>
|
||||
{otherData.map((data: unknown) => formatValue(data)).join('\n\n')}
|
||||
</CodeBlock>
|
||||
</ToolCallRow>
|
||||
)}
|
||||
</ToolCallCard>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Read tool call component - specialized for file reading operations
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import type { BaseToolCallProps } from './shared/types.js';
|
||||
import {
|
||||
ToolCallCard,
|
||||
ToolCallRow,
|
||||
StatusIndicator,
|
||||
CodeBlock,
|
||||
LocationsList,
|
||||
} from './shared/LayoutComponents.js';
|
||||
import { formatValue, safeTitle, groupContent } from './shared/utils.js';
|
||||
|
||||
/**
|
||||
* Specialized component for Read tool calls
|
||||
* Optimized for displaying file reading operations
|
||||
*/
|
||||
export const ReadToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
|
||||
const { title, status, rawInput, content, locations } = toolCall;
|
||||
const titleText = safeTitle(title);
|
||||
|
||||
// Group content by type
|
||||
const { textOutputs, errors, otherData } = groupContent(content);
|
||||
|
||||
return (
|
||||
<ToolCallCard icon="📖">
|
||||
{/* Title row */}
|
||||
<ToolCallRow label="Read">
|
||||
<StatusIndicator status={status} text={titleText} />
|
||||
</ToolCallRow>
|
||||
|
||||
{/* File path(s) */}
|
||||
{locations && locations.length > 0 && (
|
||||
<ToolCallRow label="File">
|
||||
<LocationsList locations={locations} />
|
||||
</ToolCallRow>
|
||||
)}
|
||||
|
||||
{/* Input parameters (e.g., line range, offset) */}
|
||||
{rawInput && (
|
||||
<ToolCallRow label="Options">
|
||||
<CodeBlock>{formatValue(rawInput)}</CodeBlock>
|
||||
</ToolCallRow>
|
||||
)}
|
||||
|
||||
{/* File content output */}
|
||||
{textOutputs.length > 0 && (
|
||||
<ToolCallRow label="Content">
|
||||
<CodeBlock>{textOutputs.join('\n')}</CodeBlock>
|
||||
</ToolCallRow>
|
||||
)}
|
||||
|
||||
{/* Error handling */}
|
||||
{errors.length > 0 && (
|
||||
<ToolCallRow label="Error">
|
||||
<div style={{ color: '#c74e39' }}>{errors.join('\n')}</div>
|
||||
</ToolCallRow>
|
||||
)}
|
||||
|
||||
{/* Other data */}
|
||||
{otherData.length > 0 && (
|
||||
<ToolCallRow label="Details">
|
||||
<CodeBlock>
|
||||
{otherData.map((data: unknown) => formatValue(data)).join('\n\n')}
|
||||
</CodeBlock>
|
||||
</ToolCallRow>
|
||||
)}
|
||||
</ToolCallCard>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Search tool call component - specialized for search operations
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import type { BaseToolCallProps } from './shared/types.js';
|
||||
import {
|
||||
ToolCallCard,
|
||||
ToolCallRow,
|
||||
StatusIndicator,
|
||||
CodeBlock,
|
||||
LocationsList,
|
||||
} from './shared/LayoutComponents.js';
|
||||
import { formatValue, safeTitle, groupContent } from './shared/utils.js';
|
||||
|
||||
/**
|
||||
* Specialized component for Search tool calls
|
||||
* Optimized for displaying search operations and results
|
||||
*/
|
||||
export const SearchToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
|
||||
const { title, status, rawInput, content, locations } = toolCall;
|
||||
const titleText = safeTitle(title);
|
||||
|
||||
// Group content by type
|
||||
const { textOutputs, errors, otherData } = groupContent(content);
|
||||
|
||||
return (
|
||||
<ToolCallCard icon="🔍">
|
||||
{/* Title row */}
|
||||
<ToolCallRow label="Search">
|
||||
<StatusIndicator status={status} text={titleText} />
|
||||
</ToolCallRow>
|
||||
|
||||
{/* Search query/pattern */}
|
||||
{rawInput && (
|
||||
<ToolCallRow label="Query">
|
||||
<CodeBlock>{formatValue(rawInput)}</CodeBlock>
|
||||
</ToolCallRow>
|
||||
)}
|
||||
|
||||
{/* Search results - files found */}
|
||||
{locations && locations.length > 0 && (
|
||||
<ToolCallRow label="Results">
|
||||
<LocationsList locations={locations} />
|
||||
</ToolCallRow>
|
||||
)}
|
||||
|
||||
{/* Search output details */}
|
||||
{textOutputs.length > 0 && (
|
||||
<ToolCallRow label="Matches">
|
||||
<CodeBlock>{textOutputs.join('\n')}</CodeBlock>
|
||||
</ToolCallRow>
|
||||
)}
|
||||
|
||||
{/* Error handling */}
|
||||
{errors.length > 0 && (
|
||||
<ToolCallRow label="Error">
|
||||
<div style={{ color: '#c74e39' }}>{errors.join('\n')}</div>
|
||||
</ToolCallRow>
|
||||
)}
|
||||
|
||||
{/* Other search metadata */}
|
||||
{otherData.length > 0 && (
|
||||
<ToolCallRow label="Details">
|
||||
<CodeBlock>
|
||||
{otherData.map((data: unknown) => formatValue(data)).join('\n\n')}
|
||||
</CodeBlock>
|
||||
</ToolCallRow>
|
||||
)}
|
||||
</ToolCallCard>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Think tool call component - specialized for thinking/reasoning operations
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import type { BaseToolCallProps } from './shared/types.js';
|
||||
import {
|
||||
ToolCallCard,
|
||||
ToolCallRow,
|
||||
StatusIndicator,
|
||||
CodeBlock,
|
||||
} from './shared/LayoutComponents.js';
|
||||
import { formatValue, safeTitle, groupContent } from './shared/utils.js';
|
||||
|
||||
/**
|
||||
* Specialized component for Think tool calls
|
||||
* Optimized for displaying AI reasoning and thought processes
|
||||
*/
|
||||
export const ThinkToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
|
||||
const { title, status, rawInput, content } = toolCall;
|
||||
const titleText = safeTitle(title);
|
||||
|
||||
// Group content by type
|
||||
const { textOutputs, errors, otherData } = groupContent(content);
|
||||
|
||||
return (
|
||||
<ToolCallCard icon="💭">
|
||||
{/* Title row */}
|
||||
<ToolCallRow label="Thinking">
|
||||
<StatusIndicator status={status} text={titleText} />
|
||||
</ToolCallRow>
|
||||
|
||||
{/* Thinking context/prompt */}
|
||||
{rawInput && (
|
||||
<ToolCallRow label="Context">
|
||||
<CodeBlock>{formatValue(rawInput)}</CodeBlock>
|
||||
</ToolCallRow>
|
||||
)}
|
||||
|
||||
{/* Thought content */}
|
||||
{textOutputs.length > 0 && (
|
||||
<ToolCallRow label="Thoughts">
|
||||
<div style={{ fontStyle: 'italic', opacity: 0.95 }}>
|
||||
{textOutputs.join('\n\n')}
|
||||
</div>
|
||||
</ToolCallRow>
|
||||
)}
|
||||
|
||||
{/* Error handling */}
|
||||
{errors.length > 0 && (
|
||||
<ToolCallRow label="Error">
|
||||
<div style={{ color: '#c74e39' }}>{errors.join('\n')}</div>
|
||||
</ToolCallRow>
|
||||
)}
|
||||
|
||||
{/* Other reasoning data */}
|
||||
{otherData.length > 0 && (
|
||||
<ToolCallRow label="Details">
|
||||
<CodeBlock>
|
||||
{otherData.map((data: unknown) => formatValue(data)).join('\n\n')}
|
||||
</CodeBlock>
|
||||
</ToolCallRow>
|
||||
)}
|
||||
</ToolCallCard>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Write/Edit tool call component - specialized for file writing and editing operations
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import type { BaseToolCallProps } from './shared/types.js';
|
||||
import {
|
||||
ToolCallCard,
|
||||
ToolCallRow,
|
||||
StatusIndicator,
|
||||
CodeBlock,
|
||||
LocationsList,
|
||||
DiffDisplay,
|
||||
} from './shared/LayoutComponents.js';
|
||||
import { formatValue, safeTitle, groupContent } from './shared/utils.js';
|
||||
|
||||
/**
|
||||
* Specialized component for Write/Edit tool calls
|
||||
* Optimized for displaying file writing and editing operations with diffs
|
||||
*/
|
||||
export const WriteToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
|
||||
const { kind, title, status, rawInput, content, locations } = toolCall;
|
||||
const titleText = safeTitle(title);
|
||||
const isEdit = kind.toLowerCase() === 'edit';
|
||||
|
||||
// Group content by type
|
||||
const { textOutputs, errors, diffs, otherData } = groupContent(content);
|
||||
|
||||
return (
|
||||
<ToolCallCard icon="✏️">
|
||||
{/* Title row */}
|
||||
<ToolCallRow label={isEdit ? 'Edit' : 'Write'}>
|
||||
<StatusIndicator status={status} text={titleText} />
|
||||
</ToolCallRow>
|
||||
|
||||
{/* File path(s) */}
|
||||
{locations && locations.length > 0 && (
|
||||
<ToolCallRow label="File">
|
||||
<LocationsList locations={locations} />
|
||||
</ToolCallRow>
|
||||
)}
|
||||
|
||||
{/* Input parameters (e.g., old_string, new_string for edits) */}
|
||||
{rawInput && (
|
||||
<ToolCallRow label="Changes">
|
||||
<CodeBlock>{formatValue(rawInput)}</CodeBlock>
|
||||
</ToolCallRow>
|
||||
)}
|
||||
|
||||
{/* Diff display - most important for write/edit operations */}
|
||||
{diffs.map(
|
||||
(item: import('./shared/types.js').ToolCallContent, idx: number) => (
|
||||
<ToolCallRow key={`diff-${idx}`} label="Diff">
|
||||
<DiffDisplay
|
||||
path={item.path}
|
||||
oldText={item.oldText}
|
||||
newText={item.newText}
|
||||
/>
|
||||
</ToolCallRow>
|
||||
),
|
||||
)}
|
||||
|
||||
{/* Success message or output */}
|
||||
{textOutputs.length > 0 && (
|
||||
<ToolCallRow label="Result">
|
||||
<CodeBlock>{textOutputs.join('\n')}</CodeBlock>
|
||||
</ToolCallRow>
|
||||
)}
|
||||
|
||||
{/* Error handling */}
|
||||
{errors.length > 0 && (
|
||||
<ToolCallRow label="Error">
|
||||
<div style={{ color: '#c74e39' }}>{errors.join('\n')}</div>
|
||||
</ToolCallRow>
|
||||
)}
|
||||
|
||||
{/* Other data */}
|
||||
{otherData.length > 0 && (
|
||||
<ToolCallRow label="Details">
|
||||
<CodeBlock>
|
||||
{otherData.map((data: unknown) => formatValue(data)).join('\n\n')}
|
||||
</CodeBlock>
|
||||
</ToolCallRow>
|
||||
)}
|
||||
</ToolCallCard>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Tool call component factory - routes to specialized components by kind
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import type { BaseToolCallProps } from './shared/types.js';
|
||||
import { shouldShowToolCall } from './shared/utils.js';
|
||||
import { GenericToolCall } from './GenericToolCall.js';
|
||||
import { ReadToolCall } from './ReadToolCall.js';
|
||||
import { WriteToolCall } from './WriteToolCall.js';
|
||||
import { ExecuteToolCall } from './ExecuteToolCall.js';
|
||||
import { SearchToolCall } from './SearchToolCall.js';
|
||||
import { ThinkToolCall } from './ThinkToolCall.js';
|
||||
|
||||
/**
|
||||
* Factory function that returns the appropriate tool call component based on kind
|
||||
*/
|
||||
export const getToolCallComponent = (
|
||||
kind: string,
|
||||
): React.FC<BaseToolCallProps> => {
|
||||
const normalizedKind = kind.toLowerCase();
|
||||
|
||||
// Route to specialized components
|
||||
switch (normalizedKind) {
|
||||
case 'read':
|
||||
return ReadToolCall;
|
||||
|
||||
case 'write':
|
||||
case 'edit':
|
||||
return WriteToolCall;
|
||||
|
||||
case 'execute':
|
||||
case 'bash':
|
||||
case 'command':
|
||||
return ExecuteToolCall;
|
||||
|
||||
case 'search':
|
||||
case 'grep':
|
||||
case 'glob':
|
||||
case 'find':
|
||||
return SearchToolCall;
|
||||
|
||||
case 'think':
|
||||
case 'thinking':
|
||||
return ThinkToolCall;
|
||||
|
||||
// Add more specialized components as needed
|
||||
// case 'fetch':
|
||||
// return FetchToolCall;
|
||||
// case 'delete':
|
||||
// return DeleteToolCall;
|
||||
|
||||
default:
|
||||
// Fallback to generic component
|
||||
return GenericToolCall;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Main tool call component that routes to specialized implementations
|
||||
*/
|
||||
export const ToolCallRouter: React.FC<BaseToolCallProps> = ({ toolCall }) => {
|
||||
// Check if we should show this tool call (hide internal ones)
|
||||
if (!shouldShowToolCall(toolCall.kind)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get the appropriate component for this kind
|
||||
const Component = getToolCallComponent(toolCall.kind);
|
||||
|
||||
// Render the specialized component
|
||||
return <Component toolCall={toolCall} />;
|
||||
};
|
||||
|
||||
// Re-export types for convenience
|
||||
export type { BaseToolCallProps, ToolCallData } from './shared/types.js';
|
||||
@@ -0,0 +1,161 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Shared layout components for tool call UI
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
|
||||
/**
|
||||
* Props for ToolCallCard wrapper
|
||||
*/
|
||||
interface ToolCallCardProps {
|
||||
icon: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main card wrapper with icon
|
||||
*/
|
||||
export const ToolCallCard: React.FC<ToolCallCardProps> = ({
|
||||
icon,
|
||||
children,
|
||||
}) => (
|
||||
<div className="tool-call-card">
|
||||
<div className="tool-call-icon">{icon}</div>
|
||||
<div className="tool-call-grid">{children}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
/**
|
||||
* Props for ToolCallRow
|
||||
*/
|
||||
interface ToolCallRowProps {
|
||||
label: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* A single row in the tool call grid
|
||||
*/
|
||||
export const ToolCallRow: React.FC<ToolCallRowProps> = ({
|
||||
label,
|
||||
children,
|
||||
}) => (
|
||||
<div className="tool-call-row">
|
||||
<div className="tool-call-label">{label}</div>
|
||||
<div className="tool-call-value">{children}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
/**
|
||||
* Props for StatusIndicator
|
||||
*/
|
||||
interface StatusIndicatorProps {
|
||||
status: 'pending' | 'in_progress' | 'completed' | 'failed';
|
||||
text: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Status indicator with colored dot
|
||||
*/
|
||||
export const StatusIndicator: React.FC<StatusIndicatorProps> = ({
|
||||
status,
|
||||
text,
|
||||
}) => (
|
||||
<div className={`tool-call-status-indicator ${status}`} title={status}>
|
||||
{text}
|
||||
</div>
|
||||
);
|
||||
|
||||
/**
|
||||
* Props for CodeBlock
|
||||
*/
|
||||
interface CodeBlockProps {
|
||||
children: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Code block for displaying formatted code or output
|
||||
*/
|
||||
export const CodeBlock: React.FC<CodeBlockProps> = ({ children }) => (
|
||||
<pre className="code-block">{children}</pre>
|
||||
);
|
||||
|
||||
/**
|
||||
* Props for LocationsList
|
||||
*/
|
||||
interface LocationsListProps {
|
||||
locations: Array<{
|
||||
path: string;
|
||||
line?: number | null;
|
||||
}>;
|
||||
}
|
||||
|
||||
/**
|
||||
* List of file locations
|
||||
*/
|
||||
export const LocationsList: React.FC<LocationsListProps> = ({ locations }) => (
|
||||
<>
|
||||
{locations.map((loc, idx) => (
|
||||
<div key={idx}>
|
||||
{loc.path}
|
||||
{loc.line !== null && loc.line !== undefined && `:${loc.line}`}
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
|
||||
/**
|
||||
* Props for DiffDisplay
|
||||
*/
|
||||
interface DiffDisplayProps {
|
||||
path?: string;
|
||||
oldText?: string | null;
|
||||
newText?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display diff with before/after sections
|
||||
*/
|
||||
export const DiffDisplay: React.FC<DiffDisplayProps> = ({
|
||||
path,
|
||||
oldText,
|
||||
newText,
|
||||
}) => (
|
||||
<div>
|
||||
<div>
|
||||
<strong>{path || 'Unknown file'}</strong>
|
||||
</div>
|
||||
{oldText !== undefined && (
|
||||
<div>
|
||||
<div
|
||||
style={{
|
||||
opacity: 0.5,
|
||||
fontSize: '0.85em',
|
||||
marginTop: '4px',
|
||||
}}
|
||||
>
|
||||
Before:
|
||||
</div>
|
||||
<pre className="code-block">{oldText || '(empty)'}</pre>
|
||||
</div>
|
||||
)}
|
||||
{newText !== undefined && (
|
||||
<div>
|
||||
<div
|
||||
style={{
|
||||
opacity: 0.5,
|
||||
fontSize: '0.85em',
|
||||
marginTop: '4px',
|
||||
}}
|
||||
>
|
||||
After:
|
||||
</div>
|
||||
<pre className="code-block">{newText}</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Shared types for tool call components
|
||||
*/
|
||||
|
||||
/**
|
||||
* Tool call content types
|
||||
*/
|
||||
export interface ToolCallContent {
|
||||
type: 'content' | 'diff';
|
||||
// For content type
|
||||
content?: {
|
||||
type: string;
|
||||
text?: string;
|
||||
error?: unknown;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
// For diff type
|
||||
path?: string;
|
||||
oldText?: string | null;
|
||||
newText?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tool call location type
|
||||
*/
|
||||
export interface ToolCallLocation {
|
||||
path: string;
|
||||
line?: number | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tool call status type
|
||||
*/
|
||||
export type ToolCallStatus = 'pending' | 'in_progress' | 'completed' | 'failed';
|
||||
|
||||
/**
|
||||
* Base tool call data interface
|
||||
*/
|
||||
export interface ToolCallData {
|
||||
toolCallId: string;
|
||||
kind: string;
|
||||
title: string | object;
|
||||
status: ToolCallStatus;
|
||||
rawInput?: string | object;
|
||||
content?: ToolCallContent[];
|
||||
locations?: ToolCallLocation[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Base props for all tool call components
|
||||
*/
|
||||
export interface BaseToolCallProps {
|
||||
toolCall: ToolCallData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Grouped content structure for rendering
|
||||
*/
|
||||
export interface GroupedContent {
|
||||
textOutputs: string[];
|
||||
errors: string[];
|
||||
diffs: ToolCallContent[];
|
||||
otherData: unknown[];
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Shared utility functions for tool call components
|
||||
*/
|
||||
|
||||
import type { ToolCallContent, GroupedContent } from './types.js';
|
||||
|
||||
/**
|
||||
* Format any value to a string for display
|
||||
*/
|
||||
export const formatValue = (value: unknown): string => {
|
||||
if (value === null || value === undefined) {
|
||||
return '';
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
return value;
|
||||
}
|
||||
if (typeof value === 'object') {
|
||||
try {
|
||||
return JSON.stringify(value, null, 2);
|
||||
} catch (_e) {
|
||||
return String(value);
|
||||
}
|
||||
}
|
||||
return String(value);
|
||||
};
|
||||
|
||||
/**
|
||||
* Safely convert title to string, handling object types
|
||||
*/
|
||||
export const safeTitle = (title: unknown): string => {
|
||||
if (typeof title === 'string') {
|
||||
return title;
|
||||
}
|
||||
if (title && typeof title === 'object') {
|
||||
return JSON.stringify(title);
|
||||
}
|
||||
return 'Tool Call';
|
||||
};
|
||||
|
||||
/**
|
||||
* Get icon emoji for a given tool kind
|
||||
*/
|
||||
export const getKindIcon = (kind: string): string => {
|
||||
const kindMap: Record<string, string> = {
|
||||
edit: '✏️',
|
||||
write: '✏️',
|
||||
read: '📖',
|
||||
execute: '⚡',
|
||||
fetch: '🌐',
|
||||
delete: '🗑️',
|
||||
move: '📦',
|
||||
search: '🔍',
|
||||
think: '💭',
|
||||
diff: '📝',
|
||||
};
|
||||
return kindMap[kind.toLowerCase()] || '🔧';
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if a tool call should be displayed
|
||||
* Hides internal tool calls
|
||||
*/
|
||||
export const shouldShowToolCall = (kind: string): boolean =>
|
||||
!kind.includes('internal');
|
||||
|
||||
/**
|
||||
* Group tool call content by type to avoid duplicate labels
|
||||
*/
|
||||
export const groupContent = (content?: ToolCallContent[]): GroupedContent => {
|
||||
const textOutputs: string[] = [];
|
||||
const errors: string[] = [];
|
||||
const diffs: ToolCallContent[] = [];
|
||||
const otherData: unknown[] = [];
|
||||
|
||||
content?.forEach((item) => {
|
||||
if (item.type === 'diff') {
|
||||
diffs.push(item);
|
||||
} else if (item.content) {
|
||||
const contentObj = item.content;
|
||||
|
||||
// Handle error content
|
||||
if (contentObj.type === 'error' || 'error' in contentObj) {
|
||||
const errorMsg =
|
||||
formatValue(contentObj.error) ||
|
||||
formatValue(contentObj.text) ||
|
||||
'An error occurred';
|
||||
errors.push(errorMsg);
|
||||
}
|
||||
// Handle text content
|
||||
else if (contentObj.text) {
|
||||
textOutputs.push(formatValue(contentObj.text));
|
||||
}
|
||||
// Handle other content
|
||||
else {
|
||||
otherData.push(contentObj);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { textOutputs, errors, diffs, otherData };
|
||||
};
|
||||
Reference in New Issue
Block a user