refactor(vscode-ide-companion): restructure tool call components

Restructure tool call components with dedicated container implementations

- Move tool call components to done subdirectory

- Implement specialized ToolCallContainer for each tool type

- Update component routing in ToolCallRouter

- Add isFirst/isLast props for better layout control

- Improve shared types and layout components
This commit is contained in:
yiliang114
2025-12-06 21:45:51 +08:00
parent ad301963a6
commit ad79b9bcab
9 changed files with 165 additions and 40 deletions

View File

@@ -7,15 +7,42 @@
*/ */
import { useEffect, useCallback, useMemo } from 'react'; import { useEffect, useCallback, useMemo } from 'react';
import type { BaseToolCallProps } from '../shared/types.js'; import type { BaseToolCallProps } from '../../shared/types.js';
import { ToolCallContainer } from '../shared/LayoutComponents.js';
import { import {
groupContent, groupContent,
mapToolStatusToContainerStatus, mapToolStatusToContainerStatus,
} from '../shared/utils.js'; } from '../../shared/utils.js';
import { useVSCode } from '../../../hooks/useVSCode.js'; import { FileLink } from '../../../ui/FileLink.js';
import { FileLink } from '../../ui/FileLink.js'; import type { ToolCallContainerProps } from '../../shared/LayoutComponents.js';
import { handleOpenDiff } from '../../../utils/diffUtils.js'; import { useVSCode } from '../../../../hooks/useVSCode.js';
import { handleOpenDiff } from '../../../../utils/diffUtils.js';
export const ToolCallContainer: React.FC<ToolCallContainerProps> = ({
label,
status = 'success',
children,
toolCallId: _toolCallId,
labelSuffix,
className: _className,
}) => (
<div
className={`qwen-message message-item ${_className || ''} relative pl-[30px] py-2 select-text toolcall-container toolcall-status-${status}`}
>
<div className="EditToolCall toolcall-content-wrapper flex flex-col gap-1 min-w-0 max-w-full">
<div className="flex items-baseline gap-1.5 relative min-w-0">
<span className="text-[14px] leading-none font-bold text-[var(--app-primary-foreground)]">
{label}
</span>
<span className="text-[11px] text-[var(--app-secondary-foreground)]">
{labelSuffix}
</span>
</div>
{children && (
<div className="text-[var(--app-secondary-foreground)]">{children}</div>
)}
</div>
</div>
);
/** /**
* Calculate diff summary (added/removed lines) * Calculate diff summary (added/removed lines)
@@ -58,9 +85,6 @@ export const EditToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
[vscode], [vscode],
); );
// Extract filename from path
const getFileName = (path: string): string => path.split('/').pop() || path;
// Automatically trigger openDiff when diff content is detected // Automatically trigger openDiff when diff content is detected
// Only trigger once per tool call by checking toolCallId // Only trigger once per tool call by checking toolCallId
useEffect(() => { useEffect(() => {
@@ -88,10 +112,9 @@ export const EditToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
// Error case: show error // Error case: show error
if (errors.length > 0) { if (errors.length > 0) {
const path = diffs[0]?.path || locations?.[0]?.path || ''; const path = diffs[0]?.path || locations?.[0]?.path || '';
const fileName = path ? getFileName(path) : '';
return ( return (
<ToolCallContainer <ToolCallContainer
label={fileName ? 'Edit' : 'Edit'} label={'Edit'}
status="error" status="error"
toolCallId={toolCallId} toolCallId={toolCallId}
labelSuffix={ labelSuffix={
@@ -123,7 +146,7 @@ export const EditToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
{/* IMPORTANT: Always include min-w-0/max-w-full on inner wrappers to prevent overflow. */} {/* IMPORTANT: Always include min-w-0/max-w-full on inner wrappers to prevent overflow. */}
<div className="toolcall-edit-content flex flex-col gap-1 min-w-0 max-w-full"> <div className="toolcall-edit-content flex flex-col gap-1 min-w-0 max-w-full">
<div className="flex items-center justify-between min-w-0"> <div className="flex items-center justify-between min-w-0">
<div className="flex items-center gap-2 min-w-0"> <div className="flex items-baseline gap-1.5 min-w-0">
{/* Align the inline Edit label styling with shared toolcall label: larger + bold */} {/* Align the inline Edit label styling with shared toolcall label: larger + bold */}
<span className="text-[14px] leading-none font-bold text-[var(--app-primary-foreground)]"> <span className="text-[14px] leading-none font-bold text-[var(--app-primary-foreground)]">
Edit Edit
@@ -137,7 +160,7 @@ export const EditToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
)} )}
</div> </div>
</div> </div>
<div className="inline-flex text-[var(--app-secondary-foreground)] text-[0.85em] opacity-70 flex-row items-start w-full gap-1 flex items-center"> <div className="inline-flex text-[var(--app-secondary-foreground)] text-[0.85em] opacity-70 flex-row items-start w-full gap-1 flex items-baseline">
<span className="flex-shrink-0 relative top-[-0.1em]"></span> <span className="flex-shrink-0 relative top-[-0.1em]"></span>
<span className="flex-shrink-0 w-full">{summary}</span> <span className="flex-shrink-0 w-full">{summary}</span>
</div> </div>
@@ -148,13 +171,19 @@ export const EditToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
// Success case without diff: show file in compact format // Success case without diff: show file in compact format
if (locations && locations.length > 0) { if (locations && locations.length > 0) {
const fileName = getFileName(locations[0].path);
const containerStatus = mapToolStatusToContainerStatus(toolCall.status); const containerStatus = mapToolStatusToContainerStatus(toolCall.status);
return ( return (
<ToolCallContainer <ToolCallContainer
label={`Edited ${fileName}`} label={`Edit`}
status={containerStatus} status={containerStatus}
toolCallId={toolCallId} toolCallId={toolCallId}
labelSuffix={
<FileLink
path={locations[0].path}
showFullPath={false}
className="text-xs font-mono text-[var(--app-secondary-foreground)] hover:underline"
/>
}
> >
<div className="inline-flex text-[var(--app-secondary-foreground)] text-[0.85em] opacity-70 flex-row items-start w-full gap-1 flex items-center"> <div className="inline-flex text-[var(--app-secondary-foreground)] text-[0.85em] opacity-70 flex-row items-start w-full gap-1 flex items-center">
<span className="flex-shrink-0 relative top-[-0.1em]"></span> <span className="flex-shrink-0 relative top-[-0.1em]"></span>

View File

@@ -99,4 +99,4 @@
/* Error content styling */ /* Error content styling */
.execute-toolcall-error-content { .execute-toolcall-error-content {
color: #c74e39; color: #c74e39;
} }

View File

@@ -7,10 +7,37 @@
*/ */
import type React from 'react'; import type React from 'react';
import type { BaseToolCallProps } from '../shared/types.js'; import type { BaseToolCallProps } from '../../shared/types.js';
import { ToolCallContainer } from '../shared/LayoutComponents.js'; import { safeTitle, groupContent } from '../../shared/utils.js';
import { safeTitle, groupContent } from '../shared/utils.js';
import './Execute.css'; import './Execute.css';
import type { ToolCallContainerProps } from '../../shared/LayoutComponents.js';
export const ToolCallContainer: React.FC<ToolCallContainerProps> = ({
label,
status = 'success',
children,
toolCallId: _toolCallId,
labelSuffix,
className: _className,
}) => (
<div
className={`ExecuteToolCall qwen-message message-item ${_className || ''} relative pl-[30px] py-2 select-text toolcall-container toolcall-status-${status}`}
>
<div className="toolcall-content-wrapper flex flex-col gap-0 min-w-0 max-w-full">
<div className="flex items-baseline gap-1.5 relative min-w-0">
<span className="text-[14px] leading-none font-bold text-[var(--app-primary-foreground)]">
{label}
</span>
<span className="text-[11px] text-[var(--app-secondary-foreground)]">
{labelSuffix}
</span>
</div>
{children && (
<div className="text-[var(--app-secondary-foreground)]">{children}</div>
)}
</div>
</div>
);
/** /**
* Specialized component for Execute tool calls * Specialized component for Execute tool calls
@@ -18,7 +45,9 @@ import './Execute.css';
*/ */
export const ExecuteToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => { export const ExecuteToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
const { title, content, rawInput, toolCallId } = toolCall; const { title, content, rawInput, toolCallId } = toolCall;
const commandText = safeTitle(title); const commandText = safeTitle(
(rawInput as Record<string, unknown>)?.description || title,
);
// Group content by type // Group content by type
const { textOutputs, errors } = groupContent(content); const { textOutputs, errors } = groupContent(content);
@@ -26,8 +55,8 @@ export const ExecuteToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
// Extract command from rawInput if available // Extract command from rawInput if available
let inputCommand = commandText; let inputCommand = commandText;
if (rawInput && typeof rawInput === 'object') { if (rawInput && typeof rawInput === 'object') {
const inputObj = rawInput as { command?: string }; const inputObj = rawInput as Record<string, unknown>;
inputCommand = inputObj.command || commandText; inputCommand = (inputObj.command as string | undefined) || commandText;
} else if (typeof rawInput === 'string') { } else if (typeof rawInput === 'string') {
inputCommand = rawInput; inputCommand = rawInput;
} }

View File

@@ -8,15 +8,44 @@
import type React from 'react'; import type React from 'react';
import { useCallback, useEffect, useMemo } from 'react'; import { useCallback, useEffect, useMemo } from 'react';
import type { BaseToolCallProps } from '../shared/types.js'; import type { BaseToolCallProps } from '../../shared/types.js';
import { ToolCallContainer } from '../shared/LayoutComponents.js';
import { import {
groupContent, groupContent,
mapToolStatusToContainerStatus, mapToolStatusToContainerStatus,
} from '../shared/utils.js'; } from '../../shared/utils.js';
import { FileLink } from '../../ui/FileLink.js'; import { FileLink } from '../../../ui/FileLink.js';
import { useVSCode } from '../../../hooks/useVSCode.js'; import { useVSCode } from '../../../../hooks/useVSCode.js';
import { handleOpenDiff } from '../../../utils/diffUtils.js'; import { handleOpenDiff } from '../../../../utils/diffUtils.js';
import type { ToolCallContainerProps } from '../../shared/LayoutComponents.js';
export const ToolCallContainer: React.FC<ToolCallContainerProps> = ({
label,
status = 'success',
children,
toolCallId: _toolCallId,
labelSuffix,
className: _className,
}) => (
<div
className={`ReadToolCall qwen-message message-item ${_className || ''} relative pl-[30px] py-2 select-text toolcall-container toolcall-status-${status}`}
>
<div className="toolcall-content-wrapper flex flex-col gap-1 min-w-0 max-w-full">
<div className="flex items-baseline gap-1.5 relative min-w-0">
<span className="text-[14px] leading-none font-bold text-[var(--app-primary-foreground)]">
{label}
</span>
<span className="text-[11px] text-[var(--app-secondary-foreground)]">
{labelSuffix}
</span>
</div>
{children && (
<div className="text-[var(--app-secondary-foreground)] py-1">
{children}
</div>
)}
</div>
</div>
);
/** /**
* Specialized component for Read tool calls * Specialized component for Read tool calls

View File

@@ -7,18 +7,46 @@
*/ */
import type React from 'react'; import type React from 'react';
import type { BaseToolCallProps } from '../shared/types.js'; import type { BaseToolCallProps } from '../../shared/types.js';
import { import {
ToolCallContainer,
ToolCallCard, ToolCallCard,
ToolCallRow, ToolCallRow,
LocationsList, LocationsList,
} from '../shared/LayoutComponents.js'; } from '../../shared/LayoutComponents.js';
import { import {
safeTitle, safeTitle,
groupContent, groupContent,
mapToolStatusToContainerStatus, mapToolStatusToContainerStatus,
} from '../shared/utils.js'; } from '../../shared/utils.js';
import type { ToolCallContainerProps } from '../../shared/LayoutComponents.js';
export const ToolCallContainer: React.FC<ToolCallContainerProps> = ({
label,
status = 'success',
children,
toolCallId: _toolCallId,
labelSuffix,
className: _className,
}) => (
<div
className={`SearchToolCall qwen-message message-item ${_className || ''} relative pl-[30px] py-2 select-text toolcall-container toolcall-status-${status}`}
>
<div className="toolcall-content-wrapper flex flex-col gap-0 min-w-0 max-w-full">
<div className="flex items-baseline gap-1.5 relative min-w-0">
<span className="text-[14px] leading-none font-bold text-[var(--app-primary-foreground)]">
{label}
</span>
<span className="text-[11px] text-[var(--app-secondary-foreground)]">
{labelSuffix}
</span>
</div>
{children && (
<div className="text-[var(--app-secondary-foreground)]">{children}</div>
)}
</div>
</div>
);
/** /**
* Specialized component for Search tool calls * Specialized component for Search tool calls

View File

@@ -10,14 +10,14 @@ import type React from 'react';
import type { BaseToolCallProps } from './shared/types.js'; import type { BaseToolCallProps } from './shared/types.js';
import { shouldShowToolCall } from './shared/utils.js'; import { shouldShowToolCall } from './shared/utils.js';
import { GenericToolCall } from './GenericToolCall.js'; import { GenericToolCall } from './GenericToolCall.js';
import { ReadToolCall } from './Read/ReadToolCall.js'; import { ReadToolCall } from './done/Read/ReadToolCall.js';
import { WriteToolCall } from './Write/WriteToolCall.js'; import { WriteToolCall } from './Write/WriteToolCall.js';
import { EditToolCall } from './Edit/EditToolCall.js'; import { EditToolCall } from './done/Edit/EditToolCall.js';
import { ExecuteToolCall as BashExecuteToolCall } from './Bash/Bash.js'; import { ExecuteToolCall as BashExecuteToolCall } from './Bash/Bash.js';
import { ExecuteToolCall } from './Execute/Execute.js'; import { ExecuteToolCall } from './done/Execute/Execute.js';
import { UpdatedPlanToolCall } from './UpdatedPlan/UpdatedPlanToolCall.js'; import { UpdatedPlanToolCall } from './UpdatedPlan/UpdatedPlanToolCall.js';
import { ExecuteNodeToolCall } from './ExecuteNode/ExecuteNodeToolCall.js'; import { ExecuteNodeToolCall } from './ExecuteNode/ExecuteNodeToolCall.js';
import { SearchToolCall } from './Search/SearchToolCall.js'; import { SearchToolCall } from './done/Search/SearchToolCall.js';
import { ThinkToolCall } from './Think/ThinkToolCall.js'; import { ThinkToolCall } from './Think/ThinkToolCall.js';
/** /**
@@ -92,7 +92,9 @@ export const getToolCallComponent = (
/** /**
* Main tool call component that routes to specialized implementations * Main tool call component that routes to specialized implementations
*/ */
export const ToolCallRouter: React.FC<BaseToolCallProps> = ({ toolCall }) => { export const ToolCallRouter: React.FC<
BaseToolCallProps & { isFirst?: boolean; isLast?: boolean }
> = ({ toolCall, isFirst, isLast }) => {
// Check if we should show this tool call (hide internal ones) // Check if we should show this tool call (hide internal ones)
if (!shouldShowToolCall(toolCall.kind)) { if (!shouldShowToolCall(toolCall.kind)) {
return null; return null;
@@ -102,7 +104,7 @@ export const ToolCallRouter: React.FC<BaseToolCallProps> = ({ toolCall }) => {
const Component = getToolCallComponent(toolCall.kind, toolCall); const Component = getToolCallComponent(toolCall.kind, toolCall);
// Render the specialized component // Render the specialized component
return <Component toolCall={toolCall} />; return <Component toolCall={toolCall} isFirst={isFirst} isLast={isLast} />;
}; };
// Re-export types for convenience // Re-export types for convenience

View File

@@ -14,7 +14,7 @@ import './LayoutComponents.css';
/** /**
* Props for ToolCallContainer - Claude Code style layout * Props for ToolCallContainer - Claude Code style layout
*/ */
interface ToolCallContainerProps { export interface ToolCallContainerProps {
/** Operation label (e.g., "Read", "Write", "Search") */ /** Operation label (e.g., "Read", "Write", "Search") */
label: string; label: string;
/** Status for bullet color: 'success' | 'error' | 'warning' | 'loading' | 'default' */ /** Status for bullet color: 'success' | 'error' | 'warning' | 'loading' | 'default' */

View File

@@ -56,6 +56,8 @@ export interface ToolCallData {
*/ */
export interface BaseToolCallProps { export interface BaseToolCallProps {
toolCall: ToolCallData; toolCall: ToolCallData;
isFirst?: boolean;
isLast?: boolean;
} }
/** /**

View File

@@ -20,7 +20,13 @@ export const formatValue = (value: unknown): string => {
return ''; return '';
} }
if (typeof value === 'string') { if (typeof value === 'string') {
return value; // TODO: 尝试从 string 取出 Output 部分
try {
value = (JSON.parse(value) as { output?: unknown }).output ?? value;
} catch (_error) {
// ignore JSON parse errors
}
return value as string;
} }
// Handle Error objects specially // Handle Error objects specially
if (value instanceof Error) { if (value instanceof Error) {