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 type { BaseToolCallProps } from '../shared/types.js';
import { ToolCallContainer } from '../shared/LayoutComponents.js';
import type { BaseToolCallProps } from '../../shared/types.js';
import {
groupContent,
mapToolStatusToContainerStatus,
} from '../shared/utils.js';
import { useVSCode } from '../../../hooks/useVSCode.js';
import { FileLink } from '../../ui/FileLink.js';
import { handleOpenDiff } from '../../../utils/diffUtils.js';
} from '../../shared/utils.js';
import { FileLink } from '../../../ui/FileLink.js';
import type { ToolCallContainerProps } from '../../shared/LayoutComponents.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)
@@ -58,9 +85,6 @@ export const EditToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
[vscode],
);
// Extract filename from path
const getFileName = (path: string): string => path.split('/').pop() || path;
// Automatically trigger openDiff when diff content is detected
// Only trigger once per tool call by checking toolCallId
useEffect(() => {
@@ -88,10 +112,9 @@ export const EditToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
// Error case: show error
if (errors.length > 0) {
const path = diffs[0]?.path || locations?.[0]?.path || '';
const fileName = path ? getFileName(path) : '';
return (
<ToolCallContainer
label={fileName ? 'Edit' : 'Edit'}
label={'Edit'}
status="error"
toolCallId={toolCallId}
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. */}
<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 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 */}
<span className="text-[14px] leading-none font-bold text-[var(--app-primary-foreground)]">
Edit
@@ -137,7 +160,7 @@ export const EditToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
)}
</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 w-full">{summary}</span>
</div>
@@ -148,13 +171,19 @@ export const EditToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
// Success case without diff: show file in compact format
if (locations && locations.length > 0) {
const fileName = getFileName(locations[0].path);
const containerStatus = mapToolStatusToContainerStatus(toolCall.status);
return (
<ToolCallContainer
label={`Edited ${fileName}`}
label={`Edit`}
status={containerStatus}
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">
<span className="flex-shrink-0 relative top-[-0.1em]"></span>

View File

@@ -7,10 +7,37 @@
*/
import type React from 'react';
import type { BaseToolCallProps } from '../shared/types.js';
import { ToolCallContainer } from '../shared/LayoutComponents.js';
import { safeTitle, groupContent } from '../shared/utils.js';
import type { BaseToolCallProps } from '../../shared/types.js';
import { safeTitle, groupContent } from '../../shared/utils.js';
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
@@ -18,7 +45,9 @@ import './Execute.css';
*/
export const ExecuteToolCall: React.FC<BaseToolCallProps> = ({ 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
const { textOutputs, errors } = groupContent(content);
@@ -26,8 +55,8 @@ export const ExecuteToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
// Extract command from rawInput if available
let inputCommand = commandText;
if (rawInput && typeof rawInput === 'object') {
const inputObj = rawInput as { command?: string };
inputCommand = inputObj.command || commandText;
const inputObj = rawInput as Record<string, unknown>;
inputCommand = (inputObj.command as string | undefined) || commandText;
} else if (typeof rawInput === 'string') {
inputCommand = rawInput;
}

View File

@@ -8,15 +8,44 @@
import type React from 'react';
import { useCallback, useEffect, useMemo } from 'react';
import type { BaseToolCallProps } from '../shared/types.js';
import { ToolCallContainer } from '../shared/LayoutComponents.js';
import type { BaseToolCallProps } from '../../shared/types.js';
import {
groupContent,
mapToolStatusToContainerStatus,
} from '../shared/utils.js';
import { FileLink } from '../../ui/FileLink.js';
import { useVSCode } from '../../../hooks/useVSCode.js';
import { handleOpenDiff } from '../../../utils/diffUtils.js';
} from '../../shared/utils.js';
import { FileLink } from '../../../ui/FileLink.js';
import { useVSCode } from '../../../../hooks/useVSCode.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

View File

@@ -7,18 +7,46 @@
*/
import type React from 'react';
import type { BaseToolCallProps } from '../shared/types.js';
import type { BaseToolCallProps } from '../../shared/types.js';
import {
ToolCallContainer,
ToolCallCard,
ToolCallRow,
LocationsList,
} from '../shared/LayoutComponents.js';
} from '../../shared/LayoutComponents.js';
import {
safeTitle,
groupContent,
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

View File

@@ -10,14 +10,14 @@ 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 './Read/ReadToolCall.js';
import { ReadToolCall } from './done/Read/ReadToolCall.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 } from './Execute/Execute.js';
import { ExecuteToolCall } from './done/Execute/Execute.js';
import { UpdatedPlanToolCall } from './UpdatedPlan/UpdatedPlanToolCall.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';
/**
@@ -92,7 +92,9 @@ export const getToolCallComponent = (
/**
* 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)
if (!shouldShowToolCall(toolCall.kind)) {
return null;
@@ -102,7 +104,7 @@ export const ToolCallRouter: React.FC<BaseToolCallProps> = ({ toolCall }) => {
const Component = getToolCallComponent(toolCall.kind, toolCall);
// Render the specialized component
return <Component toolCall={toolCall} />;
return <Component toolCall={toolCall} isFirst={isFirst} isLast={isLast} />;
};
// Re-export types for convenience

View File

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

View File

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

View File

@@ -20,7 +20,13 @@ export const formatValue = (value: unknown): string => {
return '';
}
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
if (value instanceof Error) {