Files
ai-diff-preview/docs/ARCHITECTURE.md
T
Gmarker689 dfa3a585c5 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回归)
2026-06-18 19:37:21 +08:00

10 KiB
Raw Blame History

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