feat:代码变更diff可视化功能实现

This commit is contained in:
Roe-xin
2026-03-02 10:00:04 +08:00
parent 3e18299099
commit 4c7ec65577
13 changed files with 1195 additions and 1 deletions

View File

@ -26,6 +26,9 @@ import {
import { optimizePrompt } from "../services/promptOptimizeService";
import { NotificationService } from "../services/notificationService";
import { TrialExpirationService } from "../services/trialExpirationService";
import { showFileDiff } from "./fileDiff";
import { changeTracker } from "../services/changeTracker";
import { generateDiff, renderDiffHtml } from "./diffRenderer";
import type { RunMode, ServiceTier } from "../types/api";
@ -38,6 +41,14 @@ let currentSession: DialogSession | null = null;
/** 最后一个活跃的 taskId用于压缩等操作 */
let lastTaskId: string | null = null;
async function trackFileChange(filePath: string, oldContent: string, newContent: string): Promise<void> {
try {
changeTracker.trackChange(filePath, oldContent, newContent);
} catch (error) {
console.warn("[MessageHandler] 记录文件变更失败:", error);
}
}
/**
* 处理用户消息
*/
@ -372,6 +383,9 @@ async function handleUserMessageWithBackend(
panel.reveal();
}
);
// 发送代码变更到前端
sendChangesToWebview(panel);
} catch (error) {
console.warn("[MessageHandler] 更新面板失败(面板可能已关闭):", error);
}
@ -774,11 +788,14 @@ async function handleFileOperation(
if (!operation.searchText || !operation.replaceText) {
throw new Error("缺少替换内容");
}
const oldContentBeforeReplace = await readFileContent(operation.filePath);
await replaceFile(
operation.filePath,
operation.searchText,
operation.replaceText
);
const newContentAfterReplace = await readFileContent(operation.filePath);
await trackFileChange(operation.filePath, oldContentBeforeReplace, newContentAfterReplace);
responseText = `✅ 文件内容替换成功: ${operation.filePath}`;
panel.webview.postMessage({
command: "receiveMessage",
@ -914,7 +931,9 @@ export async function handleUpdateFile(
content: string
) {
try {
const oldContent = await readFileContent(filePath);
await updateFile(filePath, content);
await trackFileChange(filePath, oldContent, content);
panel.webview.postMessage({
command: "fileUpdated",
filePath: filePath,
@ -979,7 +998,10 @@ export async function handleReplaceInFile(
replaceText: string
) {
try {
const oldContent = await readFileContent(filePath);
await replaceFile(filePath, searchText, replaceText);
const newContent = await readFileContent(filePath);
await trackFileChange(filePath, oldContent, newContent);
panel.webview.postMessage({
command: "fileReplaced",
filePath: filePath,
@ -1236,3 +1258,165 @@ export async function handleOptimizePrompt(
vscode.window.showErrorMessage(`提示词优化失败: ${errorMsg}`);
}
}
/**
* 处理采纳变更
*/
export async function handleAcceptChange(
panel: vscode.WebviewPanel,
changeId: string
) {
try {
const success = await changeTracker.acceptChange(changeId);
if (success) {
panel.webview.postMessage({
command: "changeAccepted",
changeId: changeId,
success: true
});
} else {
panel.webview.postMessage({
command: "changeAccepted",
changeId: changeId,
success: false,
error: "采纳变更失败"
});
}
} catch (error) {
console.error("[MessageHandler] 采纳变更失败:", error);
panel.webview.postMessage({
command: "changeAccepted",
changeId: changeId,
success: false,
error: String(error)
});
}
}
/**
* 处理拒绝变更
*/
export async function handleRejectChange(
panel: vscode.WebviewPanel,
changeId: string
) {
try {
const success = await changeTracker.rejectChange(changeId);
if (success) {
panel.webview.postMessage({
command: "changeRejected",
changeId: changeId,
success: true
});
} else {
panel.webview.postMessage({
command: "changeRejected",
changeId: changeId,
success: false,
error: "拒绝变更失败"
});
}
} catch (error) {
console.error("[MessageHandler] 拒绝变更失败:", error);
panel.webview.postMessage({
command: "changeRejected",
changeId: changeId,
success: false,
error: String(error)
});
}
}
/**
* 在对话结束时发送变更列表到前端
*/
export function sendChangesToWebview(panel: vscode.WebviewPanel) {
const session = changeTracker.endSession();
if (session && session.changes.length > 0) {
const changesWithDiff = session.changes.map(change => {
const diffLines = generateDiff(change.oldContent, change.newContent);
const diffHtml = renderDiffHtml(diffLines);
return {
...change,
diffHtml
};
});
panel.webview.postMessage({
command: "showChanges",
changes: changesWithDiff
});
}
}
/**
* 开始新的变更会话
*/
export function startChangeSession(sessionId: string) {
changeTracker.startSession(sessionId);
}
/**
* 打开文件 diff 编辑器
*/
export async function handleOpenFileDiff(
panel: vscode.WebviewPanel,
changeId: string
) {
try {
const session = changeTracker.getCurrentSession();
if (!session) {
vscode.window.showErrorMessage('没有找到变更会话');
return;
}
const change = session.changes.find(c => c.changeId === changeId);
if (!change) {
vscode.window.showErrorMessage('没有找到该变更');
return;
}
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
if (!workspaceFolder) {
vscode.window.showErrorMessage('没有打开的工作区');
return;
}
// 创建临时文件用于对比
const filePath = change.filePath;
const absolutePath = vscode.Uri.file(
path.join(workspaceFolder.uri.fsPath, filePath)
);
// 创建虚拟文档显示旧内容
const oldUri = vscode.Uri.parse(
`ic-coder-diff:${filePath}.old?${changeId}`
).with({ scheme: 'ic-coder-diff' });
// 注册文档内容提供者(如果还没注册)
if (!(global as any).__diffProviderRegistered) {
const provider = new (class implements vscode.TextDocumentContentProvider {
provideTextDocumentContent(uri: vscode.Uri): string {
const changeId = uri.query;
const session = changeTracker.getCurrentSession();
const change = session?.changes.find(c => c.changeId === changeId);
return change?.oldContent || '';
}
})();
vscode.workspace.registerTextDocumentContentProvider('ic-coder-diff', provider);
(global as any).__diffProviderRegistered = true;
}
// 打开 diff 编辑器
await vscode.commands.executeCommand(
'vscode.diff',
oldUri,
absolutePath,
`${filePath} (变更对比)`
);
} catch (error) {
console.error('[MessageHandler] 打开 diff 失败:', error);
vscode.window.showErrorMessage(`打开 diff 失败: ${error}`);
}
}