Files
2026Technology-Competition/docs/superpowers/specs/2026-08-19-fix-preview-design.md
T
范智鹏 d16d680f0f feat: AI 修复链路扩展 + 修复预览两步确认 + 自定义规则/AI 审查条目支持 AI 修复
- AI 修复:无原生 fix 的 linter 条目走 AI 修复(aiFixEngine/fixPrompt,AI 重检收敛),自定义规则与 AI 审查条目新增 AI 修复按钮(customFixEngine),自定义规则条目展开显示 AI 建议(suggestion 字段)

- 修复预览:面板触发修复先用内置 diff 预览,面板内「应用/取消」两步确认后才写入(fixPreview/fixPending,单条与分 tab 批量均支持)

- 修复面板交互:linter/custom/ai 分 tab「全部修复」、已修复+撤销、pending 按钮状态同步

- bug 修复:修复按钮失败后卡  不恢复;custom/ai 修复不稳定(空修复重试、AI 重检收敛判定放宽、降级接受最后一次有效修复)

- 移除 AI codeDiff 展示块
2026-08-20 22:26:30 +08:00

12 KiB
Raw Blame History

修复前 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-129aiFixEngine.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. fixIssuepanel/ fixAlldryRun=true 调用修复引擎,拿到最终 newText不写盘
  2. openPreviewDiffTextDocumentContentProviderscheme codeReviewerPreview)提供新文本,vscode.diff 打开内置 diff 编辑器。
  3. 结果存入 FixPendingStore,向面板 postMessage 翻转按钮(pending 单条 / batchPending 全部)——「修复」隐藏、「应用/取消」显示,常驻不消失
  4. 用户回面板点「应用」→ applyFixPreview/applyAllPreview 整文档 WorkspaceEdit 写入、记录撤销会话、保存、刷新;点「取消」→ cancelFixPreview/cancelAllPreview 关闭 diff、还原按钮、不写入。

5. 文件变更清单

新建

文件 职责
src/fix/fixPreview.ts diff 内容提供者、openPreviewDiffclosePreviewEditorapplyNewText
src/fix/fixPending.ts 待确认修复存储(PendingFix / PendingBatch / FixPendingStore
docs/superpowers/specs/2026-08-19-fix-preview-design.md 本设计书

修改

文件 变更
src/fix/fixEngine.ts FixResultnewText?: stringfixDiagnosticdryRun 参数,为真时跳过写盘
src/fix/aiFixEngine.ts aiFixDiagnosticdryRun 参数,为真时跳过写盘并返回 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 FixResultfixEngine.ts

export interface FixResult {
  success: boolean;
  attempts: number;
  message?: string;
  appliedFixes: AppliedFix[];
  newText?: string;      // 新增:最终整文档文本(dryRun 时返回)
}

6.2 引擎 dryRun

export async function fixDiagnostic(
  document: vscode.TextDocument,
  workingDir: string,
  adapter: LinterAdapter,
  diag: LinterDiagnostic,
  maxIterations: number,
  dryRun?: boolean
): Promise<FixResult>;

export async function aiFixDiagnostic(
  document, workingDir, adapter, diag, maxIterations,
  provider, options,
  dryRun?: boolean
): Promise<FixResult>;
  • dryRun=true:跳过末尾 WorkspaceEditapplyEdit,返回 newText: currentText
  • dryRun=false(默认):保持现有写盘行为,不返回 newText(或一并返回,调用方忽略)。
  • hover 入口仍走默认路径,行为零变化。

6.3 src/fix/fixPreview.ts

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<vscode.Uri | undefined>;
// 打开内置 diff 编辑器,返回右侧 newUri(供后续关闭)

export async function closePreviewEditor(uri: vscode.Uri): Promise<void>;
// 关闭对应 diff 编辑器标签并清理 provider 内容

export async function applyNewText(document: vscode.TextDocument, newText: string): Promise<boolean>;
// 整文档 WorkspaceEdit 一次性写入(recordFixes 由调用方完成)

6.4 src/fix/fixPending.ts

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/cancelFixPreviewapplyAll/cancelAllapplyAllPreview/cancelAllPreview
  • 扩展 → 面板:{ type: 'pending', key, on: boolean }(翻转单条按钮)、{ type: 'batchPending', on: boolean }(翻转顶部按钮)。

7. 核心逻辑

7.1 单个修复(fixIssuepanel 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   // 不写盘

用户回面板:

  • 点「应用」→ applyFixPreviewapplyNewText(document, newText)recordFixes → 关 diff → 删 pending → saverefreshAfterFix → 提示成功。
  • 点「取消」→ 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   // 不写盘
  • 「全部应用」→ applyAllPreviewapplyNewText(document, currentText) → 逐条 recordFixes → 关 diff → 删 batch → saverefreshAfterFix → 提示批量完成。
  • 「取消全部」→ cancelAllPreview:关 diff → 删 batch → 还原按钮。

说明:多轮引擎已在内部对传入文本反复 adapter.check,因此 fixAll 只需把上一个 fix 的 newText 作为下一个的输入(用 mockDocument 包装为 TextDocument 再传引擎),即可得到合并后的最终文本;位置漂移由引擎内 findClosestFixable 按行号就近匹配处理。

7.3 diff 提供者

  • 注册 vscode.workspace.registerTextDocumentContentProvider('codeReviewerPreview', provider)extension.ts 调 registerFixPreviewProvider 一次)。
  • 新文本 URIcodeReviewerPreview://new/<fileName>-<timestamp>,内容即 newText;原文本 URI 同样走 provider(避免脏文档时左屏显示磁盘旧内容与引擎计算不一致)。

7.4 面板按钮翻转(reviewPanel.js

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 触发的 fixIssueorigin: '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 test106 tests)回归通过。
  • 引擎 dryRun 路径回归:原有 fixEngine/aiFixEngine 测试走默认(写盘)路径不受影响。

11. 验证顺序

lint → compile → testnpm run lint / npm run compile / npm test