diff --git a/packages/extension/esbuild.config.mjs b/packages/extension/esbuild.config.mjs index 22909f12..2687a5a9 100644 --- a/packages/extension/esbuild.config.mjs +++ b/packages/extension/esbuild.config.mjs @@ -74,19 +74,6 @@ const targets = [ minify: false, logLevel: "info", }, - // catalog-card dev preview webview bundle (#47 scaffold) - { - entryPoints: ["src/catalog_card_webview.ts"], - bundle: true, - platform: "browser", - target: "es2022", - format: "iife", - outfile: "dist/catalog_card_webview.js", - sourcemap: true, - minify: false, - logLevel: "info", - loader: { ".svg": "text" }, - }, // Chat Deck webview bundle — pane-manager shell (src/deck/shell.ts + model) { entryPoints: ["src/deck/shell.ts"], diff --git a/packages/extension/media/ui/components/catalogcard.ts b/packages/extension/media/ui/components/catalogcard.ts deleted file mode 100644 index 057ea628..00000000 --- a/packages/extension/media/ui/components/catalogcard.ts +++ /dev/null @@ -1,287 +0,0 @@ -// Catalog entry card (#47, UX2) — the atom of the catalog. Krishna's p5 -// sketch, top to bottom: chip header → pulse plot (with a quiet plot-attached -// action row — the resume hand-off, unlabeled by design) → metadata / -// pulse-data / high-level-metrics panels → model catalog (sibling chips). -// -// Data contract: `entry` is schema-true (catalog-entry.schema.json fields -// verbatim); `entry.proposed` is the clearly-separated extension block whose -// fields render with the "proposed" treatment (they are the feedback artifact -// for the Krishna field-selection questions — Jack's lane to resolve); -// `pulse` is the optional hydrated block sharing pulseplot's meta/values -// shapes (models a CatalogStore hydrating pulse_path on open). - -import { defineStyle } from "../style"; -import { text } from "../atoms/text"; -import { metric } from "./metric"; -import { chip, type ChipFields } from "./chip"; -import { pulseplot, type PulsePlotMeta, type PulsePlotRecord } from "./pulseplot"; - -defineStyle( - "catalogcard", - ` - .catalogcard { display: flex; flex-direction: column; gap: var(--space-md); - padding: var(--space-lg); min-width: 0; - background: var(--bg-box); - border: var(--border-width) solid var(--border-color); - border-radius: var(--border-radius); } - .catalogcard .cc-head { display: flex; align-items: center; gap: var(--space-md); flex-wrap: wrap; } - .catalogcard .cc-plot { min-height: 200px; display: flex; } - .catalogcard .cc-panels { display: grid; gap: var(--space-sm); - grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); } - .catalogcard .cc-panel { border: var(--border-width) solid var(--border-color); - border-radius: var(--border-radius); - padding: var(--space-sm) var(--space-md); - display: flex; flex-direction: column; gap: var(--space-xs); } - .catalogcard .cc-kv { display: flex; justify-content: space-between; gap: var(--space-md); - font-size: var(--text-small); min-width: 0; } - .catalogcard .cc-kv .v { font-family: var(--text-mono); overflow: hidden; - text-overflow: ellipsis; white-space: nowrap; } - .catalogcard .proposed { border-bottom: 1px dashed var(--color-dim); opacity: 0.75; } - .catalogcard .metric.proposed { border-style: dashed; border-bottom: var(--border-width) dashed var(--color-accent-ink); opacity: 0.8; } - .catalogcard .cc-metrics { display: flex; flex-wrap: wrap; gap: var(--space-sm); } - .catalogcard .cc-metrics .metric { flex: 1 1 120px; min-width: 0; } - .catalogcard .cc-siblings { display: flex; gap: var(--space-sm); overflow-x: auto; - padding-bottom: var(--space-xs); } - .catalogcard .cc-actions { display: flex; gap: var(--space-sm); margin-left: auto; } - .catalogcard .cc-actions button { font-family: var(--text-font); font-size: var(--text-small); - padding: var(--space-xs) var(--space-md); cursor: pointer; - border: 1px solid var(--vscode-button-border, transparent); - border-radius: 2px; - color: var(--vscode-button-secondaryForeground, var(--vscode-foreground)); - background: var(--vscode-button-secondaryBackground, var(--bg-box)); } - .catalogcard .cc-actions button:hover { background: var(--vscode-button-secondaryHoverBackground, var(--bg-box)); } -`, -); - -/** Schema-true catalog-entry fields (catalog-entry.schema.json v1). */ -export interface CatalogEntry { - schema_version: string; - run_id: string; - lab_id: string; - fidelity: number; - pulse_path: string; - gate?: string; - created_at?: string; - params?: Record; - /** NOT in the schema — proposed extensions, rendered visibly marked. */ - proposed?: { - tags?: string[]; - index?: number; - iterations?: number; - wall_seconds?: number; - /** User-assigned system name — human identity over machine family. */ - system_name?: string; - }; -} - -export interface CardPulse { - meta: PulsePlotMeta; - record: PulsePlotRecord; -} - -export interface CatalogCard { - el: HTMLDivElement; -} - -/** The pulse actions — the save → tune → warm-start → promote ladder. - * Tune: refine THIS pulse (resume the chat interview with this run as - * context). Warm-Start: seed a NEW solve from this pulse's trajectory. - * Promote: send the pulse toward hardware (Intonato path; stub until then). - * Vocabulary note for the field-selection round: "promote" also means - * promote-to-catalog elsewhere (the prompt that opens this card). */ -export const PULSE_ACTIONS: ReadonlyArray<{ id: string; label: string }> = [ - { id: "tune", label: "Tune" }, - { id: "warmstart", label: "Warm-Start" }, - { id: "promote", label: "Promote" }, -]; - -export interface CatalogCardOpts { - pulse?: CardPulse; - siblings?: ChipFields[]; - /** Wired by the host; the row renders only when provided. */ - onAction?: (id: string) => void; -} - -export function catalogcard(entry: CatalogEntry, opts: CatalogCardOpts = {}): CatalogCard { - const el = document.createElement("div"); - el.className = "catalogcard"; - - // 1 — chip header - const head = document.createElement("div"); - head.className = "cc-head"; - // User-named system wins the identity slot (proposed-marked via `tag` - // styling in the chip); the derived family is the fallback. - const named = entry.proposed?.system_name; - head.append( - chip({ gate: entry.gate, system: named ?? systemDescriptor(entry.params), ...entry.proposed }).el, - text("mono small dim", entry.run_id).el, - ); - // Pulse actions live in the header, right-justified — with the identity, - // acting on the pulse it names. VS Code button styling, uniform secondary. - if (opts.onAction) { - const actions = document.createElement("div"); - actions.className = "cc-actions"; - for (const a of PULSE_ACTIONS) { - const b = document.createElement("button"); - b.textContent = a.label; - b.addEventListener("click", () => opts.onAction!(a.id)); - actions.append(b); - } - head.append(actions); - } - el.append(head); - - // 2 — pulse plot (hydrated; degrades to pulseplot's empty state) - const plotHost = document.createElement("div"); - plotHost.className = "cc-plot"; - const plot = pulseplot("Pulse not hydrated — entry carries pulse_path only."); - if (opts.pulse) { - plot.meta(opts.pulse.meta); - plot.update(opts.pulse.record); - } - plotHost.append(plot.el); - el.append(plotHost); - - // 3 — panels: metadata · pulse data · high-level metrics - const panels = document.createElement("div"); - panels.className = "cc-panels"; - panels.append( - panel("metadata", [ - kv("run", entry.run_id), - kv("system", systemDescriptor(entry.params) ?? "—"), - kv("gate", entry.gate ?? "—"), - // the user-assigned name (chip identity) vs the derived family above - ...(entry.proposed?.system_name ? [kv("name", entry.proposed.system_name, true)] : []), - ...(entry.proposed?.tags?.length ? [kv("tags", entry.proposed.tags.join(", "), true)] : []), - kv("created", entry.created_at ?? "—"), - // solver telemetry — provenance, not pulse quality (proposed fields) - ...(entry.proposed?.iterations !== undefined ? [kv("iterations", String(entry.proposed.iterations), true)] : []), - ...(entry.proposed?.wall_seconds !== undefined - ? [kv("wall", `${entry.proposed.wall_seconds.toFixed(0)}s`, true)] - : []), - ]), - panel("pulse data", [ - kv("pulse", entry.pulse_path.split("/").slice(-2).join("/")), - ...Object.entries(entry.params ?? {}).map(([k, v]) => kv(k, String(v))), - ]), - metricsPanel(entry, opts.pulse), - ); - el.append(panels); - - // 4 — model catalog: sibling chips - if (opts.siblings?.length) { - el.append(text("label-k", "model catalog").el); - const sibs = document.createElement("div"); - sibs.className = "cc-siblings"; - for (const s of opts.siblings) sibs.append(chip(s).el); - el.append(sibs); - } - - return { el }; -} - -/** Hamiltonian-based identity for the chip: the system FAMILY only - * (params.system, e.g. "transmon"). Level counts are modeling resolution, - * not identity — the same device simulated at 3 vs 4 levels is one system — - * so they stay in the pulse-data panel's params rows, not the chip. The - * real structured Hamiltonian-identity key (family + subsystem topology + - * parameters) is a Phase-3 CatalogStore schema decision. */ -function systemDescriptor(params?: Record): string | undefined { - const sys = params?.system; - return typeof sys === "string" ? sys : undefined; -} - -function panel(title: string, rows: HTMLElement[]): HTMLDivElement { - const p = document.createElement("div"); - p.className = "cc-panel"; - p.append(text("label-k", title).el, ...rows); - return p; -} - -function kv(k: string, v: string, proposed = false): HTMLDivElement { - const row = document.createElement("div"); - row.className = "cc-kv"; - row.append(text("dim", k).el, text(proposed ? "v proposed" : "v", v).el); - return row; -} - -function metricsPanel(entry: CatalogEntry, pulse?: CardPulse): HTMLDivElement { - const p = document.createElement("div"); - p.className = "cc-panel"; - p.append(text("label-k", "high-level metrics").el); - - // All four wear the same hero metric styling; provisional ones (definition - // or data source still a domain-owner decision) get a dashed border. - const heroCard = (label: string, value: string, proposed = false): HTMLDivElement => { - const m = metric(label, { variant: "hero" }); - m.value(value); - if (proposed) m.el.classList.add("proposed"); - return m.el; - }; - - const row = document.createElement("div"); - row.className = "cc-metrics"; - p.append(row); - - row.append(heroCard("fidelity", entry.fidelity.toFixed(5))); - - // Gate time — real: params.T (template units are ns). - const T = entry.params?.T; - if (typeof T === "number") row.append(heroCard("gate time", `${T} ns`)); - - // Spectral bandwidth — computed from the hydrated knots: the frequency - // containing 95% of the pulse's AC power (DC/mean removed; ZOH samples at - // 1/dt). Definitional choices (threshold, DC handling, per-drive max) are - // domain-owner calls → proposed-marked, definition in the label. Units GHz - // for the template's ns time base. - const bw = spectralBandwidth(pulse); - if (bw !== undefined) row.append(heroCard("bandwidth (95% pwr)", `${bw.toPrecision(3)} GHz`, true)); - - // Robustness — fidelity sensitivity to parameter error. Needs perturbed - // rollouts recorded at solve time; NOT derivable from saved artifacts, so - // it renders as an empty proposed card (intent, not an invented number). - row.append(heroCard("robustness", "—", true)); - - return p; -} - -/** 95%-power occupied bandwidth, max across drives: smallest f such that the - * cumulative one-sided power spectrum (DC removed) reaches 95% of total. - * Plain O(N²) DFT — knots are ≤ a few hundred points. Undefined without - * pulse data, a usable dt, or any AC power. */ -function spectralBandwidth(pulse?: CardPulse): number | undefined { - if (!pulse || !(pulse.record.dt > 0)) return undefined; - const dt = pulse.record.dt; - let worst: number | undefined; - for (const drive of pulse.record.values) { - const n = drive.length; - if (n < 2 || drive.some((v) => !Number.isFinite(v))) continue; - const mean = drive.reduce((a, b) => a + b, 0) / n; - const x = drive.map((v) => v - mean); - const half = Math.floor(n / 2); - const power: number[] = []; - for (let k = 1; k <= half; k++) { - // one-sided, DC excluded - let re = 0, - im = 0; - for (let t = 0; t < n; t++) { - const ph = (-2 * Math.PI * k * t) / n; - re += x[t] * Math.cos(ph); - im += x[t] * Math.sin(ph); - } - power.push(re * re + im * im); - } - const total = power.reduce((a, b) => a + b, 0); - if (total <= 0) continue; - let cum = 0; - for (let k = 0; k < power.length; k++) { - cum += power[k]; - if (cum >= 0.95 * total) { - const f = (k + 1) / (n * dt); // bin k+1 → frequency - if (worst === undefined || f > worst) worst = f; - break; - } - } - } - return worst; -} diff --git a/packages/extension/package.json b/packages/extension/package.json index 99b75967..d69ff97b 100644 --- a/packages/extension/package.json +++ b/packages/extension/package.json @@ -61,11 +61,6 @@ "id": "amicode.armonia", "name": "Armonia", "type": "tree" - }, - { - "id": "amicode.catalog", - "name": "Catalog", - "type": "tree" } ] }, @@ -138,10 +133,6 @@ "command": "amicode.distillNow", "title": "Amicode: Distill now (update my memory)" }, - { - "command": "amicode.catalog.remove", - "title": "Remove from Catalog" - }, { "command": "amicode.fleet.repair", "title": "Amicode: Fleet — Repair (guard + tunnel)" @@ -311,19 +302,6 @@ "when": "view == amicode.armonia", "group": "navigation" } - ], - "view/item/context": [ - { - "command": "amicode.catalog.remove", - "when": "view == amicode.catalog && viewItem == amicodeCatalogEntry", - "group": "7_modification" - } - ], - "commandPalette": [ - { - "command": "amicode.catalog.remove", - "when": "false" - } ] } }, diff --git a/packages/extension/src/catalog_card_shell.ts b/packages/extension/src/catalog_card_shell.ts deleted file mode 100644 index aa6f36e7..00000000 --- a/packages/extension/src/catalog_card_shell.ts +++ /dev/null @@ -1,145 +0,0 @@ -// Catalog-card shell (#47, v1) — hosts the catalogcard component in a webview. -// -// The card appears only through the save-to-catalog flow: a converged run's -// promote prompt (live solves via the watcher, demo replays via the replay -// command) → "Save to catalog" → `amicode.catalogCard.open` with the run dir. -// The entry is hydrated from the REAL run artifacts: run.toml (identity), -// result.toml (fidelity, params — params.gate/params.system lifted to the -// entry's top level; iterations/wall → the proposed block), and the run.log -// pulse lines (meta + newest record → the card's plot). Not palette- -// contributed — there is no card without a run to save. -// -// Persistence note: opening a card stores nothing durable; the session -// catalog (trees.ts) records POINTERS only. Where promoted artifacts persist -// is the open Phase-3 CatalogStore design (Q91/Q92). - -import * as fs from "node:fs"; -import * as path from "node:path"; -import * as vscode from "vscode"; -import { PulseStream, readTomlSafe, type PulseEvent } from "./run_dir_reader"; - -export function registerCatalogCard(ctx: vscode.ExtensionContext): void { - const open = new Map(); // run_id → live panel - ctx.subscriptions.push( - vscode.commands.registerCommand( - "amicode.catalogCard.open", - (runDir: string, systemName?: string, tags?: string[]) => { - const data = hydrateFromRunDir(runDir, systemName, tags); - if (!data) { - void vscode.window.showErrorMessage( - "Amicode: cannot build a catalog entry — run dir is missing run.toml/result.toml.", - ); - return; - } - const key = String(data.entry.run_id); - const existing = open.get(key); - if (existing) { - existing.reveal(vscode.ViewColumn.One); - return; - } // re-focus, don't re-create - const panel = vscode.window.createWebviewPanel( - "amicode.catalogCard", - `Catalog: ${data.entry.run_id}`, - vscode.ViewColumn.One, - { - enableScripts: true, - localResourceRoots: [ - vscode.Uri.joinPath(ctx.extensionUri, "dist"), - vscode.Uri.joinPath(ctx.extensionUri, "media"), - ], - }, - ); - open.set(key, panel); - panel.onDidDispose(() => open.delete(key), null, ctx.subscriptions); - panel.webview.onDidReceiveMessage((m) => { - if (m?.type !== "whatnext") return; - // Wire the save → tune → warm-start ladder to the CHAT (the agent owns the - // solve workflow): stage a concrete prompt on the clipboard and open the - // chat. Promote (team catalog) stays honestly unwired until Phase 3. - const e = data.entry; - const ident = `${e.gate ?? "gate"} on ${e.system ?? String(e.lab_id)} (run ${e.run_id}, F=${Number(e.fidelity).toFixed(5)})`; - if (m.id === "warmstart" || m.id === "tune") { - const prompt = - m.id === "warmstart" - ? `Warm-start a new solve from the banked pulse of ${ident}: load ${runDir}/pulse.jld2 as the initial trajectory (load_traj), keep the same formulation, and run it.` - : `Tune the solve for ${ident}: start from ${runDir}/pulse.jld2, keep the formulation but ask me which weights/params (Q, R, T, N, max_iter) to adjust before launching.`; - void vscode.env.clipboard.writeText(prompt).then(async () => { - await vscode.commands.executeCommand("amicode.openChat"); - void vscode.window.showInformationMessage( - `Amicode: ${m.id} prompt copied — paste into the chat to launch.`, - ); - }); - } else if (m.id === "promote") { - void vscode.window.showInformationMessage( - "Amicode: team-catalog promotion isn't wired yet (Phase 3) — the pulse stays in your local bank.", - ); - } - }); - const uri = (...p: string[]) => panel.webview.asWebviewUri(vscode.Uri.joinPath(ctx.extensionUri, ...p)); - const nonce = Math.random().toString(36).slice(2); - panel.webview.html = ` - - - - - - - -`; - }, - ), - ); -} - -/** Build the card's data from real run artifacts. Returns undefined when the - * dir lacks the promote-shaped basics. Shape mirrors the webview's CARD_DATA. - * Exported for tests. */ -export function hydrateFromRunDir( - runDir: string, - systemName?: string, - tags?: string[], -): { entry: Record; pulse?: { meta: unknown; record: unknown } } | undefined { - const manifest = readTomlSafe(path.join(runDir, "run.toml")); - const result = readTomlSafe(path.join(runDir, "result.toml")); - if (!manifest || !result) return undefined; - - const params = (result.params ?? {}) as Record; - const entry: Record = { - schema_version: "1", - run_id: String(manifest.run_id ?? path.basename(runDir)), - lab_id: String(manifest.lab_id ?? "default"), - gate: typeof params.gate === "string" ? params.gate : undefined, - fidelity: Number(result.fidelity ?? 0), - pulse_path: path.join(runDir, "pulse.jld2"), - created_at: manifest.created_at, - params, - // Not in catalog-entry.schema.json — rendered visibly marked. The - // user-assigned system name is the sharpest schema question here: human - // identity ("Emerald-Q3") vs machine params (family/levels/δ). - proposed: { - system_name: systemName, - tags, - iterations: result.iterations, - wall_seconds: result.wall_seconds, - }, - }; - - // Pulse plot from the run's own AMICODE_PULSE lines: meta + newest record. - let pulse: { meta: unknown; record: unknown } | undefined; - try { - const stream = new PulseStream(); - let meta: PulseEvent | undefined, newest: PulseEvent | undefined; - for (const line of fs.readFileSync(path.join(runDir, "run.log"), "utf8").split("\n")) { - const e = stream.onLine(line); - if (e?.type === "meta") { - meta = e; - newest = undefined; - } else if (e?.type === "record") newest = e; - } - if (meta?.type === "meta" && newest?.type === "record") pulse = { meta: meta.meta, record: newest.record }; - } catch { - /* no run.log → card renders the not-hydrated state */ - } - - return { entry, pulse }; -} diff --git a/packages/extension/src/catalog_card_webview.ts b/packages/extension/src/catalog_card_webview.ts deleted file mode 100644 index ab937b8b..00000000 --- a/packages/extension/src/catalog_card_webview.ts +++ /dev/null @@ -1,72 +0,0 @@ -// Catalog-card webview entry (#47) — mounts the card from host-injected data -// (window.__CARD_DATA__, hydrated from the real run dir by the save-to-catalog -// flow); the baked fixture below is the fallback for hostless debugging. - -import { applyBrandAccent } from "../media/ui/brand_accent"; -import { catalogcard, type CatalogEntry, type CardPulse } from "../media/ui/components/catalogcard"; - -applyBrandAccent(); // theme-calculated Harmoniqs yellow (brand-wide contract) - -declare function acquireVsCodeApi(): { postMessage(msg: unknown): void }; -declare global { - interface Window { - __CARD_DATA__?: { entry: CatalogEntry; pulse?: CardPulse }; - } -} - -// Grounded in packages/schema/test/fixtures/valid/catalog-entry.toml; the -// `proposed` block is NOT schema — it renders visibly marked (field-selection -// feedback artifact for Krishna/Andrew). -const ENTRY: CatalogEntry = { - schema_version: "1", - run_id: "r20260615-000000Z-ab12", - lab_id: "default", - gate: "X", - fidelity: 0.99995, - pulse_path: "/Users/researcher/.amico/runs/default/r20260615-000000Z-ab12/pulse.jld2", - created_at: "2026-06-15T00:00:00Z", - params: { system: "transmon", levels: 3, T: 10.0, N: 50, drive_max: 0.2 }, - proposed: { tags: ["smooth"], index: 1, iterations: 60, wall_seconds: 41 }, -}; - -const PULSE: CardPulse = { - meta: { - drives: 2, - knots: 25, - labels: ["u_1", "u_2"], - bounds: [ - [-0.2, 0.2], - [-0.2, 0.2], - ], - }, - record: { - iter: 60, - dt: 0.4, - values: [ - [ - 0.012, 0.048, 0.096, 0.141, 0.172, 0.184, 0.176, 0.149, 0.108, 0.058, 0.006, -0.043, -0.084, -0.113, -0.128, - -0.127, -0.111, -0.083, -0.047, -0.008, 0.028, 0.055, 0.068, 0.062, 0.033, - ], - [ - -0.021, -0.052, -0.079, -0.096, -0.1, -0.089, -0.065, -0.031, 0.009, 0.049, 0.084, 0.109, 0.121, 0.118, 0.1, - 0.07, 0.032, -0.009, -0.048, -0.079, -0.098, -0.102, -0.089, -0.061, -0.024, - ], - ], - }, -}; - -const SIBLINGS = [ - { gate: "X", system: "transmon", tags: ["fast"], index: 2 }, - { gate: "X", system: "transmon", tags: ["robust"], index: 3 }, - { gate: "H", system: "transmon", tags: ["smooth"], index: 7 }, -]; - -const vscodeApi = acquireVsCodeApi(); -const injected = window.__CARD_DATA__; -const card = catalogcard(injected?.entry ?? ENTRY, { - pulse: injected ? injected.pulse : PULSE, - siblings: injected ? [] : SIBLINGS, // sibling entries need a store — none yet on the real path - onAction: (id) => vscodeApi.postMessage({ type: "whatnext", id }), -}); -document.body.style.padding = "16px"; -document.body.append(card.el); diff --git a/packages/extension/src/extension.ts b/packages/extension/src/extension.ts index 95760c3e..c7c6f95a 100644 --- a/packages/extension/src/extension.ts +++ b/packages/extension/src/extension.ts @@ -6,7 +6,6 @@ import { fetchProviderSignal } from "./llm_creds.mjs"; import { resolveOpencodeBinary, OpencodeMissingError, unsupportedHostAdvice } from "./opencode_binary"; import { ChatPanel } from "./chat_panel"; import { DeckPanel } from "./deck_panel"; -import { registerCatalogCard } from "./catalog_card_shell"; import { registerTrees } from "./trees"; import { StatusBarManager } from "./status_bar"; import { @@ -35,7 +34,7 @@ import { resolveLabTomlPath, checkLabToml } from "./lab_config"; import { OpencodeEventClient } from "./sse_client"; import { RunsManager } from "./runs_manager"; import { stageDemoRun } from "./demo_replay"; -import { writeStopFile, savePulseTo, catalogPulsesDir, stopPlan, forceStop, runLogMtime } from "./run_controls"; +import { writeStopFile, savePulseTo, stopPlan, forceStop, runLogMtime } from "./run_controls"; import { watchSolverMode, applyEntitlementForMode, readSolverModeState } from "./solver_mode"; import { runSetCloudKeyCommand } from "./cloud_key"; import { amicodeOpsDir } from "./substrate/vault_store"; @@ -346,54 +345,7 @@ export async function activate(ctx: vscode.ExtensionContext): Promise { }); // 1. UI surfaces - const trees = registerTrees(ctx); - registerCatalogCard(ctx); // #47 dev scaffold — card opens via the save-to-catalog flow - ctx.subscriptions.push( - // #47 session catalog: record the save (workspaceState + tree), then open - // the card. Both prompts (demo replay, live promote) route through here. - vscode.commands.registerCommand("amicode.catalog.save", async (runDir: string) => { - const manifest = readTomlSafe(path.join(runDir, "run.toml")) ?? {}; - const result = readTomlSafe(path.join(runDir, "result.toml")) ?? {}; - const params = (result.params ?? {}) as Record; - const family = typeof params.system === "string" ? params.system : undefined; - // System identity is USER-NAMED (researchers think in named devices — - // "Emerald-Q3" — not families); the family prefills as the default and - // level counts stay in the card's params rows. Esc keeps the family. - const name = await vscode.window.showInputBox({ - prompt: "Name this system (shown on the catalog entry)", - value: family ?? "", - placeHolder: "e.g. Emerald-Q3", - }); - const system = name?.trim() ? name.trim() : family; - // Tags: the quick-digest handles for hyperparameter sweeps ("high-R", - // "T=8", "fast-ansatz") — optional, comma-separated. - const tagsRaw = await vscode.window.showInputBox({ - prompt: "Tags (comma-separated, optional)", - placeHolder: "e.g. high-R, T=8, fast", - }); - const tags = - tagsRaw - ?.split(",") - .map((t) => t.trim()) - .filter(Boolean) ?? []; - await trees.catalog.save({ - run_id: String(manifest.run_id ?? path.basename(runDir)), - runDir, - lab_id: String(manifest.lab_id ?? "default"), - gate: typeof params.gate === "string" ? params.gate : undefined, - system, - tags, - fidelity: Number(result.fidelity ?? 0), - saved_at: new Date().toISOString(), - }); - await vscode.commands.executeCommand("amicode.catalogCard.open", runDir, system, tags); - }), - vscode.commands.registerCommand("amicode.catalog.refresh", () => trees.catalog.refresh()), - // Context-menu removal: unsave the pointer; run artifacts stay on disk. - vscode.commands.registerCommand("amicode.catalog.remove", async (entry?: { run_id?: string }) => { - if (entry?.run_id) await trees.catalog.remove(entry.run_id); - }), - ); + registerTrees(ctx); statusBar = new StatusBarManager(); ctx.subscriptions.push({ dispose: () => statusBar?.dispose() }); @@ -1605,24 +1557,14 @@ export async function activate(ctx: vscode.ExtensionContext): Promise { vscode.window.showWarningMessage("Amicode: no active run."); return; } - const catalog = catalogPulsesDir(); - const picks = [catalog ? "Save to catalog" : undefined, "Save to file…"].filter(Boolean) as string[]; - const choice = await vscode.window.showQuickPick(picks, { title: "Save pulse" }); - if (!choice) return; try { - if (choice === "Save to catalog" && catalog) { - const name = `${path.basename(dir)}.jld2`; - savePulseTo(dir, path.join(catalog, name)); - vscode.window.showInformationMessage(`Amicode: saved pulse to catalog (${name}).`); - } else { - const uri = await vscode.window.showSaveDialog({ - filters: { JLD2: ["jld2"] }, - defaultUri: vscode.Uri.file(path.join(dir, "pulse.jld2")), - }); - if (uri) { - savePulseTo(dir, uri.fsPath); - vscode.window.showInformationMessage("Amicode: pulse saved."); - } + const uri = await vscode.window.showSaveDialog({ + filters: { JLD2: ["jld2"] }, + defaultUri: vscode.Uri.file(path.join(dir, "pulse.jld2")), + }); + if (uri) { + savePulseTo(dir, uri.fsPath); + vscode.window.showInformationMessage("Amicode: pulse saved."); } } catch (e) { vscode.window.showErrorMessage(`Amicode: ${(e as Error).message}`); @@ -1692,15 +1634,6 @@ export async function activate(ctx: vscode.ExtensionContext): Promise { runsManager?.pokeDiscovery(); runsManager?.selectRun(path.basename(runDir)); runsChannel.appendLine(`[demo] replayed → ${runDir}`); - const fid = Number((readTomlSafe(path.join(runDir, "result.toml")) ?? {}).fidelity ?? NaN); - if (fid >= 0.99) { - const choice = await vscode.window.showInformationMessage( - `Amicode: demo solve converged (F=${fid.toFixed(4)}). Save to catalog?`, - "Save to catalog", - "Not now", - ); - if (choice === "Save to catalog") await vscode.commands.executeCommand("amicode.catalog.save", runDir); - } } catch (e) { void vscode.window.showErrorMessage(`Amicode: replay failed — ${(e as Error).message}`); } diff --git a/packages/extension/src/runs_manager.ts b/packages/extension/src/runs_manager.ts index 315fe0fa..5d0b2958 100644 --- a/packages/extension/src/runs_manager.ts +++ b/packages/extension/src/runs_manager.ts @@ -386,15 +386,8 @@ export class RunsManager implements vscode.Disposable { private promptPromote(info: PromoteInfo): void { if (this.promotedRuns.has(info.runId)) return; this.promotedRuns.add(info.runId); - void (async () => { - const choice = await vscode.window.showInformationMessage( - `Amicode: solve converged (F=${info.fidelity.toFixed(4)}). Promote pulse to catalog?`, - "Yes — promote", - "No — keep local only", - ); - if (choice === "Yes — promote") { - await vscode.commands.executeCommand("amicode.catalog.save", info.runDir).then(undefined, () => undefined); - } - })(); + void vscode.window.showInformationMessage( + `Amicode: solve converged (F=${info.fidelity.toFixed(4)}). Use Amicode: Save pulse to save the pulse to a file.`, + ); } } diff --git a/packages/extension/src/trees.ts b/packages/extension/src/trees.ts index 51576122..3ae9406f 100644 --- a/packages/extension/src/trees.ts +++ b/packages/extension/src/trees.ts @@ -1,13 +1,13 @@ import * as vscode from "vscode"; // ============================================================================ -// TreeViews for armonia / catalog. +// TreeViews for armonia. // amicode#204: the old redundant "Vault" + "Armonia" placeholder trees are // merged into ONE Armonia panel (mounted Vaults are its roots; real rendering -// connects to ArmoniaService when it lands). The catalog tree is the #47 -// SESSION catalog: entries collected by the save-to-catalog flow, persisted in -// workspaceState — explicitly NOT the Phase-3 CatalogStore (vault-backed, -// git-lfs pulse artifacts); it seeds the UX3 browser. +// connects to ArmoniaService when it lands). +// The session catalog (SessionCatalogTree, amicode.catalog) was removed in +// #457 — the vault-backed CatalogStore (packages/amico-run) is the source of +// truth; the activity bar now shows only Armonia + Run Inspector. // ============================================================================ class PlaceholderTree implements vscode.TreeDataProvider { @@ -27,94 +27,14 @@ class PlaceholderTree implements vscode.TreeDataProvider { } } -/** A saved session-catalog entry (#47). Promote-shaped, mirrors the card's - * hydration source: enough to label the row and reopen the card. */ -export interface SessionCatalogEntry { - run_id: string; - runDir: string; - lab_id: string; - fidelity: number; - gate?: string; - /** User-named system (falls back to the derived family). */ - system?: string; - /** User-added tags — quick-digest handles for hyperparameter sweeps. */ - tags?: string[]; - saved_at: string; -} - -const CATALOG_KEY = "amicode.sessionCatalog"; - -export class SessionCatalogTree implements vscode.TreeDataProvider { - private readonly _onDidChange = new vscode.EventEmitter(); - readonly onDidChangeTreeData = this._onDidChange.event; - - constructor(private readonly ctx: vscode.ExtensionContext) {} - - private entries(): SessionCatalogEntry[] { - return this.ctx.workspaceState.get(CATALOG_KEY, []); - } - - /** Record a save (newest first, deduped by run_id) and refresh the view. */ - async save(entry: SessionCatalogEntry): Promise { - const rest = this.entries().filter((e) => e.run_id !== entry.run_id); - await this.ctx.workspaceState.update(CATALOG_KEY, [entry, ...rest]); - this._onDidChange.fire(); - } - - /** Remove the POINTER record (unsave). Non-destructive by design: the run - * dir and pulse.jld2 stay on disk — deleting artifacts (and archive/ - * supersede semantics) belongs to the Phase-3 CatalogStore (Q94/Q95). */ - async remove(run_id: string): Promise { - await this.ctx.workspaceState.update( - CATALOG_KEY, - this.entries().filter((e) => e.run_id !== run_id), - ); - this._onDidChange.fire(); - } - - getTreeItem(el: SessionCatalogEntry | string): vscode.TreeItem { - if (typeof el === "string") return new vscode.TreeItem(el, vscode.TreeItemCollapsibleState.None); - // Chip-shaped label: gate · system · fidelity (Hamiltonian-based identity; - // the lab is provenance — it lives in the tooltip + the card's metadata). - const item = new vscode.TreeItem( - `${el.gate ?? "?"} · ${el.system ?? "?"} · F=${el.fidelity.toFixed(5)}`, - vscode.TreeItemCollapsibleState.None, - ); - item.description = el.run_id; - item.tooltip = `lab: ${el.lab_id}${el.tags?.length ? `\ntags: ${el.tags.join(", ")}` : ""}\nsaved ${el.saved_at}\n${el.runDir}`; - if (el.tags?.length) item.description = `${el.run_id} · ${el.tags.join(" · ")}`; - item.command = { - command: "amicode.catalogCard.open", - title: "Open catalog card", - arguments: [el.runDir, el.system, el.tags], - }; - item.contextValue = "amicodeCatalogEntry"; // enables the row's context menu (remove) - return item; - } - - getChildren(): (SessionCatalogEntry | string)[] { - const e = this.entries(); - return e.length ? e : ["(empty — save a converged run to the catalog)"]; - } - - refresh(): void { - this._onDidChange.fire(); - } -} - export function registerTrees(ctx: vscode.ExtensionContext): { - catalog: SessionCatalogTree; armonia: PlaceholderTree; } { - const catalog = new SessionCatalogTree(ctx); // amicode#204: the single Armonia panel. Its roots are the mounted Vaults; // until ArmoniaService lands, a product empty state names what collects here. const armonia = new PlaceholderTree("Your vaults collect here — run Amicode: Set up a vault"); - ctx.subscriptions.push( - vscode.window.registerTreeDataProvider("amicode.catalog", catalog), - vscode.window.registerTreeDataProvider("amicode.armonia", armonia), - ); + ctx.subscriptions.push(vscode.window.registerTreeDataProvider("amicode.armonia", armonia)); - return { catalog, armonia }; + return { armonia }; } diff --git a/packages/extension/test/catalog_shell.test.ts b/packages/extension/test/catalog_shell.test.ts deleted file mode 100644 index 78f5c285..00000000 --- a/packages/extension/test/catalog_shell.test.ts +++ /dev/null @@ -1,149 +0,0 @@ -import { describe, it, expect, vi } from "vitest"; -import { mkdtempSync, writeFileSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import * as vscode from "vscode"; -import { hydrateFromRunDir, registerCatalogCard } from "../src/catalog_card_shell"; -import { SessionCatalogTree, type SessionCatalogEntry } from "../src/trees"; - -// The save-to-catalog flow (#47): entry hydration from real run artifacts, -// and the session catalog (pointer records in workspaceState — NOT the -// Phase-3 CatalogStore; Q91/Q92 open). - -function stageRun(opts: { pulseLines?: string; gate?: string; system?: string }): string { - const dir = mkdtempSync(join(tmpdir(), "card-run-")); - writeFileSync( - join(dir, "run.toml"), - 'schema_version = "1"\nrun_id = "r20260703-000000Z-cafe"\nlab_id = "default"\nscript_path = "/s.jl"\n' + - 'lab = "default"\ncreated_at = "2026-07-03T00:00:00Z"\norchestrator_version = "0.1.0"\n[julia]\nbinary = "julia"\n', - ); - const params = [ - opts.system ? `system = "${opts.system}"` : "", - opts.gate ? `gate = "${opts.gate}"` : "", - "levels = 3", - ] - .filter(Boolean) - .join("\n"); - writeFileSync( - join(dir, "result.toml"), - `schema_version = "1"\nfidelity = 0.9998\niterations = 60\nwall_seconds = 41.5\n[params]\n${params}\n`, - ); - if (opts.pulseLines !== undefined) writeFileSync(join(dir, "run.log"), opts.pulseLines); - return dir; -} - -describe("hydrateFromRunDir — entry from real run artifacts", () => { - it("maps identity, fidelity, params (gate lifted to top level), proposed block, and the newest pulse", () => { - const dir = stageRun({ - gate: "X", - system: "transmon", - pulseLines: - 'AMICODE_PULSE_META drives=1 knots=2 labels="u_1" bounds=-0.2:0.2\n' + - "AMICODE_PULSE iter=1 dt=0.2 a=0.1,0.2\n" + - "AMICODE_PULSE iter=2 dt=0.2 a=0.3,0.4\n", - }); - const data = hydrateFromRunDir(dir)!; - expect(data.entry).toMatchObject({ - run_id: "r20260703-000000Z-cafe", - lab_id: "default", - gate: "X", - fidelity: 0.9998, - proposed: { iterations: 60, wall_seconds: 41.5 }, - }); - expect((data.entry.params as Record).system).toBe("transmon"); - expect(data.pulse).toMatchObject({ record: { iter: 2 } }); // newest record, not the first - }); - - it("degrades: no run.log → no pulse; missing result.toml → undefined", () => { - const dir = stageRun({ gate: "X" }); - expect(hydrateFromRunDir(dir)!.pulse).toBeUndefined(); - const empty = mkdtempSync(join(tmpdir(), "card-empty-")); - expect(hydrateFromRunDir(empty)).toBeUndefined(); - }); -}); - -describe("registerCatalogCard — reveal-or-create panel dedupe", () => { - it("re-opening the same entry reveals the live panel; dispose allows re-create", async () => { - const dir = stageRun({ gate: "X", system: "transmon" }); - const ctx = { subscriptions: [], extensionUri: vscode.Uri.file("/ext") } as never; - registerCatalogCard(ctx); - const spy = vi.spyOn(vscode.window, "createWebviewPanel"); - - await vscode.commands.executeCommand("amicode.catalogCard.open", dir); - await vscode.commands.executeCommand("amicode.catalogCard.open", dir); - expect(spy).toHaveBeenCalledTimes(1); // one panel per run_id - const panel = spy.mock.results[0].value as { revealCount: number; dispose: () => void }; - expect(panel.revealCount).toBe(1); // second click re-focuses - - panel.dispose(); // user closes the tab - await vscode.commands.executeCommand("amicode.catalogCard.open", dir); - expect(spy).toHaveBeenCalledTimes(2); // closed → fresh panel - spy.mockRestore(); - }); -}); - -describe("SessionCatalogTree — pointer records, newest first", () => { - function makeCtx() { - const store = new Map(); - return { - workspaceState: { - get: (k: string, d: unknown) => (store.has(k) ? store.get(k) : d), - update: (k: string, v: unknown) => { - store.set(k, v); - return Promise.resolve(); - }, - }, - } as never; - } - const entry = (run_id: string, over: Partial = {}): SessionCatalogEntry => ({ - run_id, - runDir: `/runs/${run_id}`, - lab_id: "default", - fidelity: 0.999, - gate: "X", - system: "transmon", - saved_at: "2026-07-03T00:00:00Z", - ...over, - }); - - it("saves newest-first, dedupes by run_id, and rows open the card for the run dir", async () => { - const tree = new SessionCatalogTree(makeCtx()); - await tree.save(entry("r1")); - await tree.save(entry("r2")); - await tree.save(entry("r1", { fidelity: 0.5 })); // re-save moves to front, replaces - const rows = tree.getChildren() as SessionCatalogEntry[]; - expect(rows.map((r) => r.run_id)).toEqual(["r1", "r2"]); - expect(rows[0].fidelity).toBe(0.5); - - const item = tree.getTreeItem(rows[1]) as { label: string; command?: { command: string; arguments: unknown[] } }; - expect(item.label).toContain("transmon"); - expect(item.command?.command).toBe("amicode.catalogCard.open"); - expect(item.command?.arguments).toEqual(["/runs/r2", "transmon", undefined]); // runDir + name + tags → card - }); - - it("remove() unsaves the pointer only — remaining entries and order survive", async () => { - const tree = new SessionCatalogTree(makeCtx()); - await tree.save(entry("r1")); - await tree.save(entry("r2")); - await tree.save(entry("r3")); - await tree.remove("r2"); - const rows = tree.getChildren() as SessionCatalogEntry[]; - expect(rows.map((r) => r.run_id)).toEqual(["r3", "r1"]); - await tree.remove("r2"); // idempotent — removing a gone entry is a no-op - expect((tree.getChildren() as SessionCatalogEntry[]).length).toBe(2); - }); - - it("rows carry the context value that enables the remove menu", async () => { - const tree = new SessionCatalogTree(makeCtx()); - await tree.save(entry("r1")); - const item = tree.getTreeItem((tree.getChildren() as SessionCatalogEntry[])[0]) as { contextValue?: string }; - expect(item.contextValue).toBe("amicodeCatalogEntry"); - }); - - it("empty state renders the hint row", () => { - const tree = new SessionCatalogTree(makeCtx()); - const rows = tree.getChildren(); - expect(rows).toHaveLength(1); - expect(String(rows[0])).toContain("empty"); - }); -});