mirror of
https://github.com/Gmaker689/ai-diff-preview.git
synced 2026-09-27 03:21:52 +08:00
dfa3a585c5
新增: - 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回归)
10 KiB
10 KiB
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。
四、完整流程
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 |
九、模块依赖关系
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 |