From d4c3fb9df7a8780343a87b931db5eeacddefeb4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8C=83=E6=99=BA=E9=B9=8F?= Date: Tue, 21 Jul 2026 20:35:25 +0800 Subject: [PATCH] feat: redesign review panel UI + use VS Code theme variables in setup view --- _AI_USAGE_LOG.md | 3 + src/panel/webview.ts | 359 ++++++++++++++++++++++++++++------------- src/views/setupView.ts | 69 ++++---- 3 files changed, 282 insertions(+), 149 deletions(-) diff --git a/_AI_USAGE_LOG.md b/_AI_USAGE_LOG.md index 60cc2f7..3607b40 100644 --- a/_AI_USAGE_LOG.md +++ b/_AI_USAGE_LOG.md @@ -86,3 +86,6 @@ | 2026-07-19 22:35 | ① 用户提出 → ② 需求澄清 → ③ 方案设计 → ④ 人类审批 → ⑤ 编码实现 → ⑥ 审查验证 | 根因修复:AI 响应被截断因未设置 `max_tokens`。ChatOptions 加 maxTokens 字段;config 加 getAIMaxTokens()(默认 8192);package.json 加配置项;所有 provider 实现使用该值 | 无 | src/ai/providers/base.ts src/config/ai.ts package.json src/ai/providers/openai-compatible.ts src/ai/providers/claude.ts src/ai/providers/gemini.ts src/ai/engine.ts src/fixer/fixer.ts src/views/setupView.ts src/test/config.test.ts | deepseek-v4-flash | | 2026-07-20 20:01 | ① 用户提出 → ⑤ 编码实现 | Bug fix: SqlFluffViolation 接口字段与 sqlfluff JSON 输出不匹配(line_no→start_line_no,line_pos→start_line_pos,rule_code→code,缺 end 字段),导致行号显示 LNaN、ruleId 显示 undefined | 无 | src/adapters/sql-lint.ts | deepseek-v4-flash | | 2026-07-20 20:17 | ① 用户提出 → ⑤ 编码实现 | Bug fix: mergeResults 中用 originalRuleId 作 Map key 导致同 ruleId 的多条诊断(如两个 PRS)后覆盖前,翻译错误;改为按索引位置一一对应匹配 | 无 | src/merger/merger.ts | deepseek-v4-flash | +| 2026-07-20 20:52 | ① 用户提出 → ② 需求澄清 → ③ 方案设计 → ④ 人类审批 → ⑤ 编码实现 | 按审查面板优化 spec 重写 src/panel/webview.ts:对齐 review-panel-preview.html 原型(SVG 图标 header、severity 侧栏+圆点双指示器、来源 Badge、展开/折叠详情、错误框、严重级别分色 Tab 计数徽章、section-header)。移除 emoji 严重性图标和 diff 展示。 | 视觉伴侣(环境不支持)→ 终端 brainstorm;方案选择(用户选 A);讨论移除 diff。 | src/panel/webview.ts | deepseek-v4-flash | +| 2026-07-20 21:30 | ① 用户提出 → ⑤ 编码实现 → ⑥ 审查验证 | Bug fix: 静态分析展开详情「原文:」重复显示消息内容。合并后 suggestion 与 message 相同时不渲染详情;linter/custom 项禁止展开(仅 AI 项保留展开)。 | 三轮迭代:①删原文→②加 check dup→③加 expandable 参数 | src/panel/webview.ts | deepseek-v4-flash | +| 2026-07-20 21:42 | ① 用户提出 → ⑤ 编码实现 | 设置画面改用 `--vscode-*` CSS 变量,跟随 VS Code 主题 | 先用 `--vscode-editor-background` 但用户反馈没生效 → 改用 `--vscode-sideBar-background` 并加 `html` 规则 + fallback;replaceAll 导致双层嵌套 → 手动修复 | src/views/setupView.ts | deepseek-v4-flash | diff --git a/src/panel/webview.ts b/src/panel/webview.ts index de73963..ef72f1c 100644 --- a/src/panel/webview.ts +++ b/src/panel/webview.ts @@ -8,6 +8,32 @@ interface PanelMessage { source?: 'linter' | 'custom' | 'ai'; } +function esc(str: string): string { + return str.replace(/&/g, '&').replace(//g, '"').replace(/"/g, '"'); +} + +function svgIcon(): string { + return ``; +} + +const SVG_HEADER_ICON = svgIcon(); + +const BADGE_CLASS: Record = { linter: 'badge-linter', custom: 'badge-custom', ai: 'badge-ai' }; +const BADGE_LABEL: Record = { linter: 'Linter', custom: '自定义', ai: 'AI' }; + +function badgeHtml(source: string): string { + return `${BADGE_LABEL[source] || source}`; +} + +function severityClass(severity: string): string { + switch (severity) { + case 'error': return 'severity-error'; + case 'warning': return 'severity-warning'; + case 'info': return 'severity-info'; + default: return 'severity-info'; + } +} + export class ReviewPanel { public static currentPanel: ReviewPanel | undefined; private readonly panel: vscode.WebviewPanel; @@ -19,7 +45,7 @@ export class ReviewPanel { ) { this.panel = vscode.window.createWebviewPanel( 'codeReviewer.reviewPanel', - '代码审查报告', + '净码特工 · 代码审查报告', column, { enableScripts: true, @@ -52,23 +78,46 @@ export class ReviewPanel { } private buildHtml(report: MergedReport): string { - const total = report.linterCount + report.customRuleCount + report.aiCount; - const errorCount = report.linterDiagnostics.filter(d => d.severity === 'error').length - + report.customRuleDiagnostics.filter(d => d.severity === 'error').length - + report.aiFindings.filter(f => f.severity === 'error').length; - const warnCount = report.linterDiagnostics.filter(d => d.severity === 'warning').length - + report.customRuleDiagnostics.filter(d => d.severity === 'warning').length - + report.aiFindings.filter(f => f.severity === 'warning').length; - const infoCount = total - errorCount - warnCount; - const fileName = report.filePath.split(/[/\\]/).pop() ?? ''; - const errorBanner = report.errors.length > 0 - ? `` + const linterErrors = report.linterDiagnostics.filter(d => d.severity === 'error').length; + const linterWarnings = report.linterDiagnostics.filter(d => d.severity === 'warning').length; + const linterInfos = report.linterDiagnostics.filter(d => d.severity === 'info').length; + const customErrors = report.customRuleDiagnostics.filter(d => d.severity === 'error').length; + const customWarnings = report.customRuleDiagnostics.filter(d => d.severity === 'warning').length; + const customInfos = report.customRuleDiagnostics.filter(d => d.severity === 'info').length; + const aiErrors = report.aiFindings.filter(f => f.severity === 'error').length; + const aiWarnings = report.aiFindings.filter(f => f.severity === 'warning').length; + const aiInfos = report.aiFindings.filter(f => f.severity === 'info').length; + + const total = report.linterCount + report.customRuleCount + report.aiCount; + const totalErrors = linterErrors + customErrors + aiErrors; + const totalWarnings = linterWarnings + customWarnings + aiWarnings; + const totalInfos = linterInfos + customInfos + aiInfos; + + const errorBox = report.errors.length > 0 + ? `
✖ 执行错误
${report.errors.map(e => `
${esc(e)}
`).join('')}
` + : ''; + + const banner = report.errors.length > 0 + ? '' : report.degraded - ? '' + ? '' : ''; + const fixableLinterSet = new Set(report.fixableLinterIndices); + const fixableCustomSet = new Set(report.fixableCustomIndices); + + const tabCount = (e: number, w: number, i: number) => { + const pts: string[] = []; + if (e > 0) { pts.push(`${e}`); } + if (w > 0) { pts.push(`${w}`); } + if (i > 0) { pts.push(`${i}`); } + return pts.join(' '); + }; + + const linterToolName = report.adapterNames.length > 0 ? report.adapterNames.join(' + ') : '静态分析'; + return ` @@ -77,157 +126,237 @@ export class ReviewPanel { 代码审查报告 +
-

📋 代码审查报告

-
${fileName} · ${report.language} · ${(report.duration / 1000).toFixed(1)}s
+
+

${SVG_HEADER_ICON} 净码特工 · 代码审查报告

+
${esc(fileName)} · ${esc(report.language)} · ${(report.duration / 1000).toFixed(1)}s
+
-${errorBanner} -
-
${total}
总计
-
${errorCount}
错误
-
${warnCount}
警告
-
${infoCount}
建议
+ +${banner} +${errorBox} + +
+
${total}
总计问题
+
${totalErrors}
错误
+
${totalWarnings}
警告
+
${totalInfos}
建议
-
- - - + +
+ + +
-
- ${this.buildLinterList(report)} + +
+ ${this.buildLinterList(report, fixableLinterSet)}
-
- ${this.buildCustomList(report)} +
+ ${this.buildCustomList(report, fixableCustomSet)}
-
+
${this.buildAIList(report)}
+
- +
+ `; } - private buildLinterList(report: MergedReport): string { + private buildLinterList(report: MergedReport, fixableSet: Set): string { if (report.linterDiagnostics.length === 0) { - return '
✅ 静态分析未发现问题
'; + return '
未发现任何问题
'; } - return report.linterDiagnostics.map((d, i) => ` -
-
-
${this.sevIcon(d.severity)} ${this.escape(d.ruleId)} L${d.range.start.line + 1}
-
${this.escape(d.message)}
- ${d.suggestion ? `
建议: ${this.escape(d.suggestion)}
` : ''} -
-
- -
-
`).join(''); + const toolName = report.adapterNames.length > 0 ? report.adapterNames.join(' + ') : '静态分析'; + const hasFixable = fixableSet.size > 0; + return `
${esc(toolName)} · ${report.linterCount} 个问题${hasFixable ? '' : ''}
` + + report.linterDiagnostics.map((d, i) => this.buildIssueItem(d.severity, d.ruleId, d.message, d.range.start.line, 'linter', d.suggestion, fixableSet.has(i), undefined, false)).join(''); } - private buildCustomList(report: MergedReport): string { + private buildCustomList(report: MergedReport, fixableSet: Set): string { if (report.customRuleDiagnostics.length === 0) { - return '
✅ 自定义规则未发现问题
'; + return '
未发现规则违规
'; } - return report.customRuleDiagnostics.map((d, i) => ` -
-
-
${this.sevIcon(d.severity)} ${this.escape(d.ruleId)} L${d.range.start.line + 1}
-
${this.escape(d.message)}
-
-
- -
-
`).join(''); + const hasFixable = fixableSet.size > 0; + return `
自定义规则 · ${report.customRuleCount} 个问题${hasFixable ? '' : ''}
` + + report.customRuleDiagnostics.map((d, i) => this.buildIssueItem(d.severity, d.ruleId, d.message, d.range.start.line, 'custom', d.suggestion, fixableSet.has(i), undefined, false)).join(''); } private buildAIList(report: MergedReport): string { if (report.aiFindings.length === 0) { - return '
🤖 AI 审查未发现新问题
'; + return '
无 AI 审查建议
'; } - const parts: string[] = []; - + const parts: string[] = ['
AI 审查建议 · ' + report.aiCount + ' 条
']; for (const f of report.aiFindings) { - parts.push(` -
-
-
${this.sevIcon(f.severity)} [${f.category}] ${this.escape(f.title)}
-
${this.escape(f.description)}
- ${f.suggestion ? `
建议: ${this.escape(f.suggestion)}
` : ''} -
-
- -
-
`); + const details: string[] = []; + details.push(`
${esc(f.description)}
`); + if (f.category) { + details.push(`🎯 ${esc(f.category)}`); + } + if (f.suggestion) { + details.push(`
💡 ${esc(f.suggestion)}
`); + } + parts.push(this.buildIssueItem(f.severity, f.ruleId, f.title, f.line, 'ai', f.suggestion, true, details.join(''))); } - return parts.join(''); } - private sevIcon(severity: string): string { - switch (severity) { - case 'error': return '🔴'; - case 'warning': return '🟡'; - case 'info': return '🔵'; - default: return '⚪'; - } - } + private buildIssueItem( + severity: string, + ruleId: string, + message: string, + line: number, + source: string, + suggestion?: string, + fixable?: boolean, + detailHtml?: string, + expandable: boolean = true + ): string { + const sevCls = severityClass(severity); + const lineNum = line + 1; + const parts: string[] = []; - private escape(str: string): string { - return str.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); + parts.push(`
`); + parts.push(`
`); + parts.push('
'); + parts.push('
'); + parts.push(``); + parts.push(badgeHtml(source)); + parts.push(`${esc(ruleId)}`); + parts.push(`${esc(message)}`); + parts.push(`L${lineNum}`); + if (fixable) { + parts.push(``); + } + parts.push('
'); + + if (expandable && (detailHtml || (suggestion && suggestion !== message))) { + parts.push('
'); + if (detailHtml) { + parts.push(detailHtml); + } else if (suggestion) { + parts.push(`
💡 ${esc(suggestion)}
`); + } + parts.push('
'); + } + + parts.push('
'); + parts.push('
'); + return parts.join(''); } private handleMessage(message: PanelMessage): void { diff --git a/src/views/setupView.ts b/src/views/setupView.ts index e08625d..a1387da 100644 --- a/src/views/setupView.ts +++ b/src/views/setupView.ts @@ -280,11 +280,12 @@ export class SetupViewProvider implements vscode.WebviewViewProvider {