feat(ui): 添加文件 Diff 查看功能
当 AI 执行 write_file 或 edit_file 工具时,在工具结果中显示 View Diff 按钮, 点击后在 IDE 面板中显示文件修改前后的对比视图。 主要改动: - core: edit_file/write_file 工具返回 fileDiff 元数据 - ui: 新增 DiffEditor 组件用于显示文件差异 - ui: ChatMessage 添加 View Diff 按钮 - ui: IDE 组件支持 Diff 视图切换 - ui: useChat hook 处理 fileDiff 回调
This commit is contained in:
@@ -14,6 +14,7 @@ import {
|
||||
AlertCircle,
|
||||
CheckCircle2,
|
||||
Loader2,
|
||||
GitCompare,
|
||||
} from 'lucide-react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { useState, forwardRef } from 'react';
|
||||
@@ -22,7 +23,7 @@ import { fadeInUp, smoothTransition } from '../utils/animations';
|
||||
import { getAgentDisplayName } from '../utils/agent';
|
||||
import { Markdown } from './Markdown';
|
||||
import { FileMentionText } from './FileMentionTag';
|
||||
import type { Message, ToolCallInfo, ToolStatus, ToolMessagePart, QuestionMessagePart } from '../api/types.js';
|
||||
import type { Message, ToolCallInfo, ToolStatus, ToolMessagePart, QuestionMessagePart, FileDiffInfo } from '../api/types.js';
|
||||
import { AskUserQuestion } from './AskUserQuestion.js';
|
||||
|
||||
interface ChatMessageProps {
|
||||
@@ -31,10 +32,12 @@ interface ChatMessageProps {
|
||||
isStreaming?: boolean;
|
||||
/** 回答问题的回调(用于 ask_user_question 工具) */
|
||||
onAnswerQuestion?: (questionPartId: string, answers: string[]) => void;
|
||||
/** 查看文件 Diff 的回调 */
|
||||
onViewDiff?: (diff: FileDiffInfo) => void;
|
||||
}
|
||||
|
||||
export const ChatMessage = forwardRef<HTMLDivElement, ChatMessageProps>(
|
||||
({ message, isStreaming = false, onAnswerQuestion }, ref) => {
|
||||
({ message, isStreaming = false, onAnswerQuestion, onViewDiff }, ref) => {
|
||||
const isUser = message.role === 'user';
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
@@ -83,7 +86,7 @@ export const ChatMessage = forwardRef<HTMLDivElement, ChatMessageProps>(
|
||||
</div>
|
||||
);
|
||||
case 'tool':
|
||||
return <ToolPartItem key={part.id} part={part} />;
|
||||
return <ToolPartItem key={part.id} part={part} onViewDiff={onViewDiff} />;
|
||||
case 'question': {
|
||||
// 问题组件:即使在流式输出时也允许用户回答(除非已回答)
|
||||
const questionPart = part as QuestionMessagePart;
|
||||
@@ -250,15 +253,21 @@ export function TypingIndicator({ agentName }: TypingIndicatorProps) {
|
||||
*/
|
||||
interface ToolPartItemProps {
|
||||
part: ToolMessagePart;
|
||||
/** 查看文件 Diff 的回调 */
|
||||
onViewDiff?: (diff: FileDiffInfo) => void;
|
||||
}
|
||||
|
||||
function ToolPartItem({ part }: ToolPartItemProps) {
|
||||
function ToolPartItem({ part, onViewDiff }: ToolPartItemProps) {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const hasDetails =
|
||||
Object.keys(part.arguments).length > 0 ||
|
||||
part.result !== undefined ||
|
||||
part.error !== undefined;
|
||||
|
||||
// 判断是否为文件操作工具且有 diff 数据
|
||||
const isFileOperation = part.toolName === 'write_file' || part.toolName === 'edit_file';
|
||||
const hasDiff = isFileOperation && part.fileDiff && part.status === 'completed';
|
||||
|
||||
return (
|
||||
<div className="border border-line rounded-lg overflow-hidden bg-surface-subtle/30">
|
||||
{/* 头部:工具名称、状态、时长 */}
|
||||
@@ -278,6 +287,21 @@ function ToolPartItem({ part }: ToolPartItemProps) {
|
||||
{part.duration && (
|
||||
<span className="text-xs text-fg-subtle">{formatDuration(part.duration)}</span>
|
||||
)}
|
||||
{/* View Diff 按钮 */}
|
||||
{hasDiff && onViewDiff && (
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onViewDiff(part.fileDiff!);
|
||||
}}
|
||||
className="px-2 py-0.5 text-xs bg-primary-500/10 text-primary-400 rounded hover:bg-primary-500/20 transition-colors"
|
||||
>
|
||||
<GitCompare size={12} className="inline mr-1" />
|
||||
View Diff
|
||||
</motion.button>
|
||||
)}
|
||||
{hasDetails && (
|
||||
<span className="text-fg-subtle">
|
||||
{expanded ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
|
||||
|
||||
Reference in New Issue
Block a user