# AI Code Diff Preview — 项目文档 ## 一、功能概述 AI Code Diff Preview 是一个 VSCode 插件,为 Claude Code 提供类似 Cursor 的 AI 代码变更审查体验: - **自动收集**:监听 Claude 的 Edit/Write 操作,静默收集所有文件变更 - **总览面板**:Stop 后弹出 Webview 面板,展示每个文件的完整双栏 Diff - **内置 Diff**:点击单个文件打开 VSCode 原生 Diff 编辑器,状态栏显示 Accept/Reject 按钮 - **粒度控制**:逐文件 Accept/Reject + 全局 Accept All / Reject All - **编辑合并**:同文件连续多次编辑自动合并为一个 Diff(前次未处理时);已处理后重新编辑则独立追踪 ## 二、项目结构 ``` src/ ├── extension.ts # 插件入口,初始化模块 + 注册命令 ├── trigger/ │ └── hookHandler.ts # Hook 事件处理 (Edit/Stop 分发) ├── snapshot/ │ └── snapshotManager.ts # ★ 核心数据层: ChangeSetManager ├── diff/ │ └── diffEngine.ts # Diff 算法 (Myers) ├── render/ │ ├── reviewPanel.ts # ★ 总览 Webview 面板 │ ├── diffViewer.ts # ★ VSCode 内置 Diff 查看器 + accept/reject + 自动推进 │ ├── statusBar.ts # ★ 状态栏 (Diff 模式按钮 / 摘要模式) │ ├── inlineDecorator.ts # 内联装饰器 (保留, 未启用) │ └── codeLensProvider.ts # CodeLens (保留, 未启用) ├── transaction/ │ ├── acceptHandler.ts # Accept 处理器 (legacy) │ └── rejectHandler.ts # Reject 处理器 (legacy) └── models/ ├── types.ts # 类型定义 └── constants.ts # 常量 ``` ## 三、核心数据模型 ### 层级关系 ``` ChangeSet (一轮对话) └── FileChange[] (每个文件一个) ├── originalContent (首次编辑前的文件快照) ├── latestContent (最新文件内容) ├── diffLines[] (originalContent → latestContent 的聚合 Diff) ├── status (pending | accepted | rejected) └── EditRecord[] (每次 Edit/Write 一条) ├── beforeContent (本次编辑前快照) ├── afterContent (本次编辑后快照) ├── oldString / newString (精确替换内容) ├── diffLines[] (本次编辑的独立 Diff) └── status (pending | accepted | rejected) ``` ### 编辑合并策略 ``` 同文件多次 Edit 时: ┌─ 已有 pending FileChange? ── YES ──→ ★ 合并: 追加 EditRecord, 更新聚合 Diff │ └─ NO (首次 / 前轮已 accept/reject) └──→ 创建新 FileChange, 独立追踪 ``` **关键**: 查找 FileChange 时**优先匹配 pending 的**(而非用 `.find()` 取第一个),避免同文件多轮编辑时孤儿 FileChange 的 Bug。 ## 四、完整流程 ```mermaid flowchart TD A["Claude 对话中"] --> B["Edit / Write 工具调用"] B --> C["写入 .claude/hooks/pending.json"] C --> D["FileSystemWatcher 监听到"] D --> E["读取并删除 pending.json"] E --> F{"toolName?"} F -->|"Edit / Write"| G["hookHandler.handleEdit()"] G --> H["ChangeSetManager.recordEdit()"] H --> I{"同文件已有
pending FileChange?"} I -->|"YES"| J["★ 合并: 追加 EditRecord
更新聚合 Diff"] I -->|"NO"| K["新建 FileChange
捕获 originalContent"] J --> L{"autoShowDiffPerEdit
配置?"} K --> L L -->|"true"| M["弹出 VSCode 内置 Diff"] L -->|"false"| N["静默收集, 不弹 UI"] F -->|"Stop"| O["hookHandler.handleStop()"] O --> P["ChangeSetManager.markReady()"] P --> Q{"有 pending 变更?"} Q -->|"NO"| R["无操作"] Q -->|"YES"| S["★ 弹出总览 Webview 面板
reviewPanel.show()"] S --> T["总览面板: 每个文件一张卡片"] T --> U["卡片内含:
— 文件信息 + 增减统计
— 双栏 Diff (原始 vs 最新)
— ✔ 接受 / ✘ 拒绝 按钮"] T --> V["顶部: Accept All / Reject All"] U --> W["用户点击 ✔ 接受"] U --> X["用户点击 ✘ 拒绝"] V --> Y["用户点击 Accept All"] V --> Z["用户点击 Reject All"] W --> AA["acceptFile(fileId)
写入 latestContent 到磁盘"] X --> AB["rejectFile(fileId)
恢复 originalContent"] Y --> AC["acceptAll()
遍历所有 pending 文件"] Z --> AD["rejectAll()
遍历所有 pending 文件"] AA --> AE["刷新总览面板 + 状态栏"] AB --> AE AC --> AE AD --> AE T --> AF["用户点击文件卡片"] AF --> AG["打开 VSCode 内置 Diff 编辑器
diffViewer.showFileDiff()"] AG --> AH["Diff 界面: 状态栏切换为
$(check) Accept | $(close) Reject | $(check-all) 接受全部"] AH --> AI["用户操作 Accept/Reject"] AI --> AJ["★ 自动推进到下一个 pending 文件
diffViewer.advanceToNext()"] AJ --> AK["全部处理完 → 🎉 通知"] ``` ## 五、三种 UI 层 ### 5.1 Webview 总览面板 (`reviewPanel.ts`) 触发时机:Claude Stop 后自动弹出 / 手动 `Ctrl+Shift+D` ``` ┌─ AI Diff 总览 ───── [Accept All] [Reject All] ─┐ │ │ │ 📄 file1.ts 修改 +3 -2 · 2编辑 [✔接受][✘拒绝]│ │ ┌──── 原始代码 ────┬──── 变更后 ────────────┐ │ │ │ 1 line1 │ 1 line1 │ │ │ │ 2 -old line2 │ (空) │ │ │ │ 3 line3 │ 2 +new line2 │ │ │ │ 4 -old line4 │ (空) │ │ │ │ 5 line5 │ 3 +extra line │ │ │ │ │ 4 line5 │ │ │ └──────────────────┴────────────────────────┘ │ │ │ │ 📄 file2.ts 新增 +10 -0 · 1编辑 [✔接受][✘拒绝]│ │ ┌──── 新文件内容 ──────────────────────────┐ │ │ │ 1 +new code line 1 │ │ │ │ 2 +new code line 2 │ │ │ └──────────────────────────────────────────┘ │ └─────────────────────────────────────────────────┘ ``` ### 5.2 VSCode 内置 Diff 编辑器 (`diffViewer.ts`) 触发时机:从总览面板点击文件 / `Alt+↑/↓` 导航 - 使用 `vscode.diff` 命令打开原生 Diff 编辑器 - 左侧:原始内容 (红色删除行),右侧:变更后内容 (绿色新增行) - 临时文件机制:写入 `%TEMP%/ai-diff-preview/`,关闭 Diff 后自动删除(**无保存提示**) - **自动推进**:Accept/Reject 后自动打开下一个 pending 文件 - 若当前文件仍有 pending 编辑 → 重新打开此文件 - 否则 → 打开下一个 pending 文件 - 全部处理完 → 弹出 🎉 通知 ### 5.3 状态栏 (`statusBar.ts`) 双模式自动切换: | 模式 | 触发 | 显示 | |------|------|------| | 摘要模式 | 无活跃 Diff | `$(diff) AI Diff: 2文件 5编辑` ← 点击打开总览 | | Diff 模式 | 内置 Diff 活跃 | `$(check) Accept` `$(close) Reject` `$(check-all) 接受全部` | ## 六、Accept/Reject 行为 | 操作 | Modify 文件 | Create 文件 | |------|------------|------------| | Accept (文件级) | 写入 `latestContent` 到磁盘 | 保留文件 | | Reject (文件级) | 恢复 `originalContent` 到磁盘 | 删除文件 | | Accept All | 遍历所有 pending 文件执行 Accept | | | Reject All | 遍历所有 pending 文件执行 Reject | | ## 七、配置项 | 配置 | 类型 | 默认 | 说明 | |------|------|------|------| | `enableAutoTrigger` | boolean | true | 监听 pending.json | | `autoShowDiffPerEdit` | boolean | false | 每次 Edit 后弹内置 Diff | | `showAllDiffsOnStop` | boolean | true | Stop 后弹总览面板 | | `floatingLabelMode` | string | "statusBar" | 按钮位置 | | `maxFileSize` | number | 100000 | 文件大小上限 | ## 八、快捷键 | 快捷键 | 条件 | 功能 | |--------|------|------| | `Ctrl+Shift+D` | `aiDiffPreview.isActive` | 打开总览面板 | | `Alt+↓` | `aiDiffPreview.isActive` | 下一个变更文件 | | `Alt+↑` | `aiDiffPreview.isActive` | 上一个变更文件 | | `Tab` | Diff 活跃 | Accept 当前变更 | | `Esc` | Diff 活跃 | Reject 当前变更 | | `Ctrl+Shift+A` | `aiDiffPreview.isActive` | Accept All | | `Ctrl+Shift+R` | `aiDiffPreview.isActive` | Reject All | ## 九、模块依赖关系 ```mermaid flowchart LR extension["extension.ts
入口"] hook["hookHandler.ts
Hook 处理"] manager["snapshotManager.ts
ChangeSetManager
★ 核心数据层"] review["reviewPanel.ts
总览 Webview"] viewer["diffViewer.ts
内置 Diff + 自动推进"] bar["statusBar.ts
状态栏"] types["models/types.ts
类型"] extension --> hook extension --> manager extension --> review extension --> viewer extension --> bar hook --> manager hook --> viewer review --> manager review --> viewer viewer --> manager bar --> manager bar --> viewer manager --> types ``` ## 十、已修复的 Bug | Bug | 根因 | 修复 | |-----|------|------| | Accept 后同文件再编辑无 Accept/Reject | `recordEdit()` 更新同一 `FileChange`,Accept 后状态机断开 | 引入 `EditRecord` 粒度,Accept/Reject 后重新编辑创建新 FileChange | | Accept All / Reject All 不生效 | `acceptAllFromDiff()` 调用不存在的方法 | 改为调用 `acceptFile()`,All 方法改为收集列表后逐一处理 | | Accept 后无后续 Diff | 无推进逻辑 | `advanceToNext()` 自动打开下一个 pending 文件 | | 临时文件保存提示 | 使用 `openTextDocument({content})` 创建 untitled 文档 | 改为写入临时目录 `%TEMP%/ai-diff-preview/` | | 同文件多轮编辑孤儿 FileChange | `.find()` 返回第一个 (已 accept) 而非 pending | 改为优先匹配 pending 的 FileChange |