v0.2.0: VSCode内置Diff + QuickPick总览 + EditRecord粒度 + Bug修复

新增:
- VSCode内置Diff编辑器 (diffViewer.ts) 替代Webview,零保存提示
- EditRecord独立追踪: 每次Edit/Write不合并,支持逐次编辑Accept/Reject
- QuickPick文件列表总览,含Accept All / Reject All
- 智能Auto-Advance: Accept/Reject后自动推进,同文件多编辑智能停留
- 状态栏常驻显示  AI Diff,Diff模式下切换为操作按钮

修复:
- Accept后同文件再编辑不触发选项 (pending-priority匹配)
- Accept All / Reject All写入与状态顺序修复
- acceptFile/rejectFile改为先写入成功再标记状态
- 同文件多个FileChange孤儿编辑问题

变更:
- ChangeSetManager重构为EditRecord[] + FileChange聚合
- HookHandler精简,QuickPick移入hookHandler
- 移除InlineDecorator/CodeLens (主流程),ReviewPanel降级为备用
- package.json: publisher=YonHao Guo, repository, v0.2.0, 新配置项/快捷键

测试: 13/13通过 (7 diffEngine + 6 changeSetManager回归)
This commit is contained in:
2026-06-18 19:37:21 +08:00
parent fcda651760
commit dfa3a585c5
14 changed files with 1454 additions and 958 deletions
+241
View File
@@ -0,0 +1,241 @@
# 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{"同文件已有<br/>pending FileChange?"}
I -->|"YES"| J["★ 合并: 追加 EditRecord<br/>更新聚合 Diff"]
I -->|"NO"| K["新建 FileChange<br/>捕获 originalContent"]
J --> L{"autoShowDiffPerEdit<br/>配置?"}
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 面板<br/>reviewPanel.show()"]
S --> T["总览面板: 每个文件一张卡片"]
T --> U["卡片内含:<br/>— 文件信息 + 增减统计<br/>— 双栏 Diff (原始 vs 最新)<br/>— ✔ 接受 / ✘ 拒绝 按钮"]
T --> V["顶部: Accept All / Reject All"]
U --> W["用户点击 ✔ 接受"]
U --> X["用户点击 ✘ 拒绝"]
V --> Y["用户点击 Accept All"]
V --> Z["用户点击 Reject All"]
W --> AA["acceptFile(fileId)<br/>写入 latestContent 到磁盘"]
X --> AB["rejectFile(fileId)<br/>恢复 originalContent"]
Y --> AC["acceptAll()<br/>遍历所有 pending 文件"]
Z --> AD["rejectAll()<br/>遍历所有 pending 文件"]
AA --> AE["刷新总览面板 + 状态栏"]
AB --> AE
AC --> AE
AD --> AE
T --> AF["用户点击文件卡片"]
AF --> AG["打开 VSCode 内置 Diff 编辑器<br/>diffViewer.showFileDiff()"]
AG --> AH["Diff 界面: 状态栏切换为<br/>$(check) Accept | $(close) Reject | $(check-all) 接受全部"]
AH --> AI["用户操作 Accept/Reject"]
AI --> AJ["★ 自动推进到下一个 pending 文件<br/>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<br/>入口"]
hook["hookHandler.ts<br/>Hook 处理"]
manager["snapshotManager.ts<br/>ChangeSetManager<br/>★ 核心数据层"]
review["reviewPanel.ts<br/>总览 Webview"]
viewer["diffViewer.ts<br/>内置 Diff + 自动推进"]
bar["statusBar.ts<br/>状态栏"]
types["models/types.ts<br/>类型"]
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 |