# 修复前 Diff 预览确认功能设计书 > 日期:2026-08-19 > 状态:已批准(Stage ④ 通过);2026-08-19 修订确认交互为「面板内两步确认」 > 流程:① 用户提出 → ② 需求澄清 → ③ 方案设计 → ④ 人类审批 ## 1. 背景与目标 当前自动修复(`src/fix/`)在用户点击「修复 / AI 修复 / 全部修复」后**立即写盘**,用户无法在写入前确认改动内容,误修后只能依赖撤销功能回退。 本次功能目标:在面板触发的修复**应用前**,用 VSCode 内置 diff 编辑器展示「修改前 → 修改后」的代码对比,用户确认后才真正写入。 ## 2. 需求澄清结论(Stage ② 共识) | 决策项 | 结论 | |--------|------| | 触发时机 | 修复**前**预览确认,确认后才写入 | | 展示方式 | VSCode 内置 diff 编辑器(`vscode.diff`,左旧右新) | | 预览范围 | 单个修复 + 全部修复都走预览 | | 全部修复 | 合并成一个 diff,确认后一次性应用 | | AI 修复 | 原生 fix 与 AI fix 两条路径都预览 | | 入口范围 | **仅面板按钮**预览;hover 快速修复保持直接应用(零行为变化) | | 确认交互 | **面板内两步确认**:点「修复」→ 打开 diff + 面板按钮变「应用/取消」(常驻不消失);回面板点「应用」才写入。「全部修复」同理(「全部应用/取消」) | > 修订说明:初版设计为 diff + `showInformationMessage` 确认对话框,实测发现非模态通知几秒后自动消失(返回值 undefined 被当取消并关闭 diff)。改为面板内两步确认——按钮翻转用 webview 轻量消息完成,不全量重建面板,滚动/标签页不丢失。 ## 3. 现状关键点(已核实) - `fixEngine.ts:123-129` 与 `aiFixEngine.ts:164-170`:多轮循环在**内存字符串**上计算最终 `currentText`,最后一步才用整文档 `WorkspaceEdit` 写入。 - 计算与写入天然可分,只需让引擎支持"只算不写"(`dryRun`)即可在写入前弹 diff。 - 引擎内部已用 `mockDocument` 对内存文本执行 `adapter.check`,支持对累积文本反复计算,无需改核心算法。 ## 4. 架构概览 ``` 面板 fix/fixAll 消息 → codeReviewer.fixIssue / codeReviewer.fixAll │ (origin === 'panel') ▼ FixEngine/AiFixEngine (dryRun=true) → 返回最终 newText(不写盘) │ ▼ fixPreview.openPreviewDiff() → vscode.diff 内置 diff 编辑器(左原文本 / 右新文本) │ ▼ FixPendingStore.setSingle/setBatch + ReviewPanel.postMessage('pending'/'batchPending') │ 面板条目按钮翻转:「修复」→「应用 / 取消」 ▼ codeReviewer.applyFixPreview / applyAllPreview ← 面板「应用」按钮 │ ▼ fixPreview.applyNewText() → 整文档 WorkspaceEdit 一次性写入 + recordFixes + save + refresh codeReviewer.cancelFixPreview / cancelAllPreview → 关闭 diff + 还原按钮(不写入) ``` **数据流**: 1. `fixIssue`(panel)/ `fixAll` 以 `dryRun=true` 调用修复引擎,拿到最终 `newText`,**不写盘**。 2. `openPreviewDiff` 用 `TextDocumentContentProvider`(scheme `codeReviewerPreview`)提供新文本,`vscode.diff` 打开内置 diff 编辑器。 3. 结果存入 `FixPendingStore`,向面板 `postMessage` 翻转按钮(`pending` 单条 / `batchPending` 全部)——「修复」隐藏、「应用/取消」显示,**常驻不消失**。 4. 用户回面板点「应用」→ `applyFixPreview`/`applyAllPreview` 整文档 `WorkspaceEdit` 写入、记录撤销会话、保存、刷新;点「取消」→ `cancelFixPreview`/`cancelAllPreview` 关闭 diff、还原按钮、不写入。 ## 5. 文件变更清单 ### 新建 | 文件 | 职责 | |------|------| | `src/fix/fixPreview.ts` | diff 内容提供者、`openPreviewDiff`、`closePreviewEditor`、`applyNewText` | | `src/fix/fixPending.ts` | 待确认修复存储(PendingFix / PendingBatch / FixPendingStore) | | `docs/superpowers/specs/2026-08-19-fix-preview-design.md` | 本设计书 | ### 修改 | 文件 | 变更 | |------|------| | `src/fix/fixEngine.ts` | `FixResult` 增 `newText?: string`;`fixDiagnostic` 增 `dryRun` 参数,为真时跳过写盘 | | `src/fix/aiFixEngine.ts` | `aiFixDiagnostic` 增 `dryRun` 参数,为真时跳过写盘并返回 `newText` | | `src/activation/commands.ts` | `fixIssue` panel 路径两步确认;`fixAll` 内存累积 + 合并 diff + 存 pending;新增 `applyFixPreview`/`cancelFixPreview`/`applyAllPreview`/`cancelAllPreview` 命令 | | `src/panel/webview.ts` | 消息类型增 `applyFix`/`cancelFix`/`applyAll`/`cancelAll`;条目与顶部渲染隐藏的应用/取消按钮;`postMessage` 公开方法 | | `src/views/reviewPanel.js` | `window.addEventListener('message')` 翻转按钮显隐 | | `src/extension.ts` | 创建/传递 `FixPendingStore`;文档关闭时清 pending | | `src/i18n/messages.ts` | 新增 `report.fixAllApply`;复用 `fix.apply` / `fix.cancel` / `fix.previewTitle` | ## 6. 关键接口定义 ### 6.1 `FixResult`(fixEngine.ts) ```typescript export interface FixResult { success: boolean; attempts: number; message?: string; appliedFixes: AppliedFix[]; newText?: string; // 新增:最终整文档文本(dryRun 时返回) } ``` ### 6.2 引擎 dryRun ```typescript export async function fixDiagnostic( document: vscode.TextDocument, workingDir: string, adapter: LinterAdapter, diag: LinterDiagnostic, maxIterations: number, dryRun?: boolean ): Promise; export async function aiFixDiagnostic( document, workingDir, adapter, diag, maxIterations, provider, options, dryRun?: boolean ): Promise; ``` - `dryRun=true`:跳过末尾 `WorkspaceEdit` 与 `applyEdit`,返回 `newText: currentText`。 - `dryRun=false`(默认):保持现有写盘行为,**不返回** `newText`(或一并返回,调用方忽略)。 - hover 入口仍走默认路径,行为零变化。 ### 6.3 `src/fix/fixPreview.ts` ```typescript export class FixPreviewContentProvider implements vscode.TextDocumentContentProvider { provideTextDocumentContent(uri: vscode.Uri): string; set(uri: vscode.Uri, text: string): void; clear(uri: vscode.Uri): void; } export interface PreviewRequest { originalText: string; newText: string; title: string; fileName: string; } export async function openPreviewDiff(req: PreviewRequest): Promise; // 打开内置 diff 编辑器,返回右侧 newUri(供后续关闭) export async function closePreviewEditor(uri: vscode.Uri): Promise; // 关闭对应 diff 编辑器标签并清理 provider 内容 export async function applyNewText(document: vscode.TextDocument, newText: string): Promise; // 整文档 WorkspaceEdit 一次性写入(recordFixes 由调用方完成) ``` ### 6.4 `src/fix/fixPending.ts` ```typescript export interface PendingFix { key: string; // `${ruleId}@${line}` ruleId: string; line: number; filePath: string; originalText: string; newText: string; appliedFixes: AppliedFix[]; diffUri?: vscode.Uri; } export interface PendingBatch { filePath: string; originalText: string; newText: string; results: { ruleId: string; line: number; appliedFixes: AppliedFix[] }[]; diffUri?: vscode.Uri; } export class FixPendingStore { setSingle(fix: PendingFix): void; getSingle(filePath: string, key: string): PendingFix | undefined; deleteSingle(filePath: string, key: string): void; setBatch(batch: PendingBatch): void; getBatch(filePath: string): PendingBatch | undefined; deleteBatch(filePath: string): void; clear(filePath: string): void; } ``` ### 6.5 面板消息协议 - 新增消息:`applyFix`/`cancelFix`(带 line/ruleId)→ `codeReviewer.applyFixPreview`/`cancelFixPreview`;`applyAll`/`cancelAll` → `applyAllPreview`/`cancelAllPreview`。 - 扩展 → 面板:`{ type: 'pending', key, on: boolean }`(翻转单条按钮)、`{ type: 'batchPending', on: boolean }`(翻转顶部按钮)。 ## 7. 核心逻辑 ### 7.1 单个修复(fixIssue,panel origin) ``` document = resolveFixDocument(...) diag = 命中诊断 result = resolveFix(..., dryRun=true) // 计算,不写盘 if !result.success → 提示失败,return key = `${ruleId}@${line}` existing = pendingStore.getSingle(filePath, key) // 重复预览先关旧 diff diffUri = openPreviewDiff({ originalText, newText, title, fileName }) pendingStore.setSingle({ key, ruleId, line, filePath, originalText, newText, appliedFixes, diffUri }) ReviewPanel.postMessage({ type:'pending', key, on:true }) // 面板按钮 → 「应用 / 取消」 return // 不写盘 ``` 用户回面板: - 点「应用」→ `applyFixPreview`:`applyNewText(document, newText)` → `recordFixes` → 关 diff → 删 pending → `save` → `refreshAfterFix` → 提示成功。 - 点「取消」→ `cancelFixPreview`:关 diff → 删 pending → postMessage 还原按钮。 ### 7.2 全部修复(fixAll) ``` fixables = 可修复诊断列表(含 AI) let currentText = document.getText() results: { ruleId, line, appliedFixes }[] = [] for each fixable: fresh = 对 currentText 构造 mock doc 重跑 check 得 freshDiag result = fixDiagnostic / aiFixDiagnostic(dryRun=true) if result.success: results.push(...); currentText = result.newText! diffUri = openPreviewDiff({ originalText, newText: currentText, title, fileName }) pendingStore.setBatch({ filePath, originalText, newText, results, diffUri }) ReviewPanel.postMessage({ type:'batchPending', on:true }) // 顶部按钮 → 「全部应用 / 取消」 return // 不写盘 ``` - 「全部应用」→ `applyAllPreview`:`applyNewText(document, currentText)` → 逐条 `recordFixes` → 关 diff → 删 batch → `save` → `refreshAfterFix` → 提示批量完成。 - 「取消全部」→ `cancelAllPreview`:关 diff → 删 batch → 还原按钮。 > 说明:多轮引擎已在内部对**传入文本**反复 `adapter.check`,因此 fixAll 只需把上一个 fix 的 `newText` 作为下一个的输入(用 mockDocument 包装为 TextDocument 再传引擎),即可得到合并后的最终文本;位置漂移由引擎内 `findClosestFixable` 按行号就近匹配处理。 ### 7.3 diff 提供者 - 注册 `vscode.workspace.registerTextDocumentContentProvider('codeReviewerPreview', provider)`(extension.ts 调 `registerFixPreviewProvider` 一次)。 - 新文本 URI:`codeReviewerPreview://new/-`,内容即 `newText`;原文本 URI 同样走 provider(避免脏文档时左屏显示磁盘旧内容与引擎计算不一致)。 ### 7.4 面板按钮翻转(reviewPanel.js) ```javascript window.addEventListener('message', e => { const msg = e.data; if (msg.type === 'pending') { // 按 data-fix-key 找到「修复 / 应用 / 取消」三个按钮,按 msg.on 显隐 } else if (msg.type === 'batchPending') { // 按 data-fix-all-btn 找到「全部修复 / 全部应用 / 取消」三个按钮,按 msg.on 显隐 } }); ``` - 应用/取消按钮在 buildHtml 时默认 `display:none`,收到消息后显隐切换,**不全量重建面板**,滚动位置与标签页保持。 ### 7.5 hover 入口保持不变 - `codeActionProvider.ts` 触发的 `fixIssue` 传 `origin: 'hover'`,命令内直接 `dryRun=false` 执行,不弹 diff、不弹确认框。 ## 8. i18n 新增 key | key | zh-CN | en | ja | |-----|-------|----|----| | `fix.previewTitle` | 修复预览 | Fix preview | 修正プレビュー | | `fix.apply` | 应用 | Apply | 適用 | | `fix.cancel` | 取消 | Cancel | キャンセル | | `report.fixAllApply` | 全部应用 | Apply All | すべて適用 | ## 9. 配置变更 无新增配置。 ## 10. 测试计划 - 现有 `npm test`(106 tests)回归通过。 - 引擎 dryRun 路径回归:原有 fixEngine/aiFixEngine 测试走默认(写盘)路径不受影响。 ## 11. 验证顺序 `lint → compile → test`(`npm run lint` / `npm run compile` / `npm test`)