From bcc3ddba8df749b556cdfe0dbbfe16af020f76df Mon Sep 17 00:00:00 2001 From: Gmaker689 <1711322114@qq.com> Date: Fri, 19 Jun 2026 14:04:25 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E6=B7=BB=E5=8A=A0=E6=9B=B4=E8=AF=A6?= =?UTF-8?q?=E7=BB=86=E7=9A=84=E4=BD=BF=E7=94=A8=E8=AF=B4=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- PROJECT_INIT.md | 627 -------------------------- README.md | 21 +- assets/屏幕截图 2026-06-19 115339.png | Bin 0 -> 2959 bytes assets/屏幕截图 2026-06-19 134244.png | Bin 0 -> 19942 bytes assets/屏幕截图 2026-06-19 134307.png | Bin 0 -> 7613 bytes assets/屏幕截图 2026-06-19 134322.png | Bin 0 -> 3815 bytes docs/ARCHITECTURE.md | 410 ++++++++++++----- 7 files changed, 300 insertions(+), 758 deletions(-) delete mode 100644 PROJECT_INIT.md create mode 100644 assets/屏幕截图 2026-06-19 115339.png create mode 100644 assets/屏幕截图 2026-06-19 134244.png create mode 100644 assets/屏幕截图 2026-06-19 134307.png create mode 100644 assets/屏幕截图 2026-06-19 134322.png diff --git a/PROJECT_INIT.md b/PROJECT_INIT.md deleted file mode 100644 index 43a2ab9..0000000 --- a/PROJECT_INIT.md +++ /dev/null @@ -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 { - // 创建临时虚拟文档(不写入磁盘) - 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 { - 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) diff --git a/README.md b/README.md index 1554cd8..87ce845 100644 --- a/README.md +++ b/README.md @@ -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) \ No newline at end of file +MIT © 2026 [Gmaker689](https://github.com/Gmaker689) diff --git a/assets/屏幕截图 2026-06-19 115339.png b/assets/屏幕截图 2026-06-19 115339.png new file mode 100644 index 0000000000000000000000000000000000000000..a5e731229406f39857b8e0c015738860ad21fc42 GIT binary patch literal 2959 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 s=P=us& literal 0 HcmV?d00001 diff --git a/assets/屏幕截图 2026-06-19 134307.png b/assets/屏幕截图 2026-06-19 134307.png new file mode 100644 index 0000000000000000000000000000000000000000..81bb0813cc21ff08d051f34581916012dd05f5c8 GIT binary patch literal 7613 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 literal 0 HcmV?d00001 diff --git a/assets/屏幕截图 2026-06-19 134322.png b/assets/屏幕截图 2026-06-19 134322.png new file mode 100644 index 0000000000000000000000000000000000000000..013bdb6192e378fe915e5bccfc8ca9c2d13892ef GIT binary patch literal 3815 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 literal 0 HcmV?d00001 diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 2809499..b2fd00d 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -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
写入 .claude/hooks/pending.json"] C --> D["FileSystemWatcher 监听到"] - D --> E["读取并删除 pending.json"] + D --> E["读取并删除 pending.json
10s 时效校验"] E --> F{"toolName?"} F -->|"Edit / Write"| G["hookHandler.handleEdit()"] @@ -81,161 +99,305 @@ flowchart TD H --> I{"同文件已有
pending FileChange?"} I -->|"YES"| J["★ 合并: 追加 EditRecord
更新聚合 Diff"] I -->|"NO"| K["新建 FileChange
捕获 originalContent"] - J --> L{"autoShowDiffPerEdit
配置?"} + J --> L{"autoShowDiffPerEdit?"} K --> L - L -->|"true"| M["弹出 VSCode 内置 Diff"] - L -->|"false"| N["静默收集, 不弹 UI"] + L -->|"true"| M["diffViewer.showEditDiff()
弹出 VSCode 内置 Diff"] + L -->|"false"| N["静默收集"] F -->|"Stop"| O["hookHandler.handleStop()"] O --> P["ChangeSetManager.markReady()"] P --> Q{"有 pending 变更?"} Q -->|"NO"| R["无操作"] - Q -->|"YES"| S["★ 弹出总览 Webview 面板
reviewPanel.show()"] + Q -->|"YES"| S{"showAllDiffsOnStop?"} + S -->|"true"| T["★ 弹出 QuickPick 文件列表
hookHandler.showFileListPicker()"] + S -->|"false"| U["状态栏通知
'N 个文件有变更'"] - S --> T["总览面板: 每个文件一张卡片"] - T --> U["卡片内含:
— 文件信息 + 增减统计
— 双栏 Diff (原始 vs 最新)
— ✔ 接受 / ✘ 拒绝 按钮"] - T --> V["顶部: Accept All / Reject All"] + T --> V["QuickPick 列表:
📄 file1.ts +3 -2 · 2编辑
📄 file2.ts +10 -0 · 1编辑
──────────────
✅ Accept All
❌ 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()
打开 VSCode 内置 Diff"] + Z --> AA["状态栏右侧:
$(check) Accept | $(close) Reject"] + AA --> AB["用户操作 Accept/Reject"] + AB --> AC["★ 自动推进 advanceToNext()"] + AC --> AD["全部处理完 → 🎉 通知"] - 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["全部处理完 → 🎉 通知"] + 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
入口"] hook["hookHandler.ts
Hook 处理"] manager["snapshotManager.ts
ChangeSetManager
★ 核心数据层"] - review["reviewPanel.ts
总览 Webview"] viewer["diffViewer.ts
内置 Diff + 自动推进"] bar["statusBar.ts
状态栏"] + review["reviewPanel.ts
Webview (备用)"] + lens["codeLensProvider.ts
CodeLens"] + deco["inlineDecorator.ts
装饰器"] types["models/types.ts
类型"] 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