From 1307a3dd93f5bfeac2aeda3a55c5063e22d830dd Mon Sep 17 00:00:00 2001 From: hkimw <54717101+hkimw@users.noreply.github.com> Date: Thu, 7 May 2026 00:11:48 +0900 Subject: [PATCH 1/2] feat(panel): polish v002.1 readiness presentation --- editors/vscode-prototype/src/extension.mjs | 25 ++ .../src/kv260-status-panel.mjs | 277 ++++++++++++++++++ .../test/extension-entrypoint.test.mjs | 27 ++ .../test/kv260-status-panel.test.mjs | 75 +++++ 4 files changed, 404 insertions(+) diff --git a/editors/vscode-prototype/src/extension.mjs b/editors/vscode-prototype/src/extension.mjs index bf516c8..7190611 100644 --- a/editors/vscode-prototype/src/extension.mjs +++ b/editors/vscode-prototype/src/extension.mjs @@ -75,6 +75,7 @@ import { import { createKv260StatusPanel, formatKv260StatusPanel, + renderKv260StatusPanelHtml, } from "./kv260-status-panel.mjs"; const EXTENSION_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), ".."); @@ -452,6 +453,26 @@ function appendCommandOutput(outputChannel, commandId, result) { outputChannel.show?.(true); } +function showKv260StatusWebview(vscodeApi, panel) { + const createWebviewPanel = vscodeApi?.window?.createWebviewPanel; + if (typeof createWebviewPanel !== "function") { + return null; + } + const viewColumn = vscodeApi?.ViewColumn?.Beside ?? vscodeApi?.ViewColumn?.One ?? 1; + const webviewPanel = createWebviewPanel.call( + vscodeApi.window, + "pccxKv260Readiness", + "PCCX KV260 Readiness", + viewColumn, + { enableScripts: false }, + ); + if (!webviewPanel?.webview) { + return null; + } + webviewPanel.webview.html = renderKv260StatusPanelHtml(panel); + return webviewPanel; +} + function facadeRunnerFromRuntime(runtime = {}) { return async (facadeArgs, env = {}) => { const invocation = { @@ -1125,6 +1146,10 @@ export function createCommandHandler(commandId, vscodeApi, runtime = {}) { kind: "kv260-status-panel", panel, }; + const webviewPanel = showKv260StatusWebview(vscodeApi, panel); + if (webviewPanel) { + result.presentation = "webview"; + } vscodeApi?.window?.showInformationMessage?.( `KV260 status surface: ${panel.lab.frameCount} trace frame(s).`, result, diff --git a/editors/vscode-prototype/src/kv260-status-panel.mjs b/editors/vscode-prototype/src/kv260-status-panel.mjs index b0c2f4c..257bf39 100644 --- a/editors/vscode-prototype/src/kv260-status-panel.mjs +++ b/editors/vscode-prototype/src/kv260-status-panel.mjs @@ -68,6 +68,31 @@ const UNSUPPORTED_MARKER_PARTS = Object.freeze([ ["20 tok/s ", "achieved"], ["timing ", "closed"], ]); +const APERTURE_MARK_SVG = ` +`; +const STATUS_PRESENTATION = Object.freeze({ + pass: Object.freeze({ label: "PASS", className: "status-pass" }), + blocked: Object.freeze({ label: "FAIL", className: "status-fail" }), + not_run: Object.freeze({ label: "PENDING", className: "status-pending" }), +}); +const EVIDENCE_PATHS_BY_ITEM_ID = Object.freeze({ + serial_tty_port: "launcher.serial_probe.tty_port", + serial_login: "launcher.serial_probe.login_ok", + serial_xrt: "launcher.serial_probe.xrt_present", + serial_probe_timestamp: "launcher.serial_probe.last_preflight_at", +}); +const EVIDENCE_SOURCES_BY_ITEM_ID = Object.freeze({ + serial_tty_port: "launcher serial preflight snapshot", + serial_login: "launcher serial preflight snapshot", + serial_xrt: "launcher serial preflight snapshot", + serial_probe_timestamp: "launcher serial preflight snapshot", +}); function clone(value) { return JSON.parse(JSON.stringify(value)); @@ -351,6 +376,38 @@ function truncateText(value, maxCharacters = 96) { return `${value.slice(0, maxCharacters - 3)}...`; } +function escapeHtml(value) { + return String(value ?? "") + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">") + .replaceAll('"', """) + .replaceAll("'", "'"); +} + +function statusPresentationForItem(item) { + const state = item.state ?? (item.satisfied ? "pass" : "blocked"); + return STATUS_PRESENTATION[state] ?? STATUS_PRESENTATION.not_run; +} + +function evidencePathForItem(item) { + return EVIDENCE_PATHS_BY_ITEM_ID[item.itemId] ?? "panel.preflight.items"; +} + +function evidenceSourceForItem(item) { + return EVIDENCE_SOURCES_BY_ITEM_ID[item.itemId] ?? "readiness panel input"; +} + +function panelEmptyState(panel) { + if (panel.serialProbe.status === "not_run") { + return "Launcher status input is not configured, so no serial preflight snapshot is available. Set the launcher-side status JSON input before relying on this panel; the IDE does not probe the board."; + } + if (panel.serialProbe.status === "blocked") { + return "The launcher serial preflight reports the board is not reachable. Keep launch paths gated until the launcher writes a fresh reachable snapshot."; + } + return ""; +} + export function createPreflightProposal(launcherStatus, _traceManifest) { const probe = launcherStatus.serial_probe; return Object.freeze({ @@ -433,6 +490,226 @@ export function kv260StatusPanelJson(inputs = {}) { return `${JSON.stringify(createKv260StatusPanel(inputs), null, 2)}\n`; } +export function renderKv260StatusPanelHtml(panel = createKv260StatusPanel()) { + const emptyState = panelEmptyState(panel); + const checklistItems = panel.preflight.items.map((item) => { + const status = statusPresentationForItem(item); + const evidencePath = evidencePathForItem(item); + const evidenceSource = evidenceSourceForItem(item); + return ` +
  • +
    + ${escapeHtml(item.label)} + ${status.label} +
    +
    + Evidence path +
    +
    Artifact source
    +
    ${escapeHtml(evidenceSource)}
    +
    Artifact field
    +
    ${escapeHtml(evidencePath)}
    +
    Rendered evidence
    +
    ${escapeHtml(item.evidence)}
    +
    +
    +
  • `; + }).join(""); + return ` + + + + + KV260 Readiness + + + +
    +
    + ${APERTURE_MARK_SVG} +
    +

    KV260 Readiness

    +

    Read-only launcher and lab status surface

    +
    +
    + ${emptyState ? `

    ${escapeHtml(emptyState)}

    ` : ""} +
    + + + +
    +
      + ${checklistItems} +
    +
    + +`; +} + export function formatKv260StatusPanel(panel = createKv260StatusPanel()) { const lines = [ "KV260 Status Surface", diff --git a/editors/vscode-prototype/test/extension-entrypoint.test.mjs b/editors/vscode-prototype/test/extension-entrypoint.test.mjs index 2825131..29323e2 100644 --- a/editors/vscode-prototype/test/extension-entrypoint.test.mjs +++ b/editors/vscode-prototype/test/extension-entrypoint.test.mjs @@ -1566,6 +1566,32 @@ async function testKv260StatusPanelCommandReturnsDataOnlySurface() { assert.equal(result.panel.safety.sshExecution, false); } +async function testKv260StatusPanelCommandRendersExistingWebviewOnly() { + const webviewPanel = { webview: { html: "" } }; + const vscodeApi = { + ViewColumn: { One: 1 }, + window: { + createWebviewPanel(viewType, title, viewColumn, options) { + assert.equal(viewType, "pccxKv260Readiness"); + assert.equal(title, "PCCX KV260 Readiness"); + assert.equal(viewColumn, 1); + assert.deepEqual(options, { enableScripts: false }); + return webviewPanel; + }, + showInformationMessage() {}, + }, + }; + const handler = createCommandHandler(SHOW_KV260_STATUS_PANEL_COMMAND, vscodeApi, {}); + + const result = await handler(); + + assert.equal(result.ok, true); + assert.equal(result.presentation, "webview"); + assert.match(webviewPanel.webview.html, /class="aperture-mark"/); + assert.match(webviewPanel.webview.html, /status-pill status-pending">PENDING/); + assert.match(webviewPanel.webview.html, /
    /); +} + testKnownFacadeArgs(); testUnknownCommandsRejected(); testCheckedExampleDiagnosticsCommandStaysExampleMode(); @@ -1595,5 +1621,6 @@ await testContextBundleAuditCommandReturnsBoundedAudit(); await testDiagnosticsHandoffSummaryCommandReturnsDataOnlySurface(); await testPccxLabBackendStatusCommandReturnsStatusOnly(); await testKv260StatusPanelCommandReturnsDataOnlySurface(); +await testKv260StatusPanelCommandRendersExistingWebviewOnly(); console.log("vscode extension entrypoint tests ok"); diff --git a/editors/vscode-prototype/test/kv260-status-panel.test.mjs b/editors/vscode-prototype/test/kv260-status-panel.test.mjs index a147287..a9c1c49 100644 --- a/editors/vscode-prototype/test/kv260-status-panel.test.mjs +++ b/editors/vscode-prototype/test/kv260-status-panel.test.mjs @@ -15,6 +15,7 @@ import { createKv260StatusPanel, formatKv260StatusPanel, kv260StatusPanelJson, + renderKv260StatusPanelHtml, } from "../src/kv260-status-panel.mjs"; const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "../../.."); @@ -104,6 +105,78 @@ async function testPreflightNotRunIsGracefulDefault() { assert.match(text, /serial\.xrtPresent: preflight not run/); } +async function testRendererUsesAperturePillsEvidenceAndEmptyState() { + const pendingPanel = createKv260StatusPanel({ + launcherStatus: await readJson(LAUNCHER_FIXTURE), + traceManifest: await readJson(TRACE_FIXTURE), + }); + const pendingHtml = renderKv260StatusPanelHtml(pendingPanel); + + assert.match(pendingHtml, /class="aperture-mark"/); + assert.match(pendingHtml, /#0b5fff/); + assert.match(pendingHtml, /status-pill status-pending">PENDING/); + assert.match(pendingHtml, /
    /); + assert.match(pendingHtml, /launcher serial preflight snapshot/); + assert.match(pendingHtml, /launcher\.serial_probe\.tty_port/); + assert.match(pendingHtml, /Launcher status input is not configured/); + assert.doesNotMatch(pendingHtml, /\bAI\b|artificial intelligence/i); + + const blockedStatus = await readJson(LAUNCHER_FIXTURE); + blockedStatus.serial_probe = { + schema_version: LAUNCHER_SERIAL_PREFLIGHT_STATUS_VERSION, + status: "blocked", + tty_port: null, + login_ok: false, + kernel_uname: null, + xrt_present: false, + last_preflight_at: "2026-05-06T09:00:00Z", + }; + const blockedHtml = renderKv260StatusPanelHtml(createKv260StatusPanel({ + launcherStatus: blockedStatus, + traceManifest: await readJson(TRACE_FIXTURE), + })); + + assert.match(blockedHtml, /status-pill status-fail">FAIL/); + assert.match(blockedHtml, /board is not reachable/); + + const availableStatus = await readJson(LAUNCHER_FIXTURE); + availableStatus.serial_probe = { + schema_version: LAUNCHER_SERIAL_PREFLIGHT_STATUS_VERSION, + status: "available", + tty_port: "/dev/ttyUSB0", + login_ok: true, + kernel_uname: "Linux kv260", + xrt_present: true, + last_preflight_at: "2026-05-06T09:00:00Z", + }; + const availableHtml = renderKv260StatusPanelHtml(createKv260StatusPanel({ + launcherStatus: availableStatus, + traceManifest: await readJson(TRACE_FIXTURE), + })); + + assert.match(availableHtml, /status-pill status-pass">PASS/); +} + +async function testRendererEscapesArtifactEvidence() { + const launcherStatus = await readJson(LAUNCHER_FIXTURE); + launcherStatus.serial_probe = { + schema_version: LAUNCHER_SERIAL_PREFLIGHT_STATUS_VERSION, + status: "available", + tty_port: "", + login_ok: true, + kernel_uname: "Linux kv260", + xrt_present: true, + last_preflight_at: "2026-05-06T09:00:00Z", + }; + const html = renderKv260StatusPanelHtml(createKv260StatusPanel({ + launcherStatus, + traceManifest: await readJson(TRACE_FIXTURE), + })); + + assert.match(html, /<script>alert\(1\)<\/script>/); + assert.doesNotMatch(html, /