mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-21 09:17:53 +00:00
refactor(webview): 重构工具调用显示逻辑
- 新增多个工具调用组件,分别处理不同类型的工具调用 - 优化工具调用卡片的样式和布局 - 添加加载状态和随机加载消息 - 重构 App 组件,支持新的工具调用显示逻辑
This commit is contained in:
@@ -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