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:
2025-12-17 21:11:44 +08:00
parent 3320a2a5ba
commit fea5442d53
13 changed files with 470 additions and 22 deletions
+78 -13
View File
@@ -4,13 +4,14 @@
* 整合文件浏览器和代码编辑器的 IDE 组件
*/
import { useState, useCallback, useEffect, useRef } from 'react';
import { useState, useCallback, useEffect, useRef, useImperativeHandle, forwardRef } from 'react';
import { toast } from 'sonner';
import { cn } from '../utils/cn.js';
import { readFile, getWorkingDirectory } from '../api/client.js';
import { FileExplorer } from './FileExplorer.js';
import { CodeEditor, getLanguageFromFilename, type EditorTab } from './CodeEditor.js';
import type { ActiveFileInfo } from '../api/types.js';
import { DiffEditor } from './DiffEditor.js';
import type { ActiveFileInfo, FileDiffInfo } from '../api/types.js';
// localStorage 键名
const STORAGE_KEY_TABS = 'ai-assistant-editor-tabs';
@@ -28,12 +29,28 @@ interface IDEProps {
sidebarWidth?: number;
/** 当前活动文件变化回调 */
onActiveFileChange?: (file: ActiveFileInfo | null) => void;
/** 要显示的 Diff 信息(外部传入) */
pendingDiff?: FileDiffInfo | null;
/** Diff 关闭回调 */
onDiffClose?: () => void;
}
export function IDE({ className, sidebarWidth = 256, onActiveFileChange }: IDEProps) {
/** IDE 组件暴露的方法 */
export interface IDEHandle {
/** 显示文件 diff */
showDiff: (diff: FileDiffInfo) => void;
/** 关闭 diff 视图 */
closeDiff: () => void;
}
export const IDE = forwardRef<IDEHandle, IDEProps>(function IDE(
{ className, sidebarWidth = 256, onActiveFileChange, pendingDiff, onDiffClose },
ref
) {
const [tabs, setTabs] = useState<EditorTab[]>([]);
const [activeTabId, setActiveTabId] = useState<string | null>(null);
const [workingDirectory, setWorkingDirectory] = useState<string>('');
const [currentDiff, setCurrentDiff] = useState<FileDiffInfo | null>(null);
const isRestoringTabs = useRef(false);
const hasRestoredTabs = useRef(false);
@@ -235,6 +252,47 @@ export function IDE({ className, sidebarWidth = 256, onActiveFileChange }: IDEPr
);
}, []);
// 显示 diff 视图
const showDiff = useCallback((diff: FileDiffInfo) => {
setCurrentDiff(diff);
// 如果文件已在编辑器中打开,更新其内容
setTabs((prev) =>
prev.map((tab) => {
if (tab.path === diff.path) {
return {
...tab,
content: diff.newContent,
originalContent: diff.newContent,
};
}
return tab;
})
);
}, []);
// 关闭 diff 视图
const closeDiff = useCallback(() => {
setCurrentDiff(null);
onDiffClose?.();
}, [onDiffClose]);
// 暴露方法给父组件
useImperativeHandle(ref, () => ({
showDiff,
closeDiff,
}), [showDiff, closeDiff]);
// 处理外部传入的 pendingDiff
useEffect(() => {
if (pendingDiff) {
showDiff(pendingDiff);
}
}, [pendingDiff, showDiff]);
// 判断是否显示 diff 视图
const showDiffView = currentDiff !== null;
return (
<div className={cn('flex h-full', className)}>
{/* 文件浏览器 */}
@@ -245,17 +303,24 @@ export function IDE({ className, sidebarWidth = 256, onActiveFileChange }: IDEPr
<FileExplorer onFileSelect={handleFileSelect} workingDirectory={workingDirectory} />
</div>
{/* 代码编辑器 */}
{/* 代码编辑器 / Diff 视图 */}
<div className="flex-1 min-w-0">
<CodeEditor
tabs={tabs}
activeTabId={activeTabId}
onTabChange={handleTabChange}
onTabClose={handleTabClose}
onContentChange={handleContentChange}
onSave={handleSave}
/>
{showDiffView ? (
<DiffEditor
diff={currentDiff}
onClose={closeDiff}
/>
) : (
<CodeEditor
tabs={tabs}
activeTabId={activeTabId}
onTabChange={handleTabChange}
onTabClose={handleTabClose}
onContentChange={handleContentChange}
onSave={handleSave}
/>
)}
</div>
</div>
);
}
});