diff --git a/packages/extension/src/file_watcher.ts b/packages/extension/src/file_watcher.ts index 6d9a7076..beea80d2 100644 --- a/packages/extension/src/file_watcher.ts +++ b/packages/extension/src/file_watcher.ts @@ -59,6 +59,10 @@ class LiveRunSink implements RunSink { }); } run(c: RunCompletion): void { + // Tell the inspector the run is terminal so the badge leaves "running". + // Fires on live finish (onFinished) AND replay of an already-finished run + // (ingestRunDir) — both route through this sink. + getInspector()?.postCompletion(c.status, c.fidelity); this.opts.statusBar?.setRun({ runId: c.runId, outputDir: c.runDir, startedAt: 0, status: c.status, latestIter: this.latestIter >= 0 ? this.latestIter : undefined, diff --git a/packages/extension/src/inspector_webview.ts b/packages/extension/src/inspector_webview.ts index e7f6ae19..d34edf3e 100644 --- a/packages/extension/src/inspector_webview.ts +++ b/packages/extension/src/inspector_webview.ts @@ -1,8 +1,7 @@ // Run Inspector webview script — runs inside the sandboxed Chromium webview. -// Ported from amicode/src/spikes/inspector_webview.ts with no semantic changes: // - double-buffer image swap (zero flicker between iter frames at 5 Hz) -// - canonical Ipopt-format stats row (iter, f, inf_pr, inf_du, lat) -// - Date.now() for cross-process timestamp (performance.now origins differ). +// - status badge (idle / running / converged) + researcher metric cards +// (objective, iteration, feasibility, optimality) driven by AMICODE_ITER. declare function acquireVsCodeApi(): { postMessage(msg: unknown): void; @@ -11,38 +10,41 @@ declare function acquireVsCodeApi(): { const vscodeApi = acquireVsCodeApi(); const $ = (id: string) => document.getElementById(id) as HTMLElement; -let iterCount = 0; -let lastIterAt = performance.now(); -let smoothedHz = 0; let visibleBuffer: "a" | "b" = "a"; +function setBadge(state: "idle" | "running" | "done" | "failed", text: string): void { + const badge = $("badge"); + badge.className = "badge " + state; + badge.textContent = text; +} + window.addEventListener("message", (e) => { const msg = e.data; if (!msg || typeof msg !== "object") return; - const recv = performance.now(); switch (msg.type) { case "ping": { vscodeApi.postMessage({ type: "pong", seq: msg.seq, t0: msg.t0 }); - $("status").textContent = "pinging"; break; } case "iteration": { - iterCount++; - const dt = recv - lastIterAt; - lastIterAt = recv; - const instHz = dt > 0 ? 1000 / dt : 0; - smoothedHz = smoothedHz === 0 ? instHz : 0.9 * smoothedHz + 0.1 * instHz; - const lat = Date.now() - msg.t_post; - $("iter").textContent = String(iterCount); - $("hz").textContent = smoothedHz.toFixed(1); - $("rec").textContent = - `iter=${String(msg.iter).padStart(4, "0")}` + - ` f=${(msg.f_val as number).toExponential(6)}` + - ` inf_pr=${(msg.eq_viol as number).toExponential(3)}` + - ` inf_du=${(msg.kkt_error as number).toExponential(3)}`; - $("lat").textContent = `${lat.toFixed(0)}ms`; - $("status").textContent = msg.isFinal ? "final frame" : "streaming"; + $("m-obj-k").textContent = "objective"; + $("m-iter").textContent = String(msg.iter); + $("m-obj").textContent = (msg.f_val as number).toExponential(4); + $("m-pr").textContent = (msg.eq_viol as number).toExponential(2); + $("m-du").textContent = (msg.kkt_error as number).toExponential(2); + setBadge("running", "running"); + break; + } + case "completed": { + // Authoritative terminal state from the watcher (FINISHED on disk). + const ok = msg.status === "completed"; + setBadge(ok ? "done" : "failed", ok ? "converged" : String(msg.status)); + // Promote the hero card to the final fidelity — the number that matters. + if (ok && typeof msg.fidelity === "number") { + $("m-obj-k").textContent = "fidelity"; + $("m-obj").textContent = (msg.fidelity as number).toFixed(5); + } break; } case "refresh": { @@ -53,15 +55,12 @@ window.addEventListener("message", (e) => { const incomingBuffer = visibleBuffer === "a" ? "b" : "a"; const incomingImg = $("preview-" + incomingBuffer) as HTMLImageElement; const outgoingImg = $("preview-" + visibleBuffer) as HTMLImageElement; - const tPost = msg.t_post as number; const handleLoaded = () => { - const loadedAt = Date.now(); incomingImg.style.opacity = "1"; outgoingImg.style.opacity = "0"; visibleBuffer = incomingBuffer; - $("img-iter").textContent = String(msg.iter); - $("img-load").textContent = `${(loadedAt - tPost).toFixed(0)}ms`; + $("m-iter").textContent = String(msg.iter); }; incomingImg.src = msg.url; @@ -73,7 +72,7 @@ window.addEventListener("message", (e) => { handleLoaded(); }); } - $("status").textContent = msg.isFinal ? "final frame" : "streaming"; + setBadge("running", "running"); // a new frame means a live solve; completion arrives via "completed" break; } } diff --git a/packages/extension/src/run_inspector.ts b/packages/extension/src/run_inspector.ts index 7d4634ac..94e41b1c 100644 --- a/packages/extension/src/run_inspector.ts +++ b/packages/extension/src/run_inspector.ts @@ -22,6 +22,10 @@ class InspectorView implements vscode.WebviewViewProvider { private refreshTimer?: NodeJS.Timeout; /** Set BEFORE the webview is materialized — replayed in resolveWebviewView. */ private bufferedImage?: { fsPath: string; iter: number; isFinal: boolean }; + /** Terminal state that arrived before the webview existed (e.g. on launch the + * watcher follows `latest` → a finished run completes before the panel is + * opened). Replayed after the buffered image so the badge isn't stuck "running". */ + private bufferedCompletion?: { status: string; fidelity?: number }; constructor(private readonly ctx: vscode.ExtensionContext, private readonly runsRoot: string) {} @@ -47,6 +51,13 @@ class InspectorView implements vscode.WebviewViewProvider { this.bufferedImage = undefined; this.flushRefresh(); } + // Then replay a terminal state if the run already finished — after the + // image so "converged"/"failed" wins over the replayed frame's "running". + if (this.bufferedCompletion) { + const c = this.bufferedCompletion; + this.bufferedCompletion = undefined; + view.webview.postMessage({ type: "completed", status: c.status, fidelity: c.fidelity }); + } } // -------- public surface used by RunsRootWatcher -------- @@ -87,6 +98,22 @@ class InspectorView implements vscode.WebviewViewProvider { }); } + /** Terminal-state signal so the badge stops saying "running". The watcher + * streams frames without an isFinal marker (it can't know which frame is + * last mid-solve), so completion is delivered separately — on live finish + * AND when switching to an already-finished run. Flush any pending frame + * first so this is the last word the webview hears for the run. */ + postCompletion(status: string, fidelity?: number): void { + if (!this.view) { + // Panel not open yet — stash; resolveWebviewView replays it after the image. + this.bufferedCompletion = { status, fidelity }; + return; + } + this.clearTimer(); + this.flushRefresh(); + this.view.webview.postMessage({ type: "completed", status, fidelity }); + } + reveal(): void { // Force materialize the view via its auto-registered .focus command. // Unconditional — without an existing view, this is what creates one. @@ -134,36 +161,79 @@ class InspectorView implements vscode.WebviewViewProvider { script-src 'nonce-${nonce}'; style-src ${webview.cspSource} 'unsafe-inline';">
-