feat(brain): native canvas engine for the amico brain — retire the /brain.html iframe - #40
Merged
Merged
Conversation
…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
marked this pull request as ready for review
July 20, 2026 15:11
…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>
Contributor
Author
|
Robustness pass complete. An adversarial review swept the full diff (line-by-line parity against the deleted
One reviewed behavior change kept as a fix: under Suite: 22 engine tests + 15 server tests, typechecks clean across ui/app/opencode. 🤖 Generated with Claude Code |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
The "amico is thinking" animation (the session brain-strip) broke three independent ways in one week, all at the iframe boundary:
?auth_token=or a Basic header, so an armedOPENCODE_SERVER_PASSWORD401'd/brain.html+/brain.js→ permanently blank strip. Third bug in the cannot-carry-a-credential class.color-schememismatch 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.brain.jsran, 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.jsclaim to be generated from amicodepackages/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, sosetTheme()is a lossless repaint — no reload, no re-flush, atlas intact. Skeleton graph moved verbatim tobrain-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./brain.html+/brain.jsremoved fromPUBLIC_UI_PATHS, and the httpapi-ui regression flipped to assert those paths are authed again.Supersedes the reload-on-theme-flip workaround from #38 (@raghav — the sent-set/initialFlush reset is gone with the bridge).
Test plan
bun testpackages/ui — 283 pass (13 new)bun testpackages/opencode httpapi-ui — 15 pass (auth regression flipped)tsgotypecheck — ui, app, opencode cleanvite buildpackages/app — clean, no/brain.*in dist🤖 Generated with Claude Code