docs: 添加更详细的使用说明

This commit is contained in:
2026-06-19 14:04:25 +08:00
parent 23337a2aef
commit bcc3ddba8d
7 changed files with 300 additions and 758 deletions
-627
View File
@@ -1,627 +0,0 @@
# AI Code Diff Preview - VSCode 插件架构文档
## 一、项目概述
### 1.1 项目名称
**AI Code Diff Preview** — 类 Cursor AI 代码差异预览 VSCode 插件
### 1.2 项目目标
在 VSCode 中实现类似 Cursor 的 Accept/Reject 差异预览功能:
- AI 代码编辑自动收集到变更集
- 调用 **VSCode 内置 Diff 编辑器** 展示差异(不自定义 Webview)
- 红绿对应的 **浮动 Accept/Reject 标签**,固定在每次变更和每个文件变更上方
- 逐文件 / 逐次编辑的精细化 Accept/Reject 操作
- 多文件修改统一管理
### 1.3 核心价值
- **原生体验**:直接使用 VSCode 内置 `vscode.diff` 命令,风格统一、交互一致
- **安全隔离**:AI 编辑期间可选择不弹窗打断,对话结束后统一审查
- **浮动操作**:Accept/Reject 标签固定在编辑器顶部/变更行上方,不干扰阅读
- **精细控制**:支持单次编辑粒度(每次 Edit/Write 独立审核)、文件粒度 Accept/Reject
- **灵活触发**:可配置「每次编辑自动弹 Diff」「对话结束后统一弹完整多文件预览」
---
## 二、技术选型
### 2.1 核心技术栈
| 技术领域 | 选型方案 | 理由 |
|---------|---------|------|
| **插件框架** | VSCode Extension API | 官方原生支持,API 完善 |
| **开发语言** | TypeScript (ES2022) | 类型安全,VSCode 插件官方推荐 |
| **Diff 渲染** | `vscode.commands.executeCommand('vscode.diff', ...)` | **VSCode 内置 Diff 编辑器**,原生交互,无需自建 Webview |
| **浮动 UI** | `vscode.window.createTextEditorDecorationType` + `after` | 编辑器内 Accept/Reject 标签(非 Webview) |
| **Diff 算法** | `diff` 库 (Myers 算法) | 行级差异计算,用于数据层变更追踪 |
| **构建工具** | esbuild | 快速打包,VSCode 插件社区标准 |
| **测试框架** | Vitest | 现代化单元测试 |
| **代码规范** | ESLint + Prettier | 代码质量保证 |
### 2.2 关键依赖
```json
{
"dependencies": {
"diff": "^5.0.0", // Diff 算法核心(ChangeSetManager 中行级对比)
"uuid": "^9.0.0" // 变更块/变更集唯一标识
},
"devDependencies": {
"@types/vscode": "^1.85.0",
"@types/diff": "^5.0.0",
"@types/uuid": "^9.0.0",
"esbuild": "^0.19.0",
"typescript": "^5.3.0",
"vitest": "^1.0.0"
}
}
```
### 2.3 VSCode API 关键模块
| API 模块 | 用途 |
|---------|------|
| `vscode.commands.executeCommand('vscode.diff', uri1, uri2, title)` | **VSCode 内置 Diff 编辑器**,核心展示方案 |
| `vscode.workspace.createFileSystemWatcher` | 监听 `.claude/hooks/pending.json` 触发文件 |
| `vscode.window.createTextEditorDecorationType` | 红绿色高亮 + 浮动 Accept/Reject 标签 |
| `vscode.window.showInformationMessage` | 对话结束后弹出通知 |
| `vscode.commands.registerCommand` | 命令注册(快捷键绑定的操作) |
| `vscode.workspace.applyEdit` | 原子化文件写入 |
| `vscode.Uri.parse('untitled:...')` | 生成临时虚拟文档用于 Diff 对比 |
---
## 三、架构设计
### 3.1 工作流程
```
┌─────────────────────────────────────────────────────────────────┐
│ Claude 对话中 │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ Edit 工具 │ │ Write 工具 │ │
│ └──────┬───────┘ └──────┬───────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────────────────────────────────┐ │
│ │ 写入 .claude/hooks/pending.json │ ← PostToolUse 触发 │
│ │ { type, filePath, oldString, │ │
│ │ newString, toolName, timestamp } │ │
│ └────────────────┬─────────────────────┘ │
│ │ │
├───────────────────┼──────────────────────────────────────────────┤
│ VSCode 插件 │ │
│ ▼ │
│ ┌──────────────────────────────────────┐ │
│ │ FileSystemWatcher 监听到变更 │ ← extension.ts │
│ │ 读取 pending.json → 分发事件 │ │
│ └────────────────┬─────────────────────┘ │
│ │ │
│ ┌─────────┴─────────┐ │
│ ▼ ▼ │
│ toolName="Edit/Write" toolName="Stop" │
│ ┌─────────────────┐ ┌──────────────────────┐ │
│ │ HookHandler │ │ HookHandler │ │
│ │ .handleEdit() │ │ .handleStop() │ │
│ │ │ │ │ │
│ │ → recordEdit() │ │ → markReady() │ │
│ │ 收集变更到 │ │ 标记变更集就绪 │ │
│ │ ChangeSet │ │ │ │
│ │ │ │ → 根据配置决定行为: │ │
│ │ → 根据配置: │ │ showAllDiffsOnStop │ │
│ │ autoShowDiff- │ │ ? 弹出完整Diff预览 │ │
│ │ PerEdit │ │ : 仅显示通知 │ │
│ │ ? 立即弹出 │ └───────────┬──────────┘ │
│ │ 内置Diff │ │ │
│ │ : 静默收集 │ ▼ │
│ └─────────────────┘ ┌──────────────────────────────────────┐ │
│ │ 打开完整 Diff 文件列表 (QuickPick │ │
│ │ 或 TreeView) │ │
│ │ ┌────────────────────────────────┐ │ │
│ │ │ 📄 src/file1.ts [+3 -2] [✓][✗]│ │ │
│ │ │ 📄 src/file2.ts [+10 -0] [✓][✗]│ │ │
│ │ │ 📄 src/file3.ts [+1 -5] [✓][✗]│ │ │
│ │ └────────────────────────────────┘ │ │
│ │ 点击某文件 → 打开 VSCode 内置 Diff │ │
│ └───────────────────┬──────────────────┘ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ VSCode 原生 Diff 编辑器 │ │
│ │ ┌─────────────────────────────────────────────────────┐ │ │
│ │ │ [浮动标签] ✓ Accept本次更改 ✗ Reject本次更改 │ │ │
│ │ │ ┌──────────────────┬──────────────────────────┐ │ │
│ │ │ │ 原始代码 (左侧) │ AI 修改后 (右侧) │ │ │
│ │ │ │ │ │ │ │
│ │ │ │ - 红色删除行 │ + 绿色新增行 │ │ │
│ │ │ │ │ │ │ │
│ │ │ └──────────────────┴──────────────────────────┘ │ │
│ │ └─────────────────────────────────────────────────────┘ │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ Accept: oldContent → newContent 写入磁盘 │
│ Reject: newContent → oldContent 恢复 / 新建文件删除 │
└──────────────────────────────────────────────────────────────────┘
```
### 3.2 核心模块划分
```
src/
├── extension.ts # 插件入口
│ ├── 初始化 ChangeSetManager、HookHandler
│ ├── 注册所有命令(acceptChunk/rejectChunk/acceptAll/rejectAll/showDiff)
│ ├── 注册 FileSystemWatcher 监听 pending.json
│ └── 检查启动时已有 pending 文件
│
├── trigger/
│ └── hookHandler.ts # Hook 事件处理器
│ ├── handleEdit() # PostToolUse → 收集变更 + 可选自动弹 Diff
│ └── handleStop() # 对话结束 → 可选弹出完整文件列表
│
├── snapshot/
│ └── snapshotManager.ts # 变更集管理器 (ChangeSetManager)
│ ├── recordEdit() # 记录每次编辑(独立 EditRecord,不合并)
│ ├── markReady() # 标记变更集就绪
│ ├── applyEditRecord() # 接受:还原 oldString→newString 到磁盘
│ ├── rejectEditRecord() # 拒绝:还原 newString→oldString 到磁盘
│ ├── applyFileChange() # 接受整个文件变更
│ ├── rejectFileChange() # 拒绝整个文件变更
│ └── computeDiffLines() # 行级 Diff 计算
│
├── diff/
│ └── diffEngine.ts # Diff 计算引擎
│ ├── computeChunks() # 计算 DiffChunk 列表
│ ├── detectConflicts() # 冲突检测
│ └── applyChunks() # 批量应用(倒序避免行号偏移)
│
├── render/
│ ├── inlineDecorator.ts # 内联装饰器 + 浮动标签 ★ 核心 UI
│ │ ├── 新增行:绿色背景 + ✓ Accept 浮动标签
│ │ ├── 删除行:红色背景 + 删除线
│ │ ├── 修改区域:黄色背景高亮
│ │ ├── 浮动 Accept/Reject 标签(固定在编辑器视口顶部或变更行上方)
│ │ └── 冲突行:橙色边框
│ │
│ ├── diffViewer.ts # VSCode 内置 Diff 查看器 ★ 新增
│ │ ├── openDiff() # 调用 vscode.diff 命令展示单文件差异
│ │ ├── openDiffForEdit() # 展示单次 Edit 的差异
│ │ └── openAllDiffs() # 依次/并排展示所有文件差异
│ │
│ ├── fileListProvider.ts # 变更文件列表提供器 ★ 新增
│ │ └── QuickPick / TreeView 展示文件列表(含 Accept/Reject 按钮)
│ │
│ └── statusBar.ts # 状态栏管理器
│ └── 显示待处理变更数量 + 快速入口
│
├── transaction/
│ ├── acceptHandler.ts # Accept 处理器
│ │ ├── acceptEdit() # 接受单次编辑
│ │ ├── acceptFile() # 接受整个文件所有编辑
│ │ ├── acceptAll() # 接受全部
│ │ └── writeFileAtomic() # 原子化写入
│ │
│ └── rejectHandler.ts # Reject 处理器
│ ├── rejectEdit() # 拒绝单次编辑
│ ├── rejectFile() # 拒绝整个文件
│ ├── rejectAll() # 拒绝全部
│ └── cleanupUI() # 清理装饰器和状态
│
├── models/
│ ├── types.ts # 核心类型定义
│ └── constants.ts # 常量定义
│
└── __tests__/
└── diffEngine.test.ts # Diff 引擎单元测试
```
---
## 四、核心数据模型
### 4.1 变更集模型(含编辑粒度)
```typescript
// 一轮对话的变更集
interface ChangeSet {
id: string;
edits: EditRecord[]; // ★ 每次 Edit/Write 独立记录
createdAt: number;
status: 'collecting' | 'ready' | 'reviewed';
}
// ★ 单次编辑记录(不合并!每次 Edit/Write 都是一个独立 EditRecord)
interface EditRecord {
id: string;
filePath: string;
toolName: 'Edit' | 'Write';
timestamp: number;
/** 编辑前的文件快照 */
beforeContent: string;
/** 编辑后的文件快照 */
afterContent: string;
/** oldString → newString 的具体替换内容 */
oldString: string;
newString: string;
/** 本次编辑的 Diff 行 */
diffLines: DiffLine[];
/** 状态 */
status: 'pending' | 'accepted' | 'rejected';
}
// 文件维度的聚合变更(由多个 EditRecord 聚合而来)
interface FileChange {
id: string;
filePath: string;
type: 'create' | 'modify' | 'delete';
edits: EditRecord[]; // ★ 包含的所有编辑记录
/** 原始内容 = 所有编辑前的文件状态 */
originalContent: string;
/** 最新内容 = 所有编辑后的文件状态 */
latestContent: string;
/** 聚合 Diff */
diffLines: DiffLine[];
status: 'pending' | 'accepted' | 'rejected';
}
// 单行 Diff
interface DiffLine {
type: 'context' | 'add' | 'delete';
oldLineNum: number;
newLineNum: number;
content: string;
}
```
### 4.2 关键设计:编辑不合并
> **重要**:同一文件的多次 Edit/Write **不合并**为一个 FileChange。每次 Edit 都保留独立的 `EditRecord`,用户可以:
> - 按单次编辑粒度 Accept/Reject(精确控制每次 AI 修改)
> - 按文件粒度 Accept/Reject(接受/拒绝该文件的所有编辑)
> - 全局 Accept All / Reject All
### 4.3 Hook 触发事件
```typescript
// pending.json 文件格式
interface ClaudeHookEvent {
type: string;
filePath: string;
content: string;
oldString: string; // Edit: 替换前的文本片段
newString: string; // Edit: 替换后的文本片段 / Write: 完整内容
timestamp: number;
toolName: 'Edit' | 'Write' | 'Stop';
}
```
### 4.4 原始内容捕获策略
`ChangeSetManager.recordEdit()` 中的内容反推机制:
- **Edit**(首次编辑该文件):从当前文件中找到 `newString`,替换回 `oldString`,得到编辑前内容
- **Edit**(非首次):基于上一次 `EditRecord.afterContent` 作为本次的 `beforeContent`
- **Write**:无法获取原始内容,标记 `beforeContent = ''`,类型为 `create`
- **Write 后 Edit**:`beforeContent` 为 Write 写入后的文件内容
---
## 五、触发机制
### 5.1 文件监听模式
插件通过 `FileSystemWatcher` 监听 `.claude/hooks/pending.json` 文件的变更:
```
.claude/hooks/pending.json
│
▼ (create / change 事件)
extension.ts:
1. 读取文件内容
2. 删除 pending.json(防止重复处理)
3. 解析为 ClaudeHookEvent
4. 检查时间戳(10秒内有效)
5. 根据 toolName 分发:
- Edit/Write → hookHandler.handleEdit() → 收集 + 可选弹 Diff
- Stop → hookHandler.handleStop() → 可选弹完整文件列表
```
### 5.2 启动恢复
`extension.activate()` 时检查是否已有 pending 文件(上次对话遗留),时间窗口 15 秒内有效。
---
## 六、Diff 展示方案
### 6.1 使用 VSCode 内置 Diff 编辑器
不再自定义 Webview,直接调用 VSCode 原生命令:
```typescript
// diffViewer.ts
import * as vscode from 'vscode';
class DiffViewer {
/**
* 打开 VSCode 内置 Diff 编辑器展示单次编辑的差异
*/
async openDiffForEdit(editRecord: EditRecord): Promise<void> {
// 创建临时虚拟文档(不写入磁盘)
const beforeUri = vscode.Uri.parse(`untitled:before-${editRecord.id}.tmp`);
const afterUri = vscode.Uri.parse(`untitled:after-${editRecord.id}.tmp`);
// 写入临时内容
await this.writeTempContent(beforeUri, editRecord.beforeContent);
await this.writeTempContent(afterUri, editRecord.afterContent);
// 打开 VSCode 原生 Diff
await vscode.commands.executeCommand(
'vscode.diff',
beforeUri,
afterUri,
`${editRecord.filePath} (AI修改) — 原始 ↔ 变更后`
);
}
/**
* 展示整个文件的聚合变更
*/
async openDiffForFile(fileChange: FileChange): Promise<void> {
const beforeUri = vscode.Uri.parse(`untitled:before-file-${fileChange.id}.tmp`);
const afterUri = vscode.Uri.parse(`untitled:after-file-${fileChange.id}.tmp`);
await this.writeTempContent(beforeUri, fileChange.originalContent);
await this.writeTempContent(afterUri, fileChange.latestContent);
await vscode.commands.executeCommand(
'vscode.diff',
beforeUri,
afterUri,
`${fileChange.filePath} — 原始 ↔ 变更后`
);
}
}
```
### 6.2 浮动 Accept/Reject 标签
在 Diff 编辑器或当前编辑器中,使用 `TextEditorDecorationType` 的 `after` 属性在变更行上方渲染固定操作标签:
```typescript
// 浮动标签结构
// ┌─────────────────────────────────────────┐
// │ ✓ Accept本次更改 ✗ Reject本次更改 │ ← 固定在视口顶部
// │ [3行新增, 2行删除] │ ← 变更摘要
// │─────────────────────────────────────────│
// │ (Diff 编辑器内容区域) │
// │ + 绿色高亮新增行 │
// │ - 红色高亮删除行 │
// └─────────────────────────────────────────┘
```
标签实现方式:
- **编辑器中**:`createTextEditorDecorationType({ after: { contentText: ' ✓ Accept' } })` 在变更区域末尾添加
- **Diff 编辑器中**:利用 VSCode 原生 Diff 视图,在标题栏区域叠加操作按钮(通过 `vscode.window.onDidChangeActiveTextEditor` 跟踪)
- **替代方案**:使用 `vscode.window.createStatusBarItem` 在状态栏固定 Accept/Reject 按钮(更简单可靠)
### 6.3 文件列表 UI
对话结束后,通过 **QuickPick** 或 **TreeView** 展示所有变更文件:
```
┌──────────────────────────────────────────┐
│ AI Diff Review — 3 个文件有变更 │
│ │
│ 📄 src/utils.ts [+5 -2] [✓] [✗] │
│ 📄 src/index.ts [+10 -0] [✓] [✗] │
│ 📄 src/types.ts [+0 -3] [✓] [✗] │
│ │
│ ─────────────────────────────────────── │
│ [Accept All (3)] [Reject All (3)] │
└──────────────────────────────────────────┘
```
点击某文件 → 打开内置 Diff 编辑器展示该文件的差异。
---
## 七、配置项
```json
{
"aiDiffPreview.enableAutoTrigger": {
"type": "boolean",
"default": true,
"description": "启用自动触发(监听 pending.json)"
},
"aiDiffPreview.autoShowDiffPerEdit": {
"type": "boolean",
"default": false,
"description": "每次 Edit/Write 后自动弹出 VSCode 内置 Diff 编辑器。关闭时静默收集,等待 Stop 后统一展示"
},
"aiDiffPreview.showAllDiffsOnStop": {
"type": "boolean",
"default": true,
"description": "对话结束后弹出完整文件列表/QuickPick,展示所有变更文件。关闭时仅显示状态栏通知"
},
"aiDiffPreview.floatingLabelMode": {
"type": "string",
"default": "statusBar",
"enum": ["inline", "statusBar", "both"],
"description": "浮动 Accept/Reject 标签显示位置:inline=编辑器内文本后, statusBar=状态栏, both=两者都显示"
},
"aiDiffPreview.maxFileSize": {
"type": "number",
"default": 100000,
"description": "最大处理文件大小(字符数)"
}
}
```
### 配置组合行为矩阵
| autoShowDiffPerEdit | showAllDiffsOnStop | 行为 |
|:---:|:---:|------|
| false | true | **默认推荐**:编辑时静默收集,Stop 后弹出文件列表统一审查 |
| true | false | 每次编辑立即弹 Diff,Stop 后无额外操作 |
| true | true | 每次编辑弹 Diff + Stop 后再弹完整列表 |
| false | false | 仅状态栏显示待处理数量,用户手动触发 |
---
## 八、命令与快捷键
### 8.1 注册命令
| 命令 ID | 标题 | 说明 |
|---------|------|------|
| `aiDiffPreview.showDiffPanel` | AI Diff: 显示所有变更文件列表 | QuickPick 文件列表 |
| `aiDiffPreview.showCurrentFileDiff` | AI Diff: 查看当前文件 Diff | 打开当前活动文件的内置 Diff |
| `aiDiffPreview.acceptCurrentEdit` | AI Diff: 接受当前编辑 | 接受光标所在位置的编辑记录 |
| `aiDiffPreview.rejectCurrentEdit` | AI Diff: 拒绝当前编辑 | 拒绝光标所在位置的编辑记录 |
| `aiDiffPreview.acceptCurrentFile` | AI Diff: 接受当前文件所有编辑 | 接受当前文件全部编辑 |
| `aiDiffPreview.rejectCurrentFile` | AI Diff: 拒绝当前文件所有编辑 | 拒绝当前文件全部编辑 |
| `aiDiffPreview.acceptAll` | AI Diff: 接受所有变更 | 批量接受 |
| `aiDiffPreview.rejectAll` | AI Diff: 拒绝所有变更 | 批量拒绝 |
| `aiDiffPreview.nextDiff` | AI Diff: 下一个变更 | 跳转到下一个待处理的编辑 |
| `aiDiffPreview.prevDiff` | AI Diff: 上一个变更 | 跳转到上一个待处理的编辑 |
### 8.2 快捷键
| 快捷键 | 条件 | 功能 |
|--------|------|------|
| `Tab` | `aiDiffPreview.isActive` | 接受当前编辑块 |
| `Esc` | `aiDiffPreview.isActive` | 拒绝当前编辑块 |
| `Ctrl+Shift+A` | `aiDiffPreview.isActive` | Accept All |
| `Ctrl+Shift+R` | `aiDiffPreview.isActive` | Reject All |
| `Alt+↓` | `aiDiffPreview.isActive` | 下一个变更 |
| `Alt+↑` | `aiDiffPreview.isActive` | 上一个变更 |
| `Ctrl+Shift+D` | — | 显示所有变更文件列表 |
---
## 九、已知 Bug & 修复计划
### 9.1 Bug: Accept 后同文件再编辑不触发选项且 Diff 合并
**现象**:
1. 对文件 `A.ts` 进行修改 → 弹出 Accept/Reject 选项
2. 用户点击 Accept,修改写入磁盘
3. 对同一文件 `A.ts` 再次修改 → **不再弹出 Accept/Reject 选项**
4. 第二次修改的 Diff 内容被**合并到了第一次的 Diff 中**,无法独立审核
**根因分析**:
当前 `ChangeSetManager.recordEdit()` 的逻辑中,首次编辑该文件时捕获 `originalContent` 并创建 `FileChange`,但后续对同一文件的编辑会**更新同一个 `FileChange` 对象**(更新 `newContent` 和 `diffLines`),而不是创建独立的编辑记录。当用户 Accept 后,`FileChange.status` 变为 `accepted`,后续编辑找不到正确的状态机入口。
```typescript
// 当前代码 snapshotManager.ts 中的问题逻辑:
const existing = this.currentSet.changes.find(c => norm(c.filePath) === filePath);
if (existing) {
// ★ 问题:更新同一个 FileChange,导致多次编辑合并
existing.newContent = currentContent;
existing.diffLines = this.computeDiffLines(originalContent, currentContent);
}
```
**修复方案**:
1. **引入 `EditRecord` 粒度**:每次 Edit/Write 创建独立的 `EditRecord`(见 §4.1 数据模型)
2. **FileChange 作为聚合视图**:`FileChange.edits[]` 持有所有 `EditRecord`,支持按编辑粒度或文件粒度操作
3. **Accept 后状态重置**:Accept 某次编辑后,后续编辑创建新的 `EditRecord` 追加到 `FileChange.edits[]`
4. **独立 Diff 计算**:每个 `EditRecord` 计算自己的 `diffLines`,不再与之前的编辑合并
---
## 十、构建与开发
### 10.1 构建配置
```bash
# 构建:esbuild 打包 src/extension.ts → dist/extension.js
npm run build # esbuild --bundle --external:vscode --format=cjs --platform=node --minify
# 监听模式
npm run watch # 同上 + --sourcemap --watch
# 测试
npm run test # vitest run
npm run test:watch # vitest (watch 模式)
# 代码检查
npm run lint # eslint src --ext ts
# 打包
npm run package # vsce package
```
### 10.2 TypeScript 配置
- **Target**: ES2022
- **Module**: CommonJS
- **Strict**: true
- **输出**: `dist/` (含 declaration + sourceMap)
---
## 十一、测试策略
### 11.1 当前测试覆盖
| 测试 | 文件 | 覆盖内容 |
|------|------|---------|
| Diff 引擎 | `src/__tests__/diffEngine.test.ts` | `computeChunks`(新增/删除/修改/空文本)、`applyChunk`(新增块/删除块) |
### 11.2 待扩展测试
- `ChangeSetManager`:EditRecord 独立记录、多次编辑不合并、Accept 后再次编辑
- `HookHandler`:Edit/Stop 事件处理、autoShowDiffPerEdit 配置分支
- `DiffViewer`:vscode.diff 调用、虚拟文档生成
- **Bug 回归测试**:Accept → 再编辑 → 验证独立 EditRecord 生成
- 集成测试:完整 Edit → Stop → Diff → Accept/Reject 流程
---
## 十二、版本历史
| 版本 | 日期 | 说明 |
|------|------|------|
| **0.1.0** | 2026-06-17 | 项目初始化,Webview Review 面板架构 |
| **0.2.0** | 计划中 | ★ 重构为 VSCode 内置 Diff + 浮动标签 + EditRecord 粒度 + Bug 修复 |
### Git 提交记录
```
a7c992a refactor: 重构为 Webview Review 面板架构
3168e81 feat: 初始化 AI Code Diff Preview 插件项目
```
---
## 十三、待完成事项
### 高优先级(v0.2.0)
- [ ] **Bug 修复**:Accept 后同文件再编辑不触发选项、Diff 合并的问题(§9.1)
- [ ] **EditRecord 粒度重构**:ChangeSetManager 改为 `EditRecord[]` 独立记录模式
- [ ] **VSCode 内置 Diff 集成**:实现 `DiffViewer` 调用 `vscode.diff`
- [ ] **浮动 Accept/Reject 标签**:状态栏或编辑器内固定操作按钮
- [ ] **配置项实现**:`autoShowDiffPerEdit`、`showAllDiffsOnStop`、`floatingLabelMode`
### 中优先级
- [ ] **文件列表 UI**:QuickPick 文件列表 + Accept/Reject 按钮
- [ ] **冲突检测**:用户手动修改与 AI 建议冲突检测
- [ ] **状态栏集成**:显示待处理编辑数量
- [ ] 完善单元测试 + Bug 回归测试
### 低优先级
- [ ] Git 集成(生成 Commit 建议)
- [ ] 发布到 VSCode Marketplace
- [ ] CI/CD 配置(GitHub Actions)
---
## 十四、参考资料
- [VSCode Extension API](https://code.visualstudio.com/api)
- [VSCode 内置 Diff 命令](https://code.visualstudio.com/api/references/commands) (`vscode.diff`)
- [VSCode TextEditorDecorationType](https://code.visualstudio.com/api/references/vscode-api#TextEditorDecorationType)
- [Myers Diff Algorithm](https://blog.jcoglan.com/2017/02/12/the-myers-diff-algorithm-part-1/)
- [diff 库文档](https://github.com/kpdecker/jsdiff)
- [Cursor 官方文档](https://cursor.sh/docs)
+14 -7
View File
@@ -7,11 +7,18 @@
1. 安装 `.vsix` 并重启 VSCode
2. 将下面配置复制到项目 `.claude/settings.json`(或 `~/.claude/settings.json` 全局生效)
3. 用 Claude Code 编辑项目 → Stop 后自动弹出 QuickPick 变更列表 → VSCode 内置 Diff 逐文件审查
![AI Diff](assets/屏幕截图%202026-06-19%20134307.png)
触发AI Diff后快捷键`Ctrl+Shift+D`打开`QuickPick`预览,一键 `接受/拒绝` 变更
![QuickPick](assets/屏幕截图%202026-06-19%20134244.png)
进入单个`diff`界面可在状态栏右下角选择单个文件`接受/拒绝`
![Accepet/Reject](assets/屏幕截图%202026-06-19%20134322.png)
## Claude Code Hooks 必须配置
将以下内容写入 `.claude/settings.json`:
![settings.json路径](assets/屏幕截图%202026-06-19%20115339.png)
```json
{
"hooks": {
@@ -44,13 +51,13 @@
## 命令
| 命令 | 快捷键 | 功能 |
|------|--------|------|
| 命令 | 快捷键 | 功能 |
| --------------------------------- | ---------------- | ---------------------------------------- |
| `AI Diff: 显示所有变更文件列表` | `Ctrl+Shift+D` | QuickPick 总览 + Accept All / Reject All |
| `AI Diff: 下一个变更` | `Alt+↓` | 跳转下一个 pending 文件 |
| `AI Diff: 上一个变更` | `Alt+↑` | 跳转上一个 pending 文件 |
| `AI Diff: 接受所有变更` | `Ctrl+Shift+A` | Accept All |
| `AI Diff: 拒绝所有变更` | `Ctrl+Shift+R` | Reject All |
| `AI Diff: 下一个变更` | `Alt+↓` | 跳转下一个 pending 文件 |
| `AI Diff: 上一个变更` | `Alt+↑` | 跳转上一个 pending 文件 |
| `AI Diff: 接受所有变更` | `Ctrl+Shift+A` | Accept All |
| `AI Diff: 拒绝所有变更` | `Ctrl+Shift+R` | Reject All |
Diff 编辑器打开后,状态栏右侧自动出现 **Accept / Reject / 接受全部** 按钮。
@@ -65,4 +72,4 @@ npm run package # 生成 ai-diff-preview-*.vsix
## 许可证
MIT © 2026 [Gmaker689](https://github.com/Gmaker689)
MIT © 2026 [Gmaker689](https://github.com/Gmaker689)
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

+286 -124
View File
@@ -1,40 +1,45 @@
# AI Code Diff Preview — 项目文档
# AI Code Diff Preview — 架构文档
> **版本 0.2.0** | VSCode 插件,为 Claude Code 提供类 Cursor 的 AI 代码变更审查体验
## 一、功能概述
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(前次未处理时);已处理后重新编辑则独立追踪
- **QuickPick 总览**:Stop 后弹出文件列表,含 Accept All / Reject All
- **内置 Diff**:点击文件打开 VSCode 原生 Diff 编辑器,状态栏显示 Accept/Reject 按钮
- **粒度控制**:逐编辑 / 逐文件 / 全局 Accept/Reject
- **编辑合并**:同文件连续编辑(前次未处理时)自动合并为一个 FileChange;已处理后重新编辑则独立追踪
- **自动推进**:Accept/Reject 后自动打开下一个 pending 文件
---
## 二、项目结构
```
src/
├── extension.ts # 插件入口,初始化模块 + 注册命令
├── extension.ts # 插件入口: 初始化 + 命令注册 + 文件监听
├── trigger/
│ └── hookHandler.ts # Hook 事件处理 (Edit/Stop 分发)
│ └── hookHandler.ts # Hook 事件处理 (Edit → 收集, Stop → QuickPick)
├── snapshot/
│ └── snapshotManager.ts # ★ 核心数据层: ChangeSetManager
│ └── snapshotManager.ts # ★ 核心数据层: ChangeSetManager
├── diff/
│ └── diffEngine.ts # Diff 算法 (Myers)
│ └── diffEngine.ts # Diff 算法 (遗留模块)
├── render/
│ ├── reviewPanel.ts # ★ 总览 Webview 面板
│ ├── diffViewer.ts # ★ VSCode 内置 Diff 查看器 + accept/reject + 自动推进
│ ├── statusBar.ts # ★ 状态栏 (Diff 模式按钮 / 摘要模式)
│ ├── inlineDecorator.ts # 内联装饰器 (保留, 未启用)
│ └── codeLensProvider.ts # CodeLens (保留, 未启用)
│ ├── diffViewer.ts # ★ VSCode 内置 Diff + Accept/Reject + 自动推进
│ ├── statusBar.ts # ★ 状态栏 (常驻摘要 + Diff 模式 Accept/Reject)
│ ├── reviewPanel.ts # Webview 总览面板 (备用, 非主流程)
│ ├── codeLensProvider.ts # CodeLens (编辑器行内 Accept/Reject)
│ └── inlineDecorator.ts # 内联装饰器 (红绿高亮)
├── transaction/
│ ├── acceptHandler.ts # Accept 处理器 (legacy)
│ └── rejectHandler.ts # Reject 处理器 (legacy)
│ ├── acceptHandler.ts # Accept 处理器 (legacy)
│ └── rejectHandler.ts # Reject 处理器 (legacy)
└── models/
├── types.ts # 类型定义
└── constants.ts # 常量
├── types.ts # 类型定义
└── constants.ts # 常量 (命令 ID, 配置键, 颜色)
```
---
## 三、核心数据模型
### 层级关系
@@ -42,14 +47,16 @@ src/
```
ChangeSet (一轮对话)
└── FileChange[] (每个文件一个)
├── originalContent (首次编辑前的文件快照)
├── latestContent (最新文件内容)
├── diffLines[] (originalContent → latestContent 的聚合 Diff)
├── status (pending | accepted | rejected)
└── EditRecord[] (每次 Edit/Write 一条)
├── originalContent (首次编辑前的文件快照)
├── latestContent (最新文件内容)
├── diffLines[] (originalContent → latestContent 的聚合 Diff)
├── type (create | modify | delete)
├── status (pending | accepted | rejected)
└── EditRecord[] (每次 Edit/Write 一条, 不合并)
├── beforeContent (本次编辑前快照)
├── afterContent (本次编辑后快照)
├── oldString / newString (精确替换内容)
├── oldString (Edit: 被替换文本; Write: 空)
├── newString (Edit: 替换后文本; Write: 完整内容)
├── diffLines[] (本次编辑的独立 Diff)
└── status (pending | accepted | rejected)
```
@@ -64,16 +71,27 @@ ChangeSet (一轮对话)
└──→ 创建新 FileChange, 独立追踪
```
**关键**: 查找 FileChange 时**优先匹配 pending 的**(而非用 `.find()` 取第一个),避免同文件多轮编辑时孤儿 FileChange 的 Bug。
**关键**: 查找 FileChange 时**优先匹配 pending 的**(`changes.find(c => c.status === 'pending')`),避免同文件多轮编辑时孤儿 FileChange 的 Bug。
### 原始内容捕获
`captureOriginalContent()` 反推机制:
- **Edit (首次)**: 从当前文件中找到 `newString`,替换回 `oldString` → 编辑前内容
- **Edit (非首次)**: 使用 `pendingChange.latestContent` 作为 `beforeContent`
- **Write**: `originalContent = ''`,类型为 `create`
- **缓存**: `originalContents` Map 避免重复捕获
---
## 四、完整流程
```mermaid
flowchart TD
A["Claude 对话中"] --> B["Edit / Write 工具调用"]
B --> C["写入 .claude/hooks/pending.json"]
B --> C["PostToolUse Hook<br/>写入 .claude/hooks/pending.json"]
C --> D["FileSystemWatcher 监听到"]
D --> E["读取并删除 pending.json"]
D --> E["读取并删除 pending.json<br/>10s 时效校验"]
E --> F{"toolName?"}
F -->|"Edit / Write"| G["hookHandler.handleEdit()"]
@@ -81,161 +99,305 @@ flowchart TD
H --> I{"同文件已有<br/>pending FileChange?"}
I -->|"YES"| J["★ 合并: 追加 EditRecord<br/>更新聚合 Diff"]
I -->|"NO"| K["新建 FileChange<br/>捕获 originalContent"]
J --> L{"autoShowDiffPerEdit<br/>配置?"}
J --> L{"autoShowDiffPerEdit?"}
K --> L
L -->|"true"| M["弹出 VSCode 内置 Diff"]
L -->|"false"| N["静默收集, 不弹 UI"]
L -->|"true"| M["diffViewer.showEditDiff()<br/>弹出 VSCode 内置 Diff"]
L -->|"false"| N["静默收集"]
F -->|"Stop"| O["hookHandler.handleStop()"]
O --> P["ChangeSetManager.markReady()"]
P --> Q{"有 pending 变更?"}
Q -->|"NO"| R["无操作"]
Q -->|"YES"| S["★ 弹出总览 Webview 面板<br/>reviewPanel.show()"]
Q -->|"YES"| S{"showAllDiffsOnStop?"}
S -->|"true"| T["★ 弹出 QuickPick 文件列表<br/>hookHandler.showFileListPicker()"]
S -->|"false"| U["状态栏通知<br/>'N 个文件有变更'"]
S --> T["总览面板: 每个文件一张卡片"]
T --> U["卡片内含:<br/>— 文件信息 + 增减统计<br/>— 双栏 Diff (原始 vs 最新)<br/>— ✔ 接受 / ✘ 拒绝 按钮"]
T --> V["顶部: Accept All / Reject All"]
T --> V["QuickPick 列表:<br/>📄 file1.ts +3 -2 · 2编辑<br/>📄 file2.ts +10 -0 · 1编辑<br/>──────────────<br/>✅ Accept All<br/>❌ Reject All"]
V --> W["点击文件"]
V --> X["点击 Accept All"]
V --> Y["点击 Reject All"]
U --> W["用户点击 ✔ 接受"]
U --> X["用户点击 ✘ 拒绝"]
V --> Y["用户点击 Accept All"]
V --> Z["用户点击 Reject All"]
W --> Z["diffViewer.showFileDiff()<br/>打开 VSCode 内置 Diff"]
Z --> AA["状态栏右侧:<br/>$(check) Accept | $(close) Reject"]
AA --> AB["用户操作 Accept/Reject"]
AB --> AC["★ 自动推进 advanceToNext()"]
AC --> AD["全部处理完 → 🎉 通知"]
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["全部处理完 → 🎉 通知"]
X --> AE["changeSetManager.acceptAll()"]
Y --> AF["changeSetManager.rejectAll()"]
```
## 五、三种 UI 层
---
### 5.1 Webview 总览面板 (`reviewPanel.ts`)
## 五、UI 层
触发时机:Claude Stop 后自动弹出 / 手动 `Ctrl+Shift+D`
### 5.1 状态栏 (`statusBar.ts`)
常驻显示,双区域布局:
```
┌─ 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 │ │
│ └──────────────────────────────────────────┘ │
└─────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ [$(diff) AI Diff: 2文件 5编辑] [✓ Accept] [✗ Reject]│
│ (左侧常驻摘要, 点击打开 QuickPick) (右侧, 仅 Diff 模式) │
└─────────────────────────────────────────────────────────────┘
```
### 5.2 VSCode 内置 Diff 编辑器 (`diffViewer.ts`)
| 模式 | 条件 | 左侧 | 右侧 |
| ---- | ---- | ---- | ---- |
| 空闲 | 无变更集 | `$(diff) AI Diff` (暗色) | 隐藏 |
| 有 pending | 有待处理变更 | `$(diff) AI Diff: N文件 M编辑` (黄色) | 隐藏 |
| Diff 活跃 | 内置 Diff 打开 | 同上 | `Accept` (黄底) + `Reject` (红底) |
| 已完成 | 全部处理完 | `$(diff) AI Diff: 已完成` | 隐藏 |
触发时机:从总览面板点击文件 / `Alt+↑/↓` 导航
### 5.2 VSCode 内置 Diff (`diffViewer.ts`)
- 使用 `vscode.diff` 命令打开原生 Diff 编辑器
- 左侧:原始内容 (红色删除行),右侧:变更后内容 (绿色新增行)
- 临时文件机制:写入 `%TEMP%/ai-diff-preview/`,关闭 Diff 后自动删除(**无保存提示**)
- **自动推进**:Accept/Reject 后自动打开下一个 pending 文件
- 若当前文件仍有 pending 编辑 → 重新打开此文件
- 否则 → 打开下一个 pending 文件
- 全部处理完 → 弹出 🎉 通知
触发时机:QuickPick 点击文件 / `Alt+↑↓` 导航 / `autoShowDiffPerEdit` 自动弹出
### 5.3 状态栏 (`statusBar.ts`)
**临时文件方案**:
双模式自动切换:
- 路径:`%TEMP%/ai-diff-preview/{uuid}-{before|after}-{id}.{ext}`
- 创建:`showEditDiff()` / `showFileDiff()` 时写入
- 删除:Accept/Reject 时 `deleteSessionFiles()` 主动清理
- 批量清理:`deactivate()` 时 `cleanupTmpFiles()`
| 模式 | 触发 | 显示 |
|------|------|------|
| 摘要模式 | 无活跃 Diff | `$(diff) AI Diff: 2文件 5编辑` ← 点击打开总览 |
| Diff 模式 | 内置 Diff 活跃 | `$(check) Accept` `$(close) Reject` `$(check-all) 接受全部` |
**Session 管理**:
```typescript
interface DiffSession {
fileId: string; // FileChange.id
editId?: string; // EditRecord.id (单次编辑模式)
beforeTmpPath: string;
afterTmpPath: string;
beforeUri: vscode.Uri;
afterUri: vscode.Uri;
}
```
- **isOpening 锁**:Diff 打开过程中禁止 `onDidChangeVisibleTextEditors` 误清 session
- **关闭检测**:before/after URI 都不可见时自动清除 session
**自动推进** `advanceToNext()`:
1. 当前文件还有 pending 编辑 → 重新打开该文件 Diff
2. 否则 `findNextPending()` → 打开下一个文件 Diff
3. 无更多 pending → "🎉 所有变更已处理完毕!"
### 5.3 QuickPick 文件列表 (`hookHandler.showFileListPicker()`)
Stop 后的主 UI(当 `showAllDiffsOnStop = true`):
```
┌──────────────────────────────────────────────────┐
│ AI Diff 总览 — 2 文件 3 编辑 (+13 -2) │
│ │
│ $(edit) file1.ts +3 -2 · 2 编辑 │
│ $(new-file) file2.ts +10 -0 · 1 编辑 │
│ ────────────────────────────────────── │
│ $(check-all) ✅ Accept All — 接受所有变更 │
│ $(trash) ❌ Reject All — 拒绝所有变更 │
└──────────────────────────────────────────────────┘
```
- 点击文件 → `diffViewer.showFileDiff()` 打开内置 Diff
- 点击 Accept All / Reject All → 批量操作
### 5.4 Webview 总览面板 (`reviewPanel.ts`)
**备用方案**,非主流程。提供卡片式双栏 Diff 展示,功能与 QuickPick 重叠。
### 5.5 CodeLens (`codeLensProvider.ts`)
编辑器行内显示 Accept/Reject 按钮(当 `floatingLabelMode` 包含 inline 时):
- 文件标题行:`$(diff) AI Diff · N 次编辑` | `$(check-all) 接受全部` | `$(trash) 拒绝全部`
- 每个 EditRecord 变更区域:`AI 编辑 #xxxx · +3 -2` | `Accept` | `Reject`
### 5.6 内联装饰器 (`inlineDecorator.ts`)
在编辑器中渲染红绿高亮:
- 绿色背景 + gutter 圆点:新增行
- 红色背景 + 删除线:删除行
- 行末 `← AI 变更` 标签
---
## 六、Accept/Reject 行为
| 操作 | Modify 文件 | Create 文件 |
|------|------------|------------|
| Accept (文件级) | 写入 `latestContent` 到磁盘 | 保留文件 |
| Reject (文件级) | 恢复 `originalContent` 到磁盘 | 删除文件 |
| Accept All | 遍历所有 pending 文件执行 Accept | |
| Reject All | 遍历所有 pending 文件执行 Reject | |
| 操作 | Modify 文件 | Create 文件 | Delete 文件 |
| ---- | ---------- | ---------- | ---------- |
| Accept (文件级) | 写入 `latestContent` | 保留文件 | 保留删除状态 |
| Reject (文件级) | 恢复 `originalContent` | 删除文件 | 恢复原文件 |
| Accept All | 遍历所有 pending 文件执行 Accept | | |
| Reject All | 遍历所有 pending 文件执行 Reject | | |
## 七、配置项
### 编辑级 Accept/Reject
| 操作 | 行为 |
| ---- | ---- |
| Accept Edit | 标记该 EditRecord 为 accepted;若文件所有 edit 都 accepted → 写入 `latestContent` |
| Reject Edit | 从当前文件内容中还原 `newString → oldString`;标记为 rejected |
---
## 七、触发机制
### 文件监听
```
.claude/hooks/pending.json
│
▼ (create / change 事件)
extension.ts:
1. FileSystemWatcher 监听 pending.json
2. 读取 → 删除 (防重复处理)
3. JSON.parse → ClaudeHookEvent
4. 时间戳校验: Date.now() - timestamp > 10000 → 丢弃
5. 分发: Edit/Write → handleEdit(), Stop → handleStop()
```
### 启动恢复
`checkPendingHookFile()`: activate 时检查遗留 pending 文件,15 秒内有效。
### Claude Code Hooks 配置
```json
{
"hooks": {
"PostToolUse": [{
"matcher": "Write|Edit",
"hooks": [{ "type": "command", "command": "node -e \"...(写入 pending.json)...\"", "timeout": 5 }]
}],
"Stop": [{
"hooks": [{ "type": "command", "command": "node -e \"...(写入 type=stop 的 pending.json)...\"", "timeout": 5 }]
}]
}
}
```
---
## 八、配置项
| 配置 | 类型 | 默认 | 说明 |
|------|------|------|------|
| ---- | ---- | ---- | ---- |
| `enableAutoTrigger` | boolean | true | 监听 pending.json |
| `autoShowDiffPerEdit` | boolean | false | 每次 Edit 后弹内置 Diff |
| `showAllDiffsOnStop` | boolean | true | Stop 后弹总览面板 |
| `floatingLabelMode` | string | "statusBar" | 按钮位置 |
| `maxFileSize` | number | 100000 | 文件大小上限 |
| `showAllDiffsOnStop` | boolean | true | Stop 后弹 QuickPick 文件列表 |
| `floatingLabelMode` | string | "statusBar" | inline / statusBar / both / none |
| `maxFileSize` | number | 100000 | 文件大小上限 (字符) |
## 八、快捷键
### 配置组合
| autoShowDiffPerEdit | showAllDiffsOnStop | 行为 |
| :---: | :---: | ------ |
| false | true | **默认**: 编辑时静默收集,Stop 后弹 QuickPick 统一审查 |
| true | false | 每次编辑弹 Diff,Stop 后无额外操作 |
| true | true | 每次编辑弹 Diff + Stop 后再弹 QuickPick |
| false | false | 仅状态栏显示,用户手动触发 |
---
## 九、快捷键
| 快捷键 | 条件 | 功能 |
|--------|------|------|
| `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 |
| ------ | ---- | ---- |
| `Tab` | `isActive && editorTextFocus` | Accept 当前编辑 |
| `Esc` | `isActive && editorTextFocus` | Reject 所有变更 |
| `Ctrl+Shift+A` | `isActive` | Accept All |
| `Ctrl+Shift+R` | `isActive` | Reject All |
| `Alt+↓` | `isActive` | 下一个变更文件 |
| `Alt+↑` | `isActive` | 上一个变更文件 |
| `Ctrl+Shift+D` | `isActive` | 打开 QuickPick 文件列表 |
| `Ctrl+Shift+F` | `isActive` | 查看当前文件 Diff |
## 九、模块依赖关系
---
## 十、模块依赖关系
```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/>状态栏"]
review["reviewPanel.ts<br/>Webview (备用)"]
lens["codeLensProvider.ts<br/>CodeLens"]
deco["inlineDecorator.ts<br/>装饰器"]
types["models/types.ts<br/>类型"]
extension --> hook
extension --> manager
extension --> review
extension --> viewer
extension --> bar
extension --> review
extension --> lens
extension --> deco
hook --> manager
hook --> viewer
review --> manager
review --> viewer
viewer --> manager
bar --> manager
bar --> viewer
review --> manager
review --> viewer
lens --> manager
manager --> types
```
## 十、已修复的 Bug
---
## 十一、已修复的 Bug
| Bug | 根因 | 修复 |
|-----|------|------|
| Accept 后同文件再编辑无 Accept/Reject | `recordEdit()` 更新同一 `FileChange`,Accept 后状态机断开 | 引入 `EditRecord` 粒度,Accept/Reject 后重新编辑创建新 FileChange |
| Accept All / Reject All 不生效 | `acceptAllFromDiff()` 调用不存在的方法 | 改为调用 `acceptFile()`,All 方法改为收集列表后逐一处理 |
| --- | ---- | ---- |
| Accept 后同文件再编辑无 Accept/Reject | `recordEdit()` 更新同一 FileChange,Accept 后状态机断开 | 引入 EditRecord 粒度,Accept 后再编辑创建新 FileChange |
| Accept All / Reject All 不生效 | `acceptAllFromDiff()` 调用不存在的方法 | 改为调用 `acceptFile()`,收集列表后逐一处理 |
| Accept 后无后续 Diff | 无推进逻辑 | `advanceToNext()` 自动打开下一个 pending 文件 |
| 临时文件保存提示 | 使用 `openTextDocument({content})` 创建 untitled 文档 | 改为写入临时目录 `%TEMP%/ai-diff-preview/` |
| 同文件多轮编辑孤儿 FileChange | `.find()` 返回第一个 (已 accept) 而非 pending | 改为优先匹配 pending 的 FileChange |
| 临时文件保存提示 | 使用 `openTextDocument({content})` 创建 untitled 文档 | 改为写入 `%TEMP%/ai-diff-preview/` |
| 同文件多轮编辑孤儿 FileChange | `.find()` 返回第一个 (已 accept) 而非 pending | 优先匹配 `status === 'pending'` 的 FileChange |
| Diff 打开时 session 被误清 | `onDidChangeVisibleTextEditors` 在 Diff 加载中触发 | `isOpening` 原子锁保护 |
---
## 十二、命令注册
| 命令 ID | 功能 |
| ------- | ---- |
| `aiDiffPreview.showDiffPanel` | QuickPick 文件列表总览 |
| `aiDiffPreview.showCurrentFileDiff` | 查看当前文件 Diff |
| `aiDiffPreview.acceptCurrentEdit` | 接受当前编辑 |
| `aiDiffPreview.rejectCurrentEdit` | 拒绝当前编辑 |
| `aiDiffPreview.acceptAll` | 接受所有变更 |
| `aiDiffPreview.rejectAll` | 拒绝所有变更 |
| `aiDiffPreview.nextDiff` | 下一个变更 |
| `aiDiffPreview.prevDiff` | 上一个变更 |
| `aiDiffPreview.acceptCurrentDiff` | Diff 内接受 (状态栏) |
| `aiDiffPreview.rejectCurrentDiff` | Diff 内拒绝 (状态栏) |
| `aiDiffPreview.acceptAllFromDiff` | Diff 内接受整个文件 (状态栏) |
| `aiDiffPreview.acceptChunk` | 旧版兼容 → acceptCurrentDiff |
| `aiDiffPreview.rejectChunk` | 旧版兼容 → rejectCurrentDiff |
---
## 十三、构建
```bash
npm install # 安装依赖
npm run build # esbuild → dist/extension.js (CJS, minify)
npm run watch # 监听模式 (sourcemap)
npm run test # vitest run
npm run test:watch # vitest watch
npm run lint # eslint
npm run package # vsce package → .vsix
```
- **esbuild**: `src/extension.ts → dist/extension.js`,external: vscode,CJS 格式
- **TypeScript**: ES2022,CommonJS,strict mode