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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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
MemberAuthor

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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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
MemberAuthor

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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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
MemberAuthor

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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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
MemberAuthor

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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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
MemberAuthor

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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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
MemberAuthor

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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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
MemberAuthor

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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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
MemberAuthor

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