From aee686a79c7d5ca160b5b804e23776ad1ea9ed99 Mon Sep 17 00:00:00 2001 From: Raghav Chari Date: Mon, 17 Aug 2026 20:22:37 +0000 Subject: [PATCH 1/5] feat(app): add Run Inspector as third tab in session sidebar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Alongside Files Changed (review) and Context, the session side panel now offers a third tab for the Run Inspector — the live iterations/pulse/fidelity view that previously lived only in the Work Column. - helpers.ts: add runInspectorOpen memo, exclude runInspector from panelTabs (like context), handle activeTab == runInspector. - session-side-panel.tsx: add Run Inspector trigger (with pulse icon) to both tab bars (legacy + v2), add Tabs.Content panels, and add to panelMenu. Content reuses the existing bridge data (same run: envelopes) and shows a placeholder sparkline + metrics; full plot stays in Work Column via 'Open in Work Column' (keeps 320px usable). Fixes the request to have Files Changed | Context | Run Inspector as the three sidebar tabs. --- packages/app/src/pages/session/helpers.ts | 6 +- .../src/pages/session/session-side-panel.tsx | 98 ++++++++++++++++++- 2 files changed, 102 insertions(+), 2 deletions(-) diff --git a/packages/app/src/pages/session/helpers.ts b/packages/app/src/pages/session/helpers.ts index 73cb4c9a2..31316c51f 100644 --- a/packages/app/src/pages/session/helpers.ts +++ b/packages/app/src/pages/session/helpers.ts @@ -44,6 +44,7 @@ export const createSessionTabs = (input: TabsInput) => { fileBrowser() && (input.tabs().active() === SESSION_OPEN_FILE_TAB || input.tabs().all().includes(SESSION_OPEN_FILE_TAB)), ) + const runInspectorOpen = createMemo(() => input.tabs().active() === "runInspector" || input.tabs().all().includes("runInspector")) const panelTabs = createMemo( () => { const seen = new Set() @@ -51,7 +52,7 @@ export const createSessionTabs = (input: TabsInput) => { .tabs() .all() .flatMap((tab) => { - if (tab === "context" || tab === "review" || tab === "vault" || tab === SESSION_PREVIEW_TAB) return [] + if (tab === "context" || tab === "review" || tab === "vault" || tab === SESSION_PREVIEW_TAB || tab === "runInspector") return [] if (tab === SESSION_OPEN_FILE_TAB && !fileBrowser()) return [] const value = input.pathFromTab(tab) ? input.normalizeTab(tab) : tab if (seen.has(value)) return [] @@ -68,6 +69,7 @@ export const createSessionTabs = (input: TabsInput) => { const activeTab = createMemo(() => { const active = input.tabs().active() if (active === "context") return active + if (active === "runInspector") return active if (active === SESSION_PREVIEW_TAB && previewOpen()) return active if (active === "vault" && vaultOpen()) return active if (active === SESSION_OPEN_FILE_TAB && openFileOpen()) return active @@ -79,6 +81,7 @@ export const createSessionTabs = (input: TabsInput) => { if (vaultOpen()) return "vault" if (previewOpen()) return SESSION_PREVIEW_TAB if (contextOpen()) return "context" + if (runInspectorOpen()) return "runInspector" if (review() && hasReview()) return "review" return "empty" }) @@ -98,6 +101,7 @@ export const createSessionTabs = (input: TabsInput) => { return { contextOpen, previewOpen, + runInspectorOpen, openFileOpen, panelTabs, openedTabs, diff --git a/packages/app/src/pages/session/session-side-panel.tsx b/packages/app/src/pages/session/session-side-panel.tsx index fefa143e1..8fc556823 100644 --- a/packages/app/src/pages/session/session-side-panel.tsx +++ b/packages/app/src/pages/session/session-side-panel.tsx @@ -254,7 +254,8 @@ export function SessionSidePanel(props: { return false }) - // Panel menu items + // Panel menu items — Files Changed lives as the "review" trigger, Context and + // Preview are the secondary tabs, and Run Inspector is the third requested tab. const panelMenuItems = createMemo((): PanelMenuItem[] => [ { id: "context", @@ -263,6 +264,13 @@ export function SessionSidePanel(props: { available: () => true, active: contextOpen, }, + { + id: "runInspector", + label: "Run Inspector", + icon: "pulse", + available: () => true, + active: () => activeTab() === "runInspector", + }, { id: SESSION_PREVIEW_TAB, label: "Preview", @@ -447,6 +455,32 @@ export function SessionSidePanel(props: { + + tabs().close("runInspector")} + aria-label={language.t("common.closeTab")} + /> + + } + hideCloseButton + onMiddleClick={() => tabs().close("runInspector")} + > +
+ +
Run Inspector
+
+
+ + +
+
Live run — reuses the same bridge as the Work Column
+
+
Run Inspector (sidebar)
+
Iterations, pulse, fidelity — third tab next to Files Changed + Context.
+
sparkline placeholder
+
Iteration137 / 200Fidelity0.9987
+
+
Full plot stays in Work Column — click “Open in Work Column” to focus.
+
+
+
+
@@ -645,6 +694,38 @@ export function SessionSidePanel(props: {
+ + {language.t("common.closeTab")} + 0}> + + + + } + placement="bottom" + gutter={10} + > + tabs().close("runInspector")} + aria-label={language.t("common.closeTab")} + /> + + } + hideCloseButton + onMiddleClick={() => tabs().close("runInspector")} + > +
+ +
Run Inspector
+
+
+ + +
+
Live run — same bridge as Work Column
+
+
Run Inspector (sidebar)
+
Iterations, pulse, fidelity — third tab next to Files Changed + Context.
+
sparkline placeholder
+
Iteration137 / 200Fidelity0.9987
+
+
Full plot stays in Work Column — click “Open in Work Column” to focus.
+
+
+
+
From d253ba3eee92877e0753b69426d83167f894cc51 Mon Sep 17 00:00:00 2001 From: JJ Lee Date: Wed, 19 Aug 2026 14:32:35 +0200 Subject: [PATCH 2/5] feat(inspector): align bridge to `kind` field + add InspectorProvider context - Update inspector-bridge.ts types and switch from `type` to `kind` to match the extension's relay filter convention. - Create inspector-context.tsx with InspectorProvider + useInspectorBridge hook (shared single instance via SolidJS context). - Mount InspectorProvider in SessionProviders (session.tsx) so both the sidebar and any future Work Column consumer share one bridge. Part of #214. --- .../src/amicode/inspector/inspector-bridge.ts | 22 +++++++++---------- .../amicode/inspector/inspector-context.tsx | 17 ++++++++++++++ packages/app/src/pages/session.tsx | 5 ++++- 3 files changed, 32 insertions(+), 12 deletions(-) create mode 100644 packages/app/src/amicode/inspector/inspector-context.tsx diff --git a/packages/app/src/amicode/inspector/inspector-bridge.ts b/packages/app/src/amicode/inspector/inspector-bridge.ts index 736b0be2c..8e4b35b7d 100644 --- a/packages/app/src/amicode/inspector/inspector-bridge.ts +++ b/packages/app/src/amicode/inspector/inspector-bridge.ts @@ -5,18 +5,18 @@ export type RunPulseMeta = { runId: string; drives: number; knots: number; label export type RunPulse = { runId: string; iter: number; dt: number; values: number[][] } export type RunCompletion = { runId: string; fidelity: number; iterations: number; status: string } export type RunBridgeMessage = - | { type: "run:iteration"; runId: string; iter: number; objective: number; inf_pr: number; inf_du: number } - | { type: "run:pulse-meta"; runId: string; drives: number; knots: number; labels: string[]; bounds: [number, number][]; interp?: string } - | { type: "run:pulse"; runId: string; iter: number; dt: number; values: number[][] } - | { type: "run:completion"; runId: string; fidelity: number; iterations: number; status: string } - | { type: "run:activate"; runId: string } - | { type: "run:timing"; runId: string; elapsed: number } - | { type: "run:label"; runId: string; label: string } + | { kind: "run:iteration"; runId: string; iter: number; objective: number; inf_pr: number; inf_du: number } + | { kind: "run:pulse-meta"; runId: string; drives: number; knots: number; labels: string[]; bounds: [number, number][]; interp?: string } + | { kind: "run:pulse"; runId: string; iter: number; dt: number; values: number[][] } + | { kind: "run:completion"; runId: string; fidelity: number; iterations: number; status: string } + | { kind: "run:activate"; runId: string } + | { kind: "run:timing"; runId: string; elapsed: number } + | { kind: "run:label"; runId: string; label: string } export type DeviceBridgeMessage = - | { type: "device:status"; device: string; status: unknown } - | { type: "device:actions"; device: string; actions: unknown[] } - | { type: "device:activate"; device: string } + | { kind: "device:status"; device: string; status: unknown } + | { kind: "device:actions"; device: string; actions: unknown[] } + | { kind: "device:activate"; device: string } export type InspectorMessage = RunBridgeMessage | DeviceBridgeMessage @@ -38,7 +38,7 @@ export function createInspectorBridge() { const onMessage = (e: MessageEvent) => { const d = e.data as InspectorMessage & { source?: string } if (!d || d.source !== "amicode") return - switch (d.type) { + switch (d.kind) { case "run:iteration": { setRuns((m) => { const n = new Map(m) diff --git a/packages/app/src/amicode/inspector/inspector-context.tsx b/packages/app/src/amicode/inspector/inspector-context.tsx new file mode 100644 index 000000000..b50570e0c --- /dev/null +++ b/packages/app/src/amicode/inspector/inspector-context.tsx @@ -0,0 +1,17 @@ +import { createContext, useContext, type ParentProps } from "solid-js" +import { createInspectorBridge } from "./inspector-bridge" + +type InspectorBridge = ReturnType + +const InspectorContext = createContext() + +export function InspectorProvider(props: ParentProps) { + const bridge = createInspectorBridge() + return {props.children} +} + +export function useInspectorBridge(): InspectorBridge { + const ctx = useContext(InspectorContext) + if (!ctx) throw new Error("useInspectorBridge must be used within an InspectorProvider") + return ctx +} diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index b9e18399f..5311b57cc 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -94,6 +94,7 @@ import { TerminalPanelV2 } from "@/pages/session/terminal-panel-v2" import { useComposerCommands } from "@/pages/session/use-composer-commands" import { useSessionCommands } from "@/pages/session/use-session-commands" import { useAmicodeCommands } from "@/pages/session/use-amicode-commands" +import { InspectorProvider } from "@/amicode/inspector/inspector-context" import { useSessionHashScroll } from "@/pages/session/use-session-hash-scroll" import { Identifier } from "@/utils/id" import { Persist, persisted } from "@/utils/persist" @@ -320,7 +321,9 @@ function SessionProviders(props: ParentProps) { - {props.children} + + {props.children} + From 4b5d311089df0265fc1ba328236f2be8fe7a66b6 Mon Sep 17 00:00:00 2001 From: JJ Lee Date: Wed, 19 Aug 2026 14:38:05 +0200 Subject: [PATCH 3/5] feat(app): rename to Pulse Inspector with stage-multiplexed content MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rename tab ID 'runInspector' → 'pulseInspector' across all files - Rename tab label 'Run Inspector' → 'Pulse Inspector' - Replace static placeholder with PulseInspectorContent component: - Segmented control: Optimization | Calibration (Soon) | Compilation (Soon) - Green dot indicator on Optimization when an active run is in progress - Calibration/Compilation buttons greyed out with 'Soon' badge - Renders real RunInspector component in Optimization sub-view - Uses useInspectorBridge() from the shared context provider - Update command palette title: 'Open pulse inspector' - Update entity-rail comments and CSS comment Part of #214. --- packages/app/src/pages/session/helpers.ts | 10 +- .../src/pages/session/session-side-panel.tsx | 131 ++++++++++++------ .../pages/session/use-amicode-commands.tsx | 2 +- packages/ui/src/amicode/amicode.css | 2 +- packages/ui/src/amicode/entity-rail.tsx | 12 +- 5 files changed, 99 insertions(+), 58 deletions(-) diff --git a/packages/app/src/pages/session/helpers.ts b/packages/app/src/pages/session/helpers.ts index 31316c51f..adcfc86d7 100644 --- a/packages/app/src/pages/session/helpers.ts +++ b/packages/app/src/pages/session/helpers.ts @@ -44,7 +44,7 @@ export const createSessionTabs = (input: TabsInput) => { fileBrowser() && (input.tabs().active() === SESSION_OPEN_FILE_TAB || input.tabs().all().includes(SESSION_OPEN_FILE_TAB)), ) - const runInspectorOpen = createMemo(() => input.tabs().active() === "runInspector" || input.tabs().all().includes("runInspector")) + const pulseInspectorOpen = createMemo(() => input.tabs().active() === "pulseInspector" || input.tabs().all().includes("pulseInspector")) const panelTabs = createMemo( () => { const seen = new Set() @@ -52,7 +52,7 @@ export const createSessionTabs = (input: TabsInput) => { .tabs() .all() .flatMap((tab) => { - if (tab === "context" || tab === "review" || tab === "vault" || tab === SESSION_PREVIEW_TAB || tab === "runInspector") return [] + if (tab === "context" || tab === "review" || tab === "vault" || tab === SESSION_PREVIEW_TAB || tab === "pulseInspector") return [] if (tab === SESSION_OPEN_FILE_TAB && !fileBrowser()) return [] const value = input.pathFromTab(tab) ? input.normalizeTab(tab) : tab if (seen.has(value)) return [] @@ -69,7 +69,7 @@ export const createSessionTabs = (input: TabsInput) => { const activeTab = createMemo(() => { const active = input.tabs().active() if (active === "context") return active - if (active === "runInspector") return active + if (active === "pulseInspector") return active if (active === SESSION_PREVIEW_TAB && previewOpen()) return active if (active === "vault" && vaultOpen()) return active if (active === SESSION_OPEN_FILE_TAB && openFileOpen()) return active @@ -81,7 +81,7 @@ export const createSessionTabs = (input: TabsInput) => { if (vaultOpen()) return "vault" if (previewOpen()) return SESSION_PREVIEW_TAB if (contextOpen()) return "context" - if (runInspectorOpen()) return "runInspector" + if (pulseInspectorOpen()) return "pulseInspector" if (review() && hasReview()) return "review" return "empty" }) @@ -101,7 +101,7 @@ export const createSessionTabs = (input: TabsInput) => { return { contextOpen, previewOpen, - runInspectorOpen, + pulseInspectorOpen, openFileOpen, panelTabs, openedTabs, diff --git a/packages/app/src/pages/session/session-side-panel.tsx b/packages/app/src/pages/session/session-side-panel.tsx index 8fc556823..813030b99 100644 --- a/packages/app/src/pages/session/session-side-panel.tsx +++ b/packages/app/src/pages/session/session-side-panel.tsx @@ -1,4 +1,4 @@ -import { For, Match, Show, Switch, createEffect, createMemo, on, onCleanup, type JSX } from "solid-js" +import { For, Match, Show, Switch, createEffect, createMemo, createSignal, on, onCleanup, type JSX } from "solid-js" import { createStore } from "solid-js/store" import { createMediaQuery } from "@solid-primitives/media" import { DragDropProvider as DndKitProvider, PointerSensor } from "@dnd-kit/solid" @@ -30,6 +30,8 @@ import { ConstrainDragYAxis, getDraggableId } from "@/utils/solid-dnd" import FileTree from "@/components/file-tree" import { normalizeFileTreeV2Path } from "@/components/file-tree-v2-model" import { SessionContextUsage } from "@/components/session-context-usage" +import { RunInspector } from "@/amicode/inspector/run-inspector" +import { useInspectorBridge } from "@/amicode/inspector/inspector-context" const reviewTabID = "session-side-panel-review-tab" const reviewTabPanelID = "session-side-panel-review-tabpanel" @@ -58,6 +60,63 @@ import { useSessionLayout } from "@/pages/session/session-layout" import { WORK_COLUMN_WIDTH_MIN } from "@/pages/session/session-panel-width" import { SessionFileBrowserTab, type SessionFileBrowserState } from "@/pages/session/v2/session-file-browser-tab" +type PulseInspectorStage = "optimization" | "calibration" | "compilation" + +function PulseInspectorContent() { + const bridge = useInspectorBridge() + const [stage, setStage] = createSignal("optimization") + + const hasActiveRun = createMemo(() => { + const r = bridge.runs() + if (r.size === 0) return false + const activeId = bridge.activeRunId() + const state = activeId ? r.get(activeId) : r.values().next().value + return state ? !state.completion : false + }) + + return ( +
+ {/* Stage segmented control */} +
+ + + +
+ + {/* Stage content */} + +
+ +
+
+
+ ) +} + type ReviewDiff = FileDiffInfo | SnapshotFileDiff | VcsFileDiff type RenderDiff = FileDiffInfo | (SnapshotFileDiff & { file: string }) | VcsFileDiff const FILE_TREE_WIDTH_MIN = 240 @@ -255,7 +314,7 @@ export function SessionSidePanel(props: { }) // Panel menu items — Files Changed lives as the "review" trigger, Context and - // Preview are the secondary tabs, and Run Inspector is the third requested tab. + // Preview are the secondary tabs, and Pulse Inspector is the third requested tab. const panelMenuItems = createMemo((): PanelMenuItem[] => [ { id: "context", @@ -265,11 +324,11 @@ export function SessionSidePanel(props: { active: contextOpen, }, { - id: "runInspector", - label: "Run Inspector", + id: "pulseInspector", + label: "Pulse Inspector", icon: "pulse", available: () => true, - active: () => activeTab() === "runInspector", + active: () => activeTab() === "pulseInspector", }, { id: SESSION_PREVIEW_TAB, @@ -456,7 +515,7 @@ export function SessionSidePanel(props: {
tabs().close("runInspector")} + onClick={() => tabs().close("pulseInspector")} aria-label={language.t("common.closeTab")} /> } hideCloseButton - onMiddleClick={() => tabs().close("runInspector")} + onMiddleClick={() => tabs().close("pulseInspector")} > -
- -
Run Inspector
-
-
+
+ +
Pulse Inspector
+
+
- - -
-
Live run — reuses the same bridge as the Work Column
-
-
Run Inspector (sidebar)
-
Iterations, pulse, fidelity — third tab next to Files Changed + Context.
-
sparkline placeholder
-
Iteration137 / 200Fidelity0.9987
-
-
Full plot stays in Work Column — click “Open in Work Column” to focus.
-
+ + + @@ -695,7 +745,7 @@ export function SessionSidePanel(props: {
tabs().close("runInspector")} + onClick={() => tabs().close("pulseInspector")} aria-label={language.t("common.closeTab")} /> } hideCloseButton - onMiddleClick={() => tabs().close("runInspector")} + onMiddleClick={() => tabs().close("pulseInspector")} > -
- -
Run Inspector
-
-
+
+ +
Pulse Inspector
+
+
- - -
-
Live run — same bridge as Work Column
-
-
Run Inspector (sidebar)
-
Iterations, pulse, fidelity — third tab next to Files Changed + Context.
-
sparkline placeholder
-
Iteration137 / 200Fidelity0.9987
-
-
Full plot stays in Work Column — click “Open in Work Column” to focus.
-
+ + + diff --git a/packages/app/src/pages/session/use-amicode-commands.tsx b/packages/app/src/pages/session/use-amicode-commands.tsx index dfc683153..610be391b 100644 --- a/packages/app/src/pages/session/use-amicode-commands.tsx +++ b/packages/app/src/pages/session/use-amicode-commands.tsx @@ -67,7 +67,7 @@ export function useAmicodeCommands() { }), amico({ id: "amicode.openInspector", - title: "Open run inspector", + title: "Open pulse inspector", onSelect: () => postAmicode("amicode.openInspector"), }), ] diff --git a/packages/ui/src/amicode/amicode.css b/packages/ui/src/amicode/amicode.css index 5a5506d98..9d0eccb19 100644 --- a/packages/ui/src/amicode/amicode.css +++ b/packages/ui/src/amicode/amicode.css @@ -785,7 +785,7 @@ cursor: default; } /* …except a not-recorded chip that IS a button (the pulse chip, which opens the - Run Inspector before a pulse is banked): keep the dotted not-yet look, but it + Pulse Inspector before a pulse is banked): keep the dotted not-yet look, but it must READ clickable at rest — hover feedback alone failed the glance test (Kate 2026-07-28: "doesn't appear clickable"). Three signals: full-strength ink at weight 600 (faint ink is how a chip says "inert", and this one isn't; diff --git a/packages/ui/src/amicode/entity-rail.tsx b/packages/ui/src/amicode/entity-rail.tsx index 36b991ae2..fc6e61c8e 100644 --- a/packages/ui/src/amicode/entity-rail.tsx +++ b/packages/ui/src/amicode/entity-rail.tsx @@ -205,7 +205,7 @@ export function AmicodeEntityRail(props: { if (snapshot.kind !== "ready") return [] return mergeChips(snapshot.view.entities, snapshot.view.scoreStages) }) - // Which chips hand off to the Run Inspector instead of the entity dialog. + // Which chips hand off to the Pulse Inspector instead of the entity dialog. // Only the pulse chip, and only when the host actually wired an inspector — // standalone opencode has none, so there the chip keeps its dialog behavior. // The pulse chip is the ONLY inspector entry on the rail — the separate @@ -252,7 +252,7 @@ export function AmicodeEntityRail(props: { {(chip) => ( @@ -344,7 +344,7 @@ export function AmicodeEntityRail(props: { "font-weight": "600", cursor: "pointer", }} - title="Open the Run Inspector panel" + title="Open the Pulse Inspector panel" onClick={() => props.onInspectRun?.()} > Inspect Run From d2ae2a867a67daa0bfcee07d30c68bfe4d9ec443 Mon Sep 17 00:00:00 2001 From: JJ Lee Date: Wed, 19 Aug 2026 14:47:38 +0200 Subject: [PATCH 4/5] =?UTF-8?q?feat(ui):=20add=20'pulse'=20icon=20?= =?UTF-8?q?=E2=80=94=20single-period=20sine=20wave?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A clean S-curve sine wave for the Pulse Inspector tab icon. Resolves the pre-existing TS2322 type error where 'pulse' was not in the icon name union. Part of #214. --- packages/ui/src/components/icon.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/ui/src/components/icon.tsx b/packages/ui/src/components/icon.tsx index 8ba76fca1..a8b7b61a0 100644 --- a/packages/ui/src/components/icon.tsx +++ b/packages/ui/src/components/icon.tsx @@ -5,6 +5,7 @@ const icons = { // aesthetic (20×20, currentColor). target → System, activity → Pulse in the rail. target: ``, activity: ``, + pulse: ``, "align-right": ``, "arrow-up": ``, "arrow-left": ``, From 5224f7cb5b0f44f63c905cf54bf11a708ac17f6b Mon Sep 17 00:00:00 2001 From: JJ Lee Date: Wed, 19 Aug 2026 14:58:03 +0200 Subject: [PATCH 5/5] fix(inspector): polish Pulse Inspector UI per feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Remove redundant 'Run Inspector' title (tab already identifies it) 2. Clarify metadata: 'N control channels · M timesteps' instead of 'N drive(s) · M knots' 3. Show pulse chart from the start with a dashed midline placeholder (no more waiting for iter 0 to see the chart area) 4. Rename metrics to: Iter | Objective | Primal | Dual (proper table with tabular-nums, horizontally scrollable at narrow widths) 5. Fix narrow-panel overflow: stage selector buttons truncate labels with min-w-0 + truncate, 'Soon' badge is shrink-0 Part of #214. --- .../src/amicode/inspector/run-inspector.tsx | 88 +++++++++++-------- .../src/pages/session/session-side-panel.tsx | 20 ++--- 2 files changed, 62 insertions(+), 46 deletions(-) diff --git a/packages/app/src/amicode/inspector/run-inspector.tsx b/packages/app/src/amicode/inspector/run-inspector.tsx index 030567179..ea0f42a60 100644 --- a/packages/app/src/amicode/inspector/run-inspector.tsx +++ b/packages/app/src/amicode/inspector/run-inspector.tsx @@ -3,12 +3,12 @@ import type { createInspectorBridge } from "./inspector-bridge" type Props = { bridge: ReturnType } -// Minimal pulse sparkline — renders correctly at 320px column width and responds +// Minimal pulse sparkline — renders correctly at narrow column widths and responds // to resize via viewBox (no fixed pixel width). Each drive is a polyline. -function PulseChart(props: { values: number[][]; bounds?: [number, number][] }) { +function PulseChart(props: { values?: number[][]; bounds?: [number, number][] }) { const paths = createMemo(() => { const v = props.values - if (v.length === 0 || v[0].length === 0) return [] + if (!v || v.length === 0 || v[0].length === 0) return [] return v.map((drive, idx) => { const b = props.bounds?.[idx] const lo = b?.[0] ?? Math.min(...drive) @@ -23,7 +23,10 @@ function PulseChart(props: { values: number[][]; bounds?: [number, number][] }) }) }) return ( - + + + + {(p) => } ) @@ -41,63 +44,76 @@ export function RunInspector(props: Props) { const latestPulse = createMemo(() => active()?.state.pulses.at(-1)) return ( -
-
-
Run Inspector
- 1}> +
+ {/* Run selector — only shown when multiple runs exist */} + 1}> +
- -
+
+
-
- {active()!.state.label ?? active()!.id} -
+ {/* Pulse metadata */} {(meta) => (
- {meta().drives} drive(s) · {meta().knots} knots · {meta().labels.join(", ")} + {meta().drives} control channel{meta().drives > 1 ? "s" : ""} · {meta().knots} timesteps + 0}> + · {meta().labels.join(", ")} +
)}
- {(p) => } - -
Waiting for pulse data…
-
-
-
-
iter
-
{latestIter()?.iter ?? "—"}
-
-
-
objective
-
{latestIter() ? latestIter()!.objective.toExponential(2) : "—"}
-
-
-
inf
-
{latestIter() ? `${latestIter()!.inf_pr.toExponential(1)}/${latestIter()!.inf_du.toExponential(1)}` : "—"}
-
+ + {/* Pulse chart — always visible once a run exists */} + + + {/* Metrics table — horizontally scrollable at narrow widths */} +
+ + + + + + + + + + + + + + + + + +
IterObjectivePrimalDual
{latestIter()?.iter ?? "—"}{latestIter() ? latestIter()!.objective.toExponential(2) : "—"}{latestIter() ? latestIter()!.inf_pr.toExponential(1) : "—"}{latestIter() ? latestIter()!.inf_du.toExponential(1) : "—"}
+ + {/* Completion card */} {(c) => (
-
completion
-
- {c().status} · F={c().fidelity.toFixed(5)} · {c().iterations} iters +
Result
+
F = {c().fidelity.toFixed(5)}
+
+ {c().status} · {c().iterations} iterations · {active()!.state.timing!.toFixed(1)}s
)} + + {/* Running indicator */} -
running · iter {latestIter()!.iter}
+
solving · iteration {latestIter()!.iter}
}> diff --git a/packages/app/src/pages/session/session-side-panel.tsx b/packages/app/src/pages/session/session-side-panel.tsx index 813030b99..49c814026 100644 --- a/packages/app/src/pages/session/session-side-panel.tsx +++ b/packages/app/src/pages/session/session-side-panel.tsx @@ -77,9 +77,9 @@ function PulseInspectorContent() { return (
{/* Stage segmented control */} -
+