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);
// Error case: show command + error
if (errors.length > 0) {
return (
<ToolCallCard icon="⚡">
<ToolCallRow label="Command">
<div style={{ fontFamily: 'var(--app-monospace-font-family)' }}>
{commandText}
</div>
</ToolCallRow>
<ToolCallRow label="Error">
<div
style={{
color: '#c74e39',
fontWeight: 500,
whiteSpace: 'pre-wrap',
}}
>
{errors.join('\n')}
</div>
</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 ( return (
<ToolCallCard icon="⚡"> <ToolCallCard icon="⚡">
{/* Title row */}
<ToolCallRow label="Execute">
<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="Output">
<div
{/* Exit code or other execution details */} style={{
{otherData.length > 0 && ( fontFamily: 'var(--app-monospace-font-family)',
<ToolCallRow label="Details"> fontSize: '13px',
<CodeBlock> whiteSpace: 'pre-wrap',
{otherData.map((data: unknown) => formatValue(data)).join('\n\n')} opacity: 0.9,
</CodeBlock> }}
>
{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> </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);
const handleOpenDiff = (
path: string | undefined,
oldText: string | null | undefined,
newText: string | undefined,
) => {
if (path) {
vscode.postMessage({
type: 'openDiff',
data: { path, oldText: oldText || '', newText: newText || '' },
});
}
};
// Error case: show operation + error
if (errors.length > 0) {
return ( return (
<ToolCallCard icon={kindIcon}> <ToolCallCard icon="🔧">
{/* Title row */} <ToolCallRow label={kind}>
<ToolCallRow label="Tool"> <div>{operationText}</div>
<StatusIndicator status={status} text={titleText} />
</ToolCallRow> </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"> <ToolCallRow label="Error">
<div style={{ color: '#c74e39' }}>{errors.join('\n')}</div> <div style={{ color: '#c74e39', fontWeight: 500 }}>
{errors.join('\n')}
</div>
</ToolCallRow> </ToolCallRow>
)} </ToolCallCard>
);
}
{/* Diff rows */} // Success with diff: show diff
if (diffs.length > 0) {
return (
<ToolCallCard icon="🔧">
{diffs.map( {diffs.map(
(item: import('./shared/types.js').ToolCallContent, idx: number) => ( (item: import('./shared/types.js').ToolCallContent, idx: number) => (
<ToolCallRow key={`diff-${idx}`} label="Diff"> <div key={`diff-${idx}`} style={{ gridColumn: '1 / -1' }}>
<DiffDisplay <DiffDisplay
path={item.path} path={item.path}
oldText={item.oldText} oldText={item.oldText}
newText={item.newText} newText={item.newText}
onOpenDiff={() =>
handleOpenDiff(item.path, item.oldText, item.newText)
}
/> />
</ToolCallRow> </div>
), ),
)} )}
{/* Other data rows */}
{otherData.length > 0 && (
<ToolCallRow label="Data">
<CodeBlock>
{otherData.map((data: unknown) => formatValue(data)).join('\n\n')}
</CodeBlock>
</ToolCallRow>
)}
</ToolCallCard> </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} />
</ToolCallRow>
</ToolCallCard>
);
}
// No output
return null;
}; };

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);
// Error case: show error with operation label
if (errors.length > 0) {
return ( return (
<ToolCallCard icon="📖"> <ToolCallCard icon="📖">
{/* Title row */}
<ToolCallRow label="Read"> <ToolCallRow label="Read">
<StatusIndicator status={status} text={titleText} /> <div style={{ color: '#c74e39', fontWeight: 500 }}>
{errors.join('\n')}
</div>
</ToolCallRow> </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> </ToolCallCard>
); );
}
// Success case: show which file was read
if (locations && locations.length > 0) {
return (
<ToolCallCard icon="📖">
<ToolCallRow label="Read">
<LocationsList locations={locations} />
</ToolCallRow>
</ToolCallCard>
);
}
// No file info, don't show
return null;
}; };

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);
// Error case: show search query + error
if (errors.length > 0) {
return ( return (
<ToolCallCard icon="🔍"> <ToolCallCard icon="🔍">
{/* Title row */}
<ToolCallRow label="Search"> <ToolCallRow label="Search">
<StatusIndicator status={status} text={titleText} /> <div style={{ fontFamily: 'var(--app-monospace-font-family)' }}>
{queryText}
</div>
</ToolCallRow> </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"> <ToolCallRow label="Error">
<div style={{ color: '#c74e39' }}>{errors.join('\n')}</div> <div style={{ color: '#c74e39', fontWeight: 500 }}>
{errors.join('\n')}
</div>
</ToolCallRow> </ToolCallRow>
)}
{/* Other search metadata */}
{otherData.length > 0 && (
<ToolCallRow label="Details">
<CodeBlock>
{otherData.map((data: unknown) => formatValue(data)).join('\n\n')}
</CodeBlock>
</ToolCallRow>
)}
</ToolCallCard> </ToolCallCard>
); );
}
// Success case with results: show search query + file list
if (locations && locations.length > 0) {
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} />
</ToolCallRow>
</ToolCallCard>
);
}
// No results
return null;
}; };

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);
// Error case (rare for thinking)
if (errors.length > 0) {
return (
<ToolCallCard icon="💭">
<ToolCallRow label="Error">
<div style={{ color: '#c74e39', fontWeight: 500 }}>
{errors.join('\n')}
</div>
</ToolCallRow>
</ToolCallCard>
);
}
// Show thoughts with label
if (textOutputs.length > 0) {
const thoughts = textOutputs.join('\n\n');
const truncatedThoughts =
thoughts.length > 500 ? thoughts.substring(0, 500) + '...' : thoughts;
return ( return (
<ToolCallCard icon="💭"> <ToolCallCard icon="💭">
{/* Title row */}
<ToolCallRow label="Thinking"> <ToolCallRow label="Thinking">
<StatusIndicator status={status} text={titleText} /> <div
</ToolCallRow> style={{
fontStyle: 'italic',
{/* Thinking context/prompt */} opacity: 0.9,
{rawInput && ( lineHeight: 1.6,
<ToolCallRow label="Context"> }}
<CodeBlock>{formatValue(rawInput)}</CodeBlock> >
</ToolCallRow> {truncatedThoughts}
)}
{/* Thought content */}
{textOutputs.length > 0 && (
<ToolCallRow label="Thoughts">
<div style={{ fontStyle: 'italic', opacity: 0.95 }}>
{textOutputs.join('\n\n')}
</div> </div>
</ToolCallRow> </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> </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,31 +43,26 @@ export const WriteToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
} }
}; };
// Error case: show error with operation label
if (errors.length > 0) {
return ( return (
<ToolCallCard icon="✏️"> <ToolCallCard icon="✏️">
{/* Title row */}
<ToolCallRow label={isEdit ? 'Edit' : 'Write'}> <ToolCallRow label={isEdit ? 'Edit' : 'Write'}>
<StatusIndicator status={status} text={titleText} /> <div style={{ color: '#c74e39', fontWeight: 500 }}>
{errors.join('\n')}
</div>
</ToolCallRow> </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 (
<LocationsList locations={locations} /> <ToolCallCard icon="✏️">
</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( {diffs.map(
(item: import('./shared/types.js').ToolCallContent, idx: number) => ( (item: import('./shared/types.js').ToolCallContent, idx: number) => (
<ToolCallRow key={`diff-${idx}`} label="Diff"> <div key={`diff-${idx}`} style={{ gridColumn: '1 / -1' }}>
<DiffDisplay <DiffDisplay
path={item.path} path={item.path}
oldText={item.oldText} oldText={item.oldText}
@@ -78,32 +71,24 @@ export const WriteToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
handleOpenDiff(item.path, item.oldText, item.newText) handleOpenDiff(item.path, item.oldText, item.newText)
} }
/> />
</ToolCallRow> </div>
), ),
)} )}
{/* 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> </ToolCallCard>
); );
}
// Success case without diff: show operation + file
if (locations && locations.length > 0) {
return (
<ToolCallCard icon="✏️">
<ToolCallRow label={isEdit ? 'Edited' : 'Created'}>
<LocationsList locations={locations} />
</ToolCallRow>
</ToolCallCard>
);
}
// No output, don't show anything
return null;
}; };