mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-21 09:17:53 +00:00
refactor(vscode-ide-companion): migrate session save to CLI /chat save command
- Replace manual checkpoint file writing with CLI's native /chat save command - Add saveCheckpointViaCommand method to use CLI's built-in save functionality - Deprecate saveSessionViaAcp as CLI doesn't support session/save ACP method - Update saveCheckpoint to delegate to CLI command for complete context preservation - Enhanced error logging in acpSessionManager session load - Mark saveSessionViaAcp as deprecated with fallback to command-based save - Fix ESLint errors: remove unused imports and catch variables, wrap case block declarations This ensures checkpoints are saved with complete session context including tool calls, leveraging CLI's native save functionality instead of manual file operations. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,166 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Edit tool call component - specialized for file editing operations
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import { useState } from 'react';
|
||||
import type { BaseToolCallProps } from './shared/types.js';
|
||||
import { ToolCallContainer } from './shared/LayoutComponents.js';
|
||||
import { DiffDisplay } from './shared/DiffDisplay.js';
|
||||
import { groupContent } from './shared/utils.js';
|
||||
import { useVSCode } from '../../hooks/useVSCode.js';
|
||||
import { FileLink } from '../shared/FileLink.js';
|
||||
|
||||
/**
|
||||
* Calculate diff summary (added/removed lines)
|
||||
*/
|
||||
const getDiffSummary = (
|
||||
oldText: string | null | undefined,
|
||||
newText: string | undefined,
|
||||
): string => {
|
||||
const oldLines = oldText ? oldText.split('\n').length : 0;
|
||||
const newLines = newText ? newText.split('\n').length : 0;
|
||||
const diff = newLines - oldLines;
|
||||
|
||||
if (diff > 0) {
|
||||
return `+${diff} lines`;
|
||||
} else if (diff < 0) {
|
||||
return `${diff} lines`;
|
||||
} else {
|
||||
return 'Modified';
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Specialized component for Edit tool calls
|
||||
* Optimized for displaying file editing operations with diffs
|
||||
*/
|
||||
export const EditToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
|
||||
const { content, locations, toolCallId } = toolCall;
|
||||
const vscode = useVSCode();
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
|
||||
// Group content by type
|
||||
const { 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 || '' },
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Extract filename from path
|
||||
const getFileName = (path: string): string => path.split('/').pop() || path;
|
||||
|
||||
// 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 ${fileName}` : 'Edit'}
|
||||
status="error"
|
||||
toolCallId={toolCallId}
|
||||
>
|
||||
{errors.join('\n')}
|
||||
</ToolCallContainer>
|
||||
);
|
||||
}
|
||||
|
||||
// Success case with diff: show collapsible format
|
||||
if (diffs.length > 0) {
|
||||
const firstDiff = diffs[0];
|
||||
const path = firstDiff.path || (locations && locations[0]?.path) || '';
|
||||
const fileName = path ? getFileName(path) : '';
|
||||
const summary = getDiffSummary(firstDiff.oldText, firstDiff.newText);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
className="relative py-2 select-text cursor-pointer hover:bg-[var(--app-input-background)]"
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
>
|
||||
<span className="absolute left-2 top-[10px] text-[10px] text-[#74c991]">
|
||||
●
|
||||
</span>
|
||||
<div className="flex flex-col gap-1 pl-[30px]">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-[13px] font-medium text-[var(--app-primary-foreground)]">
|
||||
Edit {fileName}
|
||||
</span>
|
||||
{toolCallId && (
|
||||
<span className="text-[10px] opacity-30">
|
||||
[{toolCallId.slice(-8)}]
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-xs opacity-60 mr-2">
|
||||
{expanded ? '▼' : '▶'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="inline-flex text-[var(--app-secondary-foreground)] text-[0.85em] opacity-70 flex-row items-start w-full gap-1">
|
||||
<span className="flex-shrink-0 relative top-[-0.1em]">⎿</span>
|
||||
<span className="flex-shrink-0 w-full">{summary}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{expanded && (
|
||||
<div className="ml-[30px] mt-1">
|
||||
{diffs.map(
|
||||
(
|
||||
item: import('./shared/types.js').ToolCallContent,
|
||||
idx: number,
|
||||
) => (
|
||||
<DiffDisplay
|
||||
key={`diff-${idx}`}
|
||||
path={item.path}
|
||||
oldText={item.oldText}
|
||||
newText={item.newText}
|
||||
onOpenDiff={() =>
|
||||
handleOpenDiff(item.path, item.oldText, item.newText)
|
||||
}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Success case without diff: show file in compact format
|
||||
if (locations && locations.length > 0) {
|
||||
const fileName = getFileName(locations[0].path);
|
||||
return (
|
||||
<ToolCallContainer
|
||||
label={`Edited ${fileName}`}
|
||||
status="success"
|
||||
toolCallId={toolCallId}
|
||||
>
|
||||
<div className="inline-flex text-[var(--app-secondary-foreground)] text-[0.85em] opacity-70 flex-row items-start w-full gap-1">
|
||||
<span className="flex-shrink-0 relative top-[-0.1em]">⎿</span>
|
||||
<FileLink
|
||||
path={locations[0].path}
|
||||
line={locations[0].line}
|
||||
showFullPath={true}
|
||||
/>
|
||||
</div>
|
||||
</ToolCallContainer>
|
||||
);
|
||||
}
|
||||
|
||||
// No output, don't show anything
|
||||
return null;
|
||||
};
|
||||
@@ -8,82 +8,100 @@
|
||||
|
||||
import type React from 'react';
|
||||
import type { BaseToolCallProps } from './shared/types.js';
|
||||
import { ToolCallCard, ToolCallRow } from './shared/LayoutComponents.js';
|
||||
import { ToolCallContainer } from './shared/LayoutComponents.js';
|
||||
import { safeTitle, groupContent } from './shared/utils.js';
|
||||
|
||||
/**
|
||||
* Specialized component for Execute tool calls
|
||||
* Optimized for displaying command execution with stdout/stderr
|
||||
* Shows command + output (if any) or error
|
||||
* Specialized component for Execute/Bash tool calls
|
||||
* Shows: Bash bullet + description + IN/OUT card
|
||||
*/
|
||||
export const ExecuteToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
|
||||
const { title, content } = toolCall;
|
||||
const { title, content, rawInput, toolCallId } = toolCall;
|
||||
const commandText = safeTitle(title);
|
||||
|
||||
// Group content by type
|
||||
const { textOutputs, errors } = groupContent(content);
|
||||
|
||||
// Error case: show command + error
|
||||
// Extract command from rawInput if available
|
||||
let inputCommand = commandText;
|
||||
if (rawInput && typeof rawInput === 'object') {
|
||||
const inputObj = rawInput as { command?: string };
|
||||
inputCommand = inputObj.command || commandText;
|
||||
} else if (typeof rawInput === 'string') {
|
||||
inputCommand = rawInput;
|
||||
}
|
||||
|
||||
// Error case
|
||||
if (errors.length > 0) {
|
||||
return (
|
||||
<ToolCallCard icon="⚡">
|
||||
<ToolCallRow label="Command">
|
||||
<div style={{ fontFamily: 'var(--app-monospace-font-family)' }}>
|
||||
{commandText}
|
||||
<ToolCallContainer label="Bash" status="error" toolCallId={toolCallId}>
|
||||
<div className="inline-flex text-[var(--app-secondary-foreground)] text-[0.85em] opacity-70 mt-[2px] mb-[2px] flex-row items-start w-full gap-1">
|
||||
<span className="flex-shrink-0 relative top-[-0.1em]">⎿</span>
|
||||
<span className="flex-shrink-0 w-full">{commandText}</span>
|
||||
</div>
|
||||
<div className="bg-[var(--app-input-background)] border border-[var(--app-input-border)] rounded-md p-3 flex flex-col gap-3">
|
||||
<div className="grid grid-cols-[80px_1fr] gap-3">
|
||||
<div className="text-xs text-[var(--app-secondary-foreground)] font-medium pt-[2px]">
|
||||
IN
|
||||
</div>
|
||||
<div className="text-[var(--app-primary-foreground)] font-mono text-[13px] break-words">
|
||||
{inputCommand}
|
||||
</div>
|
||||
</div>
|
||||
</ToolCallRow>
|
||||
<ToolCallRow label="Error">
|
||||
<div
|
||||
style={{
|
||||
color: '#c74e39',
|
||||
fontWeight: 500,
|
||||
whiteSpace: 'pre-wrap',
|
||||
}}
|
||||
>
|
||||
{errors.join('\n')}
|
||||
<div className="grid grid-cols-[80px_1fr] gap-3">
|
||||
<div className="text-xs text-[var(--app-secondary-foreground)] font-medium pt-[2px]">
|
||||
Error
|
||||
</div>
|
||||
<div className="text-[#c74e39] font-mono text-[13px] whitespace-pre-wrap break-words">
|
||||
{errors.join('\n')}
|
||||
</div>
|
||||
</div>
|
||||
</ToolCallRow>
|
||||
</ToolCallCard>
|
||||
</div>
|
||||
</ToolCallContainer>
|
||||
);
|
||||
}
|
||||
|
||||
// Success with output: show command + output (limited)
|
||||
// Success with output
|
||||
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}
|
||||
<ToolCallContainer label="Bash" status="success" toolCallId={toolCallId}>
|
||||
<div className="inline-flex text-[var(--app-secondary-foreground)] text-[0.85em] opacity-70 mt-[2px] mb-[2px] flex-row items-start w-full gap-1">
|
||||
<span className="flex-shrink-0 relative top-[-0.1em]">⎿</span>
|
||||
<span className="flex-shrink-0 w-full">{commandText}</span>
|
||||
</div>
|
||||
<div className="bg-[var(--app-input-background)] border border-[var(--app-input-border)] rounded-md p-3 flex flex-col gap-3">
|
||||
<div className="grid grid-cols-[80px_1fr] gap-3">
|
||||
<div className="text-xs text-[var(--app-secondary-foreground)] font-medium pt-[2px]">
|
||||
IN
|
||||
</div>
|
||||
<div className="text-[var(--app-primary-foreground)] font-mono text-[13px] break-words">
|
||||
{inputCommand}
|
||||
</div>
|
||||
</div>
|
||||
</ToolCallRow>
|
||||
<ToolCallRow label="Output">
|
||||
<div
|
||||
style={{
|
||||
fontFamily: 'var(--app-monospace-font-family)',
|
||||
fontSize: '13px',
|
||||
whiteSpace: 'pre-wrap',
|
||||
opacity: 0.9,
|
||||
}}
|
||||
>
|
||||
{truncatedOutput}
|
||||
<div className="grid grid-cols-[80px_1fr] gap-3">
|
||||
<div className="text-xs text-[var(--app-secondary-foreground)] font-medium pt-[2px]">
|
||||
OUT
|
||||
</div>
|
||||
<div className="text-[var(--app-primary-foreground)] font-mono text-[13px] whitespace-pre-wrap opacity-90 break-words">
|
||||
{truncatedOutput}
|
||||
</div>
|
||||
</div>
|
||||
</ToolCallRow>
|
||||
</ToolCallCard>
|
||||
</div>
|
||||
</ToolCallContainer>
|
||||
);
|
||||
}
|
||||
|
||||
// Success without output: show command only
|
||||
// Success without output: show command with branch connector
|
||||
return (
|
||||
<ToolCallCard icon="⚡">
|
||||
<ToolCallRow label="Executed">
|
||||
<div style={{ fontFamily: 'var(--app-monospace-font-family)' }}>
|
||||
{commandText}
|
||||
</div>
|
||||
</ToolCallRow>
|
||||
</ToolCallCard>
|
||||
<ToolCallContainer label="Bash" status="success" toolCallId={toolCallId}>
|
||||
<div className="inline-flex text-[var(--app-secondary-foreground)] text-[0.85em] opacity-70 mt-[2px] mb-[2px] flex-row items-start w-full gap-1">
|
||||
<span className="flex-shrink-0 relative top-[-0.1em]">⎿</span>
|
||||
<span className="flex-shrink-0 w-full">{commandText}</span>
|
||||
</div>
|
||||
</ToolCallContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
import type React from 'react';
|
||||
import type { BaseToolCallProps } from './shared/types.js';
|
||||
import {
|
||||
ToolCallContainer,
|
||||
ToolCallCard,
|
||||
ToolCallRow,
|
||||
LocationsList,
|
||||
@@ -23,7 +24,7 @@ import { useVSCode } from '../../hooks/useVSCode.js';
|
||||
* Minimal display: show description and outcome
|
||||
*/
|
||||
export const GenericToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
|
||||
const { kind, title, content, locations } = toolCall;
|
||||
const { kind, title, content, locations, toolCallId } = toolCall;
|
||||
const operationText = safeTitle(title);
|
||||
const vscode = useVSCode();
|
||||
|
||||
@@ -43,7 +44,7 @@ export const GenericToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
|
||||
}
|
||||
};
|
||||
|
||||
// Error case: show operation + error
|
||||
// Error case: show operation + error in card layout
|
||||
if (errors.length > 0) {
|
||||
return (
|
||||
<ToolCallCard icon="🔧">
|
||||
@@ -51,15 +52,13 @@ export const GenericToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
|
||||
<div>{operationText}</div>
|
||||
</ToolCallRow>
|
||||
<ToolCallRow label="Error">
|
||||
<div style={{ color: '#c74e39', fontWeight: 500 }}>
|
||||
{errors.join('\n')}
|
||||
</div>
|
||||
<div className="text-[#c74e39] font-medium">{errors.join('\n')}</div>
|
||||
</ToolCallRow>
|
||||
</ToolCallCard>
|
||||
);
|
||||
}
|
||||
|
||||
// Success with diff: show diff
|
||||
// Success with diff: show diff in card layout
|
||||
if (diffs.length > 0) {
|
||||
return (
|
||||
<ToolCallCard icon="🔧">
|
||||
@@ -81,44 +80,54 @@ export const GenericToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
|
||||
);
|
||||
}
|
||||
|
||||
// Success with output: show operation + output (truncated)
|
||||
// Success with output: use card for long output, compact for short
|
||||
if (textOutputs.length > 0) {
|
||||
const output = textOutputs.join('\n');
|
||||
const truncatedOutput =
|
||||
output.length > 300 ? output.substring(0, 300) + '...' : output;
|
||||
const isLong = output.length > 150;
|
||||
|
||||
if (isLong) {
|
||||
const truncatedOutput =
|
||||
output.length > 300 ? output.substring(0, 300) + '...' : output;
|
||||
|
||||
return (
|
||||
<ToolCallCard icon="🔧">
|
||||
<ToolCallRow label={kind}>
|
||||
<div>{operationText}</div>
|
||||
</ToolCallRow>
|
||||
<ToolCallRow label="Output">
|
||||
<div className="whitespace-pre-wrap font-mono text-[13px] opacity-90">
|
||||
{truncatedOutput}
|
||||
</div>
|
||||
</ToolCallRow>
|
||||
</ToolCallCard>
|
||||
);
|
||||
}
|
||||
|
||||
// Short output - compact format
|
||||
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>
|
||||
<ToolCallContainer label={kind} status="success" toolCallId={toolCallId}>
|
||||
{operationText || output}
|
||||
</ToolCallContainer>
|
||||
);
|
||||
}
|
||||
|
||||
// Success with files: show operation + file list
|
||||
// Success with files: show operation + file list in compact format
|
||||
if (locations && locations.length > 0) {
|
||||
return (
|
||||
<ToolCallCard icon="🔧">
|
||||
<ToolCallRow label={kind}>
|
||||
<LocationsList locations={locations} />
|
||||
</ToolCallRow>
|
||||
</ToolCallCard>
|
||||
<ToolCallContainer label={kind} status="success" toolCallId={toolCallId}>
|
||||
<LocationsList locations={locations} />
|
||||
</ToolCallContainer>
|
||||
);
|
||||
}
|
||||
|
||||
// No output - show just the operation
|
||||
if (operationText) {
|
||||
return (
|
||||
<ToolCallContainer label={kind} status="success" toolCallId={toolCallId}>
|
||||
{operationText}
|
||||
</ToolCallContainer>
|
||||
);
|
||||
}
|
||||
|
||||
// No output
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -8,45 +8,47 @@
|
||||
|
||||
import type React from 'react';
|
||||
import type { BaseToolCallProps } from './shared/types.js';
|
||||
import {
|
||||
ToolCallCard,
|
||||
ToolCallRow,
|
||||
LocationsList,
|
||||
} from './shared/LayoutComponents.js';
|
||||
import { ToolCallContainer } from './shared/LayoutComponents.js';
|
||||
import { groupContent } from './shared/utils.js';
|
||||
|
||||
/**
|
||||
* Specialized component for Read tool calls
|
||||
* Optimized for displaying file reading operations
|
||||
* Minimal display: just show file name, hide content (too verbose)
|
||||
* Shows: Read filename (no content preview)
|
||||
*/
|
||||
export const ReadToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
|
||||
const { content, locations } = toolCall;
|
||||
const { content, locations, toolCallId } = toolCall;
|
||||
|
||||
// Group content by type
|
||||
const { errors } = groupContent(content);
|
||||
|
||||
// Error case: show error with operation label
|
||||
// Extract filename from path
|
||||
const getFileName = (path: string): string => path.split('/').pop() || path;
|
||||
|
||||
// Error case: show error
|
||||
if (errors.length > 0) {
|
||||
const path = locations?.[0]?.path || '';
|
||||
const fileName = path ? getFileName(path) : '';
|
||||
return (
|
||||
<ToolCallCard icon="📖">
|
||||
<ToolCallRow label="Read">
|
||||
<div style={{ color: '#c74e39', fontWeight: 500 }}>
|
||||
{errors.join('\n')}
|
||||
</div>
|
||||
</ToolCallRow>
|
||||
</ToolCallCard>
|
||||
<ToolCallContainer
|
||||
label={fileName ? `Read ${fileName}` : 'Read'}
|
||||
status="error"
|
||||
toolCallId={toolCallId}
|
||||
>
|
||||
{errors.join('\n')}
|
||||
</ToolCallContainer>
|
||||
);
|
||||
}
|
||||
|
||||
// Success case: show which file was read
|
||||
// Success case: show which file was read with filename in label
|
||||
if (locations && locations.length > 0) {
|
||||
const fileName = getFileName(locations[0].path);
|
||||
return (
|
||||
<ToolCallCard icon="📖">
|
||||
<ToolCallRow label="Read">
|
||||
<LocationsList locations={locations} />
|
||||
</ToolCallRow>
|
||||
</ToolCallCard>
|
||||
<ToolCallContainer
|
||||
label={`Read ${fileName}`}
|
||||
status="success"
|
||||
toolCallId={toolCallId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
import type React from 'react';
|
||||
import type { BaseToolCallProps } from './shared/types.js';
|
||||
import {
|
||||
ToolCallContainer,
|
||||
ToolCallCard,
|
||||
ToolCallRow,
|
||||
LocationsList,
|
||||
@@ -27,19 +28,15 @@ export const SearchToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
|
||||
// Group content by type
|
||||
const { errors } = groupContent(content);
|
||||
|
||||
// Error case: show search query + error
|
||||
// Error case: show search query + error in card layout
|
||||
if (errors.length > 0) {
|
||||
return (
|
||||
<ToolCallCard icon="🔍">
|
||||
<ToolCallRow label="Search">
|
||||
<div style={{ fontFamily: 'var(--app-monospace-font-family)' }}>
|
||||
{queryText}
|
||||
</div>
|
||||
<div className="font-mono">{queryText}</div>
|
||||
</ToolCallRow>
|
||||
<ToolCallRow label="Error">
|
||||
<div style={{ color: '#c74e39', fontWeight: 500 }}>
|
||||
{errors.join('\n')}
|
||||
</div>
|
||||
<div className="text-[#c74e39] font-medium">{errors.join('\n')}</div>
|
||||
</ToolCallRow>
|
||||
</ToolCallCard>
|
||||
);
|
||||
@@ -47,20 +44,37 @@ export const SearchToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
|
||||
|
||||
// Success case with results: show search query + file list
|
||||
if (locations && locations.length > 0) {
|
||||
// If multiple results, use card layout; otherwise use compact format
|
||||
if (locations.length > 1) {
|
||||
return (
|
||||
<ToolCallCard icon="🔍">
|
||||
<ToolCallRow label="Search">
|
||||
<div className="font-mono">{queryText}</div>
|
||||
</ToolCallRow>
|
||||
<ToolCallRow label={`Found (${locations.length})`}>
|
||||
<LocationsList locations={locations} />
|
||||
</ToolCallRow>
|
||||
</ToolCallCard>
|
||||
);
|
||||
}
|
||||
// Single result - compact format
|
||||
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>
|
||||
<ToolCallContainer label="Search" status="success">
|
||||
<span className="font-mono">{queryText}</span>
|
||||
<span className="mx-2 opacity-50">→</span>
|
||||
<LocationsList locations={locations} />
|
||||
</ToolCallContainer>
|
||||
);
|
||||
}
|
||||
|
||||
// No results - show query only
|
||||
if (queryText) {
|
||||
return (
|
||||
<ToolCallContainer label="Search" status="success">
|
||||
<span className="font-mono">{queryText}</span>
|
||||
</ToolCallContainer>
|
||||
);
|
||||
}
|
||||
|
||||
// No results
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -8,7 +8,11 @@
|
||||
|
||||
import type React from 'react';
|
||||
import type { BaseToolCallProps } from './shared/types.js';
|
||||
import { ToolCallCard, ToolCallRow } from './shared/LayoutComponents.js';
|
||||
import {
|
||||
ToolCallContainer,
|
||||
ToolCallCard,
|
||||
ToolCallRow,
|
||||
} from './shared/LayoutComponents.js';
|
||||
import { groupContent } from './shared/utils.js';
|
||||
|
||||
/**
|
||||
@@ -25,36 +29,37 @@ export const ThinkToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
|
||||
// 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>
|
||||
<ToolCallContainer label="Thinking" status="error">
|
||||
{errors.join('\n')}
|
||||
</ToolCallContainer>
|
||||
);
|
||||
}
|
||||
|
||||
// Show thoughts with label
|
||||
// Show thoughts - use card for long content, compact for short
|
||||
if (textOutputs.length > 0) {
|
||||
const thoughts = textOutputs.join('\n\n');
|
||||
const truncatedThoughts =
|
||||
thoughts.length > 500 ? thoughts.substring(0, 500) + '...' : thoughts;
|
||||
const isLong = thoughts.length > 200;
|
||||
|
||||
if (isLong) {
|
||||
const truncatedThoughts =
|
||||
thoughts.length > 500 ? thoughts.substring(0, 500) + '...' : thoughts;
|
||||
|
||||
return (
|
||||
<ToolCallCard icon="💭">
|
||||
<ToolCallRow label="Thinking">
|
||||
<div className="italic opacity-90 leading-relaxed">
|
||||
{truncatedThoughts}
|
||||
</div>
|
||||
</ToolCallRow>
|
||||
</ToolCallCard>
|
||||
);
|
||||
}
|
||||
|
||||
// Short thoughts - compact format
|
||||
return (
|
||||
<ToolCallCard icon="💭">
|
||||
<ToolCallRow label="Thinking">
|
||||
<div
|
||||
style={{
|
||||
fontStyle: 'italic',
|
||||
opacity: 0.9,
|
||||
lineHeight: 1.6,
|
||||
}}
|
||||
>
|
||||
{truncatedThoughts}
|
||||
</div>
|
||||
</ToolCallRow>
|
||||
</ToolCallCard>
|
||||
<ToolCallContainer label="Thinking" status="default">
|
||||
<span className="italic opacity-90">{thoughts}</span>
|
||||
</ToolCallContainer>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* TodoWrite tool call component - specialized for todo list operations
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import type { BaseToolCallProps } from './shared/types.js';
|
||||
import { ToolCallContainer } from './shared/LayoutComponents.js';
|
||||
import { groupContent } from './shared/utils.js';
|
||||
|
||||
/**
|
||||
* Specialized component for TodoWrite tool calls
|
||||
* Optimized for displaying todo list update operations
|
||||
*/
|
||||
export const TodoWriteToolCall: React.FC<BaseToolCallProps> = ({
|
||||
toolCall,
|
||||
}) => {
|
||||
const { content } = toolCall;
|
||||
|
||||
// Group content by type
|
||||
const { errors, textOutputs } = groupContent(content);
|
||||
|
||||
// Error case: show error
|
||||
if (errors.length > 0) {
|
||||
return (
|
||||
<ToolCallContainer label="Update Todos" status="error">
|
||||
{errors.join('\n')}
|
||||
</ToolCallContainer>
|
||||
);
|
||||
}
|
||||
|
||||
// Success case: show simple confirmation
|
||||
const outputText =
|
||||
textOutputs.length > 0 ? textOutputs.join(' ') : 'Todos updated';
|
||||
|
||||
// Truncate if too long
|
||||
const displayText =
|
||||
outputText.length > 100 ? outputText.substring(0, 100) + '...' : outputText;
|
||||
|
||||
return (
|
||||
<ToolCallContainer label="Update Todos" status="success">
|
||||
{displayText}
|
||||
</ToolCallContainer>
|
||||
);
|
||||
};
|
||||
@@ -3,89 +3,93 @@
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Write/Edit tool call component - specialized for file writing and editing operations
|
||||
* Write tool call component - specialized for file writing operations
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import type { BaseToolCallProps } from './shared/types.js';
|
||||
import {
|
||||
ToolCallCard,
|
||||
ToolCallRow,
|
||||
LocationsList,
|
||||
} from './shared/LayoutComponents.js';
|
||||
import { DiffDisplay } from './shared/DiffDisplay.js';
|
||||
import { ToolCallContainer } from './shared/LayoutComponents.js';
|
||||
import { groupContent } from './shared/utils.js';
|
||||
import { useVSCode } from '../../hooks/useVSCode.js';
|
||||
|
||||
/**
|
||||
* Specialized component for Write/Edit tool calls
|
||||
* Optimized for displaying file writing and editing operations with diffs
|
||||
* Follows minimal display principle: only show what matters
|
||||
* Specialized component for Write tool calls
|
||||
* Shows: Write filename + error message + content preview
|
||||
*/
|
||||
export const WriteToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
|
||||
const { kind, status: _status, content, locations } = toolCall;
|
||||
const isEdit = kind.toLowerCase() === 'edit';
|
||||
const vscode = useVSCode();
|
||||
const { content, locations, rawInput, toolCallId } = toolCall;
|
||||
|
||||
// Group content by type
|
||||
const { errors, diffs } = groupContent(content);
|
||||
const { errors, textOutputs } = 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 || '' },
|
||||
});
|
||||
}
|
||||
};
|
||||
// Extract filename from path
|
||||
const getFileName = (path: string): string => path.split('/').pop() || path;
|
||||
|
||||
// Error case: show error with operation label
|
||||
// Extract content to write from rawInput
|
||||
let writeContent = '';
|
||||
if (rawInput && typeof rawInput === 'object') {
|
||||
const inputObj = rawInput as { content?: string };
|
||||
writeContent = inputObj.content || '';
|
||||
} else if (typeof rawInput === 'string') {
|
||||
writeContent = rawInput;
|
||||
}
|
||||
|
||||
// Error case: show filename + error message + content preview
|
||||
if (errors.length > 0) {
|
||||
const path = locations?.[0]?.path || '';
|
||||
const fileName = path ? getFileName(path) : '';
|
||||
const errorMessage = errors.join('\n');
|
||||
|
||||
// Truncate content preview
|
||||
const truncatedContent =
|
||||
writeContent.length > 200
|
||||
? writeContent.substring(0, 200) + '...'
|
||||
: writeContent;
|
||||
|
||||
return (
|
||||
<ToolCallCard icon="✏️">
|
||||
<ToolCallRow label={isEdit ? 'Edit' : 'Write'}>
|
||||
<div style={{ color: '#c74e39', fontWeight: 500 }}>
|
||||
{errors.join('\n')}
|
||||
<ToolCallContainer
|
||||
label={fileName ? `Write ${fileName}` : 'Write'}
|
||||
status="error"
|
||||
toolCallId={toolCallId}
|
||||
>
|
||||
<div className="inline-flex text-[var(--app-secondary-foreground)] text-[0.85em] opacity-70 mt-[2px] mb-[2px] flex-row items-start w-full gap-1">
|
||||
<span className="flex-shrink-0 relative top-[-0.1em]">⎿</span>
|
||||
<span className="flex-shrink-0 w-full">{errorMessage}</span>
|
||||
</div>
|
||||
{truncatedContent && (
|
||||
<div className="bg-[var(--app-input-background)] border border-[var(--app-input-border)] rounded-md p-3 mt-1">
|
||||
<pre className="font-mono text-[13px] whitespace-pre-wrap break-words text-[var(--app-primary-foreground)] opacity-90">
|
||||
{truncatedContent}
|
||||
</pre>
|
||||
</div>
|
||||
</ToolCallRow>
|
||||
</ToolCallCard>
|
||||
);
|
||||
}
|
||||
|
||||
// Success case with diff: show diff (already has file path)
|
||||
if (diffs.length > 0) {
|
||||
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>
|
||||
</ToolCallContainer>
|
||||
);
|
||||
}
|
||||
|
||||
// Success case without diff: show operation + file
|
||||
// Success case: show filename + line count
|
||||
if (locations && locations.length > 0) {
|
||||
const fileName = getFileName(locations[0].path);
|
||||
const lineCount = writeContent.split('\n').length;
|
||||
return (
|
||||
<ToolCallCard icon="✏️">
|
||||
<ToolCallRow label={isEdit ? 'Edited' : 'Created'}>
|
||||
<LocationsList locations={locations} />
|
||||
</ToolCallRow>
|
||||
</ToolCallCard>
|
||||
<ToolCallContainer
|
||||
label={`Created ${fileName}`}
|
||||
status="success"
|
||||
toolCallId={toolCallId}
|
||||
>
|
||||
<div className="inline-flex text-[var(--app-secondary-foreground)] text-[0.85em] opacity-70 flex-row items-start w-full gap-1">
|
||||
<span className="flex-shrink-0 relative top-[-0.1em]">⎿</span>
|
||||
<span className="flex-shrink-0 w-full">{lineCount} lines</span>
|
||||
</div>
|
||||
</ToolCallContainer>
|
||||
);
|
||||
}
|
||||
|
||||
// Fallback: show generic success
|
||||
if (textOutputs.length > 0) {
|
||||
return (
|
||||
<ToolCallContainer label="Write" status="success" toolCallId={toolCallId}>
|
||||
{textOutputs.join('\n')}
|
||||
</ToolCallContainer>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -12,9 +12,11 @@ import { shouldShowToolCall } from './shared/utils.js';
|
||||
import { GenericToolCall } from './GenericToolCall.js';
|
||||
import { ReadToolCall } from './ReadToolCall.js';
|
||||
import { WriteToolCall } from './WriteToolCall.js';
|
||||
import { EditToolCall } from './EditToolCall.js';
|
||||
import { ExecuteToolCall } from './ExecuteToolCall.js';
|
||||
import { SearchToolCall } from './SearchToolCall.js';
|
||||
import { ThinkToolCall } from './ThinkToolCall.js';
|
||||
import { TodoWriteToolCall } from './TodoWriteToolCall.js';
|
||||
|
||||
/**
|
||||
* Factory function that returns the appropriate tool call component based on kind
|
||||
@@ -30,9 +32,11 @@ export const getToolCallComponent = (
|
||||
return ReadToolCall;
|
||||
|
||||
case 'write':
|
||||
case 'edit':
|
||||
return WriteToolCall;
|
||||
|
||||
case 'edit':
|
||||
return EditToolCall;
|
||||
|
||||
case 'execute':
|
||||
case 'bash':
|
||||
case 'command':
|
||||
@@ -48,6 +52,11 @@ export const getToolCallComponent = (
|
||||
case 'thinking':
|
||||
return ThinkToolCall;
|
||||
|
||||
case 'todowrite':
|
||||
case 'todo_write':
|
||||
case 'update_todos':
|
||||
return TodoWriteToolCall;
|
||||
|
||||
// Add more specialized components as needed
|
||||
// case 'fetch':
|
||||
// return FetchToolCall;
|
||||
|
||||
@@ -4,13 +4,81 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Shared layout components for tool call UI
|
||||
* Uses Claude Code style: bullet point + label + content
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import { FileLink } from '../../shared/FileLink.js';
|
||||
|
||||
/**
|
||||
* Props for ToolCallCard wrapper
|
||||
* Props for ToolCallContainer - Claude Code style layout
|
||||
*/
|
||||
interface ToolCallContainerProps {
|
||||
/** Operation label (e.g., "Read", "Write", "Search") */
|
||||
label: string;
|
||||
/** Status for bullet color: 'success' | 'error' | 'warning' | 'loading' | 'default' */
|
||||
status?: 'success' | 'error' | 'warning' | 'loading' | 'default';
|
||||
/** Main content to display */
|
||||
children: React.ReactNode;
|
||||
/** Tool call ID for debugging */
|
||||
toolCallId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get bullet point color classes based on status
|
||||
*/
|
||||
const getBulletColorClass = (
|
||||
status: 'success' | 'error' | 'warning' | 'loading' | 'default',
|
||||
): string => {
|
||||
switch (status) {
|
||||
case 'success':
|
||||
return 'text-[#74c991]';
|
||||
case 'error':
|
||||
return 'text-[#c74e39]';
|
||||
case 'warning':
|
||||
return 'text-[#e1c08d]';
|
||||
case 'loading':
|
||||
return 'text-[var(--app-secondary-foreground)] animate-pulse';
|
||||
default:
|
||||
return 'text-[var(--app-secondary-foreground)]';
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Main container with Claude Code style bullet point
|
||||
*/
|
||||
export const ToolCallContainer: React.FC<ToolCallContainerProps> = ({
|
||||
label,
|
||||
status = 'success',
|
||||
children,
|
||||
toolCallId,
|
||||
}) => (
|
||||
<div className="relative pl-[30px] py-2 select-text">
|
||||
<span
|
||||
className={`absolute left-2 top-[10px] text-[10px] ${getBulletColorClass(status)}`}
|
||||
>
|
||||
●
|
||||
</span>
|
||||
<div className="flex flex-col gap-1 pl-[30px]">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-[13px] font-medium text-[var(--app-primary-foreground)]">
|
||||
{label}
|
||||
</span>
|
||||
{toolCallId && (
|
||||
<span className="text-[10px] opacity-30">
|
||||
[{toolCallId.slice(-8)}]
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{children && (
|
||||
<div className="text-[var(--app-secondary-foreground)]">{children}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
/**
|
||||
* Props for ToolCallCard wrapper (legacy - for complex layouts)
|
||||
*/
|
||||
interface ToolCallCardProps {
|
||||
icon: string;
|
||||
@@ -18,15 +86,14 @@ interface ToolCallCardProps {
|
||||
}
|
||||
|
||||
/**
|
||||
* Main card wrapper with icon
|
||||
* Legacy card wrapper - kept for backward compatibility with complex layouts like diffs
|
||||
*/
|
||||
export const ToolCallCard: React.FC<ToolCallCardProps> = ({
|
||||
icon: _icon,
|
||||
children,
|
||||
}) => (
|
||||
<div className="tool-call-card">
|
||||
{/* <div className="tool-call-icon">{icon}</div> */}
|
||||
<div className="tool-call-grid">{children}</div>
|
||||
<div className="ml-[30px] grid grid-cols-[auto_1fr] gap-medium bg-[var(--app-input-background)] border border-[var(--app-input-border)] rounded-medium p-large my-medium items-start animate-[fadeIn_0.2s_ease-in]">
|
||||
<div className="flex flex-col gap-medium min-w-0">{children}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -39,15 +106,19 @@ interface ToolCallRowProps {
|
||||
}
|
||||
|
||||
/**
|
||||
* A single row in the tool call grid
|
||||
* A single row in the tool call grid (legacy - for complex layouts)
|
||||
*/
|
||||
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 className="grid grid-cols-[80px_1fr] gap-medium min-w-0">
|
||||
<div className="text-xs text-[var(--app-secondary-foreground)] font-medium pt-[2px]">
|
||||
{label}
|
||||
</div>
|
||||
<div className="text-[var(--app-primary-foreground)] min-w-0 break-words">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -59,6 +130,26 @@ interface StatusIndicatorProps {
|
||||
text: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get status color class
|
||||
*/
|
||||
const getStatusColorClass = (
|
||||
status: 'pending' | 'in_progress' | 'completed' | 'failed',
|
||||
): string => {
|
||||
switch (status) {
|
||||
case 'pending':
|
||||
return 'bg-[#ffc107]';
|
||||
case 'in_progress':
|
||||
return 'bg-[#2196f3]';
|
||||
case 'completed':
|
||||
return 'bg-[#4caf50]';
|
||||
case 'failed':
|
||||
return 'bg-[#f44336]';
|
||||
default:
|
||||
return 'bg-gray-500';
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Status indicator with colored dot
|
||||
*/
|
||||
@@ -66,7 +157,10 @@ export const StatusIndicator: React.FC<StatusIndicatorProps> = ({
|
||||
status,
|
||||
text,
|
||||
}) => (
|
||||
<div className={`tool-call-status-indicator ${status}`} title={status}>
|
||||
<div className="inline-block font-medium relative" title={status}>
|
||||
<span
|
||||
className={`inline-block w-1.5 h-1.5 rounded-full mr-1.5 align-middle ${getStatusColorClass(status)}`}
|
||||
/>
|
||||
{text}
|
||||
</div>
|
||||
);
|
||||
@@ -82,7 +176,9 @@ interface CodeBlockProps {
|
||||
* Code block for displaying formatted code or output
|
||||
*/
|
||||
export const CodeBlock: React.FC<CodeBlockProps> = ({ children }) => (
|
||||
<pre className="code-block">{children}</pre>
|
||||
<pre className="font-mono text-[var(--app-monospace-font-size)] bg-[var(--app-primary-background)] border border-[var(--app-input-border)] rounded-small p-medium overflow-x-auto mt-1 whitespace-pre-wrap break-words max-h-[300px] overflow-y-auto">
|
||||
{children}
|
||||
</pre>
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -99,7 +195,7 @@ interface LocationsListProps {
|
||||
* List of file locations with clickable links
|
||||
*/
|
||||
export const LocationsList: React.FC<LocationsListProps> = ({ locations }) => (
|
||||
<div className="locations-list">
|
||||
<div className="flex flex-col gap-1 pl-[30px]">
|
||||
{locations.map((loc, idx) => (
|
||||
<FileLink key={idx} path={loc.path} line={loc.line} showFullPath={true} />
|
||||
))}
|
||||
|
||||
@@ -39,15 +39,16 @@ export const formatValue = (value: unknown): string => {
|
||||
|
||||
/**
|
||||
* Safely convert title to string, handling object types
|
||||
* Returns empty string if no meaningful title
|
||||
*/
|
||||
export const safeTitle = (title: unknown): string => {
|
||||
if (typeof title === 'string') {
|
||||
if (typeof title === 'string' && title.trim()) {
|
||||
return title;
|
||||
}
|
||||
if (title && typeof title === 'object') {
|
||||
return JSON.stringify(title);
|
||||
}
|
||||
return 'Tool Call';
|
||||
return '';
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -88,6 +89,19 @@ export const hasToolCallOutput = (
|
||||
return true;
|
||||
}
|
||||
|
||||
// Always show execute/bash/command tool calls (they show the command in title)
|
||||
const kind = toolCall.kind.toLowerCase();
|
||||
if (kind === 'execute' || kind === 'bash' || kind === 'command') {
|
||||
// But only if they have a title
|
||||
if (
|
||||
toolCall.title &&
|
||||
typeof toolCall.title === 'string' &&
|
||||
toolCall.title.trim()
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Show if there are locations (file paths)
|
||||
if (toolCall.locations && toolCall.locations.length > 0) {
|
||||
return true;
|
||||
@@ -107,6 +121,15 @@ export const hasToolCallOutput = (
|
||||
}
|
||||
}
|
||||
|
||||
// Show if there's a meaningful title for generic tool calls
|
||||
if (
|
||||
toolCall.title &&
|
||||
typeof toolCall.title === 'string' &&
|
||||
toolCall.title.trim()
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// No output, don't show
|
||||
return false;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user