diff --git a/AMICODE-PATCHES.md b/AMICODE-PATCHES.md
index 4d25536a38..b4fd45255d 100644
--- a/AMICODE-PATCHES.md
+++ b/AMICODE-PATCHES.md
@@ -56,6 +56,7 @@ PATH="$HOME/.bun/bin:$PATH" OPENCODE_VERSION=1.17.3 bun run script/build.ts --si
```bash
./dist/opencode-local serve --port 14096
```
+
- startup log: `opencode server listening on http://127.0.0.1:14096` (+ warning: `OPENCODE_SERVER_PASSWORD` not set → unsecured)
- `GET /` → 200, SolidJS app HTML shell (`
`, script `/assets/index-BfWaaOZM.js` = the exact vite-build hash → served from the binary embed, no separate app step needed)
- `GET /assets/index-BfWaaOZM.js` → 200 text/javascript 1,694,088 B
@@ -66,6 +67,7 @@ PATH="$HOME/.bun/bin:$PATH" OPENCODE_VERSION=1.17.3 bun run script/build.ts --si
### 4. How the SolidJS app is served
`packages/opencode/src/server/shared/ui.ts`:
+
- Compiled binary: `import("opencode-web-ui.gen.ts")` resolves → `serveEmbeddedUIEffect`
serves the embedded `packages/app/dist` files (SPA fallback to `index.html`), with CSP
derived from the inline theme-preload script hash.
@@ -79,28 +81,29 @@ PATH="$HOME/.bun/bin:$PATH" OPENCODE_VERSION=1.17.3 bun run script/build.ts --si
User-visible brand sites in the served web app (packages/app + packages/ui), enumerated at tag v1.17.3.
Format: file:line — string/asset — replacement. [desktop] = ships only in the Electron desktop app, not the served web app.
-| # | site | current | replacement |
-|---|------|---------|-------------|
-| 1 | packages/app/index.html:6 | `
OpenCode` | `
Amicode` |
-| 2 | packages/app/index.html:8 | svg favicon `/favicon-v3.svg` | `/amico.svg` (new brand asset) |
-| 3 | packages/ui/src/assets/favicon/site.webmanifest:2-3 (symlinked as packages/app/public/site.webmanifest) | `name`/`short_name` "OpenCode" | "Amicode" |
-| 4 | packages/ui/src/assets/favicon/amico.svg (new; symlinked as packages/app/public/amico.svg, matching the repo's favicon symlink pattern) | (new) | favicon: brand-accent disc `#FFF676` + amico face (from amicode `packages/extension/media/amico.svg`, on-accent `#000`) |
-| 5 | packages/ui/src/components/favicon.tsx:10 | `apple-mobile-web-app-title` "OpenCode" | "Amicode" (+ svg icon Link `/amico.svg`) |
-| 6 | packages/ui/src/components/logo.tsx | `Logo` = OPENCODE letterform SVG; `Mark`/`Splash` = "O" mark | `Logo` → AMICODE wordmark text; `Mark`/`Splash` → amico face (inlined from amico.svg). Rendered at: home.tsx:1151 (watermark), pages/error.tsx:281, session-side-panel.tsx, session-new-view.tsx, app.tsx (loading splash) |
-| 7 | packages/ui/src/v2/components/wordmark-v2.tsx | OPENCODE glyph SVG | AMICODE wordmark text (rendered on session-new-design-view.tsx new-session screen) |
-| 8 | packages/ui/src/theme/context.tsx:72 | theme display label `opencode: "OpenCode"` | `"Amicode"` (key `opencode` untouched — storage/config contract) |
-| 9 | packages/app/src/i18n/en.ts | 30 brand mentions ("…models in OpenCode", "OpenCode Desktop", update/error/settings copy) | "Amicode" (29 sites; "OpenCode Zen" kept, see below) |
-| 10 | packages/app/src/desktop-menu.ts:75 [desktop] | menu label "OpenCode" | "Amicode" |
-| 11 | packages/app/src/wsl/settings-model.ts:17-18 [desktop, Windows/WSL] | "Install OpenCode"/"Update OpenCode" | "Install/Update Amicode" |
-| 13 | packages/ui/src/components/logo.css | `[data-component="logo-mark"]` aspect-ratio 4/5 (old 16x20 viewBox) | aspect-ratio 1/1 (new square amico mark) |
-| 12 | packages/ui/src/theme/themes/oc-2.json (default theme) | accent tokens `v2-{background,text,icon}-*accent*` = blue-600/700 (light), blue-400/300 (dark) | Harmoniqs accent `#FFF676` (brand.css `--color-accent`): dark text/icon/bg-accent → `#FFF676`, hover `#FFFA9E`; light bg-accent → `#FFF676`, light text/icon-accent → derived dark shades `#857A00` / hover `#6B6200` (raw `#FFF676` is unreadable on light bg) |
+| # | site | current | replacement |
+| --- | --------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| 1 | packages/app/index.html:6 | `
OpenCode` | `
Amicode` |
+| 2 | packages/app/index.html:8 | svg favicon `/favicon-v3.svg` | `/amico.svg` (new brand asset) |
+| 3 | packages/ui/src/assets/favicon/site.webmanifest:2-3 (symlinked as packages/app/public/site.webmanifest) | `name`/`short_name` "OpenCode" | "Amicode" |
+| 4 | packages/ui/src/assets/favicon/amico.svg (new; symlinked as packages/app/public/amico.svg, matching the repo's favicon symlink pattern) | (new) | favicon: brand-accent disc `#FFF676` + amico face (from amicode `packages/extension/media/amico.svg`, on-accent `#000`) |
+| 5 | packages/ui/src/components/favicon.tsx:10 | `apple-mobile-web-app-title` "OpenCode" | "Amicode" (+ svg icon Link `/amico.svg`) |
+| 6 | packages/ui/src/components/logo.tsx | `Logo` = OPENCODE letterform SVG; `Mark`/`Splash` = "O" mark | `Logo` → AMICODE wordmark text; `Mark`/`Splash` → amico face (inlined from amico.svg). Rendered at: home.tsx:1151 (watermark), pages/error.tsx:281, session-side-panel.tsx, session-new-view.tsx, app.tsx (loading splash) |
+| 7 | packages/ui/src/v2/components/wordmark-v2.tsx | OPENCODE glyph SVG | AMICODE wordmark text (rendered on session-new-design-view.tsx new-session screen) |
+| 8 | packages/ui/src/theme/context.tsx:72 | theme display label `opencode: "OpenCode"` | `"Amicode"` (key `opencode` untouched — storage/config contract) |
+| 9 | packages/app/src/i18n/en.ts | 30 brand mentions ("…models in OpenCode", "OpenCode Desktop", update/error/settings copy) | "Amicode" (29 sites; "OpenCode Zen" kept, see below) |
+| 10 | packages/app/src/desktop-menu.ts:75 [desktop] | menu label "OpenCode" | "Amicode" |
+| 11 | packages/app/src/wsl/settings-model.ts:17-18 [desktop, Windows/WSL] | "Install OpenCode"/"Update OpenCode" | "Install/Update Amicode" |
+| 13 | packages/ui/src/components/logo.css | `[data-component="logo-mark"]` aspect-ratio 4/5 (old 16x20 viewBox) | aspect-ratio 1/1 (new square amico mark) |
+| 12 | packages/ui/src/theme/themes/oc-2.json (default theme) | accent tokens `v2-{background,text,icon}-*accent*` = blue-600/700 (light), blue-400/300 (dark) | Harmoniqs accent `#FFF676` (brand.css `--color-accent`): dark text/icon/bg-accent → `#FFF676`, hover `#FFFA9E`; light bg-accent → `#FFF676`, light text/icon-accent → derived dark shades `#857A00` / hover `#6B6200` (raw `#FFF676` is unreadable on light bg) |
Deliberately left stock (and why):
+
- 25 non-English app locales + all ui locales brand strings — demo is English; en.ts is the default/fallback dictionary; bulk-editing translations is churn without review.
- "OpenCode Zen" (app en.ts:139) and "OpenCode Go" (ui en.ts:58) — proper names of the external commercial model-gateway services the binary still connects to; renaming would misrepresent a third-party service.
- Shiki syntax-theme name "OpenCode" (ui context/marked.tsx, ui pierre/worker.ts, ui pierre/index.ts) — cross-module string identifier, not user-visible; partial rename breaks code highlighting.
- Font names "OpenCode Sans/Mono" — CSS font-family identifiers.
-- Binary icon assets (favicon-96x96-v3.png, favicon-v3.ico, apple-touch-icon-v3.png, web-app-manifest-*.png, social-share.png) — need image regeneration tooling; svg favicon takes precedence in modern browsers. Morning follow-up.
+- Binary icon assets (favicon-96x96-v3.png, favicon-v3.ico, apple-touch-icon-v3.png, web-app-manifest-\*.png, social-share.png) — need image regeneration tooling; svg favicon takes precedence in modern browsers. Morning follow-up.
- All internal identifiers: `opencode-titlebar-*` DOM ids, `opencode-*` localStorage keys, `OPENCODE_*` env vars, config keys, API/SDK strings, `@opencode-ai/*` imports, `oc-theme-preload-script` id (the server CSP hash regex in packages/opencode/src/server/shared/ui.ts matches this exact id).
- Storybook/stories files — dev-only, not in the served app.
- desktop-menu.ts:203 "OpenCode Documentation" [desktop] — the link target IS opencode.ai/docs; relabeling would misattribute upstream docs.
@@ -120,26 +123,26 @@ Rebuilt with the exact T3 recipe (`OPENCODE_VERSION=1.17.3 bun run script/build.
1. `4e20def26e` — amicode: build fixes for local v1.17.3 reproduction (AMICODE-PATCHES.md, bun.lock ghostty-web drift)
2. `c566a17db7` — amicode: L1 branding — AMICODE wordmark, logo, accent. Files: packages/app/index.html, packages/ui/src/assets/favicon/{amico.svg,site.webmanifest}, packages/app/public/amico.svg (symlink), packages/ui/src/components/{favicon.tsx,logo.tsx,logo.css}, packages/ui/src/v2/components/wordmark-v2.tsx, packages/ui/src/theme/context.tsx, packages/ui/src/theme/themes/oc-2.json, packages/app/src/i18n/en.ts, packages/app/src/desktop-menu.ts, packages/app/src/wsl/settings-model.ts. User-visible strings/assets ONLY — no identifier, config-key, env-var, or API renames.
-3. `1929d3db09` — amicode: L2 renderer slot — amicode_* tool cards.
- - Renderer location: the session timeline (packages/app/src/pages/session/message-timeline.tsx:1057) delegates every message part to `Part` in **packages/ui/src/components/message-part.tsx**; tool parts dispatch at `PART_MAPPING["tool"]` via `ToolRegistry.render(part().tool) ?? GenericTool`. (Errored tool parts branch to ToolErrorCard *before* this dispatch, so amicode errors keep stock error rendering.)
- - Sole stock-code touch: packages/ui/src/components/message-part.tsx — import at **line 38**, dispatch branch at **lines 1392–1395**: `/^amicode_/.test(part().tool) ? AmicodeToolCard : (ToolRegistry.render(...) ?? GenericTool)`. Prefix regex (not per-name ToolRegistry registration) so future amicode_* tools (to_hardware, calibrate) auto-match.
+3. `1929d3db09` — amicode: L2 renderer slot — amicode\_\* tool cards.
+ - Renderer location: the session timeline (packages/app/src/pages/session/message-timeline.tsx:1057) delegates every message part to `Part` in **packages/ui/src/components/message-part.tsx**; tool parts dispatch at `PART_MAPPING["tool"]` via `ToolRegistry.render(part().tool) ?? GenericTool`. (Errored tool parts branch to ToolErrorCard _before_ this dispatch, so amicode errors keep stock error rendering.)
+ - Sole stock-code touch: packages/ui/src/components/message-part.tsx — import at **line 38**, dispatch branch at **lines 1392–1395**: `/^amicode_/.test(part().tool) ? AmicodeToolCard : (ToolRegistry.render(...) ?? GenericTool)`. Prefix regex (not per-name ToolRegistry registration) so future amicode\_\* tools (to_hardware, calibrate) auto-match.
- New files (all presentation): packages/ui/src/amicode/card.tsx (AmicodeToolCard: "AMICODE ·
" header in accent `var(--v2-text-text-accent)`, 3px accent left border `var(--v2-icon-icon-accent)`, monospace body = tool output, falls back to pretty-printed input while running); packages/ui/src/amicode/stage.ts (pure stage mapping: pick_system→System, set_model→Model, formulate→Formulation, solve→Run, else de-underscored+capitalized); packages/ui/src/amicode/stage.test.ts (3 tests, `bun test src/amicode` → 3 pass).
- Visual check of the card in a live session = morning (headless smoke can't exercise a tool-call part without a model turn).
4. `9829bd1864` — amicode: L3 entity rail + collapsed tool chips. Live feedback (Aaron, 2026-07-03): full cards repeated per call cluttered the transcript, and the raw tool return is agent-directed text (for the MODEL, not the human).
- AmicodeToolCard collapsed to a one-line chip "AMICODE · updated ✓" (or "running…"); monospace body REMOVED on purpose — do not resurrect it.
- - NEW packages/ui/src/amicode/entity-rail.tsx — `AmicodeEntityRail`: compact sticky row, chips System / Formulation / Run showing the LATEST state per stage, derived client-side by scanning the session's amicode_* tool parts (messages ULID-sorted; later parts win). Renders nothing until the session has ≥1 amicode_* part (non-amicode sessions stay stock); stages without parts show a dimmed "—". pick_system AND set_model both feed the System chip.
+ - NEW packages/ui/src/amicode/entity-rail.tsx — `AmicodeEntityRail`: compact sticky row, chips System / Formulation / Run showing the LATEST state per stage, derived client-side by scanning the session's amicode*\* tool parts (messages ULID-sorted; later parts win). Renders nothing until the session has ≥1 amicode*\* part (non-amicode sessions stay stock); stages without parts show a dimmed "—". pick_system AND set_model both feed the System chip.
- NEW packages/ui/src/components/amicode-entity-rail.tsx — one-line re-export shim so packages/app imports resolve through the EXISTING `"./*": "./src/components/*.tsx"` export wildcard (packages/ui/package.json untouched).
- stage.ts extended with pure helpers `railStage` + `chipTextFromSummary` (tolerant summary→chip parser: first parenthetical, comma tokens, rewrites omega=→ω=, delta=→δ=, drive_max=→cap, "N levels"→"N lvl", `=-`→`=−`; unknown tokens pass through raw; no/empty parenthetical → undefined). stage.test.ts: 9 tests / 20 expects, `bun test src/amicode` → 9 pass.
- Stock-code touch #2 (rail mount): packages/app/src/pages/session/message-timeline.tsx — import at **line 20**, mount at **lines 1583–1584** (bottom of the sticky `data-session-title` header inside the timeline ScrollView: once per session view, sticky at top, no prompt overlap, no scroll theft; rail hidden when `showHeader()` is false — acceptable v0). Total stock-file touches across L2+L3: message-part.tsx (dispatch) + message-timeline.tsx (mount).
5. (final night batch) — amicode: ask buttons + start-screen wordmark + default locale en.
a. ASK BUTTONS (`amicode_ask`, 7th tool in the pack). NEW packages/ui/src/amicode/{ask.ts, ask-bridge.ts, ask-card.tsx, ask.test.ts}.
- - `parseAskInput` (pure): reads `question`/`options[]` from the tool part's INPUT args; trims, drops non-string/empty options; malformed → undefined → AmicodeToolCard falls back to the collapsed chip.
- - `latestAssistantMessageID` (pure): ULID-max over role==="assistant" — staleness guard; buttons render disabled when the part is NOT in the last assistant message, after a click (local `picked` state), or when no bridge is registered (share page/read-only surfaces can never submit).
- - Send path: ask-bridge module signal. The app registers it through the EXISTING rail mount — `AmicodeEntityRail` gained an `onAsk` prop; message-timeline passes `(text) => sdk.client.session.promptAsync({ sessionID, parts: [{type:"text", text}] })` — same endpoint the prompt input's submit uses (`model`/`agent` are OPTIONAL in the server PromptInput schema → session/agent defaults apply). Bridge unregisters on rail unmount.
- - Stock-code touches (L2 family): packages/ui/src/components/message-part.tsx — `messageID={part().messageID}` added to the tool `` (1 line, needed for the staleness guard); packages/app/src/pages/session/message-timeline.tsx — the existing rail mount now passes `onAsk` (same insertion site as L3).
- - Tests: `bun test src/amicode` → 14 pass / 33 expects (stage 9 + ask 5).
- b. START-SCREEN WORDMARK (L1 family). packages/app/src/components/session/session-new-view.tsx — localized `session.new.title` headline ("Build anything" / "Créez ce que vous voulez") REPLACED by `` (AMICODE wordmark component, deliberately NOT localized — brand marks don't translate). Mark (amico face) above and localized repo-path/branch/subtitle lines kept as-is. (The new-design variant session-new-design-view.tsx already renders WordmarkV2 since L1.)
- c. DEFAULT LOCALE = en (L1 family). packages/app/src/context/language.tsx — both init sites (`warm` line ~196 and provider `initial` line ~203) now default `readStoredLocale() ?? "en"` instead of `?? detectLocale()`; an explicit stored preference (`opencode.global.dat:language`) still wins. Rationale: only en.ts is branded tonight. REVERT CONDITION: restore `?? detectLocale()` at both sites once the other locales are branded (`detectLocale` is left in place, currently unreferenced).
+ - `parseAskInput` (pure): reads `question`/`options[]` from the tool part's INPUT args; trims, drops non-string/empty options; malformed → undefined → AmicodeToolCard falls back to the collapsed chip.
+ - `latestAssistantMessageID` (pure): ULID-max over role==="assistant" — staleness guard; buttons render disabled when the part is NOT in the last assistant message, after a click (local `picked` state), or when no bridge is registered (share page/read-only surfaces can never submit).
+ - Send path: ask-bridge module signal. The app registers it through the EXISTING rail mount — `AmicodeEntityRail` gained an `onAsk` prop; message-timeline passes `(text) => sdk.client.session.promptAsync({ sessionID, parts: [{type:"text", text}] })` — same endpoint the prompt input's submit uses (`model`/`agent` are OPTIONAL in the server PromptInput schema → session/agent defaults apply). Bridge unregisters on rail unmount.
+ - Stock-code touches (L2 family): packages/ui/src/components/message-part.tsx — `messageID={part().messageID}` added to the tool `` (1 line, needed for the staleness guard); packages/app/src/pages/session/message-timeline.tsx — the existing rail mount now passes `onAsk` (same insertion site as L3).
+ - Tests: `bun test src/amicode` → 14 pass / 33 expects (stage 9 + ask 5).
+ b. START-SCREEN WORDMARK (L1 family). packages/app/src/components/session/session-new-view.tsx — localized `session.new.title` headline ("Build anything" / "Créez ce que vous voulez") REPLACED by `` (AMICODE wordmark component, deliberately NOT localized — brand marks don't translate). Mark (amico face) above and localized repo-path/branch/subtitle lines kept as-is. (The new-design variant session-new-design-view.tsx already renders WordmarkV2 since L1.)
+ c. DEFAULT LOCALE = en (L1 family). packages/app/src/context/language.tsx — both init sites (`warm` line ~196 and provider `initial` line ~203) now default `readStoredLocale() ?? "en"` instead of `?? detectLocale()`; an explicit stored preference (`opencode.global.dat:language`) still wins. Rationale: only en.ts is branded tonight. REVERT CONDITION: restore `?? detectLocale()` at both sites once the other locales are branded (`detectLocale` is left in place, currently unreferenced).
- Batch build (L1+L2+L3+ask/wordmark/locale) sha256: `a236b87fe92043eea6129a4a5b260ca2adc7fe51fc634e49405872973a7a60cb` at both `dist/opencode-local` and the vendored path. Verify: `GET /` → 200 `Amicode`; main chunk `/assets/index-Dwcv_zit.js` → 200, `AMICODE` ×1 (start-screen wordmark now in the MAIN bundle); session chunk `/assets/session-CGaG1HX5.js` → 200, `amicode-ask-card` ×1 + `amicode-entity-rail` ×1; `POST /session {}` → 200 session object; `bun test src/amicode` → 14 pass.
- ⚠️ Vendor swap while the binary is RUNNING: plain `cp` fails with ETXTBSY. Use write-temp + `mv -f` (rename) — the running process keeps the old inode, next restart picks up the new file. Done that way this batch.
6. (getting-started) — amicode: start-screen getting-started block + starter chips.
@@ -166,39 +169,48 @@ Rebuilt with the exact T3 recipe (`OPENCODE_VERSION=1.17.3 bun run script/build.
- LEGACY pending morning review: packages/ui/src/amicode/{ask-card.tsx, ask-bridge.ts, ask.ts hasUserReplyAfter/parseAskInput} + the message-part.tsx amicode_ask routing remain in place as the legacy renderer for deprecated amicode_ask calls — do NOT remove tonight.
- Question-branding build sha256: `69070a33f8d5ebdff4f38f3d68b4be5625398b581accfb577945e1e9d522f44c` (dist/opencode-local + vendored path, write-temp + mv -f swap). Verify: `GET /` → 200 `Amicode`; AMICO display labels total 4 — session chunk `session-BfBMMSAH.js` ×3 (rail/chip/ask-card) + composer-state chunk `session-composer-state-D8UQPTE4.js` ×1 (question dock, `amicode-question-brand` slot ×1 — the dock compiles into the COMPOSER chunk, not the session chunk); css asset carries the accent border rule ×1; `POST /session {}` → 200; `bun test src/amicode` → 20 pass.
10. (H-glyph working spinner) — amicode: the session timeline's working/thinking spinner is the H-robot glyph.
- - Design v2 per Aaron (binary-eye-frames concept DROPPED before commit): small monochrome H silhouette with the screen slit knocked out (single path, fill-rule=evenodd, currentColor — mount-site `style={{color}}` passes through like stock Spinner).
- - Animation choice: gentle opacity pulse reusing the EXISTING `pulse-opacity` keyframes (styles/animations.css, 0.4→1) at 1.2s — chosen over Claude-style rotation because the H is non-radial and tumbles/blurs at 16px, and pulse matches the stock spinner's own animation language. prefers-reduced-motion: static glyph, NO animation (matchMedia guard; the repo's CSS-file media-query pattern doesn't cover inline animations).
- - NEW packages/ui/src/amicode/spinner.tsx (`AmicoSpinner`, data-component="amico-spinner") + packages/ui/src/components/amico-spinner.tsx re-export shim. No pure frame logic remained after the design change → no new unit tests.
- - Stock touch: packages/app/src/pages/session/message-timeline.tsx — import swap + `` replacing `` at the sticky-header working indicator (~line 1359); unused Spinner import removed. Other spinner sites left STOCK (list for morning): session-header.tsx:362 (header circle), home.tsx:848, sidebar-items.tsx:66+124, sidebar-workspace.tsx:102, dialog-connect-provider.tsx.
- - Spinner build sha256: `83f5a9fd3b35155bf2014115b54a27ef3d9542e8b14c7a83925175a73c1f7ebc` (dist/opencode-local + vendored path, write-temp + mv -f swap). Verify: `GET /` → 200 `Amicode`; session chunk `session-CMqmIgk2.js` → `amico-spinner` ×1 + `pulse-opacity 1.2s` ×1; `POST /session {}` → 200; `bun test src/amicode` → 20 pass.
- - SWEEP (Aaron: "no spinner" — the sticky-header site isn't where he looks): AmicoSpinner now at ALL visible working indicators: session-header.tsx:362 (top-right circle, size-3.5 + tint), message-part.tsx task-tool running row (~1841; inline 16x14 style because basic-tool.css sizes only [data-component="spinner"]), session-retry.tsx:58 (retry card), home.tsx:848, sidebar-items.tsx:66+124, sidebar-workspace.tsx:102 — each with the site's exact class/tint. Stock Spinner imports removed from swapped files. Left stock on purpose: dialog-connect-provider.tsx (provider setup, rarely seen), wsl/dialog-add-server.tsx (Windows-only).
- - Sweep build sha256: `8aa718ad957607ff4d4da563d836a54451420dfb98d390a722ec09217bf2383c` (dist/opencode-local + vendored path, write-temp + mv -f swap). Verify: `GET /` → 200 `Amicode`; `amico-spinner` ×1 in served MAIN chunk `index-A7nOnjqy.js` — the component definition hoisted to the main bundle now that main-route sites (home/sidebar) import it; all call sites share that one definition, so marker count = 1 definition, not per-site; `pulse-opacity 1.2s` wired ×1; `POST /session {}` → 200; `bun test src/amicode` → 20 pass.
+
+- Design v2 per Aaron (binary-eye-frames concept DROPPED before commit): small monochrome H silhouette with the screen slit knocked out (single path, fill-rule=evenodd, currentColor — mount-site `style={{color}}` passes through like stock Spinner).
+- Animation choice: gentle opacity pulse reusing the EXISTING `pulse-opacity` keyframes (styles/animations.css, 0.4→1) at 1.2s — chosen over Claude-style rotation because the H is non-radial and tumbles/blurs at 16px, and pulse matches the stock spinner's own animation language. prefers-reduced-motion: static glyph, NO animation (matchMedia guard; the repo's CSS-file media-query pattern doesn't cover inline animations).
+- NEW packages/ui/src/amicode/spinner.tsx (`AmicoSpinner`, data-component="amico-spinner") + packages/ui/src/components/amico-spinner.tsx re-export shim. No pure frame logic remained after the design change → no new unit tests.
+- Stock touch: packages/app/src/pages/session/message-timeline.tsx — import swap + `` replacing `` at the sticky-header working indicator (~line 1359); unused Spinner import removed. Other spinner sites left STOCK (list for morning): session-header.tsx:362 (header circle), home.tsx:848, sidebar-items.tsx:66+124, sidebar-workspace.tsx:102, dialog-connect-provider.tsx.
+- Spinner build sha256: `83f5a9fd3b35155bf2014115b54a27ef3d9542e8b14c7a83925175a73c1f7ebc` (dist/opencode-local + vendored path, write-temp + mv -f swap). Verify: `GET /` → 200 `Amicode`; session chunk `session-CMqmIgk2.js` → `amico-spinner` ×1 + `pulse-opacity 1.2s` ×1; `POST /session {}` → 200; `bun test src/amicode` → 20 pass.
+- SWEEP (Aaron: "no spinner" — the sticky-header site isn't where he looks): AmicoSpinner now at ALL visible working indicators: session-header.tsx:362 (top-right circle, size-3.5 + tint), message-part.tsx task-tool running row (~1841; inline 16x14 style because basic-tool.css sizes only [data-component="spinner"]), session-retry.tsx:58 (retry card), home.tsx:848, sidebar-items.tsx:66+124, sidebar-workspace.tsx:102 — each with the site's exact class/tint. Stock Spinner imports removed from swapped files. Left stock on purpose: dialog-connect-provider.tsx (provider setup, rarely seen), wsl/dialog-add-server.tsx (Windows-only).
+- Sweep build sha256: `8aa718ad957607ff4d4da563d836a54451420dfb98d390a722ec09217bf2383c` (dist/opencode-local + vendored path, write-temp + mv -f swap). Verify: `GET /` → 200 `Amicode`; `amico-spinner` ×1 in served MAIN chunk `index-A7nOnjqy.js` — the component definition hoisted to the main bundle now that main-route sites (home/sidebar) import it; all call sites share that one definition, so marker count = 1 definition, not per-site; `pulse-opacity 1.2s` wired ×1; `POST /session {}` → 200; `bun test src/amicode` → 20 pass.
+
11. (release prep) — amicode: darwin-arm64 cross-compile + release-asset packaging.
- - packages/opencode/script/build.ts: `OPENCODE_BUILD_TARGETS` env filter (comma-separated artifact names, e.g. `opencode-linux-x64,opencode-darwin-arm64`); unset → stock behavior. Needed because stock offers only --single (current platform) or ALL 12 targets.
- - Darwin cross-compile SUCCEEDED first attempt: run WITHOUT --skip-install (the cross-platform prebuild pulls of @opentui/core / @parcel/watcher / @ff-labs/fff-bun are required for non-native targets; they did NOT dirty the tree) and WITHOUT --single. Output: dist/opencode-darwin-arm64/bin/opencode (Mach-O 64-bit arm64, ~120 MB, not smoke-testable on linux).
- - Determinism note: the linux-x64 binary from this run is BYTE-IDENTICAL to the sweep build (sha 8aa718ad…) — Bun compile is reproducible given identical source + models.dev snapshot.
- - Release assets (upstream layout: flat `opencode` inside; from dist//bin: linux `tar -czf`, darwin `zip -r`), staged at dist/release/:
- - opencode-linux-x64.tar.gz sha256 268596e61475dd79d3076dd65d39bf97926d56c28477419250ab3c678bd55b10
- - opencode-darwin-arm64.zip sha256 f1d6291485246e03a3d33eefb40b4d264d68c8e5812b11b4ffdfec4444ab9455
- (opencode.lock.json stores the ARCHIVE sha — fetch_opencode.mjs hashes the downloaded asset bytes. tar.gz embeds mtimes: upload THESE exact files, do not repack.)
- - Publication status: push/release NOT executed — the permission system denied adding a push remote (`git remote add origin`); per agent policy the coordinator's message is not user consent, and this fork's standing guardrail is no push remote. Everything is staged for a human-run publish (commands in the handoff).
+
+- packages/opencode/script/build.ts: `OPENCODE_BUILD_TARGETS` env filter (comma-separated artifact names, e.g. `opencode-linux-x64,opencode-darwin-arm64`); unset → stock behavior. Needed because stock offers only --single (current platform) or ALL 12 targets.
+- Darwin cross-compile SUCCEEDED first attempt: run WITHOUT --skip-install (the cross-platform prebuild pulls of @opentui/core / @parcel/watcher / @ff-labs/fff-bun are required for non-native targets; they did NOT dirty the tree) and WITHOUT --single. Output: dist/opencode-darwin-arm64/bin/opencode (Mach-O 64-bit arm64, ~120 MB, not smoke-testable on linux).
+- Determinism note: the linux-x64 binary from this run is BYTE-IDENTICAL to the sweep build (sha 8aa718ad…) — Bun compile is reproducible given identical source + models.dev snapshot.
+- Release assets (upstream layout: flat `opencode` inside; from dist//bin: linux `tar -czf`, darwin `zip -r`), staged at dist/release/:
+ - opencode-linux-x64.tar.gz sha256 268596e61475dd79d3076dd65d39bf97926d56c28477419250ab3c678bd55b10
+ - opencode-darwin-arm64.zip sha256 f1d6291485246e03a3d33eefb40b4d264d68c8e5812b11b4ffdfec4444ab9455
+ (opencode.lock.json stores the ARCHIVE sha — fetch_opencode.mjs hashes the downloaded asset bytes. tar.gz embeds mtimes: upload THESE exact files, do not repack.)
+- Publication status: push/release NOT executed — the permission system denied adding a push remote (`git remote add origin`); per agent policy the coordinator's message is not user consent, and this fork's standing guardrail is no push remote. Everything is staged for a human-run publish (commands in the handoff).
+
12. (vaults panel tab) — amicode: "Vaults" tab in the connections popover + `GET /amicode/vaults` endpoint + Manage-vaults prompt handoff. Spec/plan: personal vault `spec-20260703-140141-amicode-vaults-panel-tab` / `plan-20260703-143356-amicode-vaults-panel-tab` (rollout step 6, panel half, of the amico-vault CLI spec; data source = `amico-vault status --json`, harmoniqs/amico PR #184).
- - FIRST SERVER-SIDE STOCK TOUCH in the stack: packages/opencode/src/server/routes/instance/httpapi/server.ts — import + `amicodeVaultsRoute` const below docRoute (exact docRoute idiom: raw `HttpRouter.use`, `authOnlyRouterLayer`) + one `Layer.mergeAll` entry (before uiRoute). Endpoint logic in NEW packages/opencode/src/server/amicode/vaults.ts: CLI resolution ($AMICO_VAULT_BIN → $AMICO_OPS/scripts/amico-vault → ~/.amico/ops/scripts/amico-vault → PATH via Bun.which), `AbortSignal.timeout(8s)` deadline (the process-util `timeout` opt is only SIGTERM→SIGKILL grace — it never arms a deadline without `abort`), 10 s response cache, verbatim JSON relay; every failure synthesized into the SAME plural shape `{ok:false, mounts:[], error:": "}` (codes cli_not_found/timeout/exit_/bad_output) — status() never rejects. Tests: packages/opencode/test/server/amicode-vaults.test.ts (3).
- - NEW packages/ui/src/amicode/vaults.ts — tolerant single consumer of the wire shape (`last_sync` is git %cr prose or literal "unknown", rendered verbatim, "unknown"/absent → "—"; non-array/singular mounts → bad_shape; per-entry field tolerance) + vaults.test.ts (9). NEW packages/ui/src/amicode/vaults-tab.tsx (`AmicodeVaultsTab`, data-component="amicode-vaults-tab"; rows: warn/clean dot [inlined `warnings.length` check — conscious deviation from the spec's "tested helper", trivial enough that a helper would be ceremony], id, dim kind, rw/ro chip, right-aligned last_sync, dim first-warning second line "+N more"; error state carries a Retry link `amicode-vaults-retry`; skeleton when view undefined; exports AMICODE_MANAGE_VAULTS_PROMPT) + components/amicode-vaults-tab.tsx re-export shim.
- - Stock touches (app): status-popover-body.tsx — `createResource` fetch per-active-server (source = shown() && ServerConnection.key → refetch on open/server-switch; Basic auth via the EXISTING authTokenFromCredentials from utils/server.ts; rejected fetch → same error+Retry state), vaults Tabs.Trigger with count badge + Tabs.Content (list + "Manage vaults" secondary Button), props widened `{shown, onClose?}`; status-popover.tsx — `onClose={() => setShown(false)}` threaded at BOTH Body mounts (v1 line ~70, DirectoryStatusPopover body thunk); ServerStatusPopover/ServerBody untouched. en.ts — 5 keys after manageServers (tab.vaults, action.manageVaults, dialog.vaults.{empty,retry,fetchFailed}).
- - Manage-vaults mechanism: session-new-view.tsx's local startPrompt closure EXTRACTED to packages/app/src/utils/start-prompt.ts (takes the resolved prompt context — must not call usePrompt() itself); session-new-view refactored to a thin wrapper (behavior-preserving); popover onManage = onClose() + startPrompt(prompt, AMICODE_MANAGE_VAULTS_PROMPT). Prompt sentence doubles as the future vault-setup score's onset trigger.
- - Vaults build sha256: `a94226125bc8a8fa9022be8bcdecb2cef2956c19e1955a3dd5031e70de2c6eb1` (dist/opencode-local + vendored path, write-temp + mv -f swap). Verify (scratch ports 43197-43199): `GET /amicode/vaults` → 200 relaying live CLI JSON (3 mounts, last_sync "24 minutes ago"/"7 days ago" prose); CLI-absent env (fake HOME, stripped PATH) → `{"ok":false,"mounts":[],"error":"cli_not_found: amico-vault not found — is amico installed?"}`; auth parity with /doc under OPENCODE_SERVER_PASSWORD (noauth 401 / auth 200, both routes); `GET /` → 200 `Amicode`; `POST /session {}` → 200; chunk check: `status-popover-body-WTKgcSgC.js` → `amicode-vaults-tab` ×1 (served 200), "Manage vaults" i18n in `index-DVjD35cF.js`. Tests: `bun test src/amicode` → 29 pass; opencode amicode-vaults tests → 3 pass; typecheck green in ui/app/opencode. Interactive click-through (tab render, Manage handoff) NOT yet human-verified — deferred to dev-host acceptance on 43117.
+
+- FIRST SERVER-SIDE STOCK TOUCH in the stack: packages/opencode/src/server/routes/instance/httpapi/server.ts — import + `amicodeVaultsRoute` const below docRoute (exact docRoute idiom: raw `HttpRouter.use`, `authOnlyRouterLayer`) + one `Layer.mergeAll` entry (before uiRoute). Endpoint logic in NEW packages/opencode/src/server/amicode/vaults.ts: CLI resolution ($AMICO*VAULT_BIN → $AMICO_OPS/scripts/amico-vault → ~/.amico/ops/scripts/amico-vault → PATH via Bun.which), `AbortSignal.timeout(8s)` deadline (the process-util `timeout` opt is only SIGTERM→SIGKILL grace — it never arms a deadline without `abort`), 10 s response cache, verbatim JSON relay; every failure synthesized into the SAME plural shape `{ok:false, mounts:[], error:": "}` (codes cli_not_found/timeout/exit*/bad_output) — status() never rejects. Tests: packages/opencode/test/server/amicode-vaults.test.ts (3).
+- NEW packages/ui/src/amicode/vaults.ts — tolerant single consumer of the wire shape (`last_sync` is git %cr prose or literal "unknown", rendered verbatim, "unknown"/absent → "—"; non-array/singular mounts → bad_shape; per-entry field tolerance) + vaults.test.ts (9). NEW packages/ui/src/amicode/vaults-tab.tsx (`AmicodeVaultsTab`, data-component="amicode-vaults-tab"; rows: warn/clean dot [inlined `warnings.length` check — conscious deviation from the spec's "tested helper", trivial enough that a helper would be ceremony], id, dim kind, rw/ro chip, right-aligned last_sync, dim first-warning second line "+N more"; error state carries a Retry link `amicode-vaults-retry`; skeleton when view undefined; exports AMICODE_MANAGE_VAULTS_PROMPT) + components/amicode-vaults-tab.tsx re-export shim.
+- Stock touches (app): status-popover-body.tsx — `createResource` fetch per-active-server (source = shown() && ServerConnection.key → refetch on open/server-switch; Basic auth via the EXISTING authTokenFromCredentials from utils/server.ts; rejected fetch → same error+Retry state), vaults Tabs.Trigger with count badge + Tabs.Content (list + "Manage vaults" secondary Button), props widened `{shown, onClose?}`; status-popover.tsx — `onClose={() => setShown(false)}` threaded at BOTH Body mounts (v1 line ~70, DirectoryStatusPopover body thunk); ServerStatusPopover/ServerBody untouched. en.ts — 5 keys after manageServers (tab.vaults, action.manageVaults, dialog.vaults.{empty,retry,fetchFailed}).
+- Manage-vaults mechanism: session-new-view.tsx's local startPrompt closure EXTRACTED to packages/app/src/utils/start-prompt.ts (takes the resolved prompt context — must not call usePrompt() itself); session-new-view refactored to a thin wrapper (behavior-preserving); popover onManage = onClose() + startPrompt(prompt, AMICODE_MANAGE_VAULTS_PROMPT). Prompt sentence doubles as the future vault-setup score's onset trigger.
+- Vaults build sha256: `a94226125bc8a8fa9022be8bcdecb2cef2956c19e1955a3dd5031e70de2c6eb1` (dist/opencode-local + vendored path, write-temp + mv -f swap). Verify (scratch ports 43197-43199): `GET /amicode/vaults` → 200 relaying live CLI JSON (3 mounts, last_sync "24 minutes ago"/"7 days ago" prose); CLI-absent env (fake HOME, stripped PATH) → `{"ok":false,"mounts":[],"error":"cli_not_found: amico-vault not found — is amico installed?"}`; auth parity with /doc under OPENCODE_SERVER_PASSWORD (noauth 401 / auth 200, both routes); `GET /` → 200 `Amicode`; `POST /session {}` → 200; chunk check: `status-popover-body-WTKgcSgC.js` → `amicode-vaults-tab` ×1 (served 200), "Manage vaults" i18n in `index-DVjD35cF.js`. Tests: `bun test src/amicode` → 29 pass; opencode amicode-vaults tests → 3 pass; typecheck green in ui/app/opencode. Interactive click-through (tab render, Manage handoff) NOT yet human-verified — deferred to dev-host acceptance on 43117.
+
13. (problem UI — spec B) — amicode: problem-header rail + diff receipts + entity view + problem switcher + `GET /amicode/problems|problem|run-status`. Spec/plan: personal vault `spec-20260703-182503-amicode-problem-ui` / `plan-20260703-200416-amicode-problem-ui` (three-ring interaction model; depends on spec A's workspaces on amicode branch `aaron/problem-workspaces`). Commits `fc67485ad2..e6eee73c4c` (9).
- - SERVER (second stock touch to server.ts): NEW packages/opencode/src/server/amicode/problems.ts — reads `~/.amico/problems` (env `AMICODE_PROBLEMS_DIR`) + `~/.amico/runs` (env `AMICODE_RUNS_DIR`) directly, `.json` sidecars only; one-shape failure synthesis per route (codes no_problems_dir / not_found: / bad_json / bad_output); TTL caches 10 s (problems/problem) + 1 s (run-status). `score_stages` resolution chain: `problem.score.id` → workspace `interview_state.json` `score_id` (spec A never stamps problem.score — the fallback is what makes pending chips live today) → must equal root `score_manifest.json` id, else `[]`. run-status rules: FINISHED+parseable result.toml → finished; FINISHED alone → failed; else solving with last `AMICODE_ITER` (raw objective f on the wire; client renders F=1−f only when 0≤f≤1). Routes registered via Effect.gen + `yield* router.add` (chained `.add()` does NOT compose — add returns an Effect; first attempt broke 161 server tests, caught pre-commit) under authOnlyRouterLayer; `new URL(request.url, "http://localhost")` (relative request.url, pty.ts idiom). Tests: test/server/amicode-problems.test.ts (11).
- - UI PURE: NEW packages/ui/src/amicode/receipt.ts — single consumer of the AMICODE_DIFF sentinel (strict last-line parse; any failure → undefined → legacy chip, old sessions keep rendering; `…` elision key → bare ellipsis; stripSentinel exported/tested but NO current consumer — card never renders raw output, noted per spec's strip rule) + receipt.test.ts (6). NEW problem.ts — single consumer of the three endpoint wire shapes: tolerant parsers, per-kind chip renderers + generic fallback, pending merge (score_stages × present), runChipText, railState (loading/ready/stale-last-good/unavailable), entityRows/historyRows/editPromptText + problem.test.ts (14). NEW ui-bridge.ts (receipt click → rail-registered openEntity, ask-bridge idiom).
- - RAIL REWRITE: entity-rail.tsx — session gate unchanged (≥1 amicode_* part); createResource on the completed-part counter (the one implemented refetch trigger; "section open" N/A for an always-visible rail — dialogs fetch on open; server switch handled per-call by amicodeGet resolving the connection each invocation); data-driven chips + dimmed pending chips; run chip polls /amicode/run-status @2.5 s while unfinished, feeds ONLY the run chip, NEVER triggers a problem refetch (spec's #67 transposition), stops on all-terminal + unmount; failure states: stale-last-good renders normally, no-data → `status unavailable · Retry`. DELETED from stage.ts: chipTextFromSummary/TOKEN_REWRITES/railStage/RAIL_STAGES (prose-regex path gone; amicodeStage kept as card fallback label).
- - CARD → RECEIPTS: card.tsx parses the part's `output` (message-part.tsx already passed it — zero new stock touches) → one-line `AMICO · System · levels 3→4` receipt, click → entity view via ui-bridge; no-sentinel fallback = the old status chip.
- - RING-2 DIALOGS: NEW entity-view.tsx (fields table + per-field ✎ → draftPrompt handoff; history newest-first with seq anchor [missing-from-window → unanchored, no error] incl. event source tool/stage; run tier chip, free = warning "free · unvetted"; KaTeX Hamiltonians for transmon/rydberg via the fork's existing katex) + problem-switcher.tsx (rows: active dot, status, N entities, recorded; Open/New = startPrompt SUBMIT handoffs — UI stays write-free, the agent's amicode_problem moves the active pointer) + re-export shims (also exporting the parsers, vaults-tab idiom).
- - APP WIRING: utils/start-prompt.ts gains `draftPrompt` (draft+focus, no submit — the fallback branch promoted); NEW utils/amicode-fetch.ts (per-active-server Basic-auth GET, connection resolved per call); message-timeline.tsx — open-gated createResources for the two dialogs (cleared in dialog.show onClose so reopen refetches), dialog.show wiring, rail props; session-new-view.tsx + getting-started.tsx — conditional Resume chip from /amicode/problems (most-recent non-archived; the STATIC "Resume my pulse design" starter REMOVED per no-empty-chrome); en.ts — 7 `amicode.*` keys.
- - AMICODE-REPO SIDE (branch aaron/problem-workspaces, commit d2e2536): AGENTS.md + SCORE.md announce the rail ONCE when the first entity records (score version kept at 3 — guidance prose, not a stage-contract change; a bump would reset in-flight interview state). #47/#48 `problem` ref field deferred via comment on amicode#47.
- - Tests: ui `bun test src` → 70 pass / 0 fail; opencode FULL `bun test` → 3000 pass / 0 fail / 2 skip / 1 todo (skips pre-existing, not amicode); typecheck green in ui + app + opencode.
- - Problem-UI build sha256: `a73d858373931826ef0ba6a6c4bd741a065516ba8bb3bd4cf5ff559ee9729289` (dist/opencode-local + vendored path, write-temp + mv -f swap). ⚠️ build INCLUDES uncommitted branding WIP present in the tree (RacingSansOne wordmark: index.css @font-face + logo.tsx/wordmark-v2.tsx family swap + woff2 asset, mtimes 21:10-21:11 — NOT part of this patch, left unstaged for its author). Verify (scratch ports 14097/14098): all three endpoints 200 against the LIVE workspace `untitled-2026-07-03` — problems lists it active, problem returns system entity + `score_stages ["system","formulation","run","pulse","device_session"]` resolved through interview_state.json + 3 events, run-status `{ok:true,runs:[]}`; auth parity with /doc under OPENCODE_SERVER_PASSWORD (noauth 401/401, auth 200×3); `GET /` → 200 `Amicode`; chunk check: session chunk `session-Dgq0NnUP.js` → amicode-rail-problem/amicode-card-receipt/AMICODE_DIFF/amicode-entity-view ×1 each + amicode.problems.title ×2, composer chunk `session-composer-state-DvDmcuNl.js` → amicode-problem-switcher + amicode-gs-resume ×1 each. Interactive click-through (rail bind, receipt click, switcher/entity dialogs, Resume chip) NOT yet human-verified — deferred to dev-host acceptance on the extension-launched server (43117), same as patch 12.
+
+- SERVER (second stock touch to server.ts): NEW packages/opencode/src/server/amicode/problems.ts — reads `~/.amico/problems` (env `AMICODE_PROBLEMS_DIR`) + `~/.amico/runs` (env `AMICODE_RUNS_DIR`) directly, `.json` sidecars only; one-shape failure synthesis per route (codes no_problems_dir / not_found: / bad_json / bad_output); TTL caches 10 s (problems/problem) + 1 s (run-status). `score_stages` resolution chain: `problem.score.id` → workspace `interview_state.json` `score_id` (spec A never stamps problem.score — the fallback is what makes pending chips live today) → must equal root `score_manifest.json` id, else `[]`. run-status rules: FINISHED+parseable result.toml → finished; FINISHED alone → failed; else solving with last `AMICODE_ITER` (raw objective f on the wire; client renders F=1−f only when 0≤f≤1). Routes registered via Effect.gen + `yield* router.add` (chained `.add()` does NOT compose — add returns an Effect; first attempt broke 161 server tests, caught pre-commit) under authOnlyRouterLayer; `new URL(request.url, "http://localhost")` (relative request.url, pty.ts idiom). Tests: test/server/amicode-problems.test.ts (11).
+- UI PURE: NEW packages/ui/src/amicode/receipt.ts — single consumer of the AMICODE_DIFF sentinel (strict last-line parse; any failure → undefined → legacy chip, old sessions keep rendering; `…` elision key → bare ellipsis; stripSentinel exported/tested but NO current consumer — card never renders raw output, noted per spec's strip rule) + receipt.test.ts (6). NEW problem.ts — single consumer of the three endpoint wire shapes: tolerant parsers, per-kind chip renderers + generic fallback, pending merge (score_stages × present), runChipText, railState (loading/ready/stale-last-good/unavailable), entityRows/historyRows/editPromptText + problem.test.ts (14). NEW ui-bridge.ts (receipt click → rail-registered openEntity, ask-bridge idiom).
+- RAIL REWRITE: entity-rail.tsx — session gate unchanged (≥1 amicode\_\* part); createResource on the completed-part counter (the one implemented refetch trigger; "section open" N/A for an always-visible rail — dialogs fetch on open; server switch handled per-call by amicodeGet resolving the connection each invocation); data-driven chips + dimmed pending chips; run chip polls /amicode/run-status @2.5 s while unfinished, feeds ONLY the run chip, NEVER triggers a problem refetch (spec's #67 transposition), stops on all-terminal + unmount; failure states: stale-last-good renders normally, no-data → `status unavailable · Retry`. DELETED from stage.ts: chipTextFromSummary/TOKEN_REWRITES/railStage/RAIL_STAGES (prose-regex path gone; amicodeStage kept as card fallback label).
+- CARD → RECEIPTS: card.tsx parses the part's `output` (message-part.tsx already passed it — zero new stock touches) → one-line `AMICO · System · levels 3→4` receipt, click → entity view via ui-bridge; no-sentinel fallback = the old status chip.
+- RING-2 DIALOGS: NEW entity-view.tsx (fields table + per-field ✎ → draftPrompt handoff; history newest-first with seq anchor [missing-from-window → unanchored, no error] incl. event source tool/stage; run tier chip, free = warning "free · unvetted"; KaTeX Hamiltonians for transmon/rydberg via the fork's existing katex) + problem-switcher.tsx (rows: active dot, status, N entities, recorded; Open/New = startPrompt SUBMIT handoffs — UI stays write-free, the agent's amicode_problem moves the active pointer) + re-export shims (also exporting the parsers, vaults-tab idiom).
+- APP WIRING: utils/start-prompt.ts gains `draftPrompt` (draft+focus, no submit — the fallback branch promoted); NEW utils/amicode-fetch.ts (per-active-server Basic-auth GET, connection resolved per call); message-timeline.tsx — open-gated createResources for the two dialogs (cleared in dialog.show onClose so reopen refetches), dialog.show wiring, rail props; session-new-view.tsx + getting-started.tsx — conditional Resume chip from /amicode/problems (most-recent non-archived; the STATIC "Resume my pulse design" starter REMOVED per no-empty-chrome); en.ts — 7 `amicode.*` keys.
+- AMICODE-REPO SIDE (branch aaron/problem-workspaces, commit d2e2536): AGENTS.md + SCORE.md announce the rail ONCE when the first entity records (score version kept at 3 — guidance prose, not a stage-contract change; a bump would reset in-flight interview state). #47/#48 `problem` ref field deferred via comment on amicode#47.
+- Tests: ui `bun test src` → 70 pass / 0 fail; opencode FULL `bun test` → 3000 pass / 0 fail / 2 skip / 1 todo (skips pre-existing, not amicode); typecheck green in ui + app + opencode.
+- Problem-UI build sha256: `a73d858373931826ef0ba6a6c4bd741a065516ba8bb3bd4cf5ff559ee9729289` (dist/opencode-local + vendored path, write-temp + mv -f swap). ⚠️ build INCLUDES uncommitted branding WIP present in the tree (RacingSansOne wordmark: index.css @font-face + logo.tsx/wordmark-v2.tsx family swap + woff2 asset, mtimes 21:10-21:11 — NOT part of this patch, left unstaged for its author). Verify (scratch ports 14097/14098): all three endpoints 200 against the LIVE workspace `untitled-2026-07-03` — problems lists it active, problem returns system entity + `score_stages ["system","formulation","run","pulse","device_session"]` resolved through interview_state.json + 3 events, run-status `{ok:true,runs:[]}`; auth parity with /doc under OPENCODE_SERVER_PASSWORD (noauth 401/401, auth 200×3); `GET /` → 200 `Amicode`; chunk check: session chunk `session-Dgq0NnUP.js` → amicode-rail-problem/amicode-card-receipt/AMICODE_DIFF/amicode-entity-view ×1 each + amicode.problems.title ×2, composer chunk `session-composer-state-DvDmcuNl.js` → amicode-problem-switcher + amicode-gs-resume ×1 each. Interactive click-through (rail bind, receipt click, switcher/entity dialogs, Resume chip) NOT yet human-verified — deferred to dev-host acceptance on the extension-launched server (43117), same as patch 12.
+
14. (brand fonts) — amicode: AMICODE wordmark → "Racing Sans One"; default code/monospace → "JuliaMono" (Aaron's change, committed + built on his heads-up; it was the uncommitted WIP flagged in #13's build note). Commit `c155bf499b`.
- - index.css: @font-face for both (JuliaMono full glyph set — Julia Unicode; Racing Sans One latin subset, font-display swap). logo.tsx + wordmark-v2.tsx: font-family 'Racing Sans One' first, weight 700→400. settings.tsx: monoDefault/monoFallback lead with JuliaMono. theme.css: --font-family-mono leads with JuliaMono. Terminal font DELIBERATELY unchanged (JetBrainsMono Nerd Font Mono via separate terminalFallback).
- - New assets (git-added — build breaks without them): public/assets/RacingSansOne-Regular.woff2 (21 KB) + JuliaMono-Regular.woff2 (946 KB).
- - Font build sha256: `2a15da111be516516fb1fbd1a4fb5ae02ad9bddd6373ede08cdf7b28c19d163a` (dist/opencode-local + vendored path, write-temp + mv -f swap; SUPERSEDES #13's a73d8583… — same code, fonts now committed). Verify (scratch port 14099): `GET /assets/RacingSansOne-Regular.woff2` → 200 font/woff2 21804 B; `GET /assets/JuliaMono-Regular.woff2` → 200 font/woff2 946516 B; "Racing Sans One" in built css + index/new-session chunks, "JuliaMono" in `index-Dwtxigfs.css`; `GET /amicode/problems` → 200; `GET /` → 200 `Amicode`; ui `bun test src` → 70 pass; typecheck green ui+app (no snapshots assert fonts, per Aaron — confirmed nothing went red). Bonus confirmation: KaTeX_* woff2 assets now in dist — the entity view's katex import (#13) pulls its font set into the embed.
+
+- index.css: @font-face for both (JuliaMono full glyph set — Julia Unicode; Racing Sans One latin subset, font-display swap). logo.tsx + wordmark-v2.tsx: font-family 'Racing Sans One' first, weight 700→400. settings.tsx: monoDefault/monoFallback lead with JuliaMono. theme.css: --font-family-mono leads with JuliaMono. Terminal font DELIBERATELY unchanged (JetBrainsMono Nerd Font Mono via separate terminalFallback).
+- New assets (git-added — build breaks without them): public/assets/RacingSansOne-Regular.woff2 (21 KB) + JuliaMono-Regular.woff2 (946 KB).
+- Font build sha256: `2a15da111be516516fb1fbd1a4fb5ae02ad9bddd6373ede08cdf7b28c19d163a` (dist/opencode-local + vendored path, write-temp + mv -f swap; SUPERSEDES #13's a73d8583… — same code, fonts now committed). Verify (scratch port 14099): `GET /assets/RacingSansOne-Regular.woff2` → 200 font/woff2 21804 B; `GET /assets/JuliaMono-Regular.woff2` → 200 font/woff2 946516 B; "Racing Sans One" in built css + index/new-session chunks, "JuliaMono" in `index-Dwtxigfs.css`; `GET /amicode/problems` → 200; `GET /` → 200 `Amicode`; ui `bun test src` → 70 pass; typecheck green ui+app (no snapshots assert fonts, per Aaron — confirmed nothing went red). Bonus confirmation: KaTeX\_\* woff2 assets now in dist — the entity view's katex import (#13) pulls its font set into the embed.
diff --git a/packages/app/public/oc-theme-preload.js b/packages/app/public/oc-theme-preload.js
index 18846fceb6..51ca5f1481 100644
--- a/packages/app/public/oc-theme-preload.js
+++ b/packages/app/public/oc-theme-preload.js
@@ -9,6 +9,17 @@
localStorage.removeItem("opencode-theme-css-dark")
}
+ // amicode: VS Code webview bridge — the extension passes ?colorScheme=light|dark
+ // (from vscode.window.activeColorTheme). Seed the storage key BEFORE the read
+ // below so the app boots in the editor's theme (prefers-color-scheme inside a
+ // webview iframe reports the OS, not VS Code).
+ try {
+ var q = new URLSearchParams(location.search).get("colorScheme")
+ if (q === "light" || q === "dark") localStorage.setItem("opencode-color-scheme", q)
+ } catch (e) {
+ /* storage unavailable — keep defaults */
+ }
+
var scheme = localStorage.getItem("opencode-color-scheme") || "system"
var isDark = scheme === "dark" || (scheme === "system" && matchMedia("(prefers-color-scheme: dark)").matches)
var mode = isDark ? "dark" : "light"
diff --git a/packages/app/src/app.tsx b/packages/app/src/app.tsx
index 5bf18b657a..6a1bc0c167 100644
--- a/packages/app/src/app.tsx
+++ b/packages/app/src/app.tsx
@@ -7,7 +7,7 @@ import { MarkedProvider } from "@opencode-ai/ui/context/marked"
import { File } from "@opencode-ai/ui/file"
import { Font } from "@opencode-ai/ui/font"
import { Splash } from "@opencode-ai/ui/logo"
-import { ThemeProvider } from "@opencode-ai/ui/theme/context"
+import { ThemeProvider, useTheme } from "@opencode-ai/ui/theme/context"
import { MetaProvider } from "@solidjs/meta"
import { type BaseRouterProps, Navigate, Route, Router, useParams, useSearchParams } from "@solidjs/router"
import { QueryClient, QueryClientProvider } from "@tanstack/solid-query"
@@ -50,6 +50,7 @@ import DirectoryLayout, { DirectoryDataProvider } from "@/pages/directory-layout
import Layout from "@/pages/layout"
import { ErrorPage } from "./pages/error"
import { useCheckServerHealth } from "./utils/server-health"
+import { AmicodeSplash } from "@opencode-ai/ui/amicode-splash"
const HomeRoute = lazy(() => import("@/pages/home"))
const Session = lazy(() => import("@/pages/session"))
@@ -227,6 +228,35 @@ function RouterRoot(props: ParentProps<{ appChildren?: JSX.Element }>) {
)
}
+/** amicode: live theme bridge for the VS Code webview host. The extension
+ * forwards editor theme changes as window messages (outer relay → iframe);
+ * route them through the existing setColorScheme so everything re-themes. */
+function AmicodeThemeBridge() {
+ const theme = useTheme()
+ const onMsg = (e: MessageEvent) => {
+ const d = e.data as { source?: string; kind?: string; colorScheme?: string } | undefined
+ if (d?.source !== "amicode" || d.kind !== "theme") return
+ if (d.colorScheme === "light" || d.colorScheme === "dark") theme.setColorScheme(d.colorScheme)
+ }
+ window.addEventListener("message", onMsg)
+ onCleanup(() => window.removeEventListener("message", onMsg))
+ // ⌘⇧P / Ctrl+Shift+P: when embedded in the amicode webview (we have a
+ // parent), the EDITOR's Command Palette wins over the app's own palette —
+ // capture-phase so the in-app binding never sees it; forwarded over the
+ // existing allowlisted command bridge.
+ const onKey = (e: KeyboardEvent) => {
+ if (window.parent === window) return
+ if ((e.metaKey || e.ctrlKey) && e.shiftKey && (e.key === "p" || e.key === "P")) {
+ e.preventDefault()
+ e.stopPropagation()
+ window.parent.postMessage({ source: "amicode", kind: "command", command: "workbench.action.showCommands" }, "*")
+ }
+ }
+ window.addEventListener("keydown", onKey, { capture: true })
+ onCleanup(() => window.removeEventListener("keydown", onKey, { capture: true }))
+ return null
+}
+
export function AppBaseProviders(props: ParentProps<{ locale?: Locale }>) {
return (
@@ -236,6 +266,7 @@ export function AppBaseProviders(props: ParentProps<{ locale?: Locale }>) {
void window.api?.setTitlebar?.({ mode })
}}
>
+
) {
when={!checking()}
fallback={
}
>
diff --git a/packages/app/src/components/session/session-header.tsx b/packages/app/src/components/session/session-header.tsx
index 94d1a17205..0b6a9a9389 100644
--- a/packages/app/src/components/session/session-header.tsx
+++ b/packages/app/src/components/session/session-header.tsx
@@ -25,6 +25,11 @@ import { messageAgentColor } from "@/utils/agent"
import { decode64 } from "@/utils/base64"
import { Persist, persisted } from "@/utils/persist"
import { StatusPopover, StatusPopoverV2 } from "../status-popover"
+
+// AMICODE: the MCP/LSP/Plugins/Vaults status popover is opencode-operator
+// noise here ("No MCPs configured"). Hidden, not deleted — the trigger slot is
+// where a solver-health panel (server/Julia env/runs dir) belongs later.
+const AMICODE_HIDE_STATUS_POPOVER = true
import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
@@ -439,7 +444,7 @@ export function SessionHeader() {