mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-21 09:17:53 +00:00
feat(vscode-ide-companion): 改进消息排序和显示逻辑
- 添加时间戳支持,确保消息按时间顺序排列 - 更新工具调用处理逻辑,自动添加和保留时间戳 - 修改消息渲染逻辑,将所有类型的消息合并排序后统一渲染 - 优化完成的工具调用显示,修复显示顺序问题 - 调整进行中的工具调用显示,统一到消息流中展示 - 移除重复的计划展示逻辑,避免最新块重复出现 - 重构消息处理和渲染代码,提高可维护性
This commit is contained in:
@@ -7,7 +7,6 @@
|
||||
*/
|
||||
|
||||
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';
|
||||
@@ -42,7 +41,6 @@ const getDiffSummary = (
|
||||
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);
|
||||
@@ -69,46 +67,66 @@ export const EditToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
|
||||
const fileName = path ? getFileName(path) : '';
|
||||
return (
|
||||
<ToolCallContainer
|
||||
label={fileName ? `Edit ${fileName}` : 'Edit'}
|
||||
label={fileName ? 'Edit' : 'Edit'}
|
||||
status="error"
|
||||
toolCallId={toolCallId}
|
||||
labelSuffix={
|
||||
path ? (
|
||||
<FileLink
|
||||
path={path}
|
||||
showFullPath={false}
|
||||
className="text-xs font-mono text-[var(--app-secondary-foreground)] hover:underline"
|
||||
/>
|
||||
) : undefined
|
||||
}
|
||||
>
|
||||
{errors.join('\n')}
|
||||
</ToolCallContainer>
|
||||
);
|
||||
}
|
||||
|
||||
// Success case with diff: show collapsible format
|
||||
// Success case with diff: show minimal inline preview; clicking the title opens VS Code diff
|
||||
if (diffs.length > 0) {
|
||||
const firstDiff = diffs[0];
|
||||
const path = firstDiff.path || (locations && locations[0]?.path) || '';
|
||||
const fileName = path ? getFileName(path) : '';
|
||||
// const fileName = path ? getFileName(path) : '';
|
||||
const summary = getDiffSummary(firstDiff.oldText, firstDiff.newText);
|
||||
// No hooks here; define a simple click handler scoped to this block
|
||||
const openFirstDiff = () =>
|
||||
handleOpenDiff(path, firstDiff.oldText, firstDiff.newText);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
className="relative py-2 select-text cursor-pointer hover:bg-[var(--app-input-background)]"
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
onClick={openFirstDiff}
|
||||
title="Open diff in VS Code"
|
||||
>
|
||||
<span className="absolute left-2 top-[10px] text-[10px] text-[#74c991]">
|
||||
●
|
||||
</span>
|
||||
<div className="toolcall-edit-content flex flex-col gap-1 pl-[30px] max-w-full">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Keep content within overall width: pl-[30px] provides the bullet indent; */}
|
||||
{/* 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 pl-[30px] 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">
|
||||
<span className="text-[13px] font-medium text-[var(--app-primary-foreground)]">
|
||||
Edit {fileName}
|
||||
Edit
|
||||
</span>
|
||||
{toolCallId && (
|
||||
{path && (
|
||||
<FileLink
|
||||
path={path}
|
||||
showFullPath={false}
|
||||
className="text-xs font-mono text-[var(--app-secondary-foreground)] hover:underline"
|
||||
/>
|
||||
)}
|
||||
{/* {toolCallId && (
|
||||
<span className="text-[10px] opacity-30">
|
||||
[{toolCallId.slice(-8)}]
|
||||
</span>
|
||||
)}
|
||||
)} */}
|
||||
</div>
|
||||
<span className="text-xs opacity-60 mr-2">
|
||||
{expanded ? '▼' : '▶'}
|
||||
</span>
|
||||
<span className="text-xs opacity-60 ml-2">open</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>
|
||||
@@ -116,26 +134,26 @@ export const EditToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
|
||||
</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>
|
||||
)}
|
||||
{/* Content area aligned with bullet indent. Do NOT exceed container width. */}
|
||||
{/* For any custom blocks here, keep: min-w-0 max-w-full and avoid extra horizontal padding/margins. */}
|
||||
<div className="pl-[30px] mt-1 min-w-0 max-w-full overflow-hidden">
|
||||
{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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import type React from 'react';
|
||||
import type { BaseToolCallProps } from './shared/types.js';
|
||||
import { ToolCallContainer } from './shared/LayoutComponents.js';
|
||||
import { groupContent } from './shared/utils.js';
|
||||
import { FileLink } from '../shared/FileLink.js';
|
||||
|
||||
/**
|
||||
* Specialized component for Read tool calls
|
||||
@@ -23,17 +24,25 @@ export const ReadToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
|
||||
const { errors } = groupContent(content);
|
||||
|
||||
// Extract filename from path
|
||||
const getFileName = (path: string): string => path.split('/').pop() || 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 (
|
||||
<ToolCallContainer
|
||||
label={fileName ? `Read ${fileName}` : 'Read'}
|
||||
label={'Read'}
|
||||
status="error"
|
||||
toolCallId={toolCallId}
|
||||
labelSuffix={
|
||||
path ? (
|
||||
<FileLink
|
||||
path={path}
|
||||
showFullPath={false}
|
||||
className="text-xs font-mono text-[var(--app-secondary-foreground)] hover:underline"
|
||||
/>
|
||||
) : undefined
|
||||
}
|
||||
>
|
||||
{errors.join('\n')}
|
||||
</ToolCallContainer>
|
||||
@@ -42,12 +51,21 @@ export const ReadToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
|
||||
|
||||
// Success case: show which file was read with filename in label
|
||||
if (locations && locations.length > 0) {
|
||||
const fileName = getFileName(locations[0].path);
|
||||
const path = locations[0].path;
|
||||
return (
|
||||
<ToolCallContainer
|
||||
label={`Read ${fileName}`}
|
||||
label={'Read'}
|
||||
status="success"
|
||||
toolCallId={toolCallId}
|
||||
labelSuffix={
|
||||
path ? (
|
||||
<FileLink
|
||||
path={path}
|
||||
showFullPath={false}
|
||||
className="text-xs font-mono text-[var(--app-secondary-foreground)] hover:underline"
|
||||
/>
|
||||
) : undefined
|
||||
}
|
||||
>
|
||||
{null}
|
||||
</ToolCallContainer>
|
||||
|
||||
@@ -9,7 +9,69 @@
|
||||
import type React from 'react';
|
||||
import type { BaseToolCallProps } from './shared/types.js';
|
||||
import { ToolCallContainer } from './shared/LayoutComponents.js';
|
||||
import { groupContent } from './shared/utils.js';
|
||||
import { groupContent, safeTitle } from './shared/utils.js';
|
||||
import { CheckboxDisplay } from '../ui/CheckboxDisplay.js';
|
||||
|
||||
type EntryStatus = 'pending' | 'in_progress' | 'completed';
|
||||
|
||||
interface TodoEntry {
|
||||
content: string;
|
||||
status: EntryStatus;
|
||||
}
|
||||
|
||||
const mapToolStatusToBullet = (
|
||||
status: import('./shared/types.js').ToolCallStatus,
|
||||
): 'success' | 'error' | 'warning' | 'loading' | 'default' => {
|
||||
switch (status) {
|
||||
case 'completed':
|
||||
return 'success';
|
||||
case 'failed':
|
||||
return 'error';
|
||||
case 'in_progress':
|
||||
return 'warning';
|
||||
case 'pending':
|
||||
return 'loading';
|
||||
default:
|
||||
return 'default';
|
||||
}
|
||||
};
|
||||
|
||||
// 从文本中尽可能解析带有 - [ ] / - [x] 的 todo 列表
|
||||
const parseTodoEntries = (textOutputs: string[]): TodoEntry[] => {
|
||||
const text = textOutputs.join('\n');
|
||||
const lines = text.split(/\r?\n/);
|
||||
const entries: TodoEntry[] = [];
|
||||
|
||||
const todoRe = /^(?:\s*(?:[-*]|\d+[.)])\s*)?\[( |x|X|-)\]\s+(.*)$/;
|
||||
for (const line of lines) {
|
||||
const m = line.match(todoRe);
|
||||
if (m) {
|
||||
const mark = m[1];
|
||||
const title = m[2].trim();
|
||||
const status: EntryStatus =
|
||||
mark === 'x' || mark === 'X'
|
||||
? 'completed'
|
||||
: mark === '-'
|
||||
? 'in_progress'
|
||||
: 'pending';
|
||||
if (title) {
|
||||
entries.push({ content: title, status });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没匹配到,退化为将非空行当作 pending 条目
|
||||
if (entries.length === 0) {
|
||||
for (const line of lines) {
|
||||
const title = line.trim();
|
||||
if (title) {
|
||||
entries.push({ content: title, status: 'pending' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return entries;
|
||||
};
|
||||
|
||||
/**
|
||||
* Specialized component for TodoWrite tool calls
|
||||
@@ -18,12 +80,10 @@ import { groupContent } from './shared/utils.js';
|
||||
export const TodoWriteToolCall: React.FC<BaseToolCallProps> = ({
|
||||
toolCall,
|
||||
}) => {
|
||||
const { content } = toolCall;
|
||||
|
||||
// Group content by type
|
||||
const { content, status } = toolCall;
|
||||
const { errors, textOutputs } = groupContent(content);
|
||||
|
||||
// Error case: show error
|
||||
// 错误优先展示
|
||||
if (errors.length > 0) {
|
||||
return (
|
||||
<ToolCallContainer label="Update Todos" status="error">
|
||||
@@ -32,17 +92,45 @@ export const TodoWriteToolCall: React.FC<BaseToolCallProps> = ({
|
||||
);
|
||||
}
|
||||
|
||||
// Success case: show simple confirmation
|
||||
const outputText =
|
||||
textOutputs.length > 0 ? textOutputs.join(' ') : 'Todos updated';
|
||||
const entries = parseTodoEntries(textOutputs);
|
||||
|
||||
// Truncate if too long
|
||||
const displayText =
|
||||
outputText.length > 100 ? outputText.substring(0, 100) + '...' : outputText;
|
||||
const label = safeTitle(toolCall.title) || 'Update Todos';
|
||||
|
||||
return (
|
||||
<ToolCallContainer label="Update Todos" status="success">
|
||||
{displayText}
|
||||
<ToolCallContainer label={label} status={mapToolStatusToBullet(status)}>
|
||||
<ul className="Fr list-none p-0 m-0 flex flex-col gap-1">
|
||||
{entries.map((entry, idx) => {
|
||||
const isDone = entry.status === 'completed';
|
||||
const isIndeterminate = entry.status === 'in_progress';
|
||||
return (
|
||||
<li
|
||||
key={idx}
|
||||
className={[
|
||||
'Hr flex items-start gap-2 p-0 rounded text-[var(--app-primary-foreground)]',
|
||||
isDone ? 'fo opacity-70' : '',
|
||||
].join(' ')}
|
||||
>
|
||||
<label className="flex items-start gap-2">
|
||||
<CheckboxDisplay
|
||||
checked={isDone}
|
||||
indeterminate={isIndeterminate}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<div
|
||||
className={[
|
||||
'vo flex-1 text-xs leading-[1.5] text-[var(--app-primary-foreground)]',
|
||||
isDone
|
||||
? 'line-through text-[var(--app-secondary-foreground)] opacity-70'
|
||||
: 'opacity-85',
|
||||
].join(' ')}
|
||||
>
|
||||
{entry.content}
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</ToolCallContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -10,6 +10,7 @@ import type React from 'react';
|
||||
import type { BaseToolCallProps } from './shared/types.js';
|
||||
import { ToolCallContainer } from './shared/LayoutComponents.js';
|
||||
import { groupContent } from './shared/utils.js';
|
||||
import { FileLink } from '../shared/FileLink.js';
|
||||
|
||||
/**
|
||||
* Specialized component for Write tool calls
|
||||
@@ -22,7 +23,7 @@ export const WriteToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
|
||||
const { errors, textOutputs } = groupContent(content);
|
||||
|
||||
// Extract filename from path
|
||||
const getFileName = (path: string): string => path.split('/').pop() || path;
|
||||
// const getFileName = (path: string): string => path.split('/').pop() || path;
|
||||
|
||||
// Extract content to write from rawInput
|
||||
let writeContent = '';
|
||||
@@ -36,7 +37,6 @@ export const WriteToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
|
||||
// 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
|
||||
@@ -47,9 +47,18 @@ export const WriteToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
|
||||
|
||||
return (
|
||||
<ToolCallContainer
|
||||
label={fileName ? `Write ${fileName}` : 'Write'}
|
||||
label={'Write'}
|
||||
status="error"
|
||||
toolCallId={toolCallId}
|
||||
labelSuffix={
|
||||
path ? (
|
||||
<FileLink
|
||||
path={path}
|
||||
showFullPath={false}
|
||||
className="text-xs font-mono text-[var(--app-secondary-foreground)] hover:underline"
|
||||
/>
|
||||
) : undefined
|
||||
}
|
||||
>
|
||||
<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>
|
||||
@@ -68,13 +77,22 @@ export const WriteToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
|
||||
|
||||
// Success case: show filename + line count
|
||||
if (locations && locations.length > 0) {
|
||||
const fileName = getFileName(locations[0].path);
|
||||
const path = locations[0].path;
|
||||
const lineCount = writeContent.split('\n').length;
|
||||
return (
|
||||
<ToolCallContainer
|
||||
label={`Created ${fileName}`}
|
||||
label={'Created'}
|
||||
status="success"
|
||||
toolCallId={toolCallId}
|
||||
labelSuffix={
|
||||
path ? (
|
||||
<FileLink
|
||||
path={path}
|
||||
showFullPath={false}
|
||||
className="text-xs font-mono text-[var(--app-secondary-foreground)] hover:underline"
|
||||
/>
|
||||
) : undefined
|
||||
}
|
||||
>
|
||||
<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>
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
紧凑视图样式 - 超简洁版本
|
||||
======================================== */
|
||||
|
||||
.diff-compact-view {
|
||||
.diff-display-container {
|
||||
border: 1px solid var(--vscode-panel-border);
|
||||
border-radius: 4px;
|
||||
background: var(--vscode-editor-background);
|
||||
@@ -97,7 +97,7 @@
|
||||
}
|
||||
|
||||
.diff-compact-actions {
|
||||
padding: 4px 10px 6px;
|
||||
padding: 6px 10px 8px;
|
||||
border-top: 1px solid var(--vscode-panel-border);
|
||||
background: var(--vscode-editorGroupHeader-tabsBackground);
|
||||
display: flex;
|
||||
@@ -108,19 +108,16 @@
|
||||
完整视图样式
|
||||
======================================== */
|
||||
|
||||
.diff-full-view {
|
||||
border: 1px solid var(--vscode-panel-border);
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
/* 已移除完整视图,统一为简洁模式 + 预览 */
|
||||
|
||||
.diff-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px;
|
||||
background: var(--vscode-editorGroupHeader-tabsBackground);
|
||||
border-bottom: 1px solid var(--vscode-panel-border);
|
||||
/* 预览区域(仅变更行) */
|
||||
.diff-preview {
|
||||
margin: 0;
|
||||
padding: 8px 10px;
|
||||
background: var(--vscode-textCodeBlock-background, rgba(0, 0, 0, 0.06));
|
||||
border-top: 1px solid var(--vscode-panel-border);
|
||||
max-height: 320px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.diff-file-path {
|
||||
@@ -133,12 +130,32 @@
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.diff-stats-line {
|
||||
padding: 8px 12px;
|
||||
background: var(--vscode-editor-background);
|
||||
.diff-line {
|
||||
white-space: pre;
|
||||
font-family: var(--vscode-editor-font-family, 'Menlo', 'Monaco', 'Courier New', monospace);
|
||||
font-size: 0.88em;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.diff-line.added {
|
||||
background: var(--vscode-diffEditor-insertedLineBackground, rgba(76, 175, 80, 0.18));
|
||||
color: var(--vscode-diffEditor-insertedTextForeground, #b5f1cc);
|
||||
}
|
||||
|
||||
.diff-line.removed {
|
||||
background: var(--vscode-diffEditor-removedLineBackground, rgba(244, 67, 54, 0.18));
|
||||
color: var(--vscode-diffEditor-removedTextForeground, #f6b1a7);
|
||||
}
|
||||
|
||||
.diff-line.no-change {
|
||||
color: var(--vscode-descriptionForeground);
|
||||
font-size: 0.9em;
|
||||
border-bottom: 1px solid var(--vscode-panel-border);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.diff-omitted {
|
||||
color: var(--vscode-descriptionForeground);
|
||||
font-style: italic;
|
||||
padding-top: 6px;
|
||||
}
|
||||
|
||||
.diff-section {
|
||||
@@ -250,16 +267,6 @@
|
||||
.diff-stats {
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.diff-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.diff-header-actions {
|
||||
align-self: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import { useState, useMemo } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { FileLink } from '../../shared/FileLink.js';
|
||||
import {
|
||||
calculateDiffStats,
|
||||
@@ -15,6 +15,11 @@ import {
|
||||
} from '../../../utils/diffStats.js';
|
||||
import { OpenDiffIcon } from '../../icons/index.js';
|
||||
import './DiffDisplay.css';
|
||||
import {
|
||||
computeLineDiff,
|
||||
truncateOps,
|
||||
type DiffOp,
|
||||
} from '../../../utils/simpleDiff.js';
|
||||
|
||||
/**
|
||||
* Props for DiffDisplay
|
||||
@@ -24,8 +29,8 @@ interface DiffDisplayProps {
|
||||
oldText?: string | null;
|
||||
newText?: string;
|
||||
onOpenDiff?: () => void;
|
||||
/** 默认显示模式:'compact' | 'full' */
|
||||
defaultMode?: 'compact' | 'full';
|
||||
/** 是否显示统计信息 */
|
||||
showStats?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -37,20 +42,27 @@ export const DiffDisplay: React.FC<DiffDisplayProps> = ({
|
||||
oldText,
|
||||
newText,
|
||||
onOpenDiff,
|
||||
defaultMode = 'compact',
|
||||
showStats = true,
|
||||
}) => {
|
||||
// 视图模式状态:紧凑或完整
|
||||
const [viewMode, setViewMode] = useState<'compact' | 'full'>(defaultMode);
|
||||
|
||||
// 计算 diff 统计信息(仅在文本变化时重新计算)
|
||||
// 统计信息(仅在文本变化时重新计算)
|
||||
const stats = useMemo(
|
||||
() => calculateDiffStats(oldText, newText),
|
||||
[oldText, newText],
|
||||
);
|
||||
|
||||
// 渲染紧凑视图
|
||||
const renderCompactView = () => (
|
||||
<div className="diff-compact-view">
|
||||
// 仅生成变更行(增加/删除),不渲染上下文
|
||||
const ops: DiffOp[] = useMemo(
|
||||
() => computeLineDiff(oldText, newText),
|
||||
[oldText, newText],
|
||||
);
|
||||
const {
|
||||
items: previewOps,
|
||||
truncated,
|
||||
omitted,
|
||||
} = useMemo(() => truncateOps<DiffOp>(ops), [ops]);
|
||||
|
||||
return (
|
||||
<div className="diff-display-container">
|
||||
<div
|
||||
className="diff-compact-clickable"
|
||||
onClick={onOpenDiff}
|
||||
@@ -75,87 +87,74 @@ export const DiffDisplay: React.FC<DiffDisplayProps> = ({
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="diff-stats">
|
||||
{stats.added > 0 && (
|
||||
<span className="stat-added">+{stats.added}</span>
|
||||
)}
|
||||
{stats.removed > 0 && (
|
||||
<span className="stat-removed">-{stats.removed}</span>
|
||||
)}
|
||||
{stats.changed > 0 && (
|
||||
<span className="stat-changed">~{stats.changed}</span>
|
||||
)}
|
||||
{stats.total === 0 && (
|
||||
<span className="stat-no-change">No changes</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="diff-compact-actions">
|
||||
<button
|
||||
className="diff-action-button secondary"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setViewMode('full');
|
||||
}}
|
||||
title="Show full before/after content"
|
||||
>
|
||||
Show Details
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
// 渲染完整视图
|
||||
const renderFullView = () => (
|
||||
<div className="diff-full-view">
|
||||
<div className="diff-header">
|
||||
<div className="diff-file-path">
|
||||
{path && <FileLink path={path} showFullPath={true} />}
|
||||
</div>
|
||||
<div className="diff-header-actions">
|
||||
{onOpenDiff && (
|
||||
<button
|
||||
className="diff-action-button primary"
|
||||
onClick={onOpenDiff}
|
||||
title="Open in VS Code diff viewer"
|
||||
>
|
||||
<OpenDiffIcon width="14" height="14" />
|
||||
Open Diff
|
||||
</button>
|
||||
{showStats && (
|
||||
<div className="diff-stats" title={formatDiffStatsDetailed(stats)}>
|
||||
{stats.added > 0 && (
|
||||
<span className="stat-added">+{stats.added}</span>
|
||||
)}
|
||||
{stats.removed > 0 && (
|
||||
<span className="stat-removed">-{stats.removed}</span>
|
||||
)}
|
||||
{stats.changed > 0 && (
|
||||
<span className="stat-changed">~{stats.changed}</span>
|
||||
)}
|
||||
{stats.total === 0 && (
|
||||
<span className="stat-no-change">No changes</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 只绘制差异行的预览区域 */}
|
||||
<pre className="diff-preview code-block" aria-label="Diff preview">
|
||||
<div className="code-content">
|
||||
{previewOps.length === 0 && (
|
||||
<div className="diff-line no-change">(no changes)</div>
|
||||
)}
|
||||
{previewOps.map((op, idx) => {
|
||||
if (op.type === 'add') {
|
||||
const line = op.line;
|
||||
return (
|
||||
<div key={`add-${idx}`} className="diff-line added">
|
||||
+{line || ' '}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (op.type === 'remove') {
|
||||
const line = op.line;
|
||||
return (
|
||||
<div key={`rm-${idx}`} className="diff-line removed">
|
||||
-{line || ' '}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})}
|
||||
{truncated && (
|
||||
<div
|
||||
className="diff-omitted"
|
||||
title={`${omitted} lines omitted in preview`}
|
||||
>
|
||||
… {omitted} lines omitted
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</pre>
|
||||
|
||||
{/* 在预览下方提供显式打开按钮(可选) */}
|
||||
{onOpenDiff && (
|
||||
<div className="diff-compact-actions">
|
||||
<button
|
||||
className="diff-action-button secondary"
|
||||
onClick={() => setViewMode('compact')}
|
||||
title="Collapse to compact view"
|
||||
className="diff-action-button primary"
|
||||
onClick={onOpenDiff}
|
||||
title="Open in VS Code diff viewer"
|
||||
>
|
||||
Collapse
|
||||
<OpenDiffIcon width="14" height="14" />
|
||||
Open Diff
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="diff-stats-line">{formatDiffStatsDetailed(stats)}</div>
|
||||
{oldText !== undefined && (
|
||||
<div className="diff-section">
|
||||
<div className="diff-label">Before:</div>
|
||||
<pre className="code-block">
|
||||
<div className="code-content">{oldText || '(empty)'}</div>
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
{newText !== undefined && (
|
||||
<div className="diff-section">
|
||||
<div className="diff-label">After:</div>
|
||||
<pre className="code-block">
|
||||
<div className="code-content">{newText}</div>
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="diff-display-container">
|
||||
{viewMode === 'compact' ? renderCompactView() : renderFullView()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -22,6 +22,8 @@ interface ToolCallContainerProps {
|
||||
children: React.ReactNode;
|
||||
/** Tool call ID for debugging */
|
||||
toolCallId?: string;
|
||||
/** Optional trailing content rendered next to label (e.g., clickable filename) */
|
||||
labelSuffix?: React.ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -51,24 +53,30 @@ export const ToolCallContainer: React.FC<ToolCallContainerProps> = ({
|
||||
label,
|
||||
status = 'success',
|
||||
children,
|
||||
toolCallId,
|
||||
toolCallId: _toolCallId,
|
||||
labelSuffix,
|
||||
}) => (
|
||||
<div className="relative pl-[30px] py-2 select-text">
|
||||
<span
|
||||
className={`absolute left-2 top-[10px] text-[10px] ${getBulletColorClass(status)}`}
|
||||
>
|
||||
●
|
||||
</span>
|
||||
<div className="toolcall-content-wrapper flex flex-col gap-1 pl-[30px] max-w-full">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="relative pl-[30px] py-2 select-text toolcall-container">
|
||||
<div className="toolcall-content-wrapper flex flex-col gap-1 min-w-0 max-w-full">
|
||||
<div className="flex items-center gap-2 relative min-w-0">
|
||||
{/* Status icon (bullet), vertically centered with header row */}
|
||||
<span
|
||||
aria-hidden
|
||||
className={`absolute -left-[20px] top-1/2 -translate-y-1/2 text-[10px] leading-none ${getBulletColorClass(
|
||||
status,
|
||||
)}`}
|
||||
>
|
||||
●
|
||||
</span>
|
||||
<span className="text-[13px] font-medium text-[var(--app-primary-foreground)]">
|
||||
{label}
|
||||
</span>
|
||||
{toolCallId && (
|
||||
{/* {toolCallId && (
|
||||
<span className="text-[10px] opacity-30">
|
||||
[{toolCallId.slice(-8)}]
|
||||
</span>
|
||||
)}
|
||||
)} */}
|
||||
{labelSuffix}
|
||||
</div>
|
||||
{children && (
|
||||
<div className="text-[var(--app-secondary-foreground)]">{children}</div>
|
||||
@@ -92,7 +100,7 @@ export const ToolCallCard: React.FC<ToolCallCardProps> = ({
|
||||
icon: _icon,
|
||||
children,
|
||||
}) => (
|
||||
<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="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] toolcall-card">
|
||||
<div className="flex flex-col gap-medium min-w-0">{children}</div>
|
||||
</div>
|
||||
);
|
||||
@@ -195,7 +203,7 @@ interface LocationsListProps {
|
||||
* List of file locations with clickable links
|
||||
*/
|
||||
export const LocationsList: React.FC<LocationsListProps> = ({ locations }) => (
|
||||
<div className="toolcall-locations-list flex flex-col gap-1 pl-[30px] max-w-full">
|
||||
<div className="toolcall-locations-list flex flex-col gap-1 max-w-full">
|
||||
{locations.map((loc, idx) => (
|
||||
<FileLink key={idx} path={loc.path} line={loc.line} showFullPath={true} />
|
||||
))}
|
||||
|
||||
@@ -48,6 +48,7 @@ export interface ToolCallData {
|
||||
rawInput?: string | object;
|
||||
content?: ToolCallContent[];
|
||||
locations?: ToolCallLocation[];
|
||||
timestamp?: number; // 添加时间戳字段用于消息排序
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user