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
118 changes: 80 additions & 38 deletions packages/extension/media/ui/views/inspector.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
// Inspector view — pure composition of atoms/components + layout selectors.
// Owns the message protocol (runlabel / iteration / warming / completed /
// pulsemeta / pulse / ping) shared with run_inspector.ts.
// Inspector view — per-run panes (1.3) over the post-#67 native pulse protocol.
// Owns the runId-keyed message protocol shared with run_inspector.ts:
// runlabel · iteration · warming · completed · pulsemeta · pulse (+ activate/ping)
// every message carries `runId`; the view keeps ONE `panel` per runId and shows
// the ACTIVE one (host sends `activate`). A late/throttled message for a
// background run updates ITS pane only — never the visible pane's badge/plot
// (no cross-talk; #67's plot-only-pulse property preserved per pane).
//
// The live pulse renders NATIVELY from pulse data (#66) — the per-iter PNGs
// remain run-dir/archival artifacts but are no longer displayed. A run whose
// log carries no pulse lines shows a hint instead of a plot.
// The live pulse renders NATIVELY from pulse data (#66); per-iter PNGs remain
// archival. Pane MARKUP is the design lane (UX4 #49) — this is the plumbing
// reshape (freeze 2: the runId-keyed protocol, not the DOM).

import { defineStyle } from "../style";
import { mark } from "../atoms/icon";
Expand All @@ -17,6 +21,10 @@ defineStyle("inspector-view", `
body { margin: 0; height: 100vh; font-family: var(--text-font);
font-size: var(--text-body); color: var(--vscode-foreground); }
.brand { font-weight: 600; }
/* Panes carry .stack (display:flex from layout.css). Use two-class selectors so
these win over .stack on specificity — not on stylesheet order. */
.pane:not(.active) { display: none; }
.pane.active { display: flex; }
`);

const IDLE_HINT = "No solve in progress — fire one from the Amicode chat, or run “Replay demo run”.";
Expand All @@ -28,7 +36,15 @@ export interface InspectorView {
onMessage(msg: unknown): void;
}

export function createInspectorView(post: (msg: unknown) => void): InspectorView {
/** One run's pane — the β single-run view, now instanced per runId. No
* single-run globals: everything (status, plot, metrics, gotPulse) is closed
* over here, so N panes never share state. */
interface Panel {
el: HTMLElement;
apply(msg: Record<string, unknown>): void;
}

function createPanel(): Panel {
const status = pill("idle");
const runLabel = text("mono small dim");
const pulse = pulseplot(IDLE_HINT);
Expand All @@ -37,10 +53,7 @@ export function createInspectorView(post: (msg: unknown) => void): InspectorView
const feasibility = metric("feasibility");
const optimality = metric("optimality");
const metrics = [hero, iteration, feasibility, optimality];

/** Whether the current run has delivered pulse data — decides the
* completed-without-data hint. Reset on warming (a NEW run started). */
let gotPulse = false;
let gotPulse = false; // per-pane: decides the completed-without-data hint

const brand = document.createElement("div");
brand.className = "row gap-sm brand";
Expand All @@ -56,67 +69,96 @@ export function createInspectorView(post: (msg: unknown) => void): InspectorView
grid.append(...metrics.map((m) => m.el));

const el = document.createElement("div");
el.className = "stack pad-lg scroll-y";
el.className = "pane stack pad-lg scroll-y";
el.style.height = "100vh";
el.append(topbar, pulse.el, grid);

return {
el,
onMessage(msg: any): void {
if (!msg || typeof msg !== "object") return;
apply(msg: Record<string, unknown>): void {
switch (msg.type) {
case "ping": {
post({ type: "pong", seq: msg.seq, t0: msg.t0 });
break;
}
case "runlabel": {
case "runlabel":
runLabel.set(String(msg.text ?? ""));
break;
}
case "iteration": {
case "iteration":
hero.label("objective");
hero.value((msg.f_val as number).toExponential(4));
iteration.value(String(msg.iter));
feasibility.value((msg.eq_viol as number).toExponential(2));
optimality.value((msg.kkt_error as number).toExponential(2));
status.set("running", "running");
break;
}
case "warming": {
// A NEW run started but has no data yet — clear the previous run's
// plot + stats so the old pulse doesn't linger while the new solve
// compiles/warms up.
case "warming":
gotPulse = false;
pulse.waiting(WARMING_HINT);
for (const m of metrics) m.clear();
hero.label("objective");
status.set("running", "warming up");
break;
}
case "completed": {
// Authoritative terminal state from the watcher (FINISHED on disk).
const ok = msg.status === "completed";
status.set(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") {
hero.label("fidelity");
hero.value((msg.fidelity as number).toFixed(5));
}
if (!gotPulse) pulse.waiting(NO_DATA_HINT); // old runs / non-emitting scripts
if (!gotPulse) pulse.waiting(NO_DATA_HINT);
break;
}
case "pulsemeta": {
pulse.meta({ drives: msg.drives, knots: msg.knots, labels: msg.labels, bounds: msg.bounds });
case "pulsemeta":
pulse.meta({ drives: msg.drives as number, knots: msg.knots as number, labels: msg.labels as string[], bounds: msg.bounds as [number, number][] });
break;
}
case "pulse": {
// Plot-only: never touches the status pill (a throttled record can
// legally land after "completed"; the badge must not regress).
case "pulse":
// Plot-only (never the badge): a throttled record can land after
// "completed", and for a background run must not touch the visible pane.
gotPulse = true;
pulse.update({ iter: msg.iter, dt: msg.dt, values: msg.values });
pulse.update({ iter: msg.iter as number, dt: msg.dt as number, values: msg.values as number[][] });
break;
}
}
},
};
}

export function createInspectorView(post: (msg: unknown) => void): InspectorView {
const panels = new Map<string, Panel>();
let active: string | undefined;

// Shell holds the panes; an empty-state hint shows until the first run.
const empty = text("dim", IDLE_HINT);
empty.el.className = "pad-lg dim";

const el = document.createElement("div");
el.style.height = "100vh";
el.append(empty.el);

const panelFor = (runId: string): Panel => {
let p = panels.get(runId);
if (!p) {
p = createPanel();
panels.set(runId, p);
el.append(p.el);
}
return p;
};

const activate = (runId: string): void => {
active = runId;
empty.el.style.display = "none";
for (const [id, p] of panels) p.el.classList.toggle("active", id === runId);
if (!panels.has(runId)) panelFor(runId).el.classList.add("active"); // pane may arrive before data
};

return {
el,
onMessage(msg: any): void {
if (!msg || typeof msg !== "object") return;
if (msg.type === "ping") { post({ type: "pong", seq: msg.seq, t0: msg.t0 }); return; }
if (msg.type === "activate") { if (typeof msg.runId === "string") activate(msg.runId); return; }
// Every other message is runId-keyed → route to that run's pane. A message
// with no runId (legacy/none) falls back to the active pane.
const runId = typeof msg.runId === "string" ? msg.runId : active;
if (!runId) return;
panelFor(runId).apply(msg as Record<string, unknown>);
},
};
}
3 changes: 2 additions & 1 deletion packages/extension/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "amicode-v2",
"displayName": "Amicode v2",
"description": "Amico research IDE \u2014 opencode-backed chat + native VS Code surfaces for vault, catalog, and live solve inspection.",
"description": "Amico research IDE opencode-backed chat + native VS Code surfaces for vault, catalog, and live solve inspection.",
"version": "0.0.1",
"publisher": "harmoniqs",
"license": "Apache-2.0",
Expand Down Expand Up @@ -149,6 +149,7 @@
"@types/vscode": "^1.95.0",
"@vscode/vsce": "^3.2.0",
"esbuild": "^0.24.0",
"happy-dom": "^20.10.6",
"smol-toml": "^1.3.0",
"typescript": "^5.6.0",
"vitest": "^2.1.0"
Expand Down
4 changes: 4 additions & 0 deletions packages/extension/src/run_dir_reader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,10 @@ export class PulseStream {
}

export interface IterRecord { iter: number; f_val: number; inf_pr: number; inf_du: number }
/** Terminal completion, built by readTerminalState and flowed WHOLE to every
* consumer (never exploded into positional args mid-pipe) — the #84 funnel.
* Additive contract fields join HERE + readTerminalState and reach all paths
* by construction: #81's `formulation?` next, then #64 hashing / #41 usage. */
export interface RunCompletion { runId: string; runDir: string; status: RunStatus; fidelity?: number }
export interface PromoteInfo { runId: string; runDir: string; fidelity: number }

Expand Down
Loading
Loading