diff --git a/CONTEXT.md b/CONTEXT.md index 50e41a4c..549f45b6 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -108,6 +108,16 @@ _Avoid_: API key, password The self-healing SSH local-forward a `client` uses to reach the Canonical Server — one component with two launchers. The extension spawns and supervises it for interactive panels (reconnect with backoff, address candidates probed LAN-before-overlay, health surfaced in the status bar); a headless launcher (`amico fleet tunnel`) serves panel-less consumers such as scheduled jobs. Failures are always visible to its consumer — never an invisible external service. _Avoid_: port forward (as a concept name), launchd tunnel +### Surfaces + +**Home**: +The always-present first tab in the Work Column that renders the user's widget grid — profile cards, run status, problem summaries, and custom agent-authored widgets. The single canonical surface for widgets; replaces the standalone home page. Internally powered by the widget kernel (WidgetGrid, WidgetFrame, the bridge protocol, `/amicode/widgets` + `/amicode/dashboard` endpoints). +_Avoid_: Dashboard (as the surface name), widget panel + +**Widget**: +A sandboxed ES-module card rendered in an iframe within Home. Authored by the agent (`amicode_author_widget` tool) or shipped as a builtin. Communicates with the host via the bridge protocol (postMessage). Two size classes: hero (full panel width) and tile (half-width, 2-across). Each has a TOML manifest, a JS module, and optional config fields. +_Avoid_: Card (ambiguous — the UI has many cards), tile (as the concept name — tile is a size class) + ### Orthogonal axes **Domain Pack**: diff --git a/docs/adr/0009-widgets-to-work-column-home-tab.md b/docs/adr/0009-widgets-to-work-column-home-tab.md new file mode 100644 index 00000000..77818071 --- /dev/null +++ b/docs/adr/0009-widgets-to-work-column-home-tab.md @@ -0,0 +1,17 @@ +# Widgets move to the Work Column as the "Home" tab; the standalone home page is retired + +Status: proposed (2026-08-24) + +Glossary update: `CONTEXT.md` (Home, Widget) + +The widget grid — profile cards, run status, problem summaries, and custom agent-authored widgets — relocates from the standalone home page into the Work Column as an always-present first tab labeled "Home". The home page route and its dedicated rendering surface are deleted. The widget kernel (WidgetGrid, WidgetFrame, bridge protocol, `/amicode/widgets` + `/amicode/dashboard` endpoints, the `amicode_author_widget` tool, and the TOML manifest system) is reused in full. + +**Why:** Two problems converged. (1) The home page was a dead-end surface: navigating to it meant leaving the session, and returning to the session meant leaving your dashboard — researchers never saw their widgets while working. (2) The Work Column is already the unified auxiliary surface (ADR 0006 moved inspectors there for the same reason); widgets are session-contextual status that belongs in the same hierarchy, always a tab-click away during a conversation. + +**Conditions of acceptance:** The Home tab renders first in the tab strip, is always present (never closeable), and is the default-active tab when the panel opens. The 2-column widget grid (heroes full-width, tiles 2-across) works at panel widths down to ~300px, collapsing to single-column below that. The add-widget tray renders as a compact name+description list (no live iframe previews). Drag-reorder (Move mode) functions vertically. The "Pin to dashboard" button in the chat preview card is relabeled "Pin to Home". The home page route no longer exists at runtime. All existing widget tests pass unchanged. + +**Accepted costs:** The widget grid operates in a narrower viewport than it was designed for — at minimum panel width (~320px), tiles are ~145px wide each, which is tight for content-heavy widgets. Authors of existing widgets may want to test at narrow widths. The add-widget tray loses its live-preview cards (the iframe previews that showed each candidate widget running) in favor of a text list — faster to load and scan, but less visually informative. + +**Considered:** (A) New `WidgetColumn` component purpose-built for the panel (rejected: duplicates 500+ lines of tested state management — ordering, drag, visibility, config — that WidgetGrid already handles; more work, more risk, same result); (B) WidgetGrid with a `variant="panel"` prop that branches layout conditionally (rejected: turns a 550-line component into a conditional maze; the actual changes needed are small and subtractive — simplify the tray, add a CSS threshold — not a different mode). + +**Flip condition:** If the tab strip becomes too crowded with Home always occupying one slot, revisit whether Home should be a collapsible section within the Review tab rather than a top-level tab. If a future full-width surface is needed (e.g., a true dashboard mode when no session is active), the widget kernel can render there too — the backend doesn't care where the frontend mounts the grid. diff --git a/packages/app-bundle/overlay/packages/app/src/pages/session/helpers.ts b/packages/app-bundle/overlay/packages/app/src/pages/session/helpers.ts index adcfc86d..6937f2ac 100644 --- a/packages/app-bundle/overlay/packages/app/src/pages/session/helpers.ts +++ b/packages/app-bundle/overlay/packages/app/src/pages/session/helpers.ts @@ -45,6 +45,7 @@ export const createSessionTabs = (input: TabsInput) => { (input.tabs().active() === SESSION_OPEN_FILE_TAB || input.tabs().all().includes(SESSION_OPEN_FILE_TAB)), ) const pulseInspectorOpen = createMemo(() => input.tabs().active() === "pulseInspector" || input.tabs().all().includes("pulseInspector")) + const homeOpen = createMemo(() => input.tabs().active() === "home" || input.tabs().all().includes("home")) const panelTabs = createMemo( () => { const seen = new Set() @@ -52,7 +53,7 @@ export const createSessionTabs = (input: TabsInput) => { .tabs() .all() .flatMap((tab) => { - if (tab === "context" || tab === "review" || tab === "vault" || tab === SESSION_PREVIEW_TAB || tab === "pulseInspector") return [] + if (tab === "context" || tab === "review" || tab === "vault" || tab === "home" || 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 [] @@ -68,6 +69,7 @@ export const createSessionTabs = (input: TabsInput) => { }) const activeTab = createMemo(() => { const active = input.tabs().active() + if (active === "home") return active if (active === "context") return active if (active === "pulseInspector") return active if (active === SESSION_PREVIEW_TAB && previewOpen()) return active @@ -83,7 +85,7 @@ export const createSessionTabs = (input: TabsInput) => { if (contextOpen()) return "context" if (pulseInspectorOpen()) return "pulseInspector" if (review() && hasReview()) return "review" - return "empty" + return "home" }) const activeFileTab = createMemo(() => { const active = activeTab() @@ -102,6 +104,7 @@ export const createSessionTabs = (input: TabsInput) => { contextOpen, previewOpen, pulseInspectorOpen, + homeOpen, openFileOpen, panelTabs, openedTabs, diff --git a/packages/app-bundle/overlay/packages/app/src/pages/session/session-side-panel.tsx b/packages/app-bundle/overlay/packages/app/src/pages/session/session-side-panel.tsx index 616d3134..d3061942 100644 --- a/packages/app-bundle/overlay/packages/app/src/pages/session/session-side-panel.tsx +++ b/packages/app-bundle/overlay/packages/app/src/pages/session/session-side-panel.tsx @@ -1,4 +1,4 @@ -import { For, Match, Show, Switch, createEffect, createMemo, createSignal, on, onCleanup, type JSX } from "solid-js" +import { For, Match, Show, Switch, createEffect, createMemo, createResource, 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" @@ -32,6 +32,22 @@ 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" +import { + WidgetGrid, + parseWidgetsResponse, + parseDashboardResponse, + resolveTokens, + densityForViewport, + type DashboardState, + type Density, + type WidgetHostCallbacks, +} from "@opencode-ai/ui/amicode-widget-grid" +import { useNavigate, useParams } from "@solidjs/router" +import { useServer } from "@/context/server" +import { useServerSync } from "@/context/server-sync" +import { amicodeGet, amicodePost } from "@/utils/amicode-fetch" +import { sortedRootSessions } from "@/pages/layout/helpers" +import { base64Encode } from "@opencode-ai/core/util/encode" const reviewTabID = "session-side-panel-review-tab" const reviewTabPanelID = "session-side-panel-review-tabpanel" @@ -123,6 +139,130 @@ function PulseInspectorContent() { ) } +function HomeTabContent() { + const server = useServer() + const sync = useServerSync() + const sdk = useSDK() + const params = useParams() + const navigate = useNavigate() + + // Compute the most recent non-empty session that isn't the current one + const resumeSession = createMemo(() => { + const directory = sdk().directory + const store = sync().child(directory, { bootstrap: false })[0] + const sorted = sortedRootSessions(store, Date.now()) + const currentId = params.id + // Find the first session that isn't the current one and has a title (non-empty) + return sorted.find((s) => s.id !== currentId && s.title) ?? undefined + }) + + const widgetContext = createMemo(() => { + const session = resumeSession() + return { + preview: false, + resume: session ? { name: session.title, meta: undefined } : undefined, + } + }) + + const widgetCallbacks: WidgetHostCallbacks = { + fetch: (path) => amicodeGet(server.current, path), + action: async (verb) => { + if (verb === "resume-session") { + const session = resumeSession() + if (session) navigate(`/${base64Encode(session.directory)}/session/${session.id}`) + } + return { ok: true } + }, + prompt: () => {}, + open: () => {}, + } + + const [widgetsRaw, { refetch: refetchWidgets }] = createResource( + () => server.current, + () => amicodeGet(server.current, "/amicode/widgets").catch(() => undefined), + ) + const widgetInfos = createMemo(() => { + const raw = widgetsRaw() + return raw === undefined ? [] : parseWidgetsResponse(raw) + }) + + const [dashboardRaw] = createResource( + () => server.current, + () => amicodeGet(server.current, "/amicode/dashboard").catch(() => undefined), + ) + const [savedDashboard, setSavedDashboard] = createSignal(undefined) + const dashboard = createMemo(() => { + const local = savedDashboard() + if (local) return local + const raw = dashboardRaw() + return raw === undefined ? undefined : parseDashboardResponse(raw) + }) + + const widgetFrameSrcs = createMemo(() => { + const conn = server.current + if (!conn) return {} + const out: Record = {} + for (const w of widgetInfos()) + out[w.id] = new URL( + `/amicode/widget-frame?id=${encodeURIComponent(w.id)}&h=${w.hash}`, + conn.http.url, + ).toString() + return out + }) + + const readTokens = () => { + const style = getComputedStyle(document.documentElement) + const density: Density = densityForViewport(window.innerWidth, window.innerHeight) + return { tokens: resolveTokens((name) => style.getPropertyValue(name), density), density } + } + const [themeState, setThemeState] = createSignal(readTokens()) + + createEffect(() => { + const mq = window.matchMedia("(prefers-color-scheme: dark)") + const update = () => setThemeState(readTokens()) + mq.addEventListener("change", update) + onCleanup(() => mq.removeEventListener("change", update)) + }) + + const saveDashboard = (next: DashboardState) => { + setSavedDashboard(next) + void amicodePost(server.current, "/amicode/dashboard", next) + .then((res) => { + const merged = parseDashboardResponse(res) + if (merged) setSavedDashboard(merged) + }) + .catch(() => {}) + } + + return ( +
+ 0 && dashboard()} + fallback={ +
+
+ No widgets yet. Ask Amico to create one for you. +
+
+ } + > + {(dash) => ( + + )} +
+
+ ) +} + type ReviewDiff = FileDiffInfo | SnapshotFileDiff | VcsFileDiff type RenderDiff = FileDiffInfo | (SnapshotFileDiff & { file: string }) | VcsFileDiff const FILE_TREE_WIDTH_MIN = 240 @@ -257,6 +397,7 @@ export function SessionSidePanel(props: { }) const contextOpen = tabState.contextOpen const previewOpen = tabState.previewOpen + const pulseInspectorOpen = tabState.pulseInspectorOpen const openFileOpen = tabState.openFileOpen const panelTabs = tabState.panelTabs const openedTabs = tabState.openedTabs @@ -308,7 +449,7 @@ export function SessionSidePanel(props: { }) const fileBrowserVisible = createMemo(() => { const active = activeTab() - return active !== "review" && active !== "context" && active !== "empty" && active !== SESSION_PREVIEW_TAB + return active !== "review" && active !== "context" && active !== "home" && active !== "empty" && active !== SESSION_PREVIEW_TAB }) // Markdown files for the Preview tab — check both git diffs and tool-edit history @@ -325,7 +466,7 @@ export function SessionSidePanel(props: { { id: "context", label: "Context", - icon: "brain", + icon: "context-ring", available: () => true, active: contextOpen, }, @@ -474,6 +615,12 @@ export function SessionSidePanel(props: { onCleanup(stop) }} > + +
+ +
Home
+
+
+
{language.t("session.tab.review")}
{props.reviewCount()}
@@ -521,32 +669,34 @@ export function SessionSidePanel(props: {
- - tabs().close("pulseInspector")} - aria-label={language.t("common.closeTab")} - /> - - } - hideCloseButton - onMiddleClick={() => tabs().close("pulseInspector")} - > -
- -
Pulse Inspector
-
-
+
+ + tabs().close("pulseInspector")} + aria-label={language.t("common.closeTab")} + /> + + } + hideCloseButton + onMiddleClick={() => tabs().close("pulseInspector")} + > +
+ +
Pulse Inspector
+
+
+
+ + + + + +
@@ -706,15 +862,29 @@ export function SessionSidePanel(props: {
)}
+ +
+ +
Home
+
+
- {props.hasReview() - ? "Files Changed" - : language.t("session.tab.review")} +
+ +
+ {props.hasReview() + ? "Files Changed" + : language.t("session.tab.review")} +
+ +
{props.reviewCount()}
+
+
@@ -751,6 +921,7 @@ export function SessionSidePanel(props: {
+
Pulse Inspector
+
+ + + + + +
diff --git a/packages/app-bundle/overlay/packages/ui/src/amicode/widget-preview-card.tsx b/packages/app-bundle/overlay/packages/ui/src/amicode/widget-preview-card.tsx index b4f69578..d1dcdf6b 100644 --- a/packages/app-bundle/overlay/packages/ui/src/amicode/widget-preview-card.tsx +++ b/packages/app-bundle/overlay/packages/ui/src/amicode/widget-preview-card.tsx @@ -9,8 +9,8 @@ import type { WidgetPreview } from "./widget-preview" // result as a LIVE preview of the just-authored widget, inside the chat. Reuses // the exact frame kernel the home grid uses (server-served sandboxed frame + // bridge), reading the server context (frame src base, host callbacks, pin -// verb) off the ui-bridge — same idiom as the RunWindow. "Pin to dashboard" -// adds it to home. Re-authoring the same id emits a new hash → a fresh preview +// verb) off the ui-bridge — same idiom as the RunWindow. "Pin to Home" +// adds it to the Home tab. Re-authoring the same id emits a new hash → a fresh preview // with the updated code (hot-reload). No host registered (e.g. TUI) → a plain // note, never a crash. @@ -48,7 +48,7 @@ export function WidgetPreviewCard(props: { preview: WidgetPreview }) { .catch(() => setPinState("error")) } const pinLabel = () => - ({ idle: "Pin to dashboard", pinning: "Pinning…", pinned: "Pinned to dashboard ✓", error: "Couldn’t pin — retry" })[ + ({ idle: "Pin to Home", pinning: "Pinning…", pinned: "Pinned to Home ✓", error: "Couldn't pin — retry" })[ pinState() ] diff --git a/packages/extension/scripts/amicode_fixture_seed.mjs b/packages/extension/scripts/amicode_fixture_seed.mjs index 2c6e2a1f..c6795bfc 100644 --- a/packages/extension/scripts/amicode_fixture_seed.mjs +++ b/packages/extension/scripts/amicode_fixture_seed.mjs @@ -307,8 +307,6 @@ export function seedAmicodeSandbox(dir) { writeJson(join(amico, "dashboard.json"), { version: 1, widget: [ - { id: "meet-amico", hidden: true, config: {} }, - { id: "about-you", config: {}, group: "left", view: "expanded" }, { id: "ghost-widget", config: { any: "values" } }, ], views: { home: "grid" }, diff --git a/packages/extension/scripts/record_amicode_fixtures.mjs b/packages/extension/scripts/record_amicode_fixtures.mjs index a2002348..bb801168 100644 --- a/packages/extension/scripts/record_amicode_fixtures.mjs +++ b/packages/extension/scripts/record_amicode_fixtures.mjs @@ -153,9 +153,9 @@ const REQUESTS = [ { method: "GET", path: "/amicode/library", name: "library — post-upload state" }, // ── widget kernel + dashboard (slice 5) ──────────────────────────────────── { method: "GET", path: "/amicode/widgets", name: "widget registry — builtins with content hashes" }, - { method: "GET", path: "/amicode/widget-frame?id=meet-amico", name: "widget frame — served HTML + its own CSP" }, + { method: "GET", path: "/amicode/widget-frame?id=jump-back-in", name: "widget frame — served HTML + its own CSP" }, { method: "GET", path: "/amicode/widget-frame?id=not-a-widget", name: "widget frame — unknown id stub" }, - { method: "GET", path: "/amicode/widget-code?id=about-you", name: "widget code — builtin source + hash" }, + { method: "GET", path: "/amicode/widget-code?id=jump-back-in", name: "widget code — builtin source + hash" }, { method: "GET", path: "/amicode/widget-code?id=no-such", name: "widget code — not_found" }, { method: "POST", @@ -177,7 +177,6 @@ const REQUESTS = [ body: { version: 1, widget: [ - { id: "about-you", hidden: false, config: {}, group: "right" }, { id: "my-showcase", hidden: true, config: {} }, ], views: { home: "grid" }, diff --git a/packages/extension/src/amicode_service/widgets.ts b/packages/extension/src/amicode_service/widgets.ts index 9d22a644..650b263e 100644 Binary files a/packages/extension/src/amicode_service/widgets.ts and b/packages/extension/src/amicode_service/widgets.ts differ diff --git a/packages/extension/src/amicode_service/widgets_src/about-you.ts b/packages/extension/src/amicode_service/widgets_src/about-you.ts deleted file mode 100644 index f945b16f..00000000 --- a/packages/extension/src/amicode_service/widgets_src/about-you.ts +++ /dev/null @@ -1,259 +0,0 @@ -// AMICODE built-in widget: ABOUT YOU — profile hero with earned stats (which -// stats show is user config), "Amico remembers", and in-place identity edit. -// Institution suggestions + logo resolution + external links + saves all go -// through host actions (widget frames have no network). Declared v1 -// degradation: no clipboard text-paste fallback on the edit inputs. - -export const manifestToml = ` -id = "about-you" -name = "About you" -version = "1.0.0" -description = "Your profile, earned stats, and what Amico remembers" -size = "hero" -height = 250 - -[config.stats] -type = "multi-select" -options = ["problems", "runs"] -default = ["problems", "runs"] -` - -export const widgetJs = ` -export default { - mount: function (el, amico) { - var profile = null - var editing = false - var saving = false - var draft = { name: '', affiliation: '', focus: '', scholar: '', affiliation_logo: '' } - var suggestions = [] - var searchTimer = null - var searchSeq = 0 - - var esc = function (s) { - return String(s == null ? '' : s) - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - } - var initials = function (name) { - var parts = String(name || '').trim().split(/\\s+/).filter(Boolean) - if (parts.length === 0) return '?' - return parts.slice(0, 2).map(function (p) { return p[0].toUpperCase() }).join('') - } - var MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] - var sinceLabel = function (iso) { - if (!iso) return null - var m = String(iso).match(/^(\\d{4})-(\\d{2})-(\\d{2})$/) - if (!m) return null - var label = 'since ' + (MONTHS[Number(m[2]) - 1] || m[2]) + ' ' + Number(m[3]) - var started = Date.parse(iso) - if (isNaN(started)) return label - var days = Math.max(0, Math.round((Date.now() - started) / 86400000)) - return days <= 0 ? label + ' \\u00b7 today' : label + ' \\u00b7 ' + days + ' day' + (days === 1 ? '' : 's') - } - var on = function (sel, evt, fn) { - var n = el.querySelector(sel) - if (n) n['on' + evt] = fn - } - - var statBlock = function (value, label) { - return ( - '
' + - '' + value + '' + - '' + label + '
' - ) - } - - var render = function () { - if (!profile || !profile.ok) { - el.innerHTML = '' - return - } - var you = profile.you - var stats = you.stats || {} - var chosen = (amico.config && amico.config.stats) || ['problems', 'runs'] - var fresh = !stats.problems && !stats.runs - - var avatar = you.avatar - ? '' - : '
' + esc(initials(you.name)) + '
' - - var platforms = (you.platforms || []).slice(0, 3).map(function (p) { - return '◇ ' + esc(p) - }).join('  ') - - var editForm = - '
' + - '' + - '
' + - '' + - (suggestions.length - ? '
' + - suggestions.map(function (s, i) { - return '
' + esc(s.name) + ' ' + esc(s.domain || '') + '
' - }).join('') + '
' - : '') + - '
' + - '' + - '' + - '
' + - '' + - '' + - '
' - - var identity = - '
' + avatar + - '
' + - '
' + - '
' + esc(you.name || 'You') + '
' + - (you.scholar ? 'scholar ↗' : '') + - '' + - '
' + - (editing - ? editForm - : '
' + - esc(you.focus || you.affiliation || 'tell Amico about your work') + '
' + - (you.description ? '
' + esc(you.description) + '
' : '') + - (platforms ? '
' + platforms + '
' : '')) + - '
' - - var body - if (fresh && !editing) { - body = - '
' + - '
No experiments yet \\u2014 tell Amico what you're working on and it'll start remembering.
' + - '' - } else if (!editing) { - var cells = chosen.map(function (k) { - return statBlock(String(stats[k] == null ? 0 : stats[k]), k) - }).join('') - var remembers = (you.remembers || []).map(function (r) { - return '
' + - '' + - '' + esc(r.title) + '
' - }).join('') - var since = sinceLabel(stats.since) - body = - '
' + - '
' + cells + '
' + - (remembers - ? '
' + - '
Amico remembers
' + - '
' + remembers + '
' - : '') + - (since ? '
' + since + '
' : '') - } else { - body = '' - } - - el.innerHTML = - '
' + - '
About you
' + - identity + body + '
' - - on('[data-edit]', 'click', function () { - editing = !editing - if (editing) { - suggestions = [] - draft = { - name: you.name || '', - affiliation: you.affiliation || '', - focus: you.focus || '', - scholar: you.scholar || '', - affiliation_logo: you.affiliation_logo || '', - } - } - render() - }) - on('[data-scholar]', 'click', function () { - amico.action('open-external', { url: you.scholar }) - }) - on('[data-firstrun]', 'click', function () { - amico.prompt('help me get started with my first project') - }) - on('[data-cancel]', 'click', function () { - editing = false - suggestions = [] - render() - }) - on('[data-save]', 'click', function () { - var read = function (sel) { - var n = el.querySelector(sel) - return n ? n.value : '' - } - draft.name = read('[data-f-name]') - draft.affiliation = read('[data-f-affiliation]') - draft.focus = read('[data-f-focus]') - draft.scholar = read('[data-f-scholar]') - saving = true - render() - amico - .action('save-profile', draft) - .then(function () { return amico.fetch('/amicode/profile') }) - .then(function (fresh) { - profile = fresh - saving = false - editing = false - suggestions = [] - render() - }) - .catch(function () { - saving = false - render() - }) - }) - on('[data-f-affiliation]', 'input', function (e) { - var q = e.target.value - draft.affiliation = q - if (searchTimer) clearTimeout(searchTimer) - if (String(q).trim().length < 2) { - searchSeq++ - suggestions = [] - return - } - searchTimer = setTimeout(function () { - var seq = ++searchSeq - amico.action('lookup-institution', { query: q }).then(function (rows) { - if (seq !== searchSeq) return - suggestions = Array.isArray(rows) ? rows.slice(0, 5) : [] - render() - var input = el.querySelector('[data-f-affiliation]') - if (input) { - input.focus() - input.setSelectionRange(input.value.length, input.value.length) - } - }).catch(function () {}) - }, 200) - }) - var suggNodes = el.querySelectorAll('[data-sugg]') - for (var i = 0; i < suggNodes.length; i++) { - ;(function (node) { - node.onclick = function () { - var pick = suggestions[Number(node.getAttribute('data-sugg'))] - if (!pick) return - draft.affiliation = pick.name - suggestions = [] - amico.action('resolve-logo', { name: pick.name, domain: pick.domain }).then(function (r) { - if (r && typeof r.logo === 'string') draft.affiliation_logo = r.logo - }).catch(function () {}) - render() - } - })(suggNodes[i]) - } - } - - el.innerHTML = '' - amico.fetch('/amicode/profile').then(function (data) { - profile = data - render() - }).catch(function (e) { - // a hero card must never fail invisibly — show the reason - el.innerHTML = - '
' + - 'About you: profile unavailable (' + String(e && e.message ? e.message : e) + ')
' - }) - amico.onConfig(function () { render() }) - }, -} -` diff --git a/packages/extension/src/amicode_service/widgets_src/library.ts b/packages/extension/src/amicode_service/widgets_src/library.ts deleted file mode 100644 index 66d94e75..00000000 --- a/packages/extension/src/amicode_service/widgets_src/library.ts +++ /dev/null @@ -1,81 +0,0 @@ -// AMICODE built-in widget: LIBRARY — "make Amico smarter" paper uploads. -// Count/latest via amico.context.library; the PDF bytes ride the -// upload-library host action (base64); "Discuss latest" opens a chat. - -export const manifestToml = ` -id = "library" -name = "Library" -version = "1.0.0" -description = "Upload papers — Amico learns your work" -size = "tile" -height = 140 -` - -export const widgetJs = ` -export default { - mount: function (el, amico) { - var esc = function (s) { - return String(s == null ? '' : s) - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - } - var busy = false - var render = function () { - var lib = (amico.context && amico.context.library) || { count: 0 } - el.innerHTML = - '
' + - '
Library
' + - '
Make Amico smarter
' + - '
' + - (lib.count > 0 ? lib.count + ' paper' + (lib.count === 1 ? '' : 's') + ' \\u00b7 latest: ' + esc(lib.latestName || '') : 'upload papers \\u2014 Amico learns your work') + - '
' + - '
' + - '' + - (lib.latestPath ? 'Discuss latest →' : '') + - '
' + - '' + - '
' - var upload = el.querySelector('[data-upload]') - var file = el.querySelector('[data-file]') - if (upload && file) - upload.onclick = function () { - file.click() - } - if (file) - file.onchange = function () { - var f = file.files && file.files[0] - if (!f) return - busy = true - render() - var reader = new FileReader() - reader.onload = function () { - var url = String(reader.result || '') - var b64 = url.slice(url.indexOf(',') + 1) - amico.action('upload-library', { filename: f.name, dataB64: b64 }).then(function () { - busy = false - render() - }).catch(function () { - busy = false - render() - }) - } - reader.onerror = function () { - busy = false - render() - } - reader.readAsDataURL(f) - } - var discuss = el.querySelector('[data-discuss]') - if (discuss) - discuss.onclick = function () { - var lib2 = (amico.context && amico.context.library) || {} - amico.prompt('read my latest paper at ' + (lib2.latestPath || '') + ' and discuss how it should inform my research') - } - } - render() - amico.onContext(render) - }, -} -` diff --git a/packages/extension/src/amicode_service/widgets_src/meet-amico.ts b/packages/extension/src/amicode_service/widgets_src/meet-amico.ts deleted file mode 100644 index 7ae4148d..00000000 --- a/packages/extension/src/amicode_service/widgets_src/meet-amico.ts +++ /dev/null @@ -1,70 +0,0 @@ -// AMICODE built-in widget: MEET AMICO — identity hero + capabilities + the -// "Open chat" front door. Vanilla DOM, --amc-* tokens only, no backticks or -// ${} inside widgetJs (it ships inside a TS template literal). - -export const manifestToml = ` -id = "meet-amico" -name = "Meet Amico" -version = "1.0.0" -description = "Who your pal is and what it can do — the front door to a fresh chat" -size = "hero" -height = 250 -` - -export const widgetJs = ` -var CAN = [ - 'run automated experiments from a conversation', - 'verify results independently before trusting them', - 'remember your work and build on prior results', - 'tune & calibrate on real hardware', -] - -// The full amico.svg mark — MarkDetailed geometry from logo.tsx (H-bracket + -// face, viewBox 0 0 3600 3600), sized as a compact brand glyph. -var FACE = - '' - -export default { - mount: function (el, amico) { - var on = function (sel, fn) { - var n = el.querySelector(sel) - if (n) n.onclick = fn - } - var bullets = CAN.map(function (line) { - return ( - '
' + - '' + - '' + line + '
' - ) - }).join('') - el.innerHTML = - '
' + - '
Meet Amico
' + - '
' + - FACE + - '
' + - '
Amico
' + - '
Your autoresearch copilot
' + - '
powered by the autoresearch loop
' + - '
' + - '
' + - '
I can help you
' + - '
' + bullets + '
' + - '' + - '
' - on('[data-cta]', function (e) { - e.stopPropagation() - amico.prompt('') - }) - on('[data-card]', function () { - amico.prompt('') - }) - var applyDensity = function () { - var engine = el.querySelector('[data-engine]') - if (engine) engine.style.display = amico.density === 'tight' ? 'none' : '' - } - applyDensity() - amico.onTheme(applyDensity) - }, -} -` diff --git a/packages/extension/src/amicode_service/widgets_src/now-solving.ts b/packages/extension/src/amicode_service/widgets_src/now-solving.ts deleted file mode 100644 index 1fb3981d..00000000 --- a/packages/extension/src/amicode_service/widgets_src/now-solving.ts +++ /dev/null @@ -1,107 +0,0 @@ -// AMICODE built-in widget: NOW SOLVING — live run tile. Data via -// amico.context.liveRun (the host keeps polling run-status/run-series); -// config.plot picks pulse (default, per Track 1) or objective sparkline. -// No live run → empty-state. Click opens the Run entity. - -export const manifestToml = ` -id = "now-solving" -name = "Now solving" -version = "1.0.0" -description = "The run in flight — iteration, fidelity, live sparkline" -size = "tile" -height = 96 - -[config.plot] -type = "select" -options = ["pulse", "objective"] -default = "pulse" -` - -export const widgetJs = ` -var W = 96 -var H = 22 - -function scaled(ys, min, max) { - var span = max - min || 1 - var step = W / (ys.length - 1) - var d = '' - for (var i = 0; i < ys.length; i++) { - var px = (i * step).toFixed(1) - var py = (H - 2 - ((ys[i] - min) / span) * (H - 4)).toFixed(1) - d += (i === 0 ? 'M' : ' L') + px + ',' + py - } - return d -} - -function paths(run, plot) { - var pulse = run.pulse || [] - if (plot === 'pulse' && pulse.length >= 2) { - var n = Math.max(1, run.drives || 1) - var knots = Math.floor(pulse.length / n) - if (knots >= 2) { - var min = Math.min.apply(null, pulse) - var max = Math.max.apply(null, pulse) - var out = [] - for (var d = 0; d < n; d++) out.push(scaled(pulse.slice(d * knots, (d + 1) * knots), min, max)) - return out - } - } - var series = run.series || [] - if (series.length < 2) return [] - return [scaled(series, Math.min.apply(null, series), Math.max.apply(null, series))] -} - -export default { - mount: function (el, amico) { - var esc = function (s) { - return String(s == null ? '' : s) - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - } - var render = function () { - var run = amico.context && amico.context.liveRun - // tray preview (context.preview): sample run instead of the empty state - if (!run && amico.context && amico.context.preview) { - run = { - name: 'CZ gate \\u2014 transmon pair', - iteration: 42, - fidelity: 0.99871, - series: [1, 0.62, 0.41, 0.28, 0.2, 0.14, 0.1, 0.07, 0.05, 0.035, 0.022, 0.013, 0.008], - } - } - if (!run) { - el.innerHTML = '' - return - } - var plot = (amico.config && amico.config.plot) || 'pulse' - var ds = paths(run, plot) - var svg = '' - if (ds.length) { - svg = '' - for (var i = 0; i < ds.length; i++) - svg += '' - svg += '' - } - var f = typeof run.fidelity === 'number' ? run.fidelity.toFixed(5) : '\\u2014' - var iter = run.iteration == null ? '\\u2014' : String(run.iteration) - el.innerHTML = - '
' + - '
Now solving
' + - '
' + esc(run.name || 'current run') + '
' + - '
iter ' + iter + ' \\u00b7 F ' + f + '
' + - '
' + svg + '
' + - '
' - var card = el.querySelector('[data-card]') - if (card) - card.onclick = function () { - amico.open('run') - } - } - render() - amico.onContext(render) - amico.onConfig(render) - }, -} -` diff --git a/packages/extension/src/amicode_service/widgets_src/showcase.ts b/packages/extension/src/amicode_service/widgets_src/showcase.ts deleted file mode 100644 index c680eeb0..00000000 --- a/packages/extension/src/amicode_service/widgets_src/showcase.ts +++ /dev/null @@ -1,30 +0,0 @@ -// AMICODE built-in widget: SHOWCASE — trophy-case entry point to the run -// gallery. Static tile; the click is a host navigation action. - -export const manifestToml = ` -id = "showcase" -name = "Showcase" -version = "1.0.0" -description = "Run gallery — shareable cards of your solves" -size = "tile" -height = 140 -` - -export const widgetJs = ` -export default { - mount: function (el, amico) { - el.innerHTML = - '
' + - '
Showcase
' + - '
Run gallery
' + - '
shareable cards of your solves
' + - '
Browse & share →
' + - '
' - var card = el.querySelector('[data-card]') - if (card) - card.onclick = function () { - amico.action('open-gallery', {}) - } - }, -} -` diff --git a/packages/extension/test/fixtures/amicode/golden.json b/packages/extension/test/fixtures/amicode/golden.json index 7b5b7b95..36dcbfba 100644 --- a/packages/extension/test/fixtures/amicode/golden.json +++ b/packages/extension/test/fixtures/amicode/golden.json @@ -1,12 +1,12 @@ { - "recordedAt": "2026-08-24T12:14:00.196Z", + "recordedAt": "2026-08-24T19:40:29.406Z", "fork": { "version": "1.18.10", "tag": "v1.18.10-amicode.16" }, - "sandbox": "/var/folders/26/98pr37253yvcxfjlzkzg_c_r0000gn/T/amicode-fixture-IUxS6z", - "sandboxReal": "/private/var/folders/26/98pr37253yvcxfjlzkzg_c_r0000gn/T/amicode-fixture-IUxS6z", - "seededAt": 1787573638936, + "sandbox": "/var/folders/26/98pr37253yvcxfjlzkzg_c_r0000gn/T/amicode-fixture-eSkFCX", + "sandboxReal": "/private/var/folders/26/98pr37253yvcxfjlzkzg_c_r0000gn/T/amicode-fixture-eSkFCX", + "seededAt": 1787600428155, "entries": [ { "name": "cold read — synthesized identity + stats + remembers", @@ -75,7 +75,7 @@ "status": 200, "contentType": "application/json", "csp": null, - "body": "{\"ok\":true,\"name\":\"attachable-demo\",\"kind\":\"personal\",\"path\":\"/var/folders/26/98pr37253yvcxfjlzkzg_c_r0000gn/T/amicode-fixture-IUxS6z/vaults/attachable-demo\"}" + "body": "{\"ok\":true,\"name\":\"attachable-demo\",\"kind\":\"personal\",\"path\":\"/var/folders/26/98pr37253yvcxfjlzkzg_c_r0000gn/T/amicode-fixture-eSkFCX/vaults/attachable-demo\"}" }, { "name": "vaults — post-attach (cache bust, new mount)", @@ -189,7 +189,7 @@ "status": 200, "contentType": "application/json", "csp": null, - "body": "{\"ok\":true,\"found\":true,\"path\":\"/var/folders/26/98pr37253yvcxfjlzkzg_c_r0000gn/T/amicode-fixture-IUxS6z/docs/readme.md\",\"mount\":null,\"kind\":\"file\"}" + "body": "{\"ok\":true,\"found\":true,\"path\":\"/var/folders/26/98pr37253yvcxfjlzkzg_c_r0000gn/T/amicode-fixture-eSkFCX/docs/readme.md\",\"mount\":null,\"kind\":\"file\"}" }, { "name": "resolve mount-prefixed (tier 3)", @@ -200,7 +200,7 @@ "status": 200, "contentType": "application/json", "csp": null, - "body": "{\"ok\":true,\"found\":true,\"path\":\"/private/var/folders/26/98pr37253yvcxfjlzkzg_c_r0000gn/T/amicode-fixture-IUxS6z/vaults/personal-main/notes/note.md\",\"mount\":\"personal-main\",\"kind\":\"file\"}" + "body": "{\"ok\":true,\"found\":true,\"path\":\"/private/var/folders/26/98pr37253yvcxfjlzkzg_c_r0000gn/T/amicode-fixture-eSkFCX/vaults/personal-main/notes/note.md\",\"mount\":\"personal-main\",\"kind\":\"file\"}" }, { "name": "resolve relative w/ dir part (tier 4 → project dir)", @@ -211,7 +211,7 @@ "status": 200, "contentType": "application/json", "csp": null, - "body": "{\"ok\":true,\"found\":true,\"path\":\"/var/folders/26/98pr37253yvcxfjlzkzg_c_r0000gn/T/amicode-fixture-IUxS6z/docs/readme.md\",\"mount\":null,\"kind\":\"file\"}" + "body": "{\"ok\":true,\"found\":true,\"path\":\"/var/folders/26/98pr37253yvcxfjlzkzg_c_r0000gn/T/amicode-fixture-eSkFCX/docs/readme.md\",\"mount\":null,\"kind\":\"file\"}" }, { "name": "resolve bare typed-prefix — miss (tier 5)", @@ -354,7 +354,7 @@ "status": 200, "contentType": "application/json", "csp": null, - "body": "{\"ok\":true,\"run\":{\"run_id\":\"r20260810-000000Z-3d4e5f\",\"lab\":\"default\",\"status\":\"solving\",\"iteration\":2,\"fidelity\":null,\"best_f\":0.35,\"last_f\":0.35,\"elapsed_ms\":1253638932,\"series\":[{\"iter\":1,\"f\":0.4},{\"iter\":2,\"f\":0.35}],\"pulse\":{\"iter\":2,\"dt\":0.2,\"values\":[0.01,0.02,0.03,0.04,0.05,0.06]},\"pulse_meta\":{\"drives\":2,\"knots\":3,\"labels\":[\"a_1\",\"a_2\"]},\"tail\":[\"AMICODE_PULSE_META drives=2 knots=3 labels=\\\"a_1\\\",\\\"a_2\\\" bounds=-0.2:0.2,-0.2:0.2\",\"AMICODE_ITER iter=1 f=4.0e-01 inf_pr=1.0e-01 inf_du=5.0e-01\",\"AMICODE_ITER iter=2 f=3.5e-01 inf_pr=1.0e-02 inf_du=5.0e-02\",\"AMICODE_PULSE iter=2 dt=0.2 a=0.01,0.02,0.03;0.04,0.05,0.06\"]},\"error\":null}" + "body": "{\"ok\":true,\"run\":{\"run_id\":\"r20260810-000000Z-3d4e5f\",\"lab\":\"default\",\"status\":\"solving\",\"iteration\":2,\"fidelity\":null,\"best_f\":0.35,\"last_f\":0.35,\"elapsed_ms\":1280428152,\"series\":[{\"iter\":1,\"f\":0.4},{\"iter\":2,\"f\":0.35}],\"pulse\":{\"iter\":2,\"dt\":0.2,\"values\":[0.01,0.02,0.03,0.04,0.05,0.06]},\"pulse_meta\":{\"drives\":2,\"knots\":3,\"labels\":[\"a_1\",\"a_2\"]},\"tail\":[\"AMICODE_PULSE_META drives=2 knots=3 labels=\\\"a_1\\\",\\\"a_2\\\" bounds=-0.2:0.2,-0.2:0.2\",\"AMICODE_ITER iter=1 f=4.0e-01 inf_pr=1.0e-01 inf_du=5.0e-01\",\"AMICODE_ITER iter=2 f=3.5e-01 inf_pr=1.0e-02 inf_du=5.0e-02\",\"AMICODE_PULSE iter=2 dt=0.2 a=0.01,0.02,0.03;0.04,0.05,0.06\"]},\"error\":null}" }, { "name": "run series — failed (result.toml ≠ finished)", @@ -376,7 +376,7 @@ "status": 200, "contentType": "application/json", "csp": null, - "body": "{\"ok\":true,\"run\":{\"run_id\":\"r20260818-000000Z-4h5i6j\",\"lab\":\"default\",\"status\":\"stalled\",\"iteration\":2,\"fidelity\":null,\"best_f\":0.59,\"last_f\":0.59,\"elapsed_ms\":555238933,\"series\":[{\"iter\":1,\"f\":0.6},{\"iter\":2,\"f\":0.59}],\"pulse\":{\"iter\":2,\"dt\":0.2,\"values\":[0.01,0.02,0.03,0.04,0.05,0.06]},\"pulse_meta\":{\"drives\":2,\"knots\":3,\"labels\":[\"a_1\",\"a_2\"]},\"tail\":[\"AMICODE_PULSE_META drives=2 knots=3 labels=\\\"a_1\\\",\\\"a_2\\\" bounds=-0.2:0.2,-0.2:0.2\",\"AMICODE_ITER iter=1 f=6.0e-01 inf_pr=1.0e-01 inf_du=5.0e-01\",\"AMICODE_ITER iter=2 f=5.9e-01 inf_pr=1.0e-02 inf_du=5.0e-02\",\"AMICODE_PULSE iter=2 dt=0.2 a=0.01,0.02,0.03;0.04,0.05,0.06\"]},\"error\":null}" + "body": "{\"ok\":true,\"run\":{\"run_id\":\"r20260818-000000Z-4h5i6j\",\"lab\":\"default\",\"status\":\"stalled\",\"iteration\":2,\"fidelity\":null,\"best_f\":0.59,\"last_f\":0.59,\"elapsed_ms\":582028152,\"series\":[{\"iter\":1,\"f\":0.6},{\"iter\":2,\"f\":0.59}],\"pulse\":{\"iter\":2,\"dt\":0.2,\"values\":[0.01,0.02,0.03,0.04,0.05,0.06]},\"pulse_meta\":{\"drives\":2,\"knots\":3,\"labels\":[\"a_1\",\"a_2\"]},\"tail\":[\"AMICODE_PULSE_META drives=2 knots=3 labels=\\\"a_1\\\",\\\"a_2\\\" bounds=-0.2:0.2,-0.2:0.2\",\"AMICODE_ITER iter=1 f=6.0e-01 inf_pr=1.0e-01 inf_du=5.0e-01\",\"AMICODE_ITER iter=2 f=5.9e-01 inf_pr=1.0e-02 inf_du=5.0e-02\",\"AMICODE_PULSE iter=2 dt=0.2 a=0.01,0.02,0.03;0.04,0.05,0.06\"]},\"error\":null}" }, { "name": "run series — explicit lab", @@ -409,7 +409,7 @@ "status": 200, "contentType": "application/json", "csp": null, - "body": "{\"ok\":true,\"papers\":[{\"name\":\"piccolo-trajectory-2023.pdf\",\"size\":28,\"added_ms\":1785974400000,\"path\":\"/var/folders/26/98pr37253yvcxfjlzkzg_c_r0000gn/T/amicode-fixture-IUxS6z/.amico/library/piccolo-trajectory-2023.pdf\"},{\"name\":\"rydberg-blockade-2024.pdf\",\"size\":28,\"added_ms\":1785628800000,\"path\":\"/var/folders/26/98pr37253yvcxfjlzkzg_c_r0000gn/T/amicode-fixture-IUxS6z/.amico/library/rydberg-blockade-2024.pdf\"}],\"error\":null}" + "body": "{\"ok\":true,\"papers\":[{\"name\":\"piccolo-trajectory-2023.pdf\",\"size\":28,\"added_ms\":1785974400000,\"path\":\"/var/folders/26/98pr37253yvcxfjlzkzg_c_r0000gn/T/amicode-fixture-eSkFCX/.amico/library/piccolo-trajectory-2023.pdf\"},{\"name\":\"rydberg-blockade-2024.pdf\",\"size\":28,\"added_ms\":1785628800000,\"path\":\"/var/folders/26/98pr37253yvcxfjlzkzg_c_r0000gn/T/amicode-fixture-eSkFCX/.amico/library/rydberg-blockade-2024.pdf\"}],\"error\":null}" }, { "name": "library upload — valid PDF (refreshed listing)", @@ -424,7 +424,7 @@ "status": 200, "contentType": "application/json", "csp": null, - "body": "{\"ok\":true,\"papers\":[{\"name\":\"new paper.pdf\",\"size\":29,\"added_ms\":1787573639655,\"path\":\"/var/folders/26/98pr37253yvcxfjlzkzg_c_r0000gn/T/amicode-fixture-IUxS6z/.amico/library/new paper.pdf\"},{\"name\":\"piccolo-trajectory-2023.pdf\",\"size\":28,\"added_ms\":1785974400000,\"path\":\"/var/folders/26/98pr37253yvcxfjlzkzg_c_r0000gn/T/amicode-fixture-IUxS6z/.amico/library/piccolo-trajectory-2023.pdf\"},{\"name\":\"rydberg-blockade-2024.pdf\",\"size\":28,\"added_ms\":1785628800000,\"path\":\"/var/folders/26/98pr37253yvcxfjlzkzg_c_r0000gn/T/amicode-fixture-IUxS6z/.amico/library/rydberg-blockade-2024.pdf\"}],\"error\":null}" + "body": "{\"ok\":true,\"papers\":[{\"name\":\"new paper.pdf\",\"size\":29,\"added_ms\":1787600428877,\"path\":\"/var/folders/26/98pr37253yvcxfjlzkzg_c_r0000gn/T/amicode-fixture-eSkFCX/.amico/library/new paper.pdf\"},{\"name\":\"piccolo-trajectory-2023.pdf\",\"size\":28,\"added_ms\":1785974400000,\"path\":\"/var/folders/26/98pr37253yvcxfjlzkzg_c_r0000gn/T/amicode-fixture-eSkFCX/.amico/library/piccolo-trajectory-2023.pdf\"},{\"name\":\"rydberg-blockade-2024.pdf\",\"size\":28,\"added_ms\":1785628800000,\"path\":\"/var/folders/26/98pr37253yvcxfjlzkzg_c_r0000gn/T/amicode-fixture-eSkFCX/.amico/library/rydberg-blockade-2024.pdf\"}],\"error\":null}" }, { "name": "library upload — bad_filetype refusal", @@ -464,7 +464,7 @@ "status": 200, "contentType": "application/json", "csp": null, - "body": "{\"ok\":true,\"papers\":[{\"name\":\"new paper.pdf\",\"size\":29,\"added_ms\":1787573639655,\"path\":\"/var/folders/26/98pr37253yvcxfjlzkzg_c_r0000gn/T/amicode-fixture-IUxS6z/.amico/library/new paper.pdf\"},{\"name\":\"piccolo-trajectory-2023.pdf\",\"size\":28,\"added_ms\":1785974400000,\"path\":\"/var/folders/26/98pr37253yvcxfjlzkzg_c_r0000gn/T/amicode-fixture-IUxS6z/.amico/library/piccolo-trajectory-2023.pdf\"},{\"name\":\"rydberg-blockade-2024.pdf\",\"size\":28,\"added_ms\":1785628800000,\"path\":\"/var/folders/26/98pr37253yvcxfjlzkzg_c_r0000gn/T/amicode-fixture-IUxS6z/.amico/library/rydberg-blockade-2024.pdf\"}],\"error\":null}" + "body": "{\"ok\":true,\"papers\":[{\"name\":\"new paper.pdf\",\"size\":29,\"added_ms\":1787600428877,\"path\":\"/var/folders/26/98pr37253yvcxfjlzkzg_c_r0000gn/T/amicode-fixture-eSkFCX/.amico/library/new paper.pdf\"},{\"name\":\"piccolo-trajectory-2023.pdf\",\"size\":28,\"added_ms\":1785974400000,\"path\":\"/var/folders/26/98pr37253yvcxfjlzkzg_c_r0000gn/T/amicode-fixture-eSkFCX/.amico/library/piccolo-trajectory-2023.pdf\"},{\"name\":\"rydberg-blockade-2024.pdf\",\"size\":28,\"added_ms\":1785628800000,\"path\":\"/var/folders/26/98pr37253yvcxfjlzkzg_c_r0000gn/T/amicode-fixture-eSkFCX/.amico/library/rydberg-blockade-2024.pdf\"}],\"error\":null}" }, { "name": "widget registry — builtins with content hashes", @@ -475,18 +475,18 @@ "status": 200, "contentType": "application/json", "csp": null, - "body": "{\"ok\":true,\"widgets\":[{\"id\":\"meet-amico\",\"name\":\"Meet Amico\",\"version\":\"1.0.0\",\"bridge\":1,\"description\":\"Who your pal is and what it can do — the front door to a fresh chat\",\"size\":\"hero\",\"height\":250,\"config\":{},\"origin\":null,\"builtin\":true,\"overridden\":false,\"hash\":\"c32d9a4c53880146\",\"path\":null},{\"id\":\"about-you\",\"name\":\"About you\",\"version\":\"1.0.0\",\"bridge\":1,\"description\":\"Your profile, earned stats, and what Amico remembers\",\"size\":\"hero\",\"height\":250,\"config\":{\"stats\":{\"type\":\"multi-select\",\"options\":[\"problems\",\"runs\"],\"default\":[\"problems\",\"runs\"]}},\"origin\":null,\"builtin\":true,\"overridden\":false,\"hash\":\"58cd50dcf42138f7\",\"path\":null},{\"id\":\"jump-back-in\",\"name\":\"Jump back in\",\"version\":\"1.0.0\",\"bridge\":1,\"description\":\"Resume your most recent problem session\",\"size\":\"tile\",\"height\":140,\"config\":{},\"origin\":null,\"builtin\":true,\"overridden\":false,\"hash\":\"4a336aff1377dae4\",\"path\":null},{\"id\":\"now-solving\",\"name\":\"Now solving\",\"version\":\"1.0.0\",\"bridge\":1,\"description\":\"The run in flight — iteration, fidelity, live sparkline\",\"size\":\"tile\",\"height\":96,\"config\":{\"plot\":{\"type\":\"select\",\"options\":[\"pulse\",\"objective\"],\"default\":\"pulse\"}},\"origin\":null,\"builtin\":true,\"overridden\":false,\"hash\":\"0217cc8545eadef9\",\"path\":null},{\"id\":\"showcase\",\"name\":\"Showcase\",\"version\":\"1.0.0\",\"bridge\":1,\"description\":\"Run gallery — shareable cards of your solves\",\"size\":\"tile\",\"height\":140,\"config\":{},\"origin\":null,\"builtin\":true,\"overridden\":false,\"hash\":\"f1b657aebef30b1e\",\"path\":null},{\"id\":\"library\",\"name\":\"Library\",\"version\":\"1.0.0\",\"bridge\":1,\"description\":\"Upload papers — Amico learns your work\",\"size\":\"tile\",\"height\":140,\"config\":{},\"origin\":null,\"builtin\":true,\"overridden\":false,\"hash\":\"f86f8e5a391e0e1b\",\"path\":null}],\"warnings\":[],\"error\":null}" + "body": "{\"ok\":true,\"widgets\":[{\"id\":\"jump-back-in\",\"name\":\"Jump back in\",\"version\":\"1.0.0\",\"bridge\":1,\"description\":\"Resume your most recent problem session\",\"size\":\"tile\",\"height\":140,\"config\":{},\"origin\":null,\"builtin\":true,\"overridden\":false,\"hash\":\"4a336aff1377dae4\",\"path\":null},{\"id\":\"now-solving\",\"name\":\"Now solving\",\"version\":\"1.0.0\",\"bridge\":1,\"description\":\"The run in flight — iteration, fidelity, live sparkline\",\"size\":\"tile\",\"height\":96,\"config\":{\"plot\":{\"type\":\"select\",\"options\":[\"pulse\",\"objective\"],\"default\":\"pulse\"}},\"origin\":null,\"builtin\":true,\"overridden\":false,\"hash\":\"0217cc8545eadef9\",\"path\":null},{\"id\":\"showcase\",\"name\":\"Showcase\",\"version\":\"1.0.0\",\"bridge\":1,\"description\":\"Run gallery — shareable cards of your solves\",\"size\":\"tile\",\"height\":140,\"config\":{},\"origin\":null,\"builtin\":true,\"overridden\":false,\"hash\":\"f1b657aebef30b1e\",\"path\":null},{\"id\":\"library\",\"name\":\"Library\",\"version\":\"1.0.0\",\"bridge\":1,\"description\":\"Upload papers — Amico learns your work\",\"size\":\"tile\",\"height\":140,\"config\":{},\"origin\":null,\"builtin\":true,\"overridden\":false,\"hash\":\"7c01e2536a0efdac\",\"path\":null}],\"warnings\":[],\"error\":null}" }, { "name": "widget frame — served HTML + its own CSP", "request": { "method": "GET", - "path": "/amicode/widget-frame?id=meet-amico" + "path": "/amicode/widget-frame?id=jump-back-in" }, "status": 200, "contentType": "text/html", "csp": "default-src 'none'; script-src 'unsafe-inline' blob:; style-src 'unsafe-inline'; img-src https: data:", - "body": "\n\n\n\n\n\n\n\n
\n\n\n\n" + "body": "\n\n\n\n\n\n\n\n
\n\n\n\n" }, { "name": "widget frame — unknown id stub", @@ -503,12 +503,12 @@ "name": "widget code — builtin source + hash", "request": { "method": "GET", - "path": "/amicode/widget-code?id=about-you" + "path": "/amicode/widget-code?id=jump-back-in" }, "status": 200, "contentType": "application/json", "csp": null, - "body": "{\"ok\":true,\"id\":\"about-you\",\"hash\":\"58cd50dcf42138f7\",\"code\":\"\\nexport default {\\n mount: function (el, amico) {\\n var profile = null\\n var editing = false\\n var saving = false\\n var draft = { name: '', affiliation: '', focus: '', scholar: '', affiliation_logo: '' }\\n var suggestions = []\\n var searchTimer = null\\n var searchSeq = 0\\n\\n var esc = function (s) {\\n return String(s == null ? '' : s)\\n .replace(/&/g, '&')\\n .replace(//g, '>')\\n .replace(/\\\"/g, '"')\\n }\\n var initials = function (name) {\\n var parts = String(name || '').trim().split(/\\\\s+/).filter(Boolean)\\n if (parts.length === 0) return '?'\\n return parts.slice(0, 2).map(function (p) { return p[0].toUpperCase() }).join('')\\n }\\n var MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']\\n var sinceLabel = function (iso) {\\n if (!iso) return null\\n var m = String(iso).match(/^(\\\\d{4})-(\\\\d{2})-(\\\\d{2})$/)\\n if (!m) return null\\n var label = 'since ' + (MONTHS[Number(m[2]) - 1] || m[2]) + ' ' + Number(m[3])\\n var started = Date.parse(iso)\\n if (isNaN(started)) return label\\n var days = Math.max(0, Math.round((Date.now() - started) / 86400000))\\n return days <= 0 ? label + ' \\\\u00b7 today' : label + ' \\\\u00b7 ' + days + ' day' + (days === 1 ? '' : 's')\\n }\\n var on = function (sel, evt, fn) {\\n var n = el.querySelector(sel)\\n if (n) n['on' + evt] = fn\\n }\\n\\n var statBlock = function (value, label) {\\n return (\\n '
' +\\n '' + value + '' +\\n '' + label + '
'\\n )\\n }\\n\\n var render = function () {\\n if (!profile || !profile.ok) {\\n el.innerHTML = ''\\n return\\n }\\n var you = profile.you\\n var stats = you.stats || {}\\n var chosen = (amico.config && amico.config.stats) || ['problems', 'runs']\\n var fresh = !stats.problems && !stats.runs\\n\\n var avatar = you.avatar\\n ? '\\\"\\\"'\\n : '
' + esc(initials(you.name)) + '
'\\n\\n var platforms = (you.platforms || []).slice(0, 3).map(function (p) {\\n return '◇ ' + esc(p)\\n }).join('  ')\\n\\n var editForm =\\n '
' +\\n '' +\\n '
' +\\n '' +\\n (suggestions.length\\n ? '
' +\\n suggestions.map(function (s, i) {\\n return '
' + esc(s.name) + ' ' + esc(s.domain || '') + '
'\\n }).join('') + '
'\\n : '') +\\n '
' +\\n '' +\\n '' +\\n '
' +\\n '' +\\n '' +\\n '
'\\n\\n var identity =\\n '
' + avatar +\\n '
' +\\n '
' +\\n '
' + esc(you.name || 'You') + '
' +\\n (you.scholar ? 'scholar ↗' : '') +\\n '' +\\n '
' +\\n (editing\\n ? editForm\\n : '
' +\\n esc(you.focus || you.affiliation || 'tell Amico about your work') + '
' +\\n (you.description ? '
' + esc(you.description) + '
' : '') +\\n (platforms ? '
' + platforms + '
' : '')) +\\n '
'\\n\\n var body\\n if (fresh && !editing) {\\n body =\\n '
' +\\n '
No experiments yet \\\\u2014 tell Amico what you're working on and it'll start remembering.
' +\\n ''\\n } else if (!editing) {\\n var cells = chosen.map(function (k) {\\n return statBlock(String(stats[k] == null ? 0 : stats[k]), k)\\n }).join('')\\n var remembers = (you.remembers || []).map(function (r) {\\n return '
' +\\n '' +\\n '' + esc(r.title) + '
'\\n }).join('')\\n var since = sinceLabel(stats.since)\\n body =\\n '
' +\\n '
' + cells + '
' +\\n (remembers\\n ? '
' +\\n '
Amico remembers
' +\\n '
' + remembers + '
'\\n : '') +\\n (since ? '
' + since + '
' : '')\\n } else {\\n body = ''\\n }\\n\\n el.innerHTML =\\n '
' +\\n '
About you
' +\\n identity + body + '
'\\n\\n on('[data-edit]', 'click', function () {\\n editing = !editing\\n if (editing) {\\n suggestions = []\\n draft = {\\n name: you.name || '',\\n affiliation: you.affiliation || '',\\n focus: you.focus || '',\\n scholar: you.scholar || '',\\n affiliation_logo: you.affiliation_logo || '',\\n }\\n }\\n render()\\n })\\n on('[data-scholar]', 'click', function () {\\n amico.action('open-external', { url: you.scholar })\\n })\\n on('[data-firstrun]', 'click', function () {\\n amico.prompt('help me get started with my first project')\\n })\\n on('[data-cancel]', 'click', function () {\\n editing = false\\n suggestions = []\\n render()\\n })\\n on('[data-save]', 'click', function () {\\n var read = function (sel) {\\n var n = el.querySelector(sel)\\n return n ? n.value : ''\\n }\\n draft.name = read('[data-f-name]')\\n draft.affiliation = read('[data-f-affiliation]')\\n draft.focus = read('[data-f-focus]')\\n draft.scholar = read('[data-f-scholar]')\\n saving = true\\n render()\\n amico\\n .action('save-profile', draft)\\n .then(function () { return amico.fetch('/amicode/profile') })\\n .then(function (fresh) {\\n profile = fresh\\n saving = false\\n editing = false\\n suggestions = []\\n render()\\n })\\n .catch(function () {\\n saving = false\\n render()\\n })\\n })\\n on('[data-f-affiliation]', 'input', function (e) {\\n var q = e.target.value\\n draft.affiliation = q\\n if (searchTimer) clearTimeout(searchTimer)\\n if (String(q).trim().length < 2) {\\n searchSeq++\\n suggestions = []\\n return\\n }\\n searchTimer = setTimeout(function () {\\n var seq = ++searchSeq\\n amico.action('lookup-institution', { query: q }).then(function (rows) {\\n if (seq !== searchSeq) return\\n suggestions = Array.isArray(rows) ? rows.slice(0, 5) : []\\n render()\\n var input = el.querySelector('[data-f-affiliation]')\\n if (input) {\\n input.focus()\\n input.setSelectionRange(input.value.length, input.value.length)\\n }\\n }).catch(function () {})\\n }, 200)\\n })\\n var suggNodes = el.querySelectorAll('[data-sugg]')\\n for (var i = 0; i < suggNodes.length; i++) {\\n ;(function (node) {\\n node.onclick = function () {\\n var pick = suggestions[Number(node.getAttribute('data-sugg'))]\\n if (!pick) return\\n draft.affiliation = pick.name\\n suggestions = []\\n amico.action('resolve-logo', { name: pick.name, domain: pick.domain }).then(function (r) {\\n if (r && typeof r.logo === 'string') draft.affiliation_logo = r.logo\\n }).catch(function () {})\\n render()\\n }\\n })(suggNodes[i])\\n }\\n }\\n\\n el.innerHTML = ''\\n amico.fetch('/amicode/profile').then(function (data) {\\n profile = data\\n render()\\n }).catch(function (e) {\\n // a hero card must never fail invisibly — show the reason\\n el.innerHTML =\\n '
' +\\n 'About you: profile unavailable (' + String(e && e.message ? e.message : e) + ')
'\\n })\\n amico.onConfig(function () { render() })\\n },\\n}\\n\",\"error\":null}" + "body": "{\"ok\":true,\"id\":\"jump-back-in\",\"hash\":\"4a336aff1377dae4\",\"code\":\"\\nexport default {\\n mount: function (el, amico) {\\n var esc = function (s) {\\n return String(s == null ? '' : s)\\n .replace(/&/g, '&')\\n .replace(//g, '>')\\n .replace(/\\\"/g, '"')\\n }\\n var render = function () {\\n var resume = amico.context && amico.context.resume\\n // tray preview (context.preview): sample session instead of the empty state\\n if ((!resume || !resume.name) && amico.context && amico.context.preview) {\\n resume = { name: 'CZ gate \\\\u2014 transmon pair', meta: '3 runs \\\\u00b7 best F 0.9987' }\\n }\\n if (!resume || !resume.name) {\\n el.innerHTML = ''\\n return\\n }\\n el.innerHTML =\\n '
' +\\n '
Jump back in
' +\\n '
' + esc(resume.name) + '
' +\\n (resume.meta\\n ? '
' + esc(resume.meta) + '
'\\n : '') +\\n '
Resume →
' +\\n '
'\\n var card = el.querySelector('[data-card]')\\n if (card)\\n card.onclick = function () {\\n amico.action('resume-session', {})\\n }\\n }\\n render()\\n amico.onContext(render)\\n },\\n}\\n\",\"error\":null}" }, { "name": "widget code — not_found", @@ -575,7 +575,7 @@ "status": 200, "contentType": "application/json", "csp": null, - "body": "{\"ok\":true,\"dashboard\":{\"version\":1,\"widget\":[{\"key\":\"w-11d8dc\",\"id\":\"meet-amico\",\"hidden\":true,\"config\":{}},{\"group\":\"left\",\"view\":\"expanded\",\"key\":\"w-62622e\",\"id\":\"about-you\",\"hidden\":false,\"config\":{\"stats\":[\"problems\",\"runs\"]}},{\"key\":\"w-34ab1d\",\"id\":\"ghost-widget\",\"hidden\":false,\"config\":{\"any\":\"values\"},\"missing\":true},{\"key\":\"w-c89721\",\"id\":\"jump-back-in\",\"hidden\":false,\"config\":{}},{\"key\":\"w-ab0e19\",\"id\":\"now-solving\",\"hidden\":false,\"config\":{\"plot\":\"pulse\"}},{\"key\":\"w-9877f8\",\"id\":\"showcase\",\"hidden\":false,\"config\":{}},{\"key\":\"w-b718f1\",\"id\":\"library\",\"hidden\":false,\"config\":{}}],\"views\":{\"home\":\"grid\"}},\"error\":null}" + "body": "{\"ok\":true,\"dashboard\":{\"version\":1,\"widget\":[{\"key\":\"w-34ab1d\",\"id\":\"ghost-widget\",\"hidden\":false,\"config\":{\"any\":\"values\"},\"missing\":true},{\"key\":\"w-c89721\",\"id\":\"jump-back-in\",\"hidden\":false,\"config\":{}},{\"key\":\"w-ab0e19\",\"id\":\"now-solving\",\"hidden\":false,\"config\":{\"plot\":\"pulse\"}},{\"key\":\"w-9877f8\",\"id\":\"showcase\",\"hidden\":false,\"config\":{}},{\"key\":\"w-b718f1\",\"id\":\"library\",\"hidden\":false,\"config\":{}}],\"views\":{\"home\":\"grid\"}},\"error\":null}" }, { "name": "dashboard save — merge + reserved keys", @@ -585,12 +585,6 @@ "body": { "version": 1, "widget": [ - { - "id": "about-you", - "hidden": false, - "config": {}, - "group": "right" - }, { "id": "my-showcase", "hidden": true, @@ -605,7 +599,7 @@ "status": 200, "contentType": "application/json", "csp": null, - "body": "{\"ok\":true,\"dashboard\":{\"version\":1,\"widget\":[{\"group\":\"right\",\"key\":\"w-62622e\",\"id\":\"about-you\",\"hidden\":false,\"config\":{\"stats\":[\"problems\",\"runs\"]}},{\"key\":\"w-41beb5\",\"id\":\"my-showcase\",\"hidden\":true,\"config\":{}},{\"key\":\"w-11d8dc\",\"id\":\"meet-amico\",\"hidden\":false,\"config\":{}},{\"key\":\"w-c89721\",\"id\":\"jump-back-in\",\"hidden\":false,\"config\":{}},{\"key\":\"w-ab0e19\",\"id\":\"now-solving\",\"hidden\":false,\"config\":{\"plot\":\"pulse\"}},{\"key\":\"w-9877f8\",\"id\":\"showcase\",\"hidden\":false,\"config\":{}},{\"key\":\"w-b718f1\",\"id\":\"library\",\"hidden\":false,\"config\":{}}],\"views\":{\"home\":\"grid\"}},\"error\":null}" + "body": "{\"ok\":true,\"dashboard\":{\"version\":1,\"widget\":[{\"key\":\"w-41beb5\",\"id\":\"my-showcase\",\"hidden\":true,\"config\":{}},{\"key\":\"w-c89721\",\"id\":\"jump-back-in\",\"hidden\":false,\"config\":{}},{\"key\":\"w-ab0e19\",\"id\":\"now-solving\",\"hidden\":false,\"config\":{\"plot\":\"pulse\"}},{\"key\":\"w-9877f8\",\"id\":\"showcase\",\"hidden\":false,\"config\":{}},{\"key\":\"w-b718f1\",\"id\":\"library\",\"hidden\":false,\"config\":{}}],\"views\":{\"home\":\"grid\"}},\"error\":null}" }, { "name": "dashboard save — bad_body refusal", @@ -630,7 +624,7 @@ "status": 200, "contentType": "application/json", "csp": null, - "body": "{\"ok\":true,\"connections\":[{\"id\":\"company-compute\",\"state\":\"connected\",\"validated_at\":\"2026-08-24T12:12:58.935Z\",\"stale\":false,\"identity\":\"aaron\",\"entitlements\":[\"hpc\"],\"icon\":\"\",\"name\":\"Harmoniqs Cloud\"},{\"id\":\"pasqal-cloud\",\"state\":\"needs-key\",\"validated_at\":null,\"stale\":false,\"icon\":\"\",\"name\":\"Pasqal Cloud\"},{\"id\":\"slack\",\"state\":\"connected\",\"validated_at\":\"2026-08-24T12:12:58.935Z\",\"stale\":false,\"identity\":\"aaron@example\",\"icon\":\"\",\"name\":\"Slack\"},{\"id\":\"github\",\"state\":\"needs-key\",\"validated_at\":null,\"stale\":false,\"icon\":\"\",\"name\":\"GitHub\"},{\"id\":\"linear\",\"state\":\"needs-key\",\"validated_at\":null,\"stale\":false,\"icon\":\"\",\"name\":\"Linear\"},{\"id\":\"google\",\"state\":\"needs-key\",\"validated_at\":null,\"stale\":false,\"icon\":\"\",\"name\":\"Google\",\"auth_methods\":[\"token\",\"browser\"]},{\"id\":\"google-drive\",\"state\":\"needs-key\",\"validated_at\":null,\"stale\":false,\"icon\":\"\",\"name\":\"Google Drive\",\"auth_methods\":[\"token\",\"browser\"]},{\"id\":\"custom-seed1\",\"state\":\"connected\",\"validated_at\":null,\"stale\":true,\"icon\":\"L\",\"name\":\"Lab QPU\"}],\"error\":null}" + "body": "{\"ok\":true,\"connections\":[{\"id\":\"company-compute\",\"state\":\"connected\",\"validated_at\":\"2026-08-24T19:39:28.154Z\",\"stale\":false,\"identity\":\"aaron\",\"entitlements\":[\"hpc\"],\"icon\":\"\",\"name\":\"Harmoniqs Cloud\"},{\"id\":\"pasqal-cloud\",\"state\":\"needs-key\",\"validated_at\":null,\"stale\":false,\"icon\":\"\",\"name\":\"Pasqal Cloud\"},{\"id\":\"slack\",\"state\":\"connected\",\"validated_at\":\"2026-08-24T19:39:28.154Z\",\"stale\":false,\"identity\":\"aaron@example\",\"icon\":\"\",\"name\":\"Slack\"},{\"id\":\"github\",\"state\":\"needs-key\",\"validated_at\":null,\"stale\":false,\"icon\":\"\",\"name\":\"GitHub\"},{\"id\":\"linear\",\"state\":\"needs-key\",\"validated_at\":null,\"stale\":false,\"icon\":\"\",\"name\":\"Linear\"},{\"id\":\"google\",\"state\":\"needs-key\",\"validated_at\":null,\"stale\":false,\"icon\":\"\",\"name\":\"Google\",\"auth_methods\":[\"token\",\"browser\"]},{\"id\":\"google-drive\",\"state\":\"needs-key\",\"validated_at\":null,\"stale\":false,\"icon\":\"\",\"name\":\"Google Drive\",\"auth_methods\":[\"token\",\"browser\"]},{\"id\":\"custom-seed1\",\"state\":\"connected\",\"validated_at\":null,\"stale\":true,\"icon\":\"L\",\"name\":\"Lab QPU\"}],\"error\":null}" }, { "name": "connections catalog — configured filtered out", @@ -823,7 +817,7 @@ "status": 200, "contentType": "application/json", "csp": null, - "body": "{\"ok\":true,\"connections\":[{\"id\":\"company-compute\",\"state\":\"connected\",\"validated_at\":\"2026-08-24T12:12:58.935Z\",\"stale\":false,\"identity\":\"aaron\",\"entitlements\":[\"hpc\"],\"icon\":\"\",\"name\":\"Harmoniqs Cloud\"},{\"id\":\"pasqal-cloud\",\"state\":\"needs-key\",\"validated_at\":null,\"stale\":false,\"icon\":\"\",\"name\":\"Pasqal Cloud\"},{\"id\":\"slack\",\"state\":\"needs-key\",\"validated_at\":null,\"stale\":false,\"icon\":\"\",\"name\":\"Slack\"},{\"id\":\"github\",\"state\":\"needs-key\",\"validated_at\":null,\"stale\":false,\"icon\":\"\",\"name\":\"GitHub\"},{\"id\":\"linear\",\"state\":\"needs-key\",\"validated_at\":null,\"stale\":false,\"icon\":\"\",\"name\":\"Linear\"},{\"id\":\"google\",\"state\":\"needs-key\",\"validated_at\":null,\"stale\":false,\"icon\":\"\",\"name\":\"Google\",\"auth_methods\":[\"token\",\"browser\"]},{\"id\":\"google-drive\",\"state\":\"needs-key\",\"validated_at\":null,\"stale\":false,\"icon\":\"\",\"name\":\"Google Drive\",\"auth_methods\":[\"token\",\"browser\"]}],\"error\":null}" + "body": "{\"ok\":true,\"connections\":[{\"id\":\"company-compute\",\"state\":\"connected\",\"validated_at\":\"2026-08-24T19:39:28.154Z\",\"stale\":false,\"identity\":\"aaron\",\"entitlements\":[\"hpc\"],\"icon\":\"\",\"name\":\"Harmoniqs Cloud\"},{\"id\":\"pasqal-cloud\",\"state\":\"needs-key\",\"validated_at\":null,\"stale\":false,\"icon\":\"\",\"name\":\"Pasqal Cloud\"},{\"id\":\"slack\",\"state\":\"needs-key\",\"validated_at\":null,\"stale\":false,\"icon\":\"\",\"name\":\"Slack\"},{\"id\":\"github\",\"state\":\"needs-key\",\"validated_at\":null,\"stale\":false,\"icon\":\"\",\"name\":\"GitHub\"},{\"id\":\"linear\",\"state\":\"needs-key\",\"validated_at\":null,\"stale\":false,\"icon\":\"\",\"name\":\"Linear\"},{\"id\":\"google\",\"state\":\"needs-key\",\"validated_at\":null,\"stale\":false,\"icon\":\"\",\"name\":\"Google\",\"auth_methods\":[\"token\",\"browser\"]},{\"id\":\"google-drive\",\"state\":\"needs-key\",\"validated_at\":null,\"stale\":false,\"icon\":\"\",\"name\":\"Google Drive\",\"auth_methods\":[\"token\",\"browser\"]}],\"error\":null}" }, { "name": "project create — mkdir (git absent, best-effort)", @@ -837,7 +831,7 @@ "status": 200, "contentType": "application/json", "csp": null, - "body": "{\"ok\":true,\"path\":\"/var/folders/26/98pr37253yvcxfjlzkzg_c_r0000gn/T/amicode-fixture-IUxS6z/AmicodeProjects/my-new-project\",\"slug\":\"my-new-project\",\"gitInitialized\":false}" + "body": "{\"ok\":true,\"path\":\"/var/folders/26/98pr37253yvcxfjlzkzg_c_r0000gn/T/amicode-fixture-eSkFCX/AmicodeProjects/my-new-project\",\"slug\":\"my-new-project\",\"gitInitialized\":false}" }, { "name": "project create — collision", @@ -891,7 +885,7 @@ "status": 200, "contentType": "application/json", "csp": null, - "body": "{\"ok\":true,\"parentDir\":\"/var/folders/26/98pr37253yvcxfjlzkzg_c_r0000gn/T/amicode-fixture-IUxS6z/AmicodeProjects\",\"projects\":[{\"slug\":\"my-new-project\",\"path\":\"/var/folders/26/98pr37253yvcxfjlzkzg_c_r0000gn/T/amicode-fixture-IUxS6z/AmicodeProjects/my-new-project\"},{\"slug\":\"prior-project\",\"path\":\"/var/folders/26/98pr37253yvcxfjlzkzg_c_r0000gn/T/amicode-fixture-IUxS6z/AmicodeProjects/prior-project\"}]}" + "body": "{\"ok\":true,\"parentDir\":\"/var/folders/26/98pr37253yvcxfjlzkzg_c_r0000gn/T/amicode-fixture-eSkFCX/AmicodeProjects\",\"projects\":[{\"slug\":\"my-new-project\",\"path\":\"/var/folders/26/98pr37253yvcxfjlzkzg_c_r0000gn/T/amicode-fixture-eSkFCX/AmicodeProjects/my-new-project\"},{\"slug\":\"prior-project\",\"path\":\"/var/folders/26/98pr37253yvcxfjlzkzg_c_r0000gn/T/amicode-fixture-eSkFCX/AmicodeProjects/prior-project\"}]}" } ] }