Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/extension/src/file_watcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
55 changes: 27 additions & 28 deletions packages/extension/src/inspector_webview.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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": {
Expand All @@ -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;
Expand All @@ -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;
}
}
Expand Down
110 changes: 90 additions & 20 deletions packages/extension/src/run_inspector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {}

Expand All @@ -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 --------
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -134,36 +161,79 @@ class InspectorView implements vscode.WebviewViewProvider {
script-src 'nonce-${nonce}';
style-src ${webview.cspSource} 'unsafe-inline';">
<style>
body { font-family: var(--vscode-font-family); color: var(--vscode-foreground); padding: 12px; font-size: 12px;
display: flex; flex-direction: column; gap: 10px; height: 100vh; box-sizing: border-box; }
h2 { margin: 0; font-size: 13px; }
.stat { font-family: var(--vscode-editor-font-family, monospace); }
.header-row { display: flex; gap: 24px; align-items: center; flex-wrap: wrap; }
.stats-row { display: flex; gap: 24px; flex-wrap: wrap; font-size: 11px; opacity: 0.85; }
.image-host { flex: 1 1 auto; min-height: 0; min-width: 0; position: relative;
background: var(--vscode-editor-background); border: 1px solid var(--vscode-panel-border); padding: 4px;
: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; image-rendering: auto; display: block;
transition: opacity 50ms linear; }
.placeholder { opacity: 0.5; font-style: italic; place-self: center; }
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; }
</style>
</head>
<body>
<div class="header-row">
<h2>Run Inspector</h2>
<div class="stat">status: <span id="status">idle</span></div>
<div class="stat">frame: <span id="img-iter">–</span></div>
<div class="stat">last load: <span id="img-load">–</span></div>
<div class="topbar">
<div class="brand"><span class="mark">&lt;0||0&gt;</span> Run Inspector</div>
<span id="runlabel" class="runlabel"></span>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Styled but never populated anywhere — renders empty.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in e21539c (on #23, stacked above) — added setRunLabel(runId) (buffered like warming/completion) + a runlabel webview handler, wired from the watcher's switchToRun, so #runlabel shows the active runId.

<span id="badge" class="badge idle">idle</span>
</div>
<div class="image-host">
<img id="preview-a" class="preview" alt="frame preview A" style="opacity:0" />
<img id="preview-b" class="preview" alt="frame preview B" style="opacity:0" />
<div id="placeholder" class="placeholder">No solve in progress — fire one from the Amicode chat.</div>
<div id="placeholder" class="placeholder">
<span class="mark">&lt;0||0&gt;</span>
<span class="hint">No solve in progress — fire one from the Amicode chat, or run “Replay demo run”.</span>
</div>
</div>
<div class="stats-row">
<span id="ping">opencode-backed</span>
<span>iter stream: <span id="iter">0</span> recv · <span id="hz">–</span> Hz · <span id="rec">–</span> · post→recv <span id="lat">–</span></span>
<div class="metrics">
<div class="card hero"><div class="k" id="m-obj-k">objective</div><div class="v" id="m-obj">–</div></div>
<div class="card"><div class="k">iteration</div><div class="v" id="m-iter">–</div></div>
<div class="card"><div class="k">feasibility</div><div class="v" id="m-pr">–</div></div>
<div class="card"><div class="k">optimality</div><div class="v" id="m-du">–</div></div>
</div>
<script nonce="${nonce}" src="${scriptUri}"></script>
</body>
Expand Down
Loading