From 4c8ed0d8420daf03c09334a5938b27ab4cb07af0 Mon Sep 17 00:00:00 2001 From: waleedcodes <110061477+waleedcodes@users.noreply.github.com> Date: Fri, 9 Oct 2026 21:22:35 +0500 Subject: [PATCH 1/3] feat(ui): scaffold ProofCodeWebviewPanel singleton and webview creation --- src/ui/webviewPanel.ts | 328 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 328 insertions(+) create mode 100644 src/ui/webviewPanel.ts diff --git a/src/ui/webviewPanel.ts b/src/ui/webviewPanel.ts new file mode 100644 index 0000000..cd8b4d8 --- /dev/null +++ b/src/ui/webviewPanel.ts @@ -0,0 +1,328 @@ +import * as path from 'node:path'; +import * as vscode from 'vscode'; +import { VerificationReport } from '../types'; + +export class ProofCodeWebviewPanel { + public static currentPanel: ProofCodeWebviewPanel | undefined; + private readonly _panel: vscode.WebviewPanel; + private readonly _extensionUri: vscode.Uri; + private _disposables: vscode.Disposable[] = []; + private _report: VerificationReport | null = null; + private _onVerifyRequested: (() => Promise) | null = null; + private _onInitRulesRequested: (() => Promise) | null = null; + + private constructor(panel: vscode.WebviewPanel, extensionUri: vscode.Uri) { + this._panel = panel; + this._extensionUri = extensionUri; + + this._panel.onDidDispose(() => this.dispose(), null, this._disposables); + + this._panel.webview.onDidReceiveMessage( + async (message) => { + switch (message.type) { + case 'verify': + if (this._onVerifyRequested) { + await this._onVerifyRequested(); + } + break; + case 'openEvidence': + if (message.file && message.line) { + await this.openFileAtLine(message.file, message.line); + } + break; + case 'initRules': + if (this._onInitRulesRequested) { + await this._onInitRulesRequested(); + } + break; + case 'exportMarkdown': + await vscode.commands.executeCommand('proofcode.exportReport'); + break; + } + }, + null, + this._disposables + ); + } + + public getExtensionUri(): vscode.Uri { + return this._extensionUri; + } + + public static createOrShow( + extensionUri: vscode.Uri, + report: VerificationReport | null, + onVerify: () => Promise, + onInitRules: () => Promise + ): ProofCodeWebviewPanel { + const column = vscode.window.activeTextEditor + ? vscode.window.activeTextEditor.viewColumn + : undefined; + + if (ProofCodeWebviewPanel.currentPanel) { + ProofCodeWebviewPanel.currentPanel._panel.reveal(column); + ProofCodeWebviewPanel.currentPanel.setReport(report); + ProofCodeWebviewPanel.currentPanel._onVerifyRequested = onVerify; + ProofCodeWebviewPanel.currentPanel._onInitRulesRequested = onInitRules; + return ProofCodeWebviewPanel.currentPanel; + } + + const panel = vscode.window.createWebviewPanel( + 'proofcodeDashboard', + 'ProofCode Verification', + column || vscode.ViewColumn.One, + { + enableScripts: true, + retainContextWhenHidden: true, + localResourceRoots: [vscode.Uri.joinPath(extensionUri, 'resources')] + } + ); + + ProofCodeWebviewPanel.currentPanel = new ProofCodeWebviewPanel(panel, extensionUri); + ProofCodeWebviewPanel.currentPanel._onVerifyRequested = onVerify; + ProofCodeWebviewPanel.currentPanel._onInitRulesRequested = onInitRules; + ProofCodeWebviewPanel.currentPanel.setReport(report); + + return ProofCodeWebviewPanel.currentPanel; + } + + public setReport(report: VerificationReport | null): void { + this._report = report; + this._panel.webview.html = this._getHtmlForWebview(report); + } + + private async openFileAtLine(filePath: string, line: number): Promise { + try { + const fullPath = path.isAbsolute(filePath) + ? filePath + : path.resolve(this._report?.repoRoot || '', filePath); + const doc = await vscode.workspace.openTextDocument(vscode.Uri.file(fullPath)); + const editor = await vscode.window.showTextDocument(doc); + const position = new vscode.Position(Math.max(0, line - 1), 0); + editor.selection = new vscode.Selection(position, position); + editor.revealRange( + new vscode.Range(position, position), + vscode.TextEditorRevealType.InCenter + ); + } catch (err: unknown) { + vscode.window.showErrorMessage(`Failed to open evidence file: ${(err as Error).message}`); + } + } + + public dispose(): void { + ProofCodeWebviewPanel.currentPanel = undefined; + this._panel.dispose(); + while (this._disposables.length) { + const x = this._disposables.pop(); + if (x) { + x.dispose(); + } + } + } + + private _getHtmlForWebview(report: VerificationReport | null): string { + const score = report?.score.total ?? 0; + const verdict = report?.verdict ?? 'NEEDS_REVIEW'; + const verdictText = + verdict === 'READY_TO_SHIP' + ? 'READY TO SHIP' + : verdict === 'NEEDS_REVIEW' + ? 'NEEDS REVIEW' + : 'BLOCKED'; + + const verdictColor = + verdict === 'READY_TO_SHIP' + ? '#10B981' + : verdict === 'NEEDS_REVIEW' + ? '#F59E0B' + : '#EF4444'; + + const summary = report?.summary ?? { + filesChanged: 0, + linesAdded: 0, + linesDeleted: 0, + symbolsAffected: 0, + apiRoutesAffected: 0, + testsAdded: 0, + testsAffected: 0, + highRiskCount: 0, + mediumRiskCount: 0, + lowRiskCount: 0 + }; + + const risks = report?.risks ?? []; + const impact = report?.impact; + const rules = report?.rules ?? []; + const checks = report?.checks; + const breakage = report?.breakageRisks ?? []; + + return ` + + + + + ProofCode Change Verification +