From 4c7ec6557714ae02346c1f548cdf323db3a5e3e2 Mon Sep 17 00:00:00 2001 From: Roe-xin Date: Mon, 2 Mar 2026 10:00:04 +0800 Subject: [PATCH] =?UTF-8?q?feat:=E4=BB=A3=E7=A0=81=E5=8F=98=E6=9B=B4diff?= =?UTF-8?q?=E5=8F=AF=E8=A7=86=E5=8C=96=E5=8A=9F=E8=83=BD=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .npmrc | 4 +- docs/code-changes-feature.md | 45 +++++ docs/integration-guide.md | 50 +++++ src/panels/ICHelperPanel.ts | 26 +++ src/services/changeTracker.ts | 196 ++++++++++++++++++ src/services/toolExecutor.ts | 13 ++ src/types/fileChanges.ts | 47 +++++ src/utils/diffRenderer.ts | 184 +++++++++++++++++ src/utils/fileDiff.ts | 51 +++++ src/utils/messageHandler.ts | 184 +++++++++++++++++ src/views/changePanel.ts | 366 ++++++++++++++++++++++++++++++++++ src/views/inputArea.ts | 9 + src/views/webviewContent.ts | 21 ++ 13 files changed, 1195 insertions(+), 1 deletion(-) create mode 100644 docs/code-changes-feature.md create mode 100644 docs/integration-guide.md create mode 100644 src/services/changeTracker.ts create mode 100644 src/types/fileChanges.ts create mode 100644 src/utils/diffRenderer.ts create mode 100644 src/utils/fileDiff.ts create mode 100644 src/views/changePanel.ts diff --git a/.npmrc b/.npmrc index 37d1b60..82acbb6 100644 --- a/.npmrc +++ b/.npmrc @@ -1 +1,3 @@ -enable-pre-post-scripts = true \ No newline at end of file +enable-pre-post-scripts = true +shamefully-hoist = true +public-hoist-pattern[] = * \ No newline at end of file diff --git a/docs/code-changes-feature.md b/docs/code-changes-feature.md new file mode 100644 index 0000000..b844edf --- /dev/null +++ b/docs/code-changes-feature.md @@ -0,0 +1,45 @@ +# 代码变更审查功能 + +## 功能概述 + +AI 修改文件后,会在输入框上方显示"代码变更"面板,用户可以查看所有修改并选择采纳或拒绝。 + +## 核心文件 + +### 1. 数据结构 +- `src/types/fileChanges.ts` - 变更数据类型定义 + +### 2. 服务层 +- `src/services/changeTracker.ts` - 变更追踪服务(单例) + - `trackChange()` - 记录文件变更 + - `acceptChange()` - 采纳变更(保存文件) + - `rejectChange()` - 拒绝变更(恢复旧内容) + +### 3. UI 组件 +- `src/views/changePanel.ts` - 变更面板 UI +- `src/utils/diffRenderer.ts` - Diff 可视化渲染 + +### 4. 集成点 +- `src/utils/messageHandler.ts` - 消息处理 + - `trackFileChange()` - 记录变更 + - `handleAcceptChange()` - 处理采纳 + - `handleRejectChange()` - 处理拒绝 + - `sendChangesToWebview()` - 发送变更到前端 + +- `src/services/toolExecutor.ts` - 工具执行器 + - 在 `executeFileWrite()` 中记录变更 + +## 使用流程 + +1. **开始对话** - 调用 `startChangeSession(sessionId)` +2. **修改文件** - 自动调用 `trackFileChange()` +3. **对话结束** - 调用 `sendChangesToWebview()` 显示变更面板 +4. **用户操作** - 点击采纳/拒绝按钮 +5. **处理结果** - 保存或恢复文件内容 + +## 待完成工作 + +1. 在 ICHelperPanel 中集成消息处理(监听 acceptChange/rejectChange 命令) +2. 在对话结束时调用 `sendChangesToWebview()` +3. 在 Webview 中实现变更列表的动态渲染 +4. 处理前端的采纳/拒绝响应 diff --git a/docs/integration-guide.md b/docs/integration-guide.md new file mode 100644 index 0000000..fb90eab --- /dev/null +++ b/docs/integration-guide.md @@ -0,0 +1,50 @@ +# 代码变更审查功能 - 使用说明 + +## 功能说明 + +AI 修改文件后,会在输入框上方显示"代码变更"面板,用户可以: +- 查看所有修改的文件列表 +- 点击文件查看 diff 对比 +- 采纳变更(保存文件) +- 拒绝变更(恢复旧内容) + +## 已完成的集成 + +### 1. 后端集成 +- ✅ 在 `ICHelperPanel.ts` 中添加了消息监听(acceptChange/rejectChange) +- ✅ 在发送消息时启动变更追踪会话 +- ✅ 在文件操作时自动记录变更(messageHandler.ts、toolExecutor.ts) + +### 2. 前端集成 +- ✅ 在 `webviewContent.ts` 中添加了消息处理(showChanges/changeAccepted/changeRejected) +- ✅ 在 `changePanel.ts` 中实现了完整的 UI 交互逻辑 + +### 3. 核心功能 +- ✅ 变更追踪服务(changeTracker.ts) +- ✅ Diff 可视化渲染(diffRenderer.ts) +- ✅ 采纳/拒绝变更逻辑 + +## 待完成工作 + +需要在对话结束时调用 `sendChangesToWebview(panel)` 来显示变更面板。 + +建议在以下位置添加: +1. 在 `handleUserMessage` 函数中,对话流结束时 +2. 或在 `dialogManager` 的对话完成回调中 + +示例代码: +```typescript +// 对话结束时 +import { sendChangesToWebview } from '../utils/messageHandler'; + +// 在对话完成的地方调用 +sendChangesToWebview(panel); +``` + +## 测试步骤 + +1. 启动插件(F5) +2. 发送消息让 AI 修改文件 +3. 对话结束后,输入框上方应显示"代码变更"面板 +4. 点击文件查看 diff +5. 点击"采纳"或"拒绝"按钮测试功能 diff --git a/src/panels/ICHelperPanel.ts b/src/panels/ICHelperPanel.ts index cd87efc..6dc006b 100644 --- a/src/panels/ICHelperPanel.ts +++ b/src/panels/ICHelperPanel.ts @@ -13,6 +13,10 @@ import { handlePlanAction, getCurrentTaskId, setLastTaskId, + handleAcceptChange, + handleRejectChange, + startChangeSession, + handleOpenFileDiff, } from "../utils/messageHandler"; import { compactDialog } from "../services/apiClient"; import { VCDViewerPanel } from "./VCDViewerPanel"; @@ -344,6 +348,10 @@ export async function showICHelperPanel( // 切换到当前面板的任务上下文 historyManager.switchToPanelTask(panelId); + // 启动变更追踪会话 + const sessionId = `session_${panelId}_${Date.now()}`; + startChangeSession(sessionId); + // 显示进度条 panel.webview.postMessage({ type: "showProgress" }); @@ -476,6 +484,24 @@ export async function showICHelperPanel( // 退出登录 vscode.commands.executeCommand("ic-coder.logout"); break; + case "acceptChange": + // 采纳变更 + if (message.changeId) { + await handleAcceptChange(panel, message.changeId); + } + break; + case "rejectChange": + // 拒绝变更 + if (message.changeId) { + await handleRejectChange(panel, message.changeId); + } + break; + case "openFileDiff": + // 打开文件 diff + if (message.changeId) { + await handleOpenFileDiff(panel, message.changeId); + } + break; case "checkInvitationCode": // 检查邀请码验证状态 { diff --git a/src/services/changeTracker.ts b/src/services/changeTracker.ts new file mode 100644 index 0000000..c129322 --- /dev/null +++ b/src/services/changeTracker.ts @@ -0,0 +1,196 @@ +/** + * 文件变更追踪服务 + * 功能:收集和管理 AI 修改文件的变更记录 + * 依赖:types/fileChanges + * 使用场景:在文件操作时记录变更,供用户审查 + */ + +import { FileChange, ChangeSession } from '../types/fileChanges'; +import * as vscode from 'vscode'; +import * as fs from 'fs'; +import * as path from 'path'; + +class ChangeTrackerService { + private currentSession: ChangeSession | null = null; + private changeListeners: Array<(session: ChangeSession) => void> = []; + + /** + * 开始新的变更会话 + */ + startSession(sessionId: string): void { + this.currentSession = { + sessionId, + startTime: Date.now(), + changes: [], + status: 'active' + }; + } + + /** + * 记录文件变更 + */ + trackChange(filePath: string, oldContent: string, newContent: string): string { + if (!this.currentSession) { + this.startSession(`session_${Date.now()}`); + } + + const changeId = `change_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + + // 判断变更类型 + let changeType: 'create' | 'modify' | 'delete'; + if (oldContent === '' && newContent !== '') { + changeType = 'create'; + } else if (oldContent !== '' && newContent === '') { + changeType = 'delete'; + } else { + changeType = 'modify'; + } + + const change: FileChange = { + filePath, + oldContent, + newContent, + timestamp: Date.now(), + changeType, + changeId + }; + + this.currentSession!.changes.push(change); + this.notifyListeners(); + + return changeId; + } + + /** + * 结束当前会话 + */ + endSession(): ChangeSession | null { + if (this.currentSession && this.currentSession.changes.length > 0) { + this.currentSession.status = 'completed'; + const session = this.currentSession; + this.notifyListeners(); + return session; + } + this.currentSession = null; + return null; + } + + /** + * 获取当前会话 + */ + getCurrentSession(): ChangeSession | null { + return this.currentSession; + } + + /** + * 清空当前会话 + */ + clearSession(): void { + this.currentSession = null; + this.notifyListeners(); + } + + /** + * 移除指定的变更 + */ + removeChange(changeId: string): boolean { + if (!this.currentSession) { + return false; + } + + const index = this.currentSession.changes.findIndex(c => c.changeId === changeId); + if (index !== -1) { + this.currentSession.changes.splice(index, 1); + this.notifyListeners(); + return true; + } + return false; + } + + /** + * 监听变更 + */ + onChangeUpdate(listener: (session: ChangeSession) => void): void { + this.changeListeners.push(listener); + } + + /** + * 通知所有监听器 + */ + private notifyListeners(): void { + if (this.currentSession) { + this.changeListeners.forEach(listener => listener(this.currentSession!)); + } + } + + /** + * 采纳变更(保存文件) + */ + async acceptChange(changeId: string): Promise { + if (!this.currentSession) { + return false; + } + + const change = this.currentSession.changes.find(c => c.changeId === changeId); + if (!change) { + return false; + } + + try { + const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; + if (!workspaceFolder) { + return false; + } + + const absolutePath = path.join(workspaceFolder.uri.fsPath, change.filePath); + await fs.promises.writeFile(absolutePath, change.newContent, 'utf-8'); + + this.removeChange(changeId); + return true; + } catch (error) { + console.error('[ChangeTracker] 采纳变更失败:', error); + return false; + } + } + + /** + * 拒绝变更(恢复旧内容) + */ + async rejectChange(changeId: string): Promise { + if (!this.currentSession) { + return false; + } + + const change = this.currentSession.changes.find(c => c.changeId === changeId); + if (!change) { + return false; + } + + try { + const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; + if (!workspaceFolder) { + return false; + } + + const absolutePath = path.join(workspaceFolder.uri.fsPath, change.filePath); + + // 如果是新建文件,删除它 + if (change.changeType === 'create') { + if (fs.existsSync(absolutePath)) { + await fs.promises.unlink(absolutePath); + } + } else { + // 恢复旧内容 + await fs.promises.writeFile(absolutePath, change.oldContent, 'utf-8'); + } + + this.removeChange(changeId); + return true; + } catch (error) { + console.error('[ChangeTracker] 拒绝变更失败:', error); + return false; + } + } +} + +export const changeTracker = new ChangeTrackerService(); diff --git a/src/services/toolExecutor.ts b/src/services/toolExecutor.ts index d38b512..b9ae30c 100644 --- a/src/services/toolExecutor.ts +++ b/src/services/toolExecutor.ts @@ -8,6 +8,8 @@ import * as os from 'os'; import * as fs from 'fs'; import { readFileContent, readDirectory } from '../utils/readFiles'; import { createOrOverwriteFile } from '../utils/createFiles'; +import { resolveWorkspaceFilePath, showFileDiff } from '../utils/fileDiff'; +import { changeTracker } from './changeTracker'; import { generateVCD, checkIverilogAvailable, generateMultiVCD, DumpModule } from '../utils/iverilogRunner'; import { analyzeVcdFile } from '../utils/vcdParser'; import { executeWaveformTrace, WaveformTraceArgs } from '../utils/waveformTracer'; @@ -125,8 +127,19 @@ async function executeFileRead(args: FileReadArgs): Promise { * 执行 file_write 工具 */ async function executeFileWrite(args: FileWriteArgs): Promise { + const absolutePath = resolveWorkspaceFilePath(args.path); + const existedBeforeWrite = fs.existsSync(absolutePath); + const oldContent = existedBeforeWrite ? await readFileContent(args.path) : ''; + await createOrOverwriteFile(args.path, args.content); + // 记录文件变更 + try { + changeTracker.trackChange(args.path, oldContent, args.content); + } catch (error) { + console.warn('[ToolExecutor] 记录文件变更失败:', error); + } + // Verilog 文件添加知识图谱提示 const isVerilogFile = args.path.endsWith('.v') || args.path.endsWith('.sv'); if (isVerilogFile) { diff --git a/src/types/fileChanges.ts b/src/types/fileChanges.ts new file mode 100644 index 0000000..e938d6d --- /dev/null +++ b/src/types/fileChanges.ts @@ -0,0 +1,47 @@ +/** + * 文件变更追踪类型定义 + * 功能:定义代码变更的数据结构 + * 依赖:无 + * 使用场景:AI 修改文件后的变更审查 + */ + +/** + * 单个文件的变更记录 + */ +export interface FileChange { + /** 文件相对路径 */ + filePath: string; + /** 修改前的内容 */ + oldContent: string; + /** 修改后的内容 */ + newContent: string; + /** 变更时间戳 */ + timestamp: number; + /** 变更类型 */ + changeType: 'create' | 'modify' | 'delete'; + /** 变更 ID(唯一标识) */ + changeId: string; +} + +/** + * 变更会话(一次对话的所有变更) + */ +export interface ChangeSession { + /** 会话 ID */ + sessionId: string; + /** 会话开始时间 */ + startTime: number; + /** 所有文件变更 */ + changes: FileChange[]; + /** 会话状态 */ + status: 'active' | 'completed'; +} + +/** + * 变更操作结果 + */ +export interface ChangeActionResult { + success: boolean; + message: string; + changeId: string; +} diff --git a/src/utils/diffRenderer.ts b/src/utils/diffRenderer.ts new file mode 100644 index 0000000..3dc3dba --- /dev/null +++ b/src/utils/diffRenderer.ts @@ -0,0 +1,184 @@ +/** + * Diff 渲染工具 + * 功能:生成代码差异的 HTML 展示 + * 依赖:无 + * 使用场景:在变更面板中展示文件修改的 diff + */ + +interface DiffLine { + type: 'add' | 'remove' | 'context'; + content: string; + oldLineNumber?: number; + newLineNumber?: number; +} + +/** + * 简单的 diff 算法(基于行) + */ +export function generateDiff(oldContent: string, newContent: string): DiffLine[] { + const oldLines = oldContent.split('\n'); + const newLines = newContent.split('\n'); + const result: DiffLine[] = []; + + let oldIndex = 0; + let newIndex = 0; + + while (oldIndex < oldLines.length || newIndex < newLines.length) { + const oldLine = oldLines[oldIndex]; + const newLine = newLines[newIndex]; + + if (oldIndex >= oldLines.length) { + // 只剩新行 + result.push({ + type: 'add', + content: newLine, + newLineNumber: newIndex + 1 + }); + newIndex++; + } else if (newIndex >= newLines.length) { + // 只剩旧行 + result.push({ + type: 'remove', + content: oldLine, + oldLineNumber: oldIndex + 1 + }); + oldIndex++; + } else if (oldLine === newLine) { + // 相同行 + result.push({ + type: 'context', + content: oldLine, + oldLineNumber: oldIndex + 1, + newLineNumber: newIndex + 1 + }); + oldIndex++; + newIndex++; + } else { + // 不同行,标记为删除和添加 + result.push({ + type: 'remove', + content: oldLine, + oldLineNumber: oldIndex + 1 + }); + result.push({ + type: 'add', + content: newLine, + newLineNumber: newIndex + 1 + }); + oldIndex++; + newIndex++; + } + } + + return result; +} + +/** + * 将 diff 结果渲染为 HTML + */ +export function renderDiffHtml(diffLines: DiffLine[]): string { + let html = '
'; + + for (const line of diffLines) { + const lineClass = `diff-line diff-line-${line.type}`; + const oldNum = line.oldLineNumber ? `${line.oldLineNumber}` : ''; + const newNum = line.newLineNumber ? `${line.newLineNumber}` : ''; + const prefix = line.type === 'add' ? '+' : line.type === 'remove' ? '-' : ' '; + const escapedContent = escapeHtml(line.content); + + html += ` +
+ ${oldNum} + ${newNum} + ${prefix} + ${escapedContent} +
+ `; + } + + html += '
'; + return html; +} + +/** + * 转义 HTML 特殊字符 + */ +function escapeHtml(text: string): string { + const map: Record = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''' + }; + return text.replace(/[&<>"']/g, m => map[m]); +} + +/** + * 获取 diff 样式 + */ +export function getDiffStyles(): string { + return ` + .diff-viewer { + font-family: 'Consolas', 'Monaco', 'Courier New', monospace; + font-size: 12px; + line-height: 1.5; + background: var(--vscode-editor-background); + border: 1px solid var(--vscode-panel-border); + border-radius: 4px; + overflow-x: auto; + } + + .diff-line { + display: flex; + align-items: center; + padding: 2px 0; + white-space: pre; + } + + .diff-line-add { + background: rgba(40, 167, 69, 0.15); + } + + .diff-line-remove { + background: rgba(220, 53, 69, 0.15); + } + + .diff-line-context { + background: transparent; + } + + .line-num { + display: inline-block; + width: 40px; + text-align: right; + padding: 0 8px; + color: var(--vscode-editorLineNumber-foreground); + user-select: none; + flex-shrink: 0; + } + + .line-prefix { + display: inline-block; + width: 20px; + text-align: center; + font-weight: bold; + flex-shrink: 0; + } + + .diff-line-add .line-prefix { + color: #28a745; + } + + .diff-line-remove .line-prefix { + color: #dc3545; + } + + .line-content { + flex: 1; + padding-right: 8px; + overflow-x: auto; + } + `; +} + diff --git a/src/utils/fileDiff.ts b/src/utils/fileDiff.ts new file mode 100644 index 0000000..3045d70 --- /dev/null +++ b/src/utils/fileDiff.ts @@ -0,0 +1,51 @@ +import * as vscode from "vscode"; +import * as path from "path"; + +/** + * 将相对路径解析为工作区绝对路径 + */ +export function resolveWorkspaceFilePath(filePath: string): string { + if (path.isAbsolute(filePath)) { + return filePath; + } + + const workspaceFolders = vscode.workspace.workspaceFolders; + if (!workspaceFolders || workspaceFolders.length === 0) { + throw new Error("请先打开一个文件夹作为工作区,这样我就能为您修改文件了"); + } + + return path.join(workspaceFolders[0].uri.fsPath, filePath); +} + +/** + * 使用 VS Code 原生 diff 视图展示文件修改前后对比 + */ +export async function showFileDiff( + filePath: string, + oldContent: string, + title?: string +): Promise { + const absolutePath = resolveWorkspaceFilePath(filePath); + const fileUri = vscode.Uri.file(absolutePath); + const newBytes = await vscode.workspace.fs.readFile(fileUri); + const newContent = Buffer.from(newBytes).toString("utf-8"); + + if (oldContent === newContent) { + return; + } + + const language = (await vscode.workspace.openTextDocument(fileUri)).languageId; + const oldDoc = await vscode.workspace.openTextDocument({ + content: oldContent, + language, + }); + + const diffTitle = title || `${path.basename(absolutePath)} (修改前 <-> 修改后)`; + await vscode.commands.executeCommand( + "vscode.diff", + oldDoc.uri, + fileUri, + diffTitle, + { preview: false } + ); +} diff --git a/src/utils/messageHandler.ts b/src/utils/messageHandler.ts index 0dac3c1..928d144 100644 --- a/src/utils/messageHandler.ts +++ b/src/utils/messageHandler.ts @@ -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 { + 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}`); + } +} diff --git a/src/views/changePanel.ts b/src/views/changePanel.ts new file mode 100644 index 0000000..972d9f4 --- /dev/null +++ b/src/views/changePanel.ts @@ -0,0 +1,366 @@ +/** + * 代码变更面板组件 + * 功能:显示 AI 修改的文件列表和 diff 对比 + * 依赖:utils/diffRenderer + * 使用场景:对话结束后展示代码变更供用户审查 + */ + +import { getDiffStyles } from '../utils/diffRenderer'; + +/** + * 获取变更面板的 HTML 内容 + */ +export function getChangePanelContent(): string { + return ` + + `; +} + +/** + * 获取变更面板的样式 + */ +export function getChangePanelStyles(): string { + return ` + .change-panel { + margin-bottom: 12px; + border: 1px solid var(--vscode-panel-border); + border-radius: 6px; + background: var(--vscode-editor-background); + overflow: hidden; + } + + .change-panel-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px 12px; + background: var(--vscode-sideBar-background); + cursor: pointer; + user-select: none; + } + + .change-panel-header:hover { + background: var(--vscode-list-hoverBackground); + } + + .change-panel-title { + display: flex; + align-items: center; + gap: 8px; + font-size: 13px; + font-weight: 500; + } + + .change-icon { + font-size: 16px; + } + + .change-count { + background: var(--vscode-badge-background); + color: var(--vscode-badge-foreground); + padding: 2px 8px; + border-radius: 10px; + font-size: 11px; + font-weight: 600; + } + + .change-toggle-btn { + background: transparent; + border: none; + color: var(--vscode-foreground); + cursor: pointer; + padding: 4px 8px; + border-radius: 4px; + transition: background 0.2s; + } + + .change-toggle-btn:hover { + background: var(--vscode-toolbar-hoverBackground); + } + + .toggle-icon { + display: inline-block; + transition: transform 0.2s; + } + + .toggle-icon.expanded { + transform: rotate(180deg); + } + + .change-panel-body { + border-top: 1px solid var(--vscode-panel-border); + max-height: 400px; + overflow-y: auto; + } + + .change-list { + padding: 8px; + } + + .change-item { + border: 1px solid var(--vscode-panel-border); + border-radius: 4px; + margin-bottom: 8px; + overflow: hidden; + background: var(--vscode-editor-background); + } + + .change-item-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 8px 12px; + background: var(--vscode-sideBar-background); + cursor: pointer; + } + + .change-item-header:hover { + background: var(--vscode-list-hoverBackground); + } + + .change-item-info { + display: flex; + align-items: center; + gap: 8px; + flex: 1; + min-width: 0; + } + + .change-type-badge { + padding: 2px 6px; + border-radius: 3px; + font-size: 10px; + font-weight: 600; + text-transform: uppercase; + } + + .change-type-create { + background: #28a745; + color: white; + } + + .change-type-modify { + background: #ffc107; + color: black; + } + + .change-type-delete { + background: #dc3545; + color: white; + } + + .change-file-path { + font-size: 12px; + color: var(--vscode-foreground); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .change-item-actions { + display: flex; + gap: 6px; + } + + .change-action-btn { + padding: 4px 10px; + border: none; + border-radius: 3px; + font-size: 11px; + cursor: pointer; + transition: opacity 0.2s; + } + + .change-action-btn:hover { + opacity: 0.8; + } + + .accept-btn { + background: #28a745; + color: white; + } + + .reject-btn { + background: #dc3545; + color: white; + } + + .change-item-diff { + padding: 12px; + background: var(--vscode-editor-background); + border-top: 1px solid var(--vscode-panel-border); + display: none; + max-height: 300px; + overflow-y: auto; + } + + .change-item-diff.expanded { + display: block; + } + + ${getDiffStyles()} + `; +} + +/** + * 获取变更面板的脚本 + */ +export function getChangePanelScript(): string { + return ` + // 切换变更面板展开/收起 + function toggleChangePanel() { + const body = document.getElementById('changePanelBody'); + const toggleIcon = document.querySelector('.toggle-icon'); + + if (body.style.display === 'none') { + body.style.display = 'block'; + toggleIcon.classList.add('expanded'); + } else { + body.style.display = 'none'; + toggleIcon.classList.remove('expanded'); + } + } + + // 打开文件 diff(在 VS Code 中打开) + function openFileDiff(changeId) { + vscode.postMessage({ + command: 'openFileDiff', + changeId: changeId + }); + } + + // 采纳变更 + function acceptChange(changeId) { + vscode.postMessage({ + command: 'acceptChange', + changeId: changeId + }); + } + + // 拒绝变更 + function rejectChange(changeId) { + vscode.postMessage({ + command: 'rejectChange', + changeId: changeId + }); + } + + // 显示变更面板(从后端接收变更列表) + window.showChangesPanel = function(changes) { + const changePanel = document.getElementById('changePanel'); + const changeList = document.getElementById('changeList'); + const changeCount = document.getElementById('changeCount'); + + if (!changePanel || !changeList || !changeCount) { + return; + } + + // 更新变更数量 + changeCount.textContent = changes.length; + + // 清空现有列表 + changeList.innerHTML = ''; + + // 渲染每个变更项 + changes.forEach(change => { + const changeItem = createChangeItem(change); + changeList.appendChild(changeItem); + }); + + // 显示面板 + changePanel.style.display = 'block'; + }; + + // 创建单个变更项的 DOM 元素 + function createChangeItem(change) { + const item = document.createElement('div'); + item.className = 'change-item'; + item.id = 'change-item-' + change.changeId; + + const typeLabel = change.changeType === 'create' ? '新建' : + change.changeType === 'modify' ? '修改' : '删除'; + + item.innerHTML = \` +
+
+ \${typeLabel} + \${change.filePath} +
+
+ + +
+
+ \`; + + return item; + } + + // 处理采纳变更的响应 + window.handleChangeAccepted = function(changeId, success, error) { + if (success) { + // 从列表中移除该变更项 + const item = document.getElementById('change-item-' + changeId); + if (item) { + item.remove(); + } + + // 更新变更数量 + updateChangeCount(); + } else { + console.error('采纳变更失败:', error); + alert('采纳变更失败: ' + (error || '未知错误')); + } + }; + + // 处理拒绝变更的响应 + window.handleChangeRejected = function(changeId, success, error) { + if (success) { + // 从列表中移除该变更项 + const item = document.getElementById('change-item-' + changeId); + if (item) { + item.remove(); + } + + // 更新变更数量 + updateChangeCount(); + } else { + console.error('拒绝变更失败:', error); + alert('拒绝变更失败: ' + (error || '未知错误')); + } + }; + + // 更新变更数量 + function updateChangeCount() { + const changeList = document.getElementById('changeList'); + const changeCount = document.getElementById('changeCount'); + const changePanel = document.getElementById('changePanel'); + + if (changeList && changeCount) { + const count = changeList.children.length; + changeCount.textContent = count; + + // 如果没有变更了,隐藏面板 + if (count === 0 && changePanel) { + changePanel.style.display = 'none'; + } + } + } + `; +} diff --git a/src/views/inputArea.ts b/src/views/inputArea.ts index e1e41a4..9da5dc6 100644 --- a/src/views/inputArea.ts +++ b/src/views/inputArea.ts @@ -34,6 +34,11 @@ import { getExampleShowcaseStyles, getExampleShowcaseScript, } from "./exampleShowcase"; +import { + getChangePanelContent, + getChangePanelStyles, + getChangePanelScript, +} from "./changePanel"; import { sendIconSvg, stopIconSvg } from "../constants/toolIcons"; /** @@ -49,6 +54,8 @@ export function getInputAreaContent(
+ + ${getChangePanelContent()}
${getContextButtonContent()} @@ -94,6 +101,7 @@ export function getInputAreaStyles(): string { ${getContextCompressStyles()} ${getOptimizeButtonStyles()} ${getExampleShowcaseStyles()} + ${getChangePanelStyles()} .input-area { border-top: 1px solid var(--vscode-panel-border); padding-top: 15px; @@ -300,6 +308,7 @@ export function getInputAreaScript(): string { ${getContextButtonScript()} ${getContextCompressScript()} ${getOptimizeButtonScript()} + ${getChangePanelScript()} // 对话状态管理 let isConversationActive = false; diff --git a/src/views/webviewContent.ts b/src/views/webviewContent.ts index b553be3..2a36bb5 100644 --- a/src/views/webviewContent.ts +++ b/src/views/webviewContent.ts @@ -882,6 +882,27 @@ export function getWebviewContent( } break; + case 'showChanges': + // 显示代码变更 + if (typeof showChangesPanel === 'function') { + showChangesPanel(message.changes); + } + break; + + case 'changeAccepted': + // 变更已采纳 + if (typeof handleChangeAccepted === 'function') { + handleChangeAccepted(message.changeId, message.success, message.error); + } + break; + + case 'changeRejected': + // 变更已拒绝 + if (typeof handleChangeRejected === 'function') { + handleChangeRejected(message.changeId, message.success, message.error); + } + break; + default: console.log('[WebView] 未处理的消息类型:', message.command); }