diff --git a/packages/extension/media/inspector.css b/packages/extension/media/inspector.css new file mode 100644 index 000000000..7ee67ab74 --- /dev/null +++ b/packages/extension/media/inspector.css @@ -0,0 +1,52 @@ +:root { + --amico-accent: #FFF676; /* amico yellow */ + --amico-run: #FFF676; /* running — brand yellow */ + --amico-ok: #3fb950; /* converged green */ + --amico-fail: #f85149; /* failed red */ +} +* { box-sizing: border-box; } +body { font-family: var(--vscode-font-family); color: var(--vscode-foreground); + padding: 14px; font-size: 12px; display: flex; flex-direction: column; gap: 12px; + height: 100vh; overflow-y: auto; } +/* ---- top bar ---- */ +.topbar { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; } +.brand { display: flex; align-items: center; gap: 9px; font-size: 13px; font-weight: 600; } +.mark { font-family: var(--vscode-editor-font-family, monospace); color: var(--amico-accent); + letter-spacing: 1px; font-weight: 700; + border: 1px solid color-mix(in srgb, var(--amico-accent) 55%, transparent); + border-radius: 6px; padding: 1px 7px; font-size: 12px; } +.runlabel { font-family: var(--vscode-editor-font-family, monospace); font-size: 11px; opacity: 0.6; } +.badge { margin-left: auto; font-size: 10.5px; font-weight: 600; letter-spacing: 0.5px; + text-transform: uppercase; padding: 3px 10px; border-radius: 999px; + border: 1px solid currentColor; display: inline-flex; align-items: center; gap: 6px; } +.badge::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: currentColor; } +.badge.idle { color: var(--vscode-descriptionForeground); opacity: 0.7; } +.badge.running { color: var(--amico-run); } +.badge.running::before { animation: pulse 1.1s ease-in-out infinite; } +.badge.done { color: var(--amico-ok); } +.badge.failed { color: var(--amico-fail); } +@keyframes pulse { 0%,100% { opacity: 1; transform: scale(1); } 50% { opacity: 0.35; transform: scale(0.7); } } +/* ---- plot hero ---- */ +/* min-height keeps the pulse plot a real plot, not a thin bar, when the + bottom panel is short; body scrolls if the panel can't fit it all. */ +.image-host { flex: 1 1 240px; min-height: 240px; min-width: 0; position: relative; + background: var(--vscode-editor-background); + border: 1px solid var(--vscode-panel-border); border-radius: 8px; padding: 6px; + display: grid; place-items: stretch; overflow: hidden; } +img.preview { grid-column: 1; grid-row: 1; width: 100%; height: 100%; + object-fit: contain; display: block; transition: opacity 120ms ease; } +.placeholder { place-self: center; text-align: center; opacity: 0.55; display: flex; + flex-direction: column; align-items: center; gap: 10px; } +.placeholder .mark { font-size: 20px; padding: 4px 12px; opacity: 0.8; } +.placeholder .hint { font-style: italic; max-width: 240px; line-height: 1.5; } +/* ---- metric cards ---- */ +.metrics { display: grid; grid-template-columns: repeat(auto-fit, minmax(112px, 1fr)); gap: 8px; } +.card { background: color-mix(in srgb, var(--vscode-panel-border) 25%, transparent); + border: 1px solid var(--vscode-panel-border); border-radius: 7px; padding: 8px 10px; + display: flex; flex-direction: column; gap: 3px; } +.card .k { font-size: 9.5px; text-transform: uppercase; letter-spacing: 0.6px; + opacity: 0.55; font-weight: 600; } +.card .v { font-family: var(--vscode-editor-font-family, monospace); font-size: 14px; } +.card.hero { border-color: color-mix(in srgb, var(--amico-accent) 45%, var(--vscode-panel-border)); } +.card.hero .k { color: var(--amico-accent); opacity: 0.85; } +.card.hero .v { font-size: 17px; font-weight: 600; } diff --git a/packages/extension/media/inspector.html b/packages/extension/media/inspector.html new file mode 100644 index 000000000..cb81fee60 --- /dev/null +++ b/packages/extension/media/inspector.html @@ -0,0 +1,19 @@ +
+Run Inspector failed to load its view (media/inspector.html).
This usually means a corrupt or partial install — try reinstalling the extension.
+${detail}
+ +`; +} + export function registerRunInspector(ctx: vscode.ExtensionContext, runsRoot: string): InspectorView { INSPECTOR = new InspectorView(ctx, runsRoot); ctx.subscriptions.push( diff --git a/packages/extension/test/__mocks__/vscode.ts b/packages/extension/test/__mocks__/vscode.ts index 1995b5c29..f8817cc3e 100644 --- a/packages/extension/test/__mocks__/vscode.ts +++ b/packages/extension/test/__mocks__/vscode.ts @@ -6,6 +6,7 @@ export const window = { showErrorMessage: () => Promise.resolve(undefined), showWarningMessage: () => Promise.resolve(undefined), createOutputChannel: () => ({ appendLine() {}, append() {}, dispose() {} }), + registerWebviewViewProvider: () => ({ dispose() {} }), }; export const commands = { executeCommand: () => Promise.resolve(undefined) }; export const workspace = { diff --git a/packages/extension/test/inspector_view_contract.test.ts b/packages/extension/test/inspector_view_contract.test.ts new file mode 100644 index 000000000..e2d9cd052 --- /dev/null +++ b/packages/extension/test/inspector_view_contract.test.ts @@ -0,0 +1,94 @@ +import { describe, it, expect } from "vitest"; +import { readFileSync } from "node:fs"; +import { join } from "node:path"; +import { registerRunInspector } from "../src/run_inspector"; + +// Pins the plumbing⇄view contract that the run_inspector.ts split now straddles: +// the markup (media/inspector.html) and styling (media/inspector.css) are owned +// by the design lane, while run_inspector.ts + inspector_webview.ts are the +// plumbing. This test reds if a look-and-feel change drops a DOM id or a CSP +// grant the webview script depends on — i.e. it lets design iterate freely while +// guarding the exact seam the two lanes share. Renders through the public +// WebviewViewProvider surface (resolveWebviewView), not internals. + +const PKG_ROOT = join(__dirname, ".."); + +function renderInspectorHtml(): string { + const ctx = { extensionUri: { fsPath: PKG_ROOT }, subscriptions: [] as unknown[] }; + const inspector = registerRunInspector(ctx as never, "/tmp/runs-test"); + let captured = ""; + const view = { + webview: { + options: {}, + cspSource: "vscode-webview://unit", + asWebviewUri: (u: { fsPath?: string }) => ({ toString: () => "vscode-webview://unit/" + (u?.fsPath ?? String(u)) }), + postMessage: () => undefined, + set html(v: string) { captured = v; }, + get html() { return captured; }, + }, + onDidDispose: () => ({ dispose() {} }), + }; + inspector.resolveWebviewView(view as never); + return captured; +} + +// Every id the webview script reads/writes MUST exist in the design-owned +// markup, else the live inspector silently breaks with no test failure. The +// regex below recovers the literal $("id")/getElementById("id") lookups; the +// computed hot-path lookups it can't see are pinned explicitly just below. +const SCRIPT = readFileSync(join(PKG_ROOT, "src", "inspector_webview.ts"), "utf8"); +function idsReferencedByScript(): string[] { + const ids = new Set