KV260 Readiness
+Read-only launcher and lab status surface
+${escapeHtml(emptyState)}
` : ""} + +-
+ ${checklistItems}
+
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(evidencePath)}Read-only launcher and lab status surface
+${escapeHtml(emptyState)}
` : ""} + +