mirror of
https://github.com/Gmaker689/ai-diff-preview.git
synced 2026-09-26 23:11:51 +08:00
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:
@@ -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 |
|
||||
Reference in New Issue
Block a user