refactor(vscode-ide-companion): 重构工具调用组件

- 重构 ExecuteToolCall、GenericToolCall、ReadToolCall 等组件
- 统一工具调用组件的展示样式和交互逻辑
- 优化代码结构,提高可维护性

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
yiliang114
2025-11-21 01:53:25 +08:00
parent a33187ed7a
commit 748ad8f4dd
6 changed files with 303 additions and 309 deletions

View File

@@ -8,63 +8,82 @@
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 { ToolCallCard, ToolCallRow } from './shared/LayoutComponents.js';
ToolCallCard, import { safeTitle, groupContent } from './shared/utils.js';
ToolCallRow,
StatusIndicator,
CodeBlock,
} from './shared/LayoutComponents.js';
import { formatValue, safeTitle, groupContent } from './shared/utils.js';
/** /**
* Specialized component for Execute tool calls * Specialized component for Execute tool calls
* Optimized for displaying command execution with stdout/stderr * Optimized for displaying command execution with stdout/stderr
* Shows command + output (if any) or error
*/ */
export const ExecuteToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => { export const ExecuteToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
const { title, status, rawInput, content } = toolCall; const { title, content } = toolCall;
const titleText = safeTitle(title); const commandText = safeTitle(title);
// Group content by type // Group content by type
const { textOutputs, errors, otherData } = groupContent(content); const { textOutputs, errors } = groupContent(content);
return ( // Error case: show command + error
<ToolCallCard icon="⚡"> if (errors.length > 0) {
{/* Title row */} return (
<ToolCallRow label="Execute"> <ToolCallCard icon="⚡">
<StatusIndicator status={status} text={titleText} />
</ToolCallRow>
{/* Command */}
{rawInput && (
<ToolCallRow label="Command"> <ToolCallRow label="Command">
<CodeBlock>{formatValue(rawInput)}</CodeBlock> <div style={{ fontFamily: 'var(--app-monospace-font-family)' }}>
</ToolCallRow> {commandText}
)}
{/* 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> </div>
</ToolCallRow> </ToolCallRow>
)} <ToolCallRow label="Error">
<div
{/* Exit code or other execution details */} style={{
{otherData.length > 0 && ( color: '#c74e39',
<ToolCallRow label="Details"> fontWeight: 500,
<CodeBlock> whiteSpace: 'pre-wrap',
{otherData.map((data: unknown) => formatValue(data)).join('\n\n')} }}
</CodeBlock> >
{errors.join('\n')}
</div>
</ToolCallRow> </ToolCallRow>
)} </ToolCallCard>
);
}
// Success with output: show command + output (limited)
if (textOutputs.length > 0) {
const output = textOutputs.join('\n');
const truncatedOutput =
output.length > 500 ? output.substring(0, 500) + '...' : output;
return (
<ToolCallCard icon="⚡">
<ToolCallRow label="Command">
<div style={{ fontFamily: 'var(--app-monospace-font-family)' }}>
{commandText}
</div>
</ToolCallRow>
<ToolCallRow label="Output">
<div
style={{
fontFamily: 'var(--app-monospace-font-family)',
fontSize: '13px',
whiteSpace: 'pre-wrap',
opacity: 0.9,
}}
>
{truncatedOutput}
</div>
</ToolCallRow>
</ToolCallCard>
);
}
// Success without output: show command only
return (
<ToolCallCard icon="⚡">
<ToolCallRow label="Executed">
<div style={{ fontFamily: 'var(--app-monospace-font-family)' }}>
{commandText}
</div>
</ToolCallRow>
</ToolCallCard> </ToolCallCard>
); );
}; };

View File

@@ -11,86 +11,114 @@ import type { BaseToolCallProps } from './shared/types.js';
import { import {
ToolCallCard, ToolCallCard,
ToolCallRow, ToolCallRow,
StatusIndicator,
CodeBlock,
LocationsList, LocationsList,
} from './shared/LayoutComponents.js'; } from './shared/LayoutComponents.js';
import { DiffDisplay } from './shared/DiffDisplay.js'; import { DiffDisplay } from './shared/DiffDisplay.js';
import { import { safeTitle, groupContent } from './shared/utils.js';
formatValue, import { useVSCode } from '../../hooks/useVSCode.js';
safeTitle,
getKindIcon,
groupContent,
} from './shared/utils.js';
/** /**
* Generic tool call component that can display any tool call type * Generic tool call component that can display any tool call type
* Used as fallback for unknown tool call kinds * Used as fallback for unknown tool call kinds
* Minimal display: show description and outcome
*/ */
export const GenericToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => { export const GenericToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
const { kind, title, status, rawInput, content, locations } = toolCall; const { kind, title, content, locations } = toolCall;
const kindIcon = getKindIcon(kind); const operationText = safeTitle(title);
const titleText = safeTitle(title); const vscode = useVSCode();
// Group content by type // Group content by type
const { textOutputs, errors, diffs, otherData } = groupContent(content); const { textOutputs, errors, diffs } = groupContent(content);
return ( const handleOpenDiff = (
<ToolCallCard icon={kindIcon}> path: string | undefined,
{/* Title row */} oldText: string | null | undefined,
<ToolCallRow label="Tool"> newText: string | undefined,
<StatusIndicator status={status} text={titleText} /> ) => {
</ToolCallRow> if (path) {
vscode.postMessage({
type: 'openDiff',
data: { path, oldText: oldText || '', newText: newText || '' },
});
}
};
{/* Input row */} // Error case: show operation + error
{rawInput && ( if (errors.length > 0) {
<ToolCallRow label="Input"> return (
<CodeBlock>{formatValue(rawInput)}</CodeBlock> <ToolCallCard icon="🔧">
<ToolCallRow label={kind}>
<div>{operationText}</div>
</ToolCallRow> </ToolCallRow>
)} <ToolCallRow label="Error">
<div style={{ color: '#c74e39', fontWeight: 500 }}>
{errors.join('\n')}
</div>
</ToolCallRow>
</ToolCallCard>
);
}
{/* Locations row */} // Success with diff: show diff
{locations && locations.length > 0 && ( if (diffs.length > 0) {
<ToolCallRow label="Files"> return (
<ToolCallCard icon="🔧">
{diffs.map(
(item: import('./shared/types.js').ToolCallContent, idx: number) => (
<div key={`diff-${idx}`} style={{ gridColumn: '1 / -1' }}>
<DiffDisplay
path={item.path}
oldText={item.oldText}
newText={item.newText}
onOpenDiff={() =>
handleOpenDiff(item.path, item.oldText, item.newText)
}
/>
</div>
),
)}
</ToolCallCard>
);
}
// Success with output: show operation + output (truncated)
if (textOutputs.length > 0) {
const output = textOutputs.join('\n');
const truncatedOutput =
output.length > 300 ? output.substring(0, 300) + '...' : output;
return (
<ToolCallCard icon="🔧">
<ToolCallRow label={kind}>
<div>{operationText}</div>
</ToolCallRow>
<ToolCallRow label="Output">
<div
style={{
whiteSpace: 'pre-wrap',
fontFamily: 'var(--app-monospace-font-family)',
fontSize: '13px',
opacity: 0.9,
}}
>
{truncatedOutput}
</div>
</ToolCallRow>
</ToolCallCard>
);
}
// Success with files: show operation + file list
if (locations && locations.length > 0) {
return (
<ToolCallCard icon="🔧">
<ToolCallRow label={kind}>
<LocationsList locations={locations} /> <LocationsList locations={locations} />
</ToolCallRow> </ToolCallRow>
)} </ToolCallCard>
);
}
{/* Output row - combined text outputs */} // No output
{textOutputs.length > 0 && ( return null;
<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>
);
}; };

View File

@@ -11,66 +11,45 @@ import type { BaseToolCallProps } from './shared/types.js';
import { import {
ToolCallCard, ToolCallCard,
ToolCallRow, ToolCallRow,
StatusIndicator,
CodeBlock,
LocationsList, LocationsList,
} from './shared/LayoutComponents.js'; } from './shared/LayoutComponents.js';
import { formatValue, safeTitle, groupContent } from './shared/utils.js'; import { groupContent } from './shared/utils.js';
/** /**
* Specialized component for Read tool calls * Specialized component for Read tool calls
* Optimized for displaying file reading operations * Optimized for displaying file reading operations
* Minimal display: just show file name, hide content (too verbose)
*/ */
export const ReadToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => { export const ReadToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
const { title, status, rawInput, content, locations } = toolCall; const { content, locations } = toolCall;
const titleText = safeTitle(title);
// Group content by type // Group content by type
const { textOutputs, errors, otherData } = groupContent(content); const { errors } = groupContent(content);
return ( // Error case: show error with operation label
<ToolCallCard icon="📖"> if (errors.length > 0) {
{/* Title row */} return (
<ToolCallRow label="Read"> <ToolCallCard icon="📖">
<StatusIndicator status={status} text={titleText} /> <ToolCallRow label="Read">
</ToolCallRow> <div style={{ color: '#c74e39', fontWeight: 500 }}>
{errors.join('\n')}
</div>
</ToolCallRow>
</ToolCallCard>
);
}
{/* File path(s) */} // Success case: show which file was read
{locations && locations.length > 0 && ( if (locations && locations.length > 0) {
<ToolCallRow label="File"> return (
<ToolCallCard icon="📖">
<ToolCallRow label="Read">
<LocationsList locations={locations} /> <LocationsList locations={locations} />
</ToolCallRow> </ToolCallRow>
)} </ToolCallCard>
);
}
{/* Input parameters (e.g., line range, offset) */} // No file info, don't show
{rawInput && ( return null;
<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>
);
}; };

View File

@@ -11,66 +11,56 @@ import type { BaseToolCallProps } from './shared/types.js';
import { import {
ToolCallCard, ToolCallCard,
ToolCallRow, ToolCallRow,
StatusIndicator,
CodeBlock,
LocationsList, LocationsList,
} from './shared/LayoutComponents.js'; } from './shared/LayoutComponents.js';
import { formatValue, safeTitle, groupContent } from './shared/utils.js'; import { safeTitle, groupContent } from './shared/utils.js';
/** /**
* Specialized component for Search tool calls * Specialized component for Search tool calls
* Optimized for displaying search operations and results * Optimized for displaying search operations and results
* Shows query + result count or file list
*/ */
export const SearchToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => { export const SearchToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
const { title, status, rawInput, content, locations } = toolCall; const { title, content, locations } = toolCall;
const titleText = safeTitle(title); const queryText = safeTitle(title);
// Group content by type // Group content by type
const { textOutputs, errors, otherData } = groupContent(content); const { errors } = groupContent(content);
return ( // Error case: show search query + error
<ToolCallCard icon="🔍"> if (errors.length > 0) {
{/* Title row */} return (
<ToolCallRow label="Search"> <ToolCallCard icon="🔍">
<StatusIndicator status={status} text={titleText} /> <ToolCallRow label="Search">
</ToolCallRow> <div style={{ fontFamily: 'var(--app-monospace-font-family)' }}>
{queryText}
{/* Search query/pattern */} </div>
{rawInput && (
<ToolCallRow label="Query">
<CodeBlock>{formatValue(rawInput)}</CodeBlock>
</ToolCallRow> </ToolCallRow>
)} <ToolCallRow label="Error">
<div style={{ color: '#c74e39', fontWeight: 500 }}>
{errors.join('\n')}
</div>
</ToolCallRow>
</ToolCallCard>
);
}
{/* Search results - files found */} // Success case with results: show search query + file list
{locations && locations.length > 0 && ( if (locations && locations.length > 0) {
<ToolCallRow label="Results"> return (
<ToolCallCard icon="🔍">
<ToolCallRow label="Search">
<div style={{ fontFamily: 'var(--app-monospace-font-family)' }}>
{queryText}
</div>
</ToolCallRow>
<ToolCallRow label={`Found (${locations.length})`}>
<LocationsList locations={locations} /> <LocationsList locations={locations} />
</ToolCallRow> </ToolCallRow>
)} </ToolCallCard>
);
}
{/* Search output details */} // No results
{textOutputs.length > 0 && ( return null;
<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>
);
}; };

View File

@@ -8,63 +8,56 @@
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 { ToolCallCard, ToolCallRow } from './shared/LayoutComponents.js';
ToolCallCard, import { groupContent } from './shared/utils.js';
ToolCallRow,
StatusIndicator,
CodeBlock,
} from './shared/LayoutComponents.js';
import { formatValue, safeTitle, groupContent } from './shared/utils.js';
/** /**
* Specialized component for Think tool calls * Specialized component for Think tool calls
* Optimized for displaying AI reasoning and thought processes * Optimized for displaying AI reasoning and thought processes
* Minimal display: just show the thoughts (no context)
*/ */
export const ThinkToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => { export const ThinkToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
const { title, status, rawInput, content } = toolCall; const { content } = toolCall;
const titleText = safeTitle(title);
// Group content by type // Group content by type
const { textOutputs, errors, otherData } = groupContent(content); const { textOutputs, errors } = groupContent(content);
return ( // Error case (rare for thinking)
<ToolCallCard icon="💭"> if (errors.length > 0) {
{/* Title row */} return (
<ToolCallRow label="Thinking"> <ToolCallCard icon="💭">
<StatusIndicator status={status} text={titleText} /> <ToolCallRow label="Error">
</ToolCallRow> <div style={{ color: '#c74e39', fontWeight: 500 }}>
{errors.join('\n')}
{/* 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> </div>
</ToolCallRow> </ToolCallRow>
)} </ToolCallCard>
);
}
{/* Error handling */} // Show thoughts with label
{errors.length > 0 && ( if (textOutputs.length > 0) {
<ToolCallRow label="Error"> const thoughts = textOutputs.join('\n\n');
<div style={{ color: '#c74e39' }}>{errors.join('\n')}</div> const truncatedThoughts =
</ToolCallRow> thoughts.length > 500 ? thoughts.substring(0, 500) + '...' : thoughts;
)}
{/* Other reasoning data */} return (
{otherData.length > 0 && ( <ToolCallCard icon="💭">
<ToolCallRow label="Details"> <ToolCallRow label="Thinking">
<CodeBlock> <div
{otherData.map((data: unknown) => formatValue(data)).join('\n\n')} style={{
</CodeBlock> fontStyle: 'italic',
opacity: 0.9,
lineHeight: 1.6,
}}
>
{truncatedThoughts}
</div>
</ToolCallRow> </ToolCallRow>
)} </ToolCallCard>
</ToolCallCard> );
); }
// Empty thoughts
return null;
}; };

View File

@@ -11,26 +11,24 @@ import type { BaseToolCallProps } from './shared/types.js';
import { import {
ToolCallCard, ToolCallCard,
ToolCallRow, ToolCallRow,
StatusIndicator,
CodeBlock,
LocationsList, LocationsList,
} from './shared/LayoutComponents.js'; } from './shared/LayoutComponents.js';
import { DiffDisplay } from './shared/DiffDisplay.js'; import { DiffDisplay } from './shared/DiffDisplay.js';
import { formatValue, safeTitle, groupContent } from './shared/utils.js'; import { groupContent } from './shared/utils.js';
import { useVSCode } from '../../hooks/useVSCode.js'; import { useVSCode } from '../../hooks/useVSCode.js';
/** /**
* Specialized component for Write/Edit tool calls * Specialized component for Write/Edit tool calls
* Optimized for displaying file writing and editing operations with diffs * Optimized for displaying file writing and editing operations with diffs
* Follows minimal display principle: only show what matters
*/ */
export const WriteToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => { export const WriteToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
const { kind, title, status, rawInput, content, locations } = toolCall; const { kind, status: _status, content, locations } = toolCall;
const titleText = safeTitle(title);
const isEdit = kind.toLowerCase() === 'edit'; const isEdit = kind.toLowerCase() === 'edit';
const vscode = useVSCode(); const vscode = useVSCode();
// Group content by type // Group content by type
const { textOutputs, errors, diffs, otherData } = groupContent(content); const { errors, diffs } = groupContent(content);
const handleOpenDiff = ( const handleOpenDiff = (
path: string | undefined, path: string | undefined,
@@ -45,65 +43,52 @@ export const WriteToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
} }
}; };
return ( // Error case: show error with operation label
<ToolCallCard icon="✏️"> if (errors.length > 0) {
{/* Title row */} return (
<ToolCallRow label={isEdit ? 'Edit' : 'Write'}> <ToolCallCard icon="✏️">
<StatusIndicator status={status} text={titleText} /> <ToolCallRow label={isEdit ? 'Edit' : 'Write'}>
</ToolCallRow> <div style={{ color: '#c74e39', fontWeight: 500 }}>
{errors.join('\n')}
</div>
</ToolCallRow>
</ToolCallCard>
);
}
{/* File path(s) */} // Success case with diff: show diff (already has file path)
{locations && locations.length > 0 && ( if (diffs.length > 0) {
<ToolCallRow label="File"> return (
<ToolCallCard icon="✏️">
{diffs.map(
(item: import('./shared/types.js').ToolCallContent, idx: number) => (
<div key={`diff-${idx}`} style={{ gridColumn: '1 / -1' }}>
<DiffDisplay
path={item.path}
oldText={item.oldText}
newText={item.newText}
onOpenDiff={() =>
handleOpenDiff(item.path, item.oldText, item.newText)
}
/>
</div>
),
)}
</ToolCallCard>
);
}
// Success case without diff: show operation + file
if (locations && locations.length > 0) {
return (
<ToolCallCard icon="✏️">
<ToolCallRow label={isEdit ? 'Edited' : 'Created'}>
<LocationsList locations={locations} /> <LocationsList locations={locations} />
</ToolCallRow> </ToolCallRow>
)} </ToolCallCard>
);
}
{/* Input parameters (e.g., old_string, new_string for edits) */} // No output, don't show anything
{rawInput && ( return null;
<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}
onOpenDiff={() =>
handleOpenDiff(item.path, item.oldText, 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>
);
}; };