Skip to content

feat(brain): native canvas engine for the amico brain — retire the /brain.html iframe - #40

Merged
kateebonner merged 3 commits into
local/amicodefrom
kate/native-brain-strip
Jul 20, 2026
Merged

feat(brain): native canvas engine for the amico brain — retire the /brain.html iframe#40
kateebonner merged 3 commits into
local/amicodefrom
kate/native-brain-strip

Conversation

@kateebonner

Copy link
Copy Markdown
Contributor

Why

The "amico is thinking" animation (the session brain-strip) broke three independent ways in one week, all at the iframe boundary:

  1. Auth (4f02fc3): iframe document requests can't carry ?auth_token= or a Basic header, so an armed OPENCODE_SERVER_PASSWORD 401'd /brain.html + /brain.js → permanently blank strip. Third bug in the cannot-carry-a-credential class.
  2. Theme (b6eaa0d): a parent/child color-scheme mismatch makes the browser composite the transparent frame opaque white; the VS Code webview themes asynchronously, so the mount snapshot was routinely wrong. The workaround reloaded the iframe on every flip and re-flushed the whole event history.
  3. Unwanted first frame: before brain.js ran, the page painted its own stylesheet ground (body { background: var(--surface) }) plus the prototype chrome (wordmark, play/pause/tempo controls, colophon) that script only hides after load.

Bonus: brain.html/brain.js claim to be generated from amicode packages/extension/media/brain/ — do not hand-edit, but that source directory no longer exists upstream. The asset was an unregenerable orphan.

What

  • packages/ui/src/amicode/brain-engine.ts — the live-embed half of the prototype ported to a framework-free native module: same settle layout, musical clock, integrate-and-fire pulses, claims, atlas, ambient scintillation. Both theme palettes are baked in, so setTheme() is a lossless repaint — no reload, no re-flush, atlas intact. Skeleton graph moved verbatim to brain-data.ts.
  • brain-strip.tsx — renders an in-document <canvas> and drives the engine with direct calls. No iframe, no postMessage handshake, no ready/initialFlush dance. Transparent ground from frame zero kills the first-frame flash by construction.
  • Server surface shrinks: retired assets deleted, /brain.html + /brain.js removed from PUBLIC_UI_PATHS, and the httpapi-ui regression flipped to assert those paths are authed again.
  • 13 headless bun tests pin the engine: boot, scheme-from-frame-zero, transparent first frame, commit/consider/graft semantics, charting, lossless theme flip, pause/resume/destroy. The iframe needed a server + browser to exercise — which is why it kept breaking unseen.

Supersedes the reload-on-theme-flip workaround from #38 (@raghav — the sent-set/initialFlush reset is gone with the bridge).

Test plan

  • bun test packages/ui — 283 pass (13 new)
  • bun test packages/opencode httpapi-ui — 15 pass (auth regression flipped)
  • tsgo typecheck — ui, app, opencode clean
  • vite build packages/app — clean, no /brain.* in dist
  • Visual pass in the amicode dev host (collapsed close-up ↔ expanded whole-network, theme flip mid-session, hover glances)

🤖 Generated with Claude Code

…rain.html iframe

The "amico is thinking" animation broke three independent ways at the
iframe boundary in one week: document requests can't carry server auth
(armed password => 401 => blank strip, 4f02fc3), a parent/child
color-scheme mismatch composites the transparent frame opaque white
(async webview theming => white box + full reload on every flip,
b6eaa0d), and the page painted its own stylesheet ground + prototype
chrome as an unwanted first frame before script hid them. The generated
brain.html/brain.js also lost their upstream source (amicode
media/brain was deleted) — an unregenerable orphan.

Port the live-embed half of the prototype to a framework-free module,
packages/ui amicode/brain-engine.ts: same settle layout, musical clock,
integrate-and-fire pulses, claims, atlas, and ambient scintillation;
both theme palettes baked in so setTheme() is a lossless repaint (no
reload, no re-flush, atlas intact). brain-strip.tsx renders an
in-document <canvas> and drives the engine with direct calls — no
fetch, no postMessage handshake, transparent ground from frame zero.

Remove the retired assets and their PUBLIC_UI_PATHS exemptions (the
unauthenticated surface shrinks by two paths); flip the httpapi-ui
regression to assert /brain.html and /brain.js are authed again. Pin
the engine with 13 headless bun tests — boot, scheme-from-frame-zero,
transparent first frame, commit/consider/graft semantics, charting,
lossless theme flip, pause/resume/destroy — the iframe was untestable
without a server + browser, which is why it kept breaking unseen.

typecheck (tsgo): ui, app, opencode clean. bun test: ui 283 pass,
httpapi-ui 15 pass. vite build: clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@kateebonner
kateebonner marked this pull request as ready for review July 20, 2026 15:11
kateebonner and others added 2 commits July 20, 2026 11:22
…nder crashes

- tick() rejects non-finite timestamps (a NaN nowMs poisons clock.beat
  permanently — every projection NaNs from then on)
- resize() falls back on NaN/negative/zero boxes instead of zeroing the
  world scale (division by zero in the camera fit)
- a render error halts the loop with one console.error instead of
  spinning the rAF chain half-rendered forever; resume() re-arms
- prefers-reduced-motion is tracked live (the iframe only re-read it on
  reload); listener cleaned up in destroy()
- the touch-debounce map prunes stale entries past 512 keys (marathon
  sessions)
- 5 hostile-input tests: NaN tick, junk resize, sub-2-commit chart,
  50 rapid theme flips, injected canvas throw -> halt + recovery

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… cap, boot-size seed

From the adversarial review of the port (all probe-verified):

- pause() now cancels the scheduled frame and tick() re-checks halted at
  its tail: pause→resume within one frame no longer breeds parallel rAF
  chains, and a mid-frame pause (from a dueQueue callback) cannot
  reschedule. destroy() cancels too.
- graft population capped at 300: every unique file AND search pattern
  grafts a node+edge, the render loop is O(nodes+edges) per frame, and
  the iframe's theme-flip reloads no longer mask the growth. Evicts the
  oldest graft that is not charted, not pending a plate, and not where
  amico stands; in-flight pulses to an evictee arrive on a detached
  object harmlessly.
- brain-strip seeds the engine with the strip's real height (refs run
  before layout, so clientHeight is 0 at creation and the camera briefly
  took the wrong close-up/whole-network branch) and re-measures onMount.
- resize() interface: both args optional — omit to re-measure.
- 4 new tests: animated pipeline with a hand-driven clock (a commit
  claims only after its pulse arrives; a chart waits for the pump to
  drain then plates every commit; destroy with pulses in flight),
  graft-cap eviction under 340 unique patterns.

Review also confirmed: the reduced-motion path now charting plates is a
fix, not a regression — the old live-embed could never plate under
prefers-reduced-motion and stranded pending charts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@kateebonner

Copy link
Copy Markdown
Contributor Author

Robustness pass complete. An adversarial review swept the full diff (line-by-line parity against the deleted brain.js live-embed paths, probe-verified failure scenarios, leak hunt). Verdict: no Critical defects; parity checked clean (palettes verbatim, layout constants, camera branches, decay/easing, debounce — including one bug preserved bug-for-bug). All confirmed findings are fixed:

Finding Fix Commit
NaN tick() timestamp poisons the clock permanently (probe-verified kill) reject non-finite timestamps bfb13b5
a throw in the draw body kills the rAF chain unrecoverably try/catch → visible halt, resume() re-arms bfb13b5
pause()resume() in one frame breeds parallel rAF chains cancel scheduled frame + tail halted re-check ce43d52
unbounded session growth (grafts + debounce map) no longer masked by iframe reloads graft cap 300 w/ atlas-safe eviction; live.recent pruning both
engine boots at 800×224 fallback before layout → wrong camera branch flash height seed + onMount re-measure ce43d52
animated pipeline had zero test coverage hand-driven-clock tests: pulse-arrival claiming, chart-waits-for-drain, destroy mid-flight, graft eviction ce43d52

One reviewed behavior change kept as a fix: under prefers-reduced-motion, live mode now charts plates (the old embed could never plate under RM and silently stranded pending charts).

Suite: 22 engine tests + 15 server tests, typechecks clean across ui/app/opencode.

🤖 Generated with Claude Code

@kateebonner
kateebonner merged commit bbe47b6 into local/amicode Jul 20, 2026
0 of 4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant