From b6eaa0dbc73eb8c334bc3aa7dc72c4a07f7c84fa Mon Sep 17 00:00:00 2001 From: Raghav Chari Date: Mon, 20 Jul 2026 14:16:30 +0200 Subject: [PATCH] fix(brain-strip): reactive colorScheme so the graph doesn't paint blank white in the VS Code webview MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The session brain-strip renders its graph in a nested /brain.html iframe. Its src baked colorScheme=${scheme} from a ONE-TIME snapshot read at mount. Inside the amicode VS Code webview the theme arrives asynchronously (chat_panel seeds it via ?colorScheme= -> app preload -> data-color-scheme on documentElement), so at brain-strip mount that attribute is often not set yet. The snapshot then falls to its default, parent and child color-schemes mismatch, and — per the code's own comment — the browser paints the transparent iframe OPAQUE WHITE. That is the blank box users saw where the graph should be. Fix: - Track the scheme reactively (createSignal + the existing MutationObserver now setScheme) so the iframe src re-syncs to the real theme instead of a stale mount-time snapshot. - On every `ready` handshake from the brain (the initial one AND the one after a scheme-driven iframe reload), clear the sent-set and restore initialFlush so the reloaded brain re-receives the full event history — otherwise a reload would come back empty (every id already marked sent). typecheck (tsgo -b): clean. Rebuilt the darwin-arm64 binary (smoke test passed) and verified in the amicode dev host. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../app/src/pages/session/brain-strip.tsx | 31 ++++++++++++++----- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/packages/app/src/pages/session/brain-strip.tsx b/packages/app/src/pages/session/brain-strip.tsx index e293a1e252..c3003cf102 100644 --- a/packages/app/src/pages/session/brain-strip.tsx +++ b/packages/app/src/pages/session/brain-strip.tsx @@ -86,17 +86,26 @@ function BrainFrame(props: { sessionID: string }) { ) const expanded = () => !questionOpen() && (manual() ?? (busy() || linger())) - const scheme = document.documentElement.dataset.colorScheme === "light" ? "light" : "dark" let frame: HTMLIFrameElement | undefined + const currentScheme = () => (document.documentElement.dataset.colorScheme === "light" ? "light" : "dark") // live theme flips must reach the iframe: a color-scheme mismatch between // parent and child makes the browser paint the transparent frame opaque - // white — so forward every change (and once on ready for the initial state) - const currentScheme = () => (document.documentElement.dataset.colorScheme === "light" ? "light" : "dark") - const themeObserver = new MutationObserver(() => post({ kind: "theme", colorScheme: currentScheme() })) + // white. Inside the VS Code webview the boot theme arrives asynchronously + // (chat_panel seeds it via ?colorScheme= → app preload → data-color-scheme), + // so a one-time snapshot read at mount can be wrong and bake a white frame. + // Track the scheme REACTIVELY so the iframe src re-syncs, AND re-post the + // current scheme every time the brain reports ready (the initial handshake) + // plus on every subsequent flip. + const [scheme, setScheme] = createSignal(currentScheme()) + const themeObserver = new MutationObserver(() => { + const s = currentScheme() + setScheme(s) + post({ kind: "theme", colorScheme: s }) + }) themeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ["data-color-scheme"] }) onCleanup(() => themeObserver.disconnect()) createEffect(() => { - if (ready()) post({ kind: "theme", colorScheme: currentScheme() }) + if (ready()) post({ kind: "theme", colorScheme: scheme() }) }) const [ready, setReady] = createSignal(false) const sent = new Set() @@ -104,7 +113,15 @@ function BrainFrame(props: { sessionID: string }) { const onBrainMessage = (e: MessageEvent) => { if (e.origin !== location.origin) return const d = e.data as { source?: string; kind?: string } | undefined - if (d?.source === "amico-brain" && d.kind === "ready") setReady(true) + if (d?.source === "amico-brain" && d.kind === "ready") { + // A colorScheme flip re-mounts the iframe (reactive src), which re-sends + // `ready`. Clear the sent-set and restore initialFlush so the reloaded + // brain gets the full event history re-flushed — otherwise it would come + // back empty (every id already in `sent`). + sent.clear() + initialFlush = true + setReady(true) + } } window.addEventListener("message", onBrainMessage) onCleanup(() => window.removeEventListener("message", onBrainMessage)) @@ -158,7 +175,7 @@ function BrainFrame(props: { sessionID: string }) { >