Local fork of sst/opencode @ v1.18.12 (merge 1f09d0dc57, 2026-08-04) on branch notturno/merge-upstream-2026-08-04, pending merge of PR #119 into local/amicode. Remote: upstream only
(fetch = github.com/sst/opencode, push URL disabled to no_push_disabled). Never push.
Merged sst/opencode dev @ 7fe99387 (v1.18.12; v1.18.11 + v1.18.12 together, 139 files, +4342/−357) into local/amicode @ cf4f06b41. Zero conflicts, zero fork files touched — parity manifest clean on the merged tree. First pass produced by the notturno merge-sentinel machinery (hand-completed locally: the sentinel's hermetic clone lacked a git identity, so its in-CI merge died at commit time with a phantom "merge conflict (0 path(s))" — harmoniqs/amico#322, module fix in harmoniqs/amico#324).
- Adopted upstream's, all bugfix-tier: MCP SSE reconnect-loop fix; provider configs with interleaved reasoning fields (
reasoning_text/ custom field names — same bucket as patch #24's provider/model port; verified landed inpackages/llm/src/protocols/openai-responses.ts); Azure GPT-5.5+ reasoning fix; composer draft-store (large-image lag); project search across all known recent projects; stale-assistant-error clearing on resume; legacy-config skip for v2 servers; desktop fixes (external links, title bar, dir picker, file tree, gutter). - Kept fork's: everything — the merge touched no amicode-patched file;
session-side-panel.tsxdeletions (ADR 0001) stand. - Fork deviation (e2e): upstream's new
project-picker-recent-search.spec.tsskipped — it drives upstream's home-projects surface ("Add project" on the home page), which the fork doesn't render (NewHome has no add-project entry; the legacy interface is compile-time retired,settings.tsxnewLayoutDesigns = createMemo(() => true);home-projects-view.tsxis dead code here). The five-cap + search-all logic ships intact indialog-select-directory.tsx; re-enable per #120. - Build + smoke-verify on the dev host before tagging
v1.18.12-amicode.1(release ritual unchanged; channel gate gotcha 2 still applies:OPENCODE_CHANNEL=dev).
Merged sst/opencode dev @ 19231fce4b (1,229 upstream commits) into the fork's 472. 78 conflicts
resolved by policy: versions/lockfile mechanical; ~40 files hand-merged. Notables:
- Adopted upstream's: session-ui package split (fork's
message-part-groups/message-part-skillmoved there with their consumers), review/diffs side panel (fork's vault tab grafted in;reviewOpen = tabsOpen— same persisted key), controller-driven composer/settings/home, AppNodeBuilder test harness (fork'spromptAgnosticMatcherkept inside it), bounded SSE + heartbeat, native per-event-location contract (supersedes the fork's connection-pinned streams; the app reducer filters per-directory client-side). - Kept fork's: branding/fonts/accents, KaTeX macros (now threaded into upstream's hand-rolled
renderKatexToken), AmicoSpinner sites, entity-rail card dispatch (new@opencode-ai/ui/amicode-*shims;packages/uinow depends on@opencode-ai/session-uifor ask-card's data context — module-level acyclic), titlebar inline tab strip (server-widesync.session.getreplaces the deleteddirBase64), prompt-agnostic cassette matcher. - Dropped: old
message-timeline.tsxmounts (entity rail, AmicoSpinner, ask bridge) — upstream deleted the file; ports intopages/session/timeline/are FOLLOW-UPS.debug-bar.tsxstays deleted.showSessionProgressBarrow dropped (its consumer died with the timeline). Fork's markdown polish (~75 lines, heading hierarchy + inline-code chip) needs porting into session-ui's markdown. - prompt.ts silent-turn guard REFINED: fires only on reasoning-bearing turns with no error.
Upstream treats mid-stream provider explosions as finish
unknown(no error, no reasoning) and its run-process tests lock "end the run" for that shape — the unrefined guard re-invoked them. - i18n parity: upstream's strict parity test demands every en.ts key in every locale; the 72 amicode-era English keys were filled into all 17 app locales as ENGLISH fallbacks (verbatim, placeholders preserved). Non-English translations still deliberately unreviewed.
- Test-suite gotcha (this machine): the Amicode session exports
OPENCODE_CONFIG_CONTENT(skills.paths + permissions) — it leaks into anybun testspawned from an Amicode shell and breaks skill/permission fixtures. Run suites withenv -u OPENCODE_CONFIG_CONTENT -u OPENCODE_SERVER_PASSWORD bun test. - Known reds, all pre-existing or upstream-flaky: pasqal connections (8) + amicode widgets (15)
fail identically on
origin/local/amicode(WIP features);httpapi-v2-pty"serves location-wrapped PTY routes" times out ~1-in-3 on clean upstream on this machine. - Build recipe below is unchanged (channel gate gotcha 2 still applies:
OPENCODE_CHANNEL=dev).
Toolchain: bun 1.3.14 (~/.bun/bin/bun — must ALSO be on PATH, see gotcha 1). Node/pnpm not used by this repo (pure Bun workspace).
cd ~/harmoniqs/opencode
PATH="$HOME/.bun/bin:$PATH" bun installGotcha 1: plain ~/.bun/bin/bun install FAILS — tree-sitter-powershell's node-gyp
postinstall shim re-invokes bun by name (exit 127: node-gyp: 3: bun: not found).
Prepending ~/.bun/bin to PATH fixes it; install then completes clean (2729 packages,
root postinstall packages/core fix-node-pty + husky OK). No source patches needed.
- CI (
.github/workflows/publish.yml, jobbuild-cli) runs./packages/opencode/script/build.tswithOPENCODE_VERSION/OPENCODE_RELEASEset, then uploadspackages/opencode/dist/opencode-linux*. packages/opencode/script/build.tsis the whole recipe:- imports
script/generate.ts→ fetcheshttps://models.dev/api.json(network needed; override withMODELS_DEV_API_JSON=<file>orOPENCODE_MODELS_URL) → inlined asOPENCODE_MODELS_DEVdefine. - Web UI embed: runs
bun run --cwd packages/app build(vite build →packages/app/dist, SolidJS; Sentry plugin auto-disabled withoutSENTRY_*env), then generates an in-memory virtual moduleopencode-web-ui.gen.tsthat imports every dist filewith { type: "file" }and exports a{ "relative/path": embedded-file-path }map. Skip with--skip-embed-web-ui. Bun.build({ compile: { target: "bun-linux-x64", outfile: "dist/opencode-linux-x64/bin/opencode" }, ... })— i.e.bun build --compile, minified ESM, entrypointssrc/index.ts+ opentuiparser.worker.js+src/cli/tui/worker.ts+ the generated web-ui module.--singlelimits targets to current platform (native, non-baseline, non-musl);--skip-installskips the cross-platformbun install --os=* --cpu=*prebuild pulls (only needed for cross-compiling; keeps the tree clean).- Runs its own smoke test (
<binary> --version).
- Version/channel come from
packages/script/src/index.ts(Script): without env it fetches npm + uses the git branch as channel → setOPENCODE_VERSION=1.17.3(forces channellatest, keepsScript.releasefalse so nogh release uploadruns). - Gotcha 2 (channel gate — bit releases amicode.1/.2): channel
latestcompiles the embedded web UI withVITE_OPENCODE_CHANNEL="prod"(packages/app/vite.js), which defaultssettings.general.newLayoutDesignsOFF (packages/app/src/context/settings.tsx) — hiding EVERY amicode surface (home cards, institution card, v2 titlebar) at runtime, though the code is compiled in. ALWAYS also setOPENCODE_CHANNEL=dev(any non-latestchannel maps todevfor the UI). Verify a build:grep -aoh 'newLayoutDesigns:.\{1,45\}' <binary>→ find the default var, its assignment must be=!0. Fixed builds: v1.17.3-amicode.3 (2026-07-07, same source as amicode.2).
- imports
cd ~/harmoniqs/opencode/packages/opencode
PATH="$HOME/.bun/bin:$PATH" OPENCODE_VERSION=1.17.3 OPENCODE_CHANNEL=dev bun run script/build.ts --single --skip-install
# artifact: packages/opencode/dist/opencode-linux-x64/bin/opencode
# convenience copy: ~/harmoniqs/opencode/dist/opencode-local
# OPENCODE_CHANNEL=dev is REQUIRED — see gotcha 2 (channel gate) above.
# darwin cross-compile (release asset): drop --single --skip-install, set
# OPENCODE_BUILD_TARGETS="opencode-darwin-arm64"./dist/opencode-local serve --port 14096- startup log:
opencode server listening on http://127.0.0.1:14096(+ warning:OPENCODE_SERVER_PASSWORDnot set → unsecured) GET /→ 200, SolidJS app HTML shell (<title>OpenCode</title>,<div id="root">, 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 BGET /health→ 200 but it's the SPA index.html fallback — no dedicated /health route at v1.17.3POST /session {}→ 200{"id":"ses_…","slug":"shiny-cabin","projectID":"…","directory":…,"cost":0,"tokens":{…},"title":"New session - …","version":"1.17.3","time":{created,updated}}GET /session→ 200 array of sessions;GET /doc→ 200 (OpenAPI)
packages/opencode/src/server/shared/ui.ts:
- Compiled binary:
import("opencode-web-ui.gen.ts")resolves →serveEmbeddedUIEffectserves the embeddedpackages/app/distfiles (SPA fallback toindex.html), with CSP derived from the inline theme-preload script hash. - Run-from-source (
bun run src/index.ts serve): the generated module doesn't exist → catch → reverse-proxies https://app.opencode.ai instead. So run-from-source does NOT serve local app assets; only the compiled binary embeds them. (Env kill-switch:disableEmbeddedWebUi→ proxy mode.)
amicode (harmoniqs/amicode, packages/extension) vendors a compiled binary of this fork; it
never runs it from source (per § 4, run-from-source proxies the public app.opencode.ai, so the
amicode-branded surfaces only exist in a compiled build).
- Default = pinned release.
opencode.lock.jsonsource: release→ a plainpnpm installdownloads the pinned, gate-ON release asset. Teammates need no clone/bun. Changing this fork is an explicit command, never a lock edit. - Iterate: clone this fork beside amicode (
../opencode), then from amicode runpnpm --filter amicode opencode:build→ invokesscript/build.tswithOPENCODE_CHANNEL=dev(gotcha 2) and re-vendors the binary. Reload the Extension Dev Host to pick it up. - Publish: push a branch here, tag a release (the release workflow builds both targets with
devand asserts the gate ON), then from amicodepnpm --filter amicode opencode:pin <tag>to rewrite amicode's lock (downloads + sha256-verifies both assets). - amicode's
packages/extension/scripts/assert_ui_gate.shreds CI/release if any vendored binary ships with the gate OFF — the automated form of gotcha 2's grep.
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 | <title>OpenCode</title> |
<title>Amicode</title> |
| 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.
- All internal identifiers:
opencode-titlebar-*DOM ids,opencode-*localStorage keys,OPENCODE_*env vars, config keys, API/SDK strings,@opencode-ai/*imports,oc-theme-preload-scriptid (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.
Rebuilt with the exact T3 recipe (OPENCODE_VERSION=1.17.3 bun run script/build.ts --single --skip-install in packages/opencode, ~/.bun/bin on PATH).
- Binary:
~/harmoniqs/opencode/dist/opencode-local(copy ofpackages/opencode/dist/opencode-linux-x64/bin/opencode) - sha256 (L1 branding build):
930fd0457d9ffb389027ca23d09cc0b1ab9d6aaa1a2022864dc8e1b748d3c5f4 - sha256 (L1+L2 build, current):
8f4c6751ca5c112e25652de7adb9a3b6b710f64fa4de8fb53ceb8949cf3e1ecb— L2 verify:GET /→ 200<title>Amicode</title>; card code compiled into the lazy session chunk (/assets/session-U3zHIEN6.js→ 200,amicode-card×5,AMICODE×1);POST /session {}→ 200 session object (no regression) - sha256 (L1+L2+L3 build, current):
7f60c60de1a23c82cd6477f2b6e67bb3956982a23cdbbc57db731f56927c5eff— L3 verify:GET /→ 200<title>Amicode</title>; session chunk/assets/session-BQYupOEO.js→ 200 (amicode-entity-rail×1,amicode-card×3);POST /session {}→ 200 session object (no regression). Vendored copy refreshed to the same sha (cp over the live file per coordinator — running server keeps the old inode; Aaron picks it up on next debug restart). - Vendor swap refreshed (2026-07-03): L1+L2 binary copied over
~/harmoniqs/amicode/packages/extension/vendor/opencode/linux-x64/opencode(idle-checked first;--version→ 1.17.3; backup remains atopencode.stock). NOTE: the.sha256sidecar is left at the MANIFEST value on purpose —fetch:opencodeskips download only when sidecar == manifest sha, so overwriting it with the local binary's hash would make the next fetch re-download stock and clobber the swap. - Verify (serve --port 14096):
GET /→ 200 with<title>Amicode</title>+href="/amico.svg";GET /amico.svg→ 200 image/svg+xml 441 B; main JS/assets/index-CeQonklQ.js:AMICODE×1 (wordmark; WordmarkV2 lives in the lazy session chunk),Amicode×31 (i18n/meta);POST /session {}→ 200 session object, version 1.17.3 (no functional regression);/site.webmanifest→ name/short_name Amicode. (/healthis SPA fallback at this tag — not a health check.)
4e20def26e— amicode: build fixes for local v1.17.3 reproduction (AMICODE-PATCHES.md, bun.lock ghostty-web drift)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.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
Partin packages/ui/src/components/message-part.tsx; tool parts dispatch atPART_MAPPING["tool"]viaToolRegistry.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 bordervar(--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).
- Renderer location: the session timeline (packages/app/src/pages/session/message-timeline.tsx:1057) delegates every message part to
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/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-titleheader inside the timeline ScrollView: once per session view, sticky at top, no prompt overlap, no scroll theft; rail hidden whenshowHeader()is false — acceptable v0). Total stock-file touches across L2+L3: message-part.tsx (dispatch) + message-timeline.tsx (mount).
- (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): readsquestion/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 (localpickedstate), 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 —
AmicodeEntityRailgained anonAskprop; message-timeline passes(text) => sdk.client.session.promptAsync({ sessionID, parts: [{type:"text", text}] })— same endpoint the prompt input's submit uses (model/agentare 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<Dynamic>(1 line, needed for the staleness guard); packages/app/src/pages/session/message-timeline.tsx — the existing rail mount now passesonAsk(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 — localizedsession.new.titleheadline ("Build anything" / "Créez ce que vous voulez") REPLACED by<Logo class="w-56 max-w-full" />(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 (warmline ~196 and providerinitialline ~203) now defaultreadStoredLocale() ?? "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 (detectLocaleis left in place, currently unreferenced). - Batch build (L1+L2+L3+ask/wordmark/locale) sha256:
a236b87fe92043eea6129a4a5b260ca2adc7fe51fc634e49405872973a7a60cbat bothdist/opencode-localand the vendored path. Verify:GET /→ 200<title>Amicode</title>; 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: plaincpfails 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.
- (getting-started) — amicode: start-screen getting-started block + starter chips.
- NEW packages/ui/src/amicode/getting-started.tsx —
AmicodeGettingStarted(data-component="amicode-getting-started"): en-only tagline "Pulse design, from conversation to calibrated waveform." (not localized, consistent with patch 5c), dimmed how-it-works row "① Describe your system / ② Watch the solve live / ③ Send to hardware & calibrate" (flex-wrap, nowrap per step → wraps up to 3 lines on narrow panes), three accent-bordered starter chips (ask-button family) exported asAMICODE_STARTERS. Static content — no unit tests needed. - NEW packages/ui/src/components/amicode-getting-started.tsx — re-export shim (same wildcard-export pattern as the rail).
- Stock touch (L1 family): packages/app/src/components/session/session-new-view.tsx — mounts the block under the wordmark, above the path/branch lines (kept).
- Submit wiring = DIRECT (not the prefill fallback):
startPromptsets the composer draft viausePrompt().set([{type:"text", content, start:0, end:len}], len)— the submit button'sblank()gate reads the SAME prompt store (prompt-input.tsx:317) — then next frame clicks the composer's own[data-action="prompt-submit"]:not([disabled])button so the REALhandleSubmitruns (worktree resolution, session.create, promote/handoff, navigation, optimistic UI, composer model/agent selection — none of it replicated). Degradation: if no enabled submit button exists, the text stays pre-filled and[data-component="prompt-input"]gets focus (user hits Enter).
- NEW packages/ui/src/amicode/getting-started.tsx —
- (live-session UX bundle, 2026-07-03) — four parts:
a. ASK-CARD GUARD FIX (live bug: buttons rendered locked). Old criterion "part must be in the LAST assistant message" was too strict — the model streams text/messages after calling amicode_ask. New criterion: buttons stay active until a USER message exists later (ULID order) than the card's message (
hasUserReplyAfter(messages, messageID)replaceslatestAssistantMessageIDin ask.ts, the AskBridge interface, the rail registration, and the card'sactive()); localpickedlock and no-bridge disable unchanged. b. ASK OPTION DETAILS.parseAskInputaccepts optionaldetails: string[](Track-A commit 778e1bb): validated against the RAW options length, all-strings; mismatch/non-strings → treated as absent (never rejects the card); alignment with options preserved through the invalid-option filter. ask-card renders each detail as a dim 11px second line inside its button (data-slot="amicode-ask-option-detail"). c. COMPOSER PLACEHOLDER. Rotation = i18n keys viaEXAMPLESarray (packages/app/src/components/prompt-input.tsx:99). en.tsprompt.example.1-5values → the five pulse-design examples; EXAMPLES trimmed to those five keys (stock touch; keys 6-25 left dormant in every dictionary for easy revert). "Ask anything..." prefix (prompt.placeholder.normal) unchanged. d. AMICO RELABEL (three-layer vocabulary: Amico = persona, Amicode = product). In-chat display labels only — entity-rail header, collapsed chip, ask-card header: "AMICODE" → "AMICO". Start-screen wordmark,<title>, favicon/manifest, getting-started block and all other chrome stay AMICODE/Amicode. Alldata-component/data-slotamicode-*identifiers unchanged. No unit test asserted the display label.- Tests after bundle:
bun test src/amicode→ 20 pass / 41 expects. - Bundle build sha256:
1c6fede30dc1c8eac7a4d544e0001b54b8e6399268c56ab2ee29582b7a80ffe2(dist/opencode-local + vendored path, write-temp + mv -f swap). Verify:GET /→ 200<title>Amicode</title>; session chunksession-IeOwC1wP.js→ ask-card ×1 + rail ×1 markers, display labelAMICO×3; composer-state chunksession-composer-state-VTIq3S7H.js→ getting-started ×1; en dictionary chunkindex-B2YGTpT0.js→ "Warm-start from my last pulse" ×1;POST /session {}→ 200.
- Tests after bundle:
- (brand mark v2) — amicode: "digi" Harmoniqs H-robot replaces the amico smile on the fork's brand-mark surfaces.
- packages/ui/src/components/logo.tsx —
Mark/Splashrender the H-robot (inlined shapes; body pathfill="currentColor"ridingcolor: var(--icon-strong-base), display rect#0A0A0A, glyphs#FFF676); viewBox0 0 64 56(8:7, NOT square).Logowordmark, data-component hooks, and prop pass-through unchanged — call sites untouched. - packages/ui/src/components/logo.css —
[data-component="logo-mark"]aspect-ratio 1/1 → 8/7. - packages/ui/src/assets/favicon/amico.svg — replaced with the FAVICON variant: 72×72 yellow chip (
rx=14,#FFF676) + mark in<g transform="translate(4,8)" color="#17181A">. Public symlinkpackages/app/public/amico.svgfollows automatically; favicon.tsx/index.html links and site.webmanifest unchanged (verified resolving). - Canonical source of the mark also lives at amicode:
packages/extension/media/amico.svg— kept in sync MANUALLY; if the mark changes again, update both. - Brand-v2 build sha256:
0de38685480961a70775c64d81f46ed3a4e8197b2af6ebe7074bc0fceaa4aab3(dist/opencode-local + vendored path, write-temp + mv -f swap). Verify:GET /→ 200<title>Amicode</title>;GET /amico.svg→ 200 image/svg+xml 1324 B (yellow chip rx=14 + robot path); main chunkindex-Drab4s7t.js→ robot pathM2 2h16v14h28V2h16v52×1 ANDAMICODEwordmark ×1;POST /session {}→ 200;bun test src/amicode→ 20 pass.
- packages/ui/src/components/logo.tsx —
- (native question form branding) — consolidation: opencode's NATIVE question tool (packages/opencode/src/tool/question.ts, rendered by the question dock) is now the ONE ask mechanism;
amicode_askis DEPRECATED (L0 prompts updated on the night branch).- packages/app/src/pages/session/composer/session-question-dock.tsx (~line 434, inside the DockPrompt
headerfragment): "AMICO · Question" brand line inserted INSIDEquestion-header-title, above the progress summary — same label family as amicode-ask-card (accent AMICO, faint ·, base Question); display-only, no behavior change (radio options, descriptions, multiple, custom answer row, progress header all untouched). - packages/ui/src/components/message-part.css (question block,
[data-slot="question-body"]): accent left border3px solid var(--v2-icon-icon-accent)— matches the rail/cards treatment. - 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<title>Amicode</title>; AMICO display labels total 4 — session chunksession-BfBMMSAH.js×3 (rail/chip/ask-card) + composer-state chunksession-composer-state-D8UQPTE4.js×1 (question dock,amicode-question-brandslot ×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.
- packages/app/src/pages/session/composer/session-question-dock.tsx (~line 434, inside the DockPrompt
- (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-opacitykeyframes (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 +
<AmicoSpinner class="size-4" .../>replacing<Spinner .../>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<title>Amicode</title>; session chunksession-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<title>Amicode</title>;amico-spinner×1 in served MAIN chunkindex-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.2swired ×1;POST /session {}→ 200;bun test src/amicode→ 20 pass.
- (release prep) — amicode: darwin-arm64 cross-compile + release-asset packaging.
- packages/opencode/script/build.ts:
OPENCODE_BUILD_TARGETSenv 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
opencodeinside; from dist//bin: linuxtar -czf, darwinzip -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).
- (vaults panel tab) — amicode: "Vaults" tab in the connections popover +
GET /amicode/vaultsendpoint + Manage-vaults prompt handoff. Spec/plan: personal vaultspec-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 +
amicodeVaultsRouteconst below docRoute (exact docRoute idiom: rawHttpRouter.use,authOnlyRouterLayer) + oneLayer.mergeAllentry (before uiRoute). Endpoint logic in NEW packages/opencode/src/server/amicode/vaults.ts: CLI resolution ($AMICOVAULT_BIN → $AMICO_OPS/scripts/amico-vault → ~/.amico/ops/scripts/amico-vault → PATH via Bun.which),AbortSignal.timeout(8s)deadline (the process-utiltimeoutopt is only SIGTERM→SIGKILL grace — it never arms a deadline withoutabort), 10 s response cache, verbatim JSON relay; every failure synthesized into the SAME plural shape{ok:false, mounts:[], error:"<code>: <detail>"}(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_syncis 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 [inlinedwarnings.lengthcheck — 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 linkamicode-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 —
createResourcefetch 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<title>Amicode</title>;POST /session {}→ 200; chunk check:status-popover-body-WTKgcSgC.js→amicode-vaults-tab×1 (served 200), "Manage vaults" i18n inindex-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.
- (problem UI — spec B) — amicode: problem-header rail + diff receipts + entity view + problem switcher +
GET /amicode/problems|problem|run-status. Spec/plan: personal vaultspec-20260703-182503-amicode-problem-ui/plan-20260703-200416-amicode-problem-ui(three-ring interaction model; depends on spec A's workspaces on amicode branchaaron/problem-workspaces). Commitsfc67485ad2..e6eee73c4c(9).
- SERVER (second stock touch to server.ts): NEW packages/opencode/src/server/amicode/problems.ts — reads
~/.amico/problems(envAMICODE_PROBLEMS_DIR) +~/.amico/runs(envAMICODE_RUNS_DIR) directly,.jsonsidecars 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_stagesresolution chain:problem.score.id→ workspaceinterview_state.jsonscore_id(spec A never stamps problem.score — the fallback is what makes pending chips live today) → must equal rootscore_manifest.jsonid, else[]. run-status rules: FINISHED+parseable result.toml → finished; FINISHED alone → failed; else solving with lastAMICODE_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-lineAMICO · System · levels 3→4receipt, 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 — 7amicode.*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
problemref field deferred via comment on amicode#47. - Tests: ui
bun test src→ 70 pass / 0 fail; opencode FULLbun 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 workspaceuntitled-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<title>Amicode</title>; chunk check: session chunksession-Dgq0NnUP.js→ amicode-rail-problem/amicode-card-receipt/AMICODE_DIFF/amicode-entity-view ×1 each + amicode.problems.title ×2, composer chunksession-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.
- (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" inindex-Dwtxigfs.css;GET /amicode/problems→ 200;GET /→ 200<title>Amicode</title>; uibun 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.
-
(home CTA fallback) — amicode(home): "Open chat" works on a fresh profile.
- BUG:
startWithPrompt(fork wiring for the Meet-Amico card, patch 5ef6b7e0e) dead-ended silently when the persisted client-side project list was empty (fresh browser profile against a bareopencode serve): the!projectbranch calledopenNewSession(), which needs the SAME emptynewSessionProject()and hitsif (!conn || !project) return. Primary home CTA did nothing, no error. Hit live 2026-07-08 (web UI on a scratch dir). - FIX (packages/app/src/pages/home.tsx,
startWithPromptonly): when no project is tracked, fall back to the focused server's own working directory —focusedSync().data.path.directory(synced from GET /path; "" until loaded, so the falsy guard holds) — open+touch it as a project (self-heals the home page), thentabs.newDraftwith the prompt preserved. Deliberately NOTsync.data.project: the server's "global" project record has worktree "/". - Regression spec: packages/app/e2e/regression/home-open-chat-empty-projects.spec.ts —
fresh profile (NO localStorage seed), mocked server, click the CTA (
exact: true— the whole card is also a button whose accessible name contains "Open chat"), expect navigation to/new-session?draftId=+ the cwd persisted as a tracked project. Verified failing on the unfixed code, passing with the fix. Playwright note: config reuses any server on port 3000 (reuseExistingServer) — run withPLAYWRIGHT_PORT=<free>if something else (e.g. the harmoniqs website dev server) holds 3000. - Checks:
tsgo -bclean;bun run test:unit376 pass / 0 fail.
- BUG:
-
(Aero wordmark, re-added) — amicode: AMICODE wordmark/logo font-family gains
'Aero'ahead of the existing fallback chain, both call sites: logo.tsx ('Aero', var(--font-family-sans, ...)) and wordmark-v2.tsx ('Aero', 'Racing Sans One', var(--font-family-mono, ...)). Additive only — no fallback removed, no sizing/weight/spacing touched. This had been iterated as uncommitted working-tree WIP before the branch fast-forwarded 57 commits to currentorigin/local/amicode(e9b695191); re-applied fresh against the current font stacks (logo.tsx's had independently moved to a sans-serif stack + weight 750/letter-spacing 4 in the interim — preserved as-is, only the family list changed).⚠️ LICENSE-PENDING — release blocker. "Aero" by Nirmal Biswas (Picatype) is donationware, USD 25+ to nirmalbiswas@gmail.com. Bundled atpackages/app/public/assets/Aero-Regular.ttffor PREVIEW ONLY; not locked as the official face. Before any release: pay the license and record proof here, or drop'Aero'from bothfont-familychains + remove its@font-face(index.css) + the asset.- index.css: new
@font-facefor "Aero" (truetype, weight 400,font-display: swap), inserted immediately before the existing Racing Sans One face; carries the license-pending comment inline. - New asset (git-added):
public/assets/Aero-Regular.ttf(44,464 B). - Verify:
bun run typecheckgreen in bothpackages/uiandpackages/app(tsgo, no errors). Visual/glyph confirmation is manual-only (see build note under #16, which builds and vendors this alongside the paste fix).
- index.css: new
-
(composer paste bridge) — amicode: wire the main chat composer's
handlePaste(prompt-input/attachments.ts) to the extension-clipboard bridge that patch #14's PR already shipped for a different input (home-cards.tsx'sreadClipboardViaBridge/pasteFallback, commit5a2f1dde44) — that bridge answers{source:"amicode", kind:"clipboard-request"}on the extension side (packages/extension/src/chat_panel.ts, already onmain), but nothing in the composer's own paste path ever sent it. Root cause matches home-cards.tsx's own comment verbatim: the chat iframe is sandboxed + cross-origin, sonavigator.clipboardis denied and the nativepasteevent'sclipboardDataarrives empty — Cmd/Ctrl+V into the composer was a silent no-op.- platform.tsx: new optional capability
readClipboardText?(): Promise<string | null>, alongside the existingreadClipboardImage?. - entry.tsx: implements it — no-op (
null) when not framed (window.parent === window, i.e. plain browser/opencode.ai/desktop — zero behavior change there); when framed, posts{source:"amicode", kind:"clipboard-request", nonce}towindow.parentand resolves on the matching{kind:"clipboard", nonce}reply or a 1500 ms timeout — same nonce/timeout/listener shape as the provenreadClipboardViaBridge, just exposed as a platform capability instead of a component-local closure (this file already hosts the analogousreadClipboardImage). - attachments.ts:
handlePasteno longer bails out whenevent.clipboardDatais null (only when it's ALSO missingreadClipboardText, i.e. non-amicode contexts are untouched); after the existing file/image-bridge attempts, falls back toreadClipboardText()when there's still no plain text. - prompt-input.tsx: threads
readClipboardText: platform.readClipboardTextintocreatePromptAttachments, mirroringreadClipboardImage. - No shared bridge module extracted — matches this codebase's existing precedent (home-cards.tsx's bridge is also component-local, not a shared util); two call sites isn't yet worth a shared abstraction.
- Tests: app
bun test(66 files) → 376 pass / 0 fail, incl. the untouchedattachments.test.ts(pure-function coverage only —handlePasteitself has no unit coverage in this codebase, matching home-cards.tsx's own bridge having none either; verification here is typecheck + build + the manual live-paste check below). Typecheck green inpackages/app. - Build sha256 (composer-only build, superseded by the generalization below):
564a4cd783524e0f50541b716d9db4048b2e02430ebc77786a11a4504431c085. - ADDENDUM (same session, live-test finding — Aaron): composer-only wiring wasn't enough. Confirmed live: pasting into a different editable (the generic "Connect <provider>" API-key dialog,
dialog-connect-provider.tsx— one of many such generic settings/connect forms) was still a silent no-op, because that dialog is a plain<input>with no knowledge of the bridge at all. This is the exact gap patch #14's era of WIP had already found and fixed once before (composer-only interception "left every OTHER editable broken (provider API-key field, etc.)") — generalized properly this time instead of re-patching one dialog at a time:- entry.tsx: new
installGlobalPasteFallback(), installed once at app mount (alongsidereadClipboardText, not a separate file — same feature area). A singledocument-levelpastelistener: activates only when the event target is a form field orcontenteditableAND the nativeevent.clipboardDatagave no plain text (the sandboxed-iframe symptom) — everywhere clipboardData already works (plain browser tabs, desktop) it's a pure no-op, no framed-check needed at the call site. On activation: fetches text via the samereadClipboardTextbridge, then splices it into<input>/<textarea>viaselectionStart/selectionEnd+ dispatches a syntheticinputevent (Solid'sonInputpicks it up), orexecCommand("insertText", …)forcontenteditable. - Composer double-fire is a non-issue by construction:
attachments.ts'shandlePastecallsstopPropagation()on every paste it handles (unconditionally now thatreadClipboardTextis always provided when framed), so the event never bubbles to this newdocumentlistener for the composer specifically. - Tests: same 376/376 pass (app suite), typecheck green — no regressions. No unit coverage added, same house-precedent reasoning as above (DOM/postMessage bridge code, build+manual verified here).
- entry.tsx: new
- Final build sha256 (covers #15 + #16 + this addendum together):
115216e82a6db15843fca46d5b32b587ba9a1e542297501cf2b45c394f745ea9. Verify: smoke test--version→1.17.3; string-literal checks confirm the underlying protocol compiled in (Aero×5,clipboard-request×2,readClipboardText×3 — note: the newinstallGlobalPasteFallback/isFormFieldare local identifiers, not string literals, so they're minifier-renamed and don't grep-match; absence there is expected, not a failure signal); channel gate ON (newLayoutDesignssentinel=!0). Interactive Cmd/Ctrl+V confirmation (composer AND the generalized fallback) is live-VS-Code-only, NOT yet human-verified past Aaron's one negative report on the pre-addendum build — same deferred-to-dev-host-acceptance caveat as patches #12/#13.
- platform.tsx: new optional capability
-
(mark drift fix, synced to amicode PR #99 final) — amicode: consolidated the fork's brand mark to ONE geometry, matching amicode's redesigned mark. The "kept in sync manually" cross-repo promise from patch #8 had already silently failed.
- Trigger: amicode's mark was redesigned (PR #99) without a corresponding update here — the fork still rendered the OLD "digi" pixel-accented H-robot everywhere. PR #99 went through several iterations before landing on its final geometry; this entry tracks that FINAL state (square viewBox
0 0 3600 3600), not the intermediate "hackathon mark" (viewBox116 287 3377 3035) an earlier draft of this fork PR had copied — that intermediate geometry is now itself stale and was replaced here. - Two copies of near-identical geometry lived in THIS repo (logo.tsx's
Robotused byMark/Splash, and spinner.tsx'sAmicoSpinner), plus a third in favicon/amico.svg. Consolidated to a singleMARK_PATHexported from logo.tsx. - Geometry now mirrors amicode PR #99's two authored SVGs (amicode:
packages/extension/media/amico{,_reduced}.svg), both square0 0 3600 3600:MARK_PATH= amico_reduced.svg's outer-bracket path (fill-rule evenodd screen knockout). Used byMark,Splash,AmicoSpinner, and mirrored as a literal in favicon/amico.svg — every SMALL context, matching amicode's own "small → reduced" rule.MarkDetailed= amico.svg's full detailed mark (bracket path + internal circuit-pattern rects/polygons). Used ONLY by the Meet Amico home card (w-12/48px), large enough for the detail to resolve.Mark/Splash/MarkDetailedviewBox0 0 64 56/116 287 3377 3035→0 0 3600 3600; logo.css aspect-ratio8/7→1/1(the mark is square now).
- Still theme-adaptive via currentColor + var(--icon-strong-base) — this is a live webview DOM, so currentColor resolves (unlike amicode's native VS Code tab icon, which needs committed {light,dark} files; see amicode PR #99).
- NOT a re-established cross-repo sync promise —
MARK_PATHis the single source of truth WITHIN this repo; it happens to match amicode's current geometry, kept aligned by hand when the mark changes. - Tests: ui
bun test src/amicodegreen; typecheck green ui + app. - NOT done this round: full native
bun run script/build.tscompile + vendored-binary swap — this patch only touches the embedded web UI. Deferred to the next amicode.N release tag, same split as patches #8/#11.
- (rich chat output, 2026-07-10) — amicode: assistant text style parity with Claude Code. packages/opencode/src/session/prompt/anthropic.txt ONLY (the provider prompt every Claude-model session gets).
- Trigger (Aaron, live comparison): amicode replies rendered as flat unstyled paragraphs — no headings, no bold, no lists — while Claude Code's chat output is structured markdown. The renderer was never the problem (packages/ui markdown.tsx/css fully style headings/bold/tables/LaTeX); the prompt was: "output will be displayed on a command line interface. Your responses should be short and concise."
- Change: "# Tone and style" → "# Communicating with the user", modeled on Claude Code's guidance — output renders as rich GFM (use
inline code/bold/lists/headings/tables deliberately), match structure to content (simple question → plain prose), lead with the outcome, readable-beats-brief (complete sentences, no fragments/arrow chains), write for a teammate catching up. Emoji rule, text-vs-tool-communication rule, and never-create-files rule kept verbatim. Line 3 "interactive CLI tool" → "interactive coding agent" (the primary surface is the extension chat panel, not a monospace TTY). - Scope note: agents with a custom
prompt(e.g. the extension'spulse-designer) BYPASS this file entirely (src/session/llm/request.ts — agent.prompt replaces SystemPrompt.provider). Interview-session parity comes from the amicode repo's companion patch to packages/extension/AGENTS.md ("Style & formatting" section, injected asinstructionsfor every session) — same guidance, so plain chat and interview sessions agree. - default.txt/gpt.txt/gemini.txt untouched — non-Claude models keep stock style.
- Patch commit
b81fb756f2. Build sha2563c26d8d9261dcf55b878e678fe359791e5f22e6833f87f5d9ce12b457c18e061atdist/opencode-localAND the amicode vendored path — built clean at the pinned ref viafetch_opencode.mjs(amicode opencode.lock.json ref bumped tob81fb756f2); byte-identical to the pre-commit--any-refdirty build, so the bun compile is deterministic for identical source. Verify:grep -ac "Being readable matters more than being brief" <binary>→ 1; channel gateNG=!0ON; fetch smoke test--version→ 1.17.3. Amicode-side: vsix repackaged (55.64 MB, 133 files) + installed on erlich; extension vitest 484/484 green. - ADDENDUM 3 (2026-07-11, live-test round 2): custom-prompt agents now get communicating.txt too. Aaron's "still looks the same" screenshot was a pulse-designer INTERVIEW session — exactly the custom-prompt bypass documented in addendum 2. A live API probe of the plain-chat path (default agent, deepseek pin, POST /session/:id/message) confirmed the shared prompt WORKS — response came back with bold, a table, LaTeX, structured prose — so the gap was only the bypass (plus old sessions' flat history anchoring the model; test in a fresh session). Fix:
llm/request.ts— custom-prompt agents appendPROMPT_COMMUNICATINGunlessagent.mode === "subagent"(subagent final text is model-directed data, NOT user-facing — must stay style-free). The pulse-designer agent (mode unset → user-facing) now gets the rules in the system prompt proper instead of relying on AGENTS.md instructions alone. Typecheck green. - ADDENDUM 2 (same day — "does this need to be model specific?"): consolidated to ONE model-agnostic prompt. NEW
prompt/communicating.txt(the canonical "Communicating with the user" section + structure-matching examples + an explicit supersede clause overriding any per-model CLI/terseness/length rule).system.ts provider()now returns[providerBase(model), PROMPT_COMMUNICATING]— EVERY model family (beast/gpt/codex/gemini/anthropic/trinity/kimi/default) gets it appended; the inlined copies added earlier in anthropic.txt/default.txt/gemini.txt were slimmed back to near-stock (minus their CLI-concise lines) so there is one source of truth. gpt/beast/codex/kimi/trinity left untouched — the supersede clause outranks their style rules without diffing them (upstream-merge friendliness). Typecheck green (tsgo). REMAINING GAP by design: agents with a customprompt(request.ts:60) still bypass provider() — deliberate, because Task-tool subagents return model-directed raw data and must NOT get user-facing style rules; the user-facing pulse-designer agent is covered via the amicode AGENTS.md instructions instead. - ADDENDUM (same day, live-test finding — Aaron: "doesn't seem any different"): anthropic.txt was the WRONG file for the live setup. The prompt file is selected per model id (
system.ts provider()), and the extension's model pin resolved toopencode/deepseek-v4-flash-free(noanthropickey in auth.json —preferredModel()creds fallback), so live sessions load default.txt — whose style rules are far harsher ("You MUST answer concisely with fewer than 4 lines… One word answers are best", monospace-CLI framing, one-word-answer examples) and steamroll the AGENTS.md instructions. Fix: same "Communicating with the user" section ported to default.txt (tone section + verbosity examples replaced with structure-matching examples; the second stray "fewer than 4 lines" mandate near Tool-usage-policy replaced too; "interactive CLI tool" → "interactive coding agent") and gemini.txt (Google creds exist locally → reachable; "Minimal Output: fewer than 3 lines" / monospace bullets replaced, No-Chitchat kept). gpt/beast/codex/kimi/trinity untouched (no matching creds; port on demand). Root-cause note for the future: to get Claude-quality prose AND the anthropic.txt path, add Anthropic creds — the pin then resolves toanthropic/claude-sonnet-5.
-
(rydberg Hamiltonian fix, 2026-07-11) — amicode: the System card's composite Hamiltonian showed a driven-cavity form for a Rydberg atom (Aaron, live: platform walked transmon→cavity→transmon-cavity→rydberg and the card rendered
Ĥ/ℏ = -Δ n̂ + ε(t)(â+â†)for a 3-lvl atom — "just not right"). packages/ui/src/amicode/system-render.tssystemHamiltonianLatexcomposed role-keyed drifts but appended ONE hardcoded bosonic quadrature driveε(t)(â+â†)for every system.- Fix: role-aware
driveTerm()—atom→Ω(t)/2 (|r⟩⟨1| + h.c.)(3-level Rydberg convention: |0⟩ dark, |1⟩↔|r⟩ laser-driven); everything else keeps the quadrature drive. Deduped per distinct role like the drifts, so mixed atom+cavity systems show both flavors. Atom drift-Δ n̂→-Δ |r⟩⟨r|(detuning belongs on the Rydberg level; n̂ read as a bosonic number operator next to the old â-drive). - Tests: system-render.test.ts +3 (bare rydberg atom → Rabi drive, NO ε(t)/â; two atoms + vdW → deduped single drive + C₆ term; atom+cavity → both drives). ui
bun test src/amicode115 pass; typecheck green both packages.
- Fix: role-aware
-
(markdown heading hierarchy, 2026-07-11) — amicode: section titles in chat rendered indistinguishable from body prose (Aaron, live: "section titles aren't rendering properly"). Root cause was upstream DESIGN, not a bug: packages/ui/src/components/markdown.css had
/* Headings: Same size, distinguished by color and spacing */— all h1–h6 at 14px medium, the same size/weight as body, colored--text-strongwhich the whole markdown container ALREADY uses (zero distinction), with margin-top 0 + margin-bottom 24px (gap on the wrong side — title floats orphaned above its section). The prompt work (#18) made the model EMIT headings; this made them visible.- markdown.css: real hierarchy — h1 18px / h2 16px / h3 15px / h4-h6 14px, all weight 600 (no semibold token exists; 400/500 only, raw 600 consistent with the file's raw px), margin 24px above + 8px below (binds title to its section; container's
> *:first-childrule still zeroes the top of a leading heading).strong/bmedium(500) → 600 — against the 400 body, 500 was near-invisible as "bold". - CSS-only, no tests to update; cascade checked (message-part.css/index.css define no competing heading rules).
- markdown.css: real hierarchy — h1 18px / h2 16px / h3 15px / h4-h6 14px, all weight 600 (no semibold token exists; 400/500 only, raw 600 consistent with the file's raw px), margin 24px above + 8px below (binds title to its section; container's
-
(expandable skill chips, 2026-07-11) — amicode: skill loads rendered as a name-only chip with
hideDetails(Aaron: "when skills are used do I get a visual notification like in Claude Code?" — the chip existed, but nothing showed WHAT got injected). packages/ui/src/components/message-part.tsx skill renderer + NEW message-part-skill.ts.skillBody()(pure, tested — message-part-skill.ts): strips the<skill_content>/<skill_files>transport wrapper and the model-facing plumbing lines (base-directory, relative-path note, sampling note, the# Skill: nameheading the chip already shows) from the skill tool's output, leaving the SKILL.md instructions.- Renderer:
hideDetailsdropped; body renders via the standardtool-output+Markdownidiom (same as list/glob), so the chip stays collapsed by default and expands to the injected instructions. No output yet (running) → no chevron, unchanged look. - Companion (amicode repo, PR #132 commit b135875): injected opencode config gains
permission.skill: "allow"— the staged skill set is surface+entitlement curated, so the skill tool's default per-name "ask" was friction without a boundary and a headless-session hang risk. - Tests: message-part-skill.test.ts +5; ui
bun test158 pass; typecheck (tsgo) green.
-
(Connections auth-path scaffold, 2026-07-21) — amicode#194: the Connections tab scaffolds every candidate Pasqal auth path behind the wire contract, so the UI is ready whichever mechanism the direction lands on (decision open; server routes are a separate slice). packages/ui/src/amicode/connections.ts + connections-tab.tsx, app labels.
- Wire (all tolerant, absent today → cards render byte-identical, regression-tested):
auth_methods(known: credentials/browser/device-code/token; unknowns dropped), three mid-flow stateswaiting-browser/waiting-code/choose-project, plususer_code/verification_url/code_expires_at(device flow) andprojects(post-auth picker; name falls back to id). - Model: cardModel branches for the three states (pending tone, no form/actions; waiting row carries the cancel exit; choose-project with no wire projects keeps cancel open instead of an empty picker). Method model:
connectionAuthMethods(advertised wins, else legacy per-id single method — chooser renders only at ≥2),methodEntryKind(browser/device-code → start button; token → pasqal token+project or legacy base_url+token; credentials → #169 form unchanged). - Gates:
startAuthPayloadonly for wire-advertised interactive methods;chooseProjectPayloadonly for wire-offered project ids;pasqalTokenSubmitPayloadtrims + refuses empties. A stale UI can never start a flow the server no longer offers. - Tab: method chooser chips, start button, user-code block (selectable, mono), project radio list, cancel row — new callbacks (
onStartAuth/onChooseProject/onCancelAuth) are OPTIONAL props; the app passes labels only, so nothing new is reachable in prod until the producer advertises methods. - i18n: en.ts keys only (locale-fallback precedent from #166). Stories: connections.stories.tsx — full state atlas matrix + interactive walkthrough, mock wire data.
- Tests: connections-paths.test.ts +17 (parsing tolerance, model branches, gates, legacy regression); existing exhaustive state loops extended. ui bun test src/amicode 264 pass; typecheck (tsgo) green both packages.
- Wire (all tolerant, absent today → cards render byte-identical, regression-tested):
-
(Pasqal keychain silent re-auth — Jack's workaround, 2026-07-21) — amicode#194: the interim auth path from Jack's live-API session (pasqal-auth-bundle). Pasqal's API accepts ONLY password-grant + service-account tokens (refresh tokens 403 on the
gtyclaim; browser/device grants client-disabled), and user tokens live ~24h — so the fork server keeps the password in the OS keychain and silently re-mints on expiry. Design of record: ADR 0001 addendum (amicode repo, branch kate/adr-0001-pasqal-keychain). New packages/opencode/src/server/amicode/pasqal-secret.ts + connections.ts wiring.pasqal-secret.ts: the keychain seam. Mechanism is Jack's choice (Kate) —@napi-rs/keyring(macOS Keychain / Linux Secret Service / Windows Cred Manager), lazyrequireso a missing/unbuilt native addon degrades this ONE feature (never crashes opencode) to SESSION-MEMORY (ADR 0001's named fallback — password in process RAM, re-prompt after restart; never a plaintext file). Injectable viasetPasqalSecretStore(inMemorySecretStore())so tests never touch a real keychain.- connections.ts: submit stores {username,password} in the keychain on valid+token (the credential-file poison guard still makes a password in pasqal.json impossible — token stays token-only at rest).
attemptPasqalSilentReauthre-runs the #164 validator with the stored secret when the token is expired (revalidate + background revalidation both call it): valid → fresh token rewritten + connected; invalid (password changed/revoked) → keychain+token wiped, needs-key; unreachable/config → claim STANDS (offline, never a verdict). Disconnect wipes the keychain slot. Password rides the child env only — never argv, never a status field, never a response. - Service account (
client_credentials) is the documented exit condition (retires the password entirely, ADR-clean); request is with Pasqal (Baptiste Peyras). Browser/device/token/refresh paths are evidence-ruled-out (see #194); the scaffold's tolerant method model harmlessly still parsesauth_methodsif a future server ever advertises them. - CAVEAT (cannot be verified here): the real @napi-rs/keyring backend + its load inside the bun-compiled binary needs a real-machine smoke test before trusted, and the dep must be added to packages/opencode/package.json (node-pty/tree-sitter precedent). All LOGIC is tested behind the injected in-memory store.
- Tests: pasqal-secret.test.ts +5; amicode-connections.test.ts +6 silent-reauth (store-on-submit / re-mint / reject-wipes / unreachable-stands / disconnect-wipes / secret-free-response) + hermetic secret-store isolation in setup; existing expiry test made explicit about the no-password fallback. opencode
bun test test/server/amicode-connections.test.ts test/server/pasqal-secret.test.ts test/server/amicode-connections-routes.test.ts→ 105 pass. typecheck (tsgo) green. (15 unrelatedbuilt-in widget sourcesfailures are a pre-existing missing-build-artifact issue, not this diff.)
-
(upstream provider/model port — Jack, 2026-07-27) — first deliberate partial upstream sync. Our base is sst/opencode
v1.17.3(2026-06-10); upstream was 1,150 commits ahead at v1.18.7. Rather than a full merge (54 fork-touched files overlap), only the provider/model bucket was ported, because Claude 5 was silently broken at our base.- THE BUG:
anthropicOpus47OrLater()matched/opus-(\d+)[.-](\d+)/— requires BOTH major and minor.claude-opus-5has no minor andclaude-sonnet-5isn't "opus", so neither matched. Both fell through to the legacythinking:{type:"enabled",budgetTokens:16000}path with onlyhigh/maxtiers, instead of the 5-tiertype:"adaptive"path. Sonnet 5 released 2026-06-29 and Opus 5 on 2026-07-24 — both AFTER our fork point. Upstream fixed this in #38757 (generalize Claude adaptive thinking): the version regex now tolerates a missing minor and defaults unversioned Claude IDs to modern. - NOT a "new models" problem: the catalog is fetched live from models.dev at BUILD time (
script/generate.ts;amicode-release.ymlpins no snapshot), so Opus 5 already appeared in the picker — just hobbled. QA-ing "can I select Opus 5?" gives a false negative on the need to sync. - WHAT WAS PORTED (deliberately minimal — the END STATE of files, not 112 commits of history):
packages/opencode/src/provider/transform.ts— replaced wholesale with upstream/dev. Fork had never touched it → 0 conflicts. Also carries #38424 (prompt cache keys by SDK), #36624/#36543 (derive variants from reasoning metadata), and the Grok/GLM/Bedrock-Mantle variant fixes.packages/opencode/test/provider/transform.test.ts— replaced wholesale with upstream/dev (our copy asserted 6 now-superseded behaviors).packages/core/src/models-dev.ts— ADDITIVE only: upstream'sReasoningOptionunion + optionalreasoning_optionsfield onModel. transform.ts reads it; without it the port is 4 type errors.packages/opencode/src/session/llm/request.ts— 3-way merged (git merge-fileours/v1.17.3/upstream), 0 conflicts. Brings upstream #33392 (strict: falseon tools for@ai-sdk/openai,@ai-sdk/azure,@ai-sdk/amazon-bedrock/mantle). amicode patch #18 (PROMPT_COMMUNICATING) is preserved — different region of the file.
- WHAT WAS DELIBERATELY NOT PORTED: the Effect
layer-node → app-noderefactor and the@opencode-ai/schemaextraction (upstream guttedcore/src/model.ts127→41 andcore/src/provider.ts85→25 into a new package). Cherry-picking the bucket's 112 commits would drag both in. Take file end-states, not history. - MEASURED BLAST RADIUS: ran both transform versions over all 454 reasoning models in the live models.dev catalog — 418 identical, 36 changed, 0 errors. The 36: Claude 5 (opus/sonnet, across
anthropic+ allamazon-bedrockregions +opencodezen) goeshigh,max→low,medium,high,xhigh,maxwithtype:"adaptive"; xAI Grok 4.3/4.5/4.20/build and GLM-5.2 gain effort variants where they previously had NONE;claude-opus-4-5keeps itslow,medium,highkeys but now also sendsbudgetTokens:16000alongsideeffort. Claude 4.8/4.6/4.1 unchanged. - VERIFICATION:
tsgo --noEmitgreen on packages/opencode AND packages/core.bun test test/provider/→ 503 pass / 0 fail.bun test test/session/ test/server/→ identical results before and after the port (3 pre-existingllm-native-recordedfailures + a pre-existing Bun 1.3.14 teardown segfault, both reproduced byte-for-byte on the unported tree). - STILL UNVERIFIED (needs live API creds): whether the OLD legacy
thinking:{type:"enabled",budgetTokens}form actually 400s against Opus 5 or merely degrades — i.e. whether this was "hobbled" or "unusable". Also unexercised by CI: the Grok/GLM effort variants and the opus-4-5budgetTokens+effortcombination. - FUTURE SYNC COST: this is a knowing trade — porting file end-states now makes a future clean upstream merge harder on these 4 files. Accepted. NOTE the bug class recurs every model generation (version-regex parsing of model IDs will break again at Opus 6), which argues for a standing narrow sync lane on
transform.tsrather than one-off unfreezes. - SEPARATE RISK SPOTTED (not fixed here): the release build does a bare
fetch(models.dev/api.json)with no fallback, so a models.dev outage hard-fails the build. Consider pinningMODELS_DEV_API_JSONfor the hackathon build.
- THE BUG:
-
(clickable chat links — file:// opens in the editor, 2026-08-01) — amicode: markdown links in chat were dead wholesale (Aaron, live: vault-note links authored per the amico-vault skill's new
file://convention — "clicking does nothing"). Diagnosis, three layers, all confirmed in source: (1) the marked link renderer emits<a href class="external-link" target="_blank">intact — no sanitization (packages/ui/src/context/marked.tsx:572); (2) session-ui's markdown component had NO anchor click handler at all — its only listener was the code-copy button, so anchors fell to defaulttarget="_blank"navigation, which the sandboxed cross-origin chat iframe blocks — every scheme dead, not just file://; (3) even past (2), two downstream filters would have dropped file://: the app platformopenExternal(http/https/mailto only, packages/app/src/entry.tsx:87) and the extension bridge (https-only regex, amicode packages/extension/src/chat_bridge.ts).- FORK: packages/session-ui/src/components/markdown.tsx — NEW
setupExternalLinks(root): delegated click handler ona.external-link, framed contexts only (window.parent !== window; a plain browser tab keeps native behavior).preventDefault+ postMessage to parent: https/mailto →{source:"amicode", kind:"open-external", url}, file:// → NEW bridge kind{kind:"open-file", url}; any other scheme untouched (native fallback, dead-in-iframe as before). Wired next tosetupCodeCopywith the same mount-once-in-effect + onCleanup pattern. - EXTENSION (amicode repo): chat_bridge.ts — new
open-filebranch: kind + string +^file://gate in one condition (non-file →false, not consumed — open-external idiom); thendecodeURIComponent(new URL(url).pathname), must bepath.isAbsolute+ ≤4096 +fs.existsSync→vscode.commands.executeCommand("vscode.open", vscode.Uri.file(fsPath)); unparseable/unreachable → consumed-silent (save-file idiom). Posix path semantics (fleet is mac/linux). Relay allowlists (chat_panel.ts lane-1, deck/shell.ts) gain"open-file". - Skill companion (already landed, becomes live with this build): amico-vault SKILL.md "Reporting writes in chat" — vault writes are reported as absolute
file://markdown links, one per file. - Tests: extension
chat_bridge.test.ts+2 (opens an existing file incl. a %-decoded space path; rejects non-file schemes as not-consumed, missing files/non-absolute as consumed-silent) → file 9/9; full extension suite 843/846 — the 3 failures are PRE-EXISTING and environmental (server_auth.test.ts spawn-env resolves the machine's provisioned~/.amico/amicode/venvs/pasqal-connectorinterpreter; logic committed at HEAD in extension.ts/pasqal_python.ts, untouched by this diff). session-uibun test src/components→ 69 pass / 0 fail. Extensiontsc --noEmitclean. - Build sha256:
df1dbe0de082a65c181e6aba7546acd4694842e76cc035c69b95352d08c5dac1(packages/opencode/dist/opencode-darwin-arm64/bin/opencode + convenience copy dist/opencode-local; vendored into amicode packages/extension/vendor/opencode/darwin-arm64/opencode AND swapped into the installed extension~/.vscode/extensions/harmoniqs.amicode-0.1.2/vendor/...— plain cp, ETXTBSY not hit). Verify: binary containsopen-file×8; channel-gate sentinelnewLayoutDesigns!0present; smoke--version→ 1.17.3. Extension dist (extension.jsopen-file ×2,deck_shell.js×1) copied into the installed extension's dist/. - Interactive click →
vscode.openin the live webview NOT yet human-verified — needs a window reload (extension host + spawned server both restart from the new artifacts); same deferred-acceptance caveat as #12/#13/#16. - Divergence note: the vendored binary is now AHEAD of amicode
opencode.lock.json's pinned fork ref until the fork commits (session-ui markdown.tsx) and the lock bumps — normal amicode.N tag flow, not done here.
- FORK: packages/session-ui/src/components/markdown.tsx — NEW
-
(harmonic wave indicator — de-shimmer the tool-status title, 2026-07-28) — amicode: the chat had two "working" indicators that read as identical — the fork's thinking line and the STOCK tool-group header (
packages/ui/src/components/tool-status-title.tsx, e.g. "Working in shell" → "Worked in shell") — because both were shimmering text with no glyph. Earlier steps of this run gave the thinking line a standing-wave glyph (AmicoWave,packages/ui/src/amicode/amico-wave.tsx) and deleted theamc-text-shimmerCSS; this patch removes the shimmer from the last surface still asking for it.tool-status-title.tsx: all five<TextShimmer text={...} active={...} offset={...} />render positions (swap-mode active/done, suffix-mode prefix/active/done) replaced with plain<span>{text}</span>.TextShimmerimport removed. The now-orphanedprefixLenmemo (it existed only to feed the shimmer'soffsetphase-alignment prop) removed too — confirmed viaoxlintbefore/after (498→499→498 warnings) that leaving it in place trippedno-unused-vars, so dropping it was the correct call rather than "keep everything."- Deliberately PRESERVED: the
common()prefix-splitting logic, the width-morph animation (animate()/finish(), therequestAnimationFrame, the storedwidth, thedata-readyflag), thedata-component/data-active/data-modeattributes, thearia-label, and the active→done text swap itself. None of that is the shimmer — it's the genuinely good word-morph behavior. text-shimmer.tsxuntouched — it has many other live callers (basic-tool.tsx,message-part.tsx×7,session-turn.tsx,v2/components/basic-tool-v2.tsxviatext-shimmer-v2.tsx, plus stories), confirmed viarg -n 'TextShimmer' packages/. Not a candidate for deletion.- The dropped
offsetprop existed only so the shimmer's gradient phase stayed continuous across the prefix/tail split; with no shimmer there's nothing to phase-align, so it has no replacement. Verified live (Storybook,UI/AnimatedCountListstories) that the prefix and tail still read as one unbroken word with no seam: in the swap-modePlaygroundstory, driving an active→done transition showed the mid-animation frame rendering both the active and done spans simultaneously under the animating width (data-ready="true", containerstyle="width: 0px"mid-transition, then settling) — the word-morph is intact. The suffix/prefix-tail mode (data-mode="suffix") is currently unreachable from any real call site — both app usages (message-part.tsxcontext-tool-group and shell-group titles) and the only story passsplit={false}— so it was verified by transiently flipping one story'ssplitprop off (Doneexport, "Exploring"/"Explored"), confirmingdata-mode="suffix"renders"Explor"+"ed"as an unbroken "Explored" with no visible gap, then reverting that story edit before commit (git statusshows only the two files below). - Tests: ui
bun test src→ 402 pass / 0 fail (unchanged). typecheck (tsgo) clean inpackages/ui.oxlint packages/ui/src→ 498 warnings / 0 errors both before and after (no new warnings onceprefixLenwas dropped).
-
(text-card questions — free-form asks get a first-class card, 2026-08-02) — amicode#245 + ADR
docs/adr/0002-free-form-questions-render-as-text-cards.md: the question schema grows an optionalkind("choice"default |"text") on the agent-facing AND server shapes (v1packages/schema/src/v1/question.ts+ v2packages/schema/src/question.ts, both inbasesoPromptandInfocarry it; absent → choice, unknown kind fails decoding loudly). Three renderers branch a text card (header + bare text input + submit; NO option rows, NO "Type your own answer" pseudo-option; submit gated on non-empty trimmed text; answers ride the EXISTING typed-custom-answer path; dismissal unchanged): app dock (session-question-dock.tsx+ newsession-question-dock.helpers.ts— ALSO honors thecustomflag for CHOICE cards, the TUI pattern it previously ignored), TUI (question.tsx), CLI (question.shared.ts+footer.question.tsx). Prose guard (session/prompt.ts) behavior UNCHANGED (still fires on prose questions in active interviews, once per assistant message, never after a question-tool call); predicate + nudge extracted tosession/prose-guard.ts(turn-output.ts pattern); nudge text now bilingual (options-with-recommended-first for choice,kind: "text"for free-form). Tool descriptions teach the text kind (tool/question.txt+ coretool/question.ts). Legacy SDK + client codegen regenerated (QuestionInfo/QuestionV2Info gainkind?: "choice" | "text"). Upstream-sync watch: the dock's question component is already a high-conflict file — keep the text-card branch and thecustomRow()gate honest on syncs.- Tests: schema
test/question.test.ts(decode ×4 shapes: absent/choice/text/unknown), opencodetest/cli/run/question.shared.test.ts(+5: text card shape/submit/empty-blocked, choice regression, custom:false), tuitest/routes/session/question.test.tsx(+5: render coverage incl. typed-answer submit through a stub server), appsession-question-dock.helpers.test.ts(+6), opencodetest/session/prose-guard.test.ts(+10: predicate truth table + bilingual nudge), tool tests (+2 description, +1 kind passthrough).
- Tests: schema
The upstream sync covered only local/amicode; live work sat on unmerged feature branches.
Merged into amicode/merge-upstream-dev on top of the sync:
ann/thinking-words-updated(Quantizing/Obsidianing),feat/amicode-system-card-model,fix/amicode-receipt-currency— clean merges.pr/amico-working-indicator— the harmonic wave indicator (AmicoWave in the thinking line, H-mark static; tool-status-title de-shimmered) + collapsed receipt runs + AmicoSkillChip. Conflicts: message-part count×useV2Actions threading unioned; shims amicode-receipt(-runs).kate/context-tree-vault-zoom— framed external links route over the open-external bridge (kept upstream's openExternal; branch's global link interceptor dropped — patch #25 covers it).feat/warrant-receipts-captured(contains approval-surfaces) — capability warrants: rail warrant chip, approval card (deliberately NOT onAsk), /amicode/warrants + /amicode/approve.feat/harmonic-wave-indicator— recorded-Hamiltonian rendering, system-card physics rows, KaTeX dedupe (spine/aside-lane cancel against their own reverts).
DELIBERATELY NOT MERGED: amico/issue-56-living-chat (34 commits — glass sweep, Latent
Constellation, bottom-dock composer, thought camera). It redesigns exactly the surfaces
upstream replaced (old message-timeline, session-new-design-view, prompt-input, markdown —
all deleted/moved upstream), 23 conflicted files. Merging would hybridize two design
directions into neither. It stays on its branch; reviving the glass recipe is a deliberate
port project onto the NEW ui, not a merge.
- (thinking block — two-row restructure, de-glitched, 2026-08-02) — amicode: the working indicator shattered beside a long activity heading (Aaron, live screenshot: "Superposing… 12s · ↑" on line one, "8.7k" and "tokens" orphaned on lines two and three, H-mark and wave drifting out of row). Root causes, all three mechanical:
.amc-thinking-metahad nowhite-space: nowrap;.amc-thinkingcarriedmin-width: 0next to aTextRevealheading that claims an explicit measured pixel width, so the heading won the flex shrink fight;align-items: baselinethen scattered the wrapped block's pieces. Fix = restructure, not patches:packages/ui/src/amicode/thinking-line.tsx— now a two-row GRID block that OWNS the H-mark (markprop, default on): row 1 = mark + AmicoWave + cycling gerund, row 2 = the meta (elapsed · ↑ tokens · esc), always rendered from mount so block height never pops when tokens first arrive. DOM order is word-first so the grid's first baseline is the VERB's — the sibling heading (session-turn-thinking, nowalign-items: baseline) aligns with the verb, not the mark's bottom edge. a11y: ticker isaria-hiddenwith a single sr-onlyrole=status— the oldaria-live="polite"wrapper re-announced every word rotation every 2s.packages/ui/src/amicode/amicode.css—.amc-thinking*rewritten:inline-gridauto auto 1fr, meta atgrid-column: 2 / -1(starts under the wave), verbmin-width: 16ch(covers the longest gerund + "…" so word rotation stops shifting the layout), metanowrap+overflow: hidden+ ellipsis, inline children (> * + *margins) so the whole meta truncates as one line.packages/app/src/pages/session/timeline/message-timeline.tsx—TimelineThinkingRowdrops its<AmicoMark/>+ the inline-styled.amc-thinking-rowwrapper span (import removed); headingTextRevealgainstruncateso it ellipsizes instead of squeezing the block.packages/session-ui/src/components/message-part.tsx— the.amc-laneworking mount dropsamc-lane-head+<AmicoMark/>(import trimmed; block owns the mark).packages/session-ui/src/components/session-turn.css—[data-slot="session-turn-thinking"]gains> [data-slot="amc-thinking"] { flex-shrink: 0 }+ the baseline alignment.- Stories updated to the new DOM: thinking-line.stories.tsx (frame no longer double-marks), amico-wave.stories.tsx ThinkingRow mimic (+ stale "not mounted anywhere" notes corrected here and in amico-wave.tsx), amico-presence.stories.tsx (lane-head only for stepping_in/settling, ThinkingLine brings its own mark when on).
- Pure logic (
thinking.tswords/formatting/turnTokens) untouched. Tests: uibun test src/amicode→ 424 pass / 0 fail; typecheck (tsgo) clean in ui + session-ui + app. - Build (working tree, uncommitted —
local 5b1a880fc…+dirty): vendored darwin-arm64 sha25660204bec44b50b678d2c1500bf5147d1a0af54ed8bfa3441b8092ed7b4951c3e, smoke--version→ 1.18.10,assert_ui_gate.sh→ ON (shorthand memo). Verify (scratch port 14231):GET /→ 200<title>Amicode</title>; css asset carries.amc-thinking{display:inline-grid;grid-template-columns:auto auto 1fr}+16ch+.amc-thinking-meta{…nowrap…ellipsis;grid-area:2/2/auto/-1}+amc-thinking-meta>*+*{margin-left:6px}+flex-shrink:0; main chunk carriesAmico is thinking×1 andamc-thinking-word/-meta/-elapsed×1 each;POST /session {}→ 200. Interactive eyeball (narrow/wide panes, live turn) deferred to dev-host reload — the served-asset checks are the pre-flight.
- (report-a-bug composer button, 2026-08-03) — amicode/opencode#116 (part of amicode#249): a bug-glyph icon button in the v2 composer's bottom control row — right-anchored, immediately left of send — gated on the
amicode_bug_report=1boot param (same adopt-once convention asamicode_hide_project: entry.tsx adopts fromlocation.searchat boot; standalone opencode never sets it, so the row is byte-identical there). Click postsamicode.reportBugover the app↔extension bridge ({source:"amicode",kind:"command",command}— chat_panel allowlist contract); with a bug dock OPEN it instead reveals/re-expands and posts nothing.- Boot-param gate: NEW
packages/app/src/utils/amicode-bug-report.ts(adoptBugReportFlag(search)— deterministic set so tests drive both directions — +bugReportEnabled()); adopted inentry.tsxbesideadoptHiddenProject. The gate is consulted inPromptInputV2Composer, NOT inside the button: gated-off ⇒trailingControl={undefined}⇒ the row's layout never shifts (an always-mounted-but-empty slot wrapper would still eat its 4px margin). - Bridge helpers extracted: NEW
packages/app/src/utils/amicode-bridge.ts(inAmicode+postAmicode, moved verbatim out ofuse-amicode-commands.tsx, which re-exports both — the existing envelope contract test and the four importers untouched). - Dock seam (the #117 contract): NEW
packages/app/src/pages/session/composer/bug-dock.ts— module-scope singleton (vault-panel pattern) exposingisOpen(),open(),close(),reveal(),revealNonce().reveal()re-sets open AND bumps a generation nonce so an already-open (collapsed) dock still gets an observable re-expand signal. The button side only readsisOpen()and callsreveal(); #117 builds the real dock UI against this API. NEWreport-bug.tsbeside it holdsREPORT_BUG_COMMAND+reportBug(dock = bugDock)(dock open → reveal, zero posts; else one bridge post) — component-free so the contract stays unit-testable (the app has NO component-render test surface: bun test resolves solid-js to its server build and cannot run Solid's compile-time JSX transform; visuals live in stories). - The button: NEW
packages/app/src/components/report-bug-button.tsx(+.css) —IconButtonV2ghost-muted/large(28×28, matching the row) with the newbugglyph,aria-label="Report a bug"+TooltipV2(inactive while disabled). State contract in the css, keyed on[data-action="report-bug"](IconButtonV2 hard-pinsdata-component): color ONLYvar(--v2-state-fg-danger)(red-800 light / red-500 dark — yellow stays the sole accent), mutedopacity: 0.55at rest → full on hover (the provider-icon pattern), pressed/focus full with the base 2px--v2-border-border-focusring, disabled0.3+not-allowed(clearly deader than rest),border-radius: var(--radius-md)(control token), 0.16s opacity transition + reduced-motion cut. Forced[data-state]hooks honored so the story can render the matrix. - Glyph:
bugadded to the v2 icon family (packages/ui/src/v2/components/icon.tsx) — stroke-family beetle,currentColor, 16 viewBox; never emoji, no literal hex. - Slot:
PromptInputV2(session-ui) gainstrailingControl?: JSX.Element— the modelControl optional-control pattern — rendered in amr-1 flex items-centerwrapper (4px grid) immediately left ofPromptInputV2SubmitButton.IconButtonV2'sstateunion widened with"focus" | "disabled"(its css already handled those[data-state]s; the type just blocked them). - Story:
packages/app/src/components/report-bug-button.stories.tsx— InComposerRow (faithful row mirror with the REAL send button), States (all five), Schemes (light+dark panes via the amico-waveresolveThemeVariantV2-inline-tokens pattern — thedata-color-schemewrapper is a storybook no-op). Verified headless viastorybook build+ Playwright screenshots of all three. - Tests: NEW
report-bug.test.ts(4: exact envelope +"*"origin when dock closed; reveal + nonce-bump + zero posts when open; posts again after dismiss; injected-seam reveal) andamicode-bug-report.test.ts(2: gate off without param / on with=1; only exact1enables). Appbun run test:unit783/783; ui 441/441; session-ui 94/94; tsgo clean in all three; oxlint 0 new findings (the one error in the touched session-ui file is the pre-existing\200Boctal escape, on base).
- Boot-param gate: NEW
- (bug-report dock + sentinel watcher, 2026-08-03) — amicode/opencode#117 (part of amicode#249; lifecycle: amicode ADR 0004): the bug session's in-flow home — a new member of the composer's dock family (todo/question/permission/revert), gated on the same
amicode_bug_report=1boot param as the #116 button. Opens on the bridgeopen-bug-report {sessionID}down-message; body is an iframe pinned to the bug session's route (the deck's URL-pinned chat-iframe idiom); the sentinel watcher matches^AMICODE_BUG_FILED[ \t]+(\S+)(multiline, line-start anchored; horizontal-whitespace separator so a bare sentinel can't capture the next line's first word) in the hosted session's streamed text parts, postsbug-filed {sessionID, url}exactly once, and switches to the terminal end-state (issue link) until the extension closes the dock. Collapse (chevron) never posts and never kills; the close control postsbug-report-closed {sessionID}exactly once and dismisses locally (the extension aborts + hard-deletes).- Controller: NEW
packages/app/src/pages/session/composer/bug-dock-controller.ts— component-free singleton (createBugDockController(deps)with injectable dock seam / post / enabled for tests;bugDockControllerthe module instance). State machine: closed → chat → filed; owns the #116 seam'sopen()/close()(the button side only readsisOpen()+reveal()). Idempotent opens: duplicate same-idopen-bug-report= reveal (re-expand; the filed end-state survives), different-id while open dropped (the extension's single-open guard owns which session is live), malformed dropped. Extension-initiatedclose-bug-report {sessionID}(the DOWN contract this slice chose — the PRD specced onlyopen-bug-reportdown; #250's "archive + tell the app to close the dock" rides it) closes silently, posting nothing back.bugDockFrameSrc(...)pins the frame URL: app origin +/server/<key>/session/<id>+colorScheme/auth_token/amicode_hide_projectparams +amicode_pane=bug-dock(namespaced pane instance — never fights the main window's persisted UI state). - Component: NEW
packages/app/src/pages/session/composer/session-bug-dock.tsx—SessionBugDock(wired: revealNonce re-expand effect, the sentinel watcher scoped to the hosted session's sync-store parts, src pinned per session viaon(sessionID)+ untrack so a theme flip never reloads mid-report) overBugDockView(presentational, exported for stories). Family idiom: spring-animated max-height between measured content and the 42px header, header row toggles on click/Enter/Space, bodyvisibility-hidden while collapsed (the iframe STAYS mounted — collapse keeps the session alive). Header: danger-token bug glyph + "Bug report" + faint collapsed status + chevron/closeIconButtonV2s (aria-labels + tooltips). Filed end-state: success-token check + "Issue filed — this session is archived." + Open-issueButtonV2(platform.openExternal → the bridge in-webview; suppressed for thefiled-via-browsertoken). Live theme flips forward into the frame via adata-color-schemeMutationObserver postMessage (the split-frame idiom). - Wiring: the session composer region renders
<SessionBugDock/>gated onbugReportEnabled(), OUTSIDE the question/permission block gates (a blocked composer must never hide a live bug dock); the draft view (new-session-view.tsx) mounts it above its composer too — the #116 button renders there and a dead click is a bug. Down-messages are handled at APP level:AmicodeThemeBridge's existing window-message listener forwardsopen-bug-report/close-bug-reportto the controller (self-gated on kind + the boot param), so an open can't be missed between pages. Boot param off ⇒ no render, no adoption — standalone opencode byte-identical. - Bridge shapes (the cross-repo contract with amicode#250): DOWN
{source:"amicode",kind:"open-bug-report",sessionID}· DOWN{source:"amicode",kind:"close-bug-report",sessionID}· UP{source:"amicode",kind:"bug-filed",sessionID,url}(url may be the literalfiled-via-browser) · UP{source:"amicode",kind:"bug-report-closed",sessionID}— the up-kinds join the chat_panel allowlist on the extension side. - Story: NEW
session-bug-dock.stories.tsx— States (expanded chat / collapsed / filed+link / filed-via-browser) and Schemes (light+dark panes, the resolveThemeVariantV2 inline-token pattern). Verified viastorybook build(both entries in the built index). - Tests: NEW
bug-dock-controller.test.ts(28) — one TDD cycle per AC: open/adopt + frame-src params (AC1); chevron collapse/re-expand + reveal(), zero posts, dock seam untouched (AC2); close postsbug-report-closedexactly once + the real default-post envelope via spy + extension close silent + wrong-id ignored (AC3); sentinel matching filed / filed-via-browser / mid-line no-match / confirm-gate veto / bare-sentinel + parts scan +file()latch (exactly once) + end-state close paths (AC4); boot-param gate inert + duplicate/different-id/malformed opens idempotent + fresh adopt after full close (AC5). Appbun run test:unit811/811 (783 base + 28 new); tsgo clean; oxlint 0 errors (only the base-patternas unknown asenvelope-assertion warning, same as #116's test); storybook build green.
- Controller: NEW