Skip to content

Redesign interface — Claude-design shell + beast-mode engine foundations#46

Open
slavazeph-coder wants to merge 33 commits into
mainfrom
claude/redesign-interface-Kuacb
Open

Redesign interface — Claude-design shell + beast-mode engine foundations#46
slavazeph-coder wants to merge 33 commits into
mainfrom
claude/redesign-interface-Kuacb

Conversation

@slavazeph-coder

@slavazeph-coder slavazeph-coder commented May 17, 2026

Copy link
Copy Markdown
Owner

Why

BrainSNN had 97 panel components stacked into a 1024-line App.jsx with no
navigation. Four discoverability band-aids (CommandPalette, LayerExplorer,
RoleTour, HotkeyMap) existed because there was no shell. The product had
outgrown its vertical-scroll-of-everything UI.

Dual mandate: interface stays lean, engine becomes a beast. This PR
executes the full multi-track redesign plan in 17 commits.

Lean track — the new front door

Shell tokens + Claude.ai-style theme (113c69e, 0a219e3, acd17e7)

  • New src/styles/tokens.css — canonical :root sheet, imported first.
  • Single rust accent (#c96442), warm-dark surfaces, hairline borders
    (--shadow: none). Source Serif 4 added for headlines (was referenced
    but never loaded); Inter retained for body.
  • Light-mode + high-contrast token branches activated — utils/theme.js
    was wired for them but the CSS overrides were dormant.
  • Inline-hex sweep across 95 component files + the 3D scene
    (BrainScene + brain/*.jsx via new utils/threeTokens.js with a
    theme-reactive useThreeTokens() hook).
  • Severity-tier color sweep across 53 files in src/utils/ and
    src/components/: five new semantic tokens (--severity-mid / -ok / -info / -purple / -pink) replace #fdab43, #5ee69a, #77dbe4, etc.
  • Migration aliases (--primary / --gold) removed at the end of the
    sweep; CI grep guard would catch any straggler.

AppShell + 7 workspaces (0153355, 6467ec9, 4f4b8cf)

  • src/shell/AppShell.jsx — Topbar · WorkspaceTabs (left rail) ·
    persistent BrainViewport · Composer · workspace body · Sidebar
    (InspectorPanel + lazy role-tour + milestone widgets).
  • src/shell/NewApp.jsx mirrors all state/effects from the old App.jsx
    and exposes a single session prop to every workspace; ~16 onApply*
    handlers memoized via useCallback.
  • 7 workspaces — Home · Analyze · Defend · Brain · Knowledge ·
    Training · Connect
    — each lazy-imports its drawer panels and
    wraps them in ErrorBoundary. Connect has 4 sub-tabs (Share /
    External / System / Discover).
  • src/shell/bus.js — 50-line typed event bus replaces scattered
    window.dispatchEvent patterns. Drives shell:goto, shell:flash,
    shell:compose.
  • src/utils/flashLayer.js — extracts the cyan-ring scroll from
    CommandPalette + hotkeys.js, extends it to dispatch a workspace
    switch before scrolling so cross-workspace jumps work.
  • BrainScene mounts once inside AppShell and never unmounts on tab
    navigation. The R3F Canvas would otherwise pay ~400-800 ms remount
    cost per workspace switch.
  • ?shell=new flag added then flipped to default; legacy App.jsx
    retired entirely. ?shell=old still recognized for one release —
    logs a console.info and continues into the AppShell.
  • Retired LayerExplorerPanel.jsx + HotkeyMap.jsx (the overlay; the
    hotkeys.js chord dispatch stays live).

Composer wiring + accessibility (06f377c, 15a38a0, cfefe16)

  • Composer (persistent top input) routes each mode to its workspace
    and panel:
    • scan → Defend → CognitiveFirewallPanel auto-scans
    • autopsy → Defend → AutopsyPanel.setRaw
    • rag → Knowledge → NeuroRagPanel.setQuestion
    • diff → Connect → DiffPanel.setTextA
  • WorkspaceTabs upgraded to a proper role="tablist" with arrow-key
    nav, roving tabindex, and aria-selected.
  • AppShell adds a skip-to-content link, makes the workspace body
    role="tabpanel", and moves focus to the workspace heading on
    every tab change (after first mount).
  • ToastContainer split between role="status" (polite info / success)
    and role="alert" (assertive warnings / errors). Container labeled
    aria-label="Notifications".
  • OnboardingWalkthrough rewired to dispatch shell:goto for each of
    its 5 DOM anchor classes before highlighting, so the walkthrough
    switches workspaces between steps.

Beast track — engine upgrades

Worker pool + firewall worker (91183fd, 6aa03ba)

  • src/utils/workerPool.js — Comlink-lite postMessage proxy. Round-
    robins N module workers (min(cores − 1, 4)); sync fallback for
    SSR / test env.
  • src/workers/firewall.worker.js — hosts scoreContent /
    scoreContentWithRules / setRules off the main thread.
  • scoreContentAsync is the opt-in async API: threshold-gated (≥ 500
    chars routes to worker, short text stays sync). Active rules are
    serialized + sent with each scoreWithRules call so custom /
    evolved / pack rules propagate to the worker side correctly. The
    worker temporarily swaps active rules around the call so the full
    pipeline (language detection, template detection, language pack
    routing) runs identically to the main thread.

Transformers.js MiniLM in a worker + IDB cache (0494571)

  • src/workers/embeddings.worker.js — loads Xenova/all-MiniLM-L6-v2
    in a dedicated worker. Public API (embed, embedBatch,
    getStatus, warmup) over the standard pool envelope.
  • src/utils/embeddingsStore.js — IDB-backed cache via
    openStore('embeddings'). Stores { vec: Float32Array, t };
    refreshes t on read for LRU accuracy. Soft cap 5000 / prune-to
    4500, amortized prune every ~100 writes.
  • src/utils/embeddings.js rewritten as a thin facade. Existing API
    (initEmbeddings, embed, embedBatch, isReady,
    subscribeStatus, getEmbeddingStatus, clearEmbeddingCache)
    preserved. Idle warm-prefetch (requestIdleCallback) on return
    visits.
  • One-shot migrator copies the legacy brainsnn_embeddings_v1
    localStorage blob into IDB on first read, with rollback safety.

Unified IDB persistence (91183fd + 6aa03ba)

  • src/utils/store.jsopenStore(name) IDB wrapper with lazy
    schema upgrades, get/set/delete/keys/values/clear/storageEstimate.
  • localStorage fallback (brainsnn_store/<collection>/<id>) when IDB
    unavailable.
  • attachVersionChange(db) releases handles on schema upgrades so
    cross-tab upgrades don't deadlock; onblocked rejects cleanly.

Per-workspace bundle splits (548e4dd, 4f4b8cf)

  • vite.config.js manual chunks: per-workspace ws-<name> chunks,
    vendor splits (react / three / postprocessing), shared utils.
  • Both shells lazy-loaded from main.jsx via React.lazy.
  • After legacy App.jsx retirement: dozens of properly tree-shakeable
    per-panel chunks (1-22 kB each, lazy on tab/details open).

Offline-first PWA + Background Sync + Web Locks (f4eb196, 39d03fb, e2cd2ba, 659536e, 5b68e9e, 53a111f)

  • public/sw.js bumped to brainsnn-v2: precache shell on install,
    hashed /assets/* stale-while-revalidate, cached API GET responses
    so share cards render offline.
  • Non-GET requests to /api/* and /[rqidaxntvwb]/* auto-enqueued
    via Background Sync API when offline; response is 202 + { queued: true }. Replay handler drains the IDB queue on sync event.
  • src/utils/offlineQueue.jsfetchOrQueue(), replayNow(),
    onOnlineChange(). Wired into SyncPanel + SessionRoomsPanel with
    visible "● offline — submissions will queue" indicators.
  • src/utils/multiTab.jsBroadcastChannel('brainsnn-state')
    wrapper with stable per-tab origin id. Theme changes, snapshot
    mutations, archive mutations, dictionary mutations, feedback
    mutations all broadcast. SnapshotPanel + theme.js subscribe.
  • src/utils/atomicWrites.js — Web Locks API wrapper.
    withLock(name, fn) serializes cross-tab writes. Wired into
    snapshots.js, scanArchive.js, personalDictionary.js (incl.
    the racy bumpHit), feedback.js. Per-tab Promise-chain fallback
    where Web Locks unavailable.
  • src/utils/fileSystemSave.js — File System Access API wrapper for
    the Portability panel: pick a file once, "Save" overwrites it.
    Falls back to a regular download anchor on Safari / Firefox.

Runtime capability probe (2b71327)

  • src/utils/capabilities.js + CapabilitiesPanel.jsx (Layer 103).
    Surfaces which Beast features the current browser actually
    delivers (cores, Worker, IDB, BroadcastChannel, Web Locks,
    Background Sync, OffscreenCanvas, File System Access, WebGPU).
  • WebGPU renderer swap intentionally NOT activated — would silently
    disable Bloom + Outline (postprocessing 6.x is WebGL2-only). Lays
    the foundation for a focused follow-up PR.

Build impact

Before (main):
  index-*.js  ~700 kB  (everything: shell + 97 panels + react + utils)
  three split, postprocessing split

After this PR (legacy retired, full splits):
  index-*.js                148 kB   shell + critical glue
  ws-defend-*               82 kB    firewall family eager
  ws-home-*                 19 kB
  ws-analyze-*              16 kB
  ws-knowledge-*             3 kB
  ws-training-*              3 kB
  ws-brain-*                 4 kB
  ws-connect-*               6 kB
  ~50 per-panel chunks at 1-22 kB each, lazy on demand
  legacy-app-*               REMOVED
  embeddings.worker-*        1.2 kB
  firewall.worker-*          9.6 kB
  react-*                  242 kB    vendor
  three-*                  827 kB    vendor, eager (viewport)
  postprocessing-*          78 kB

Initial JS for default load dropped ~40 % gzip vs main.

What's deliberately deferred

  • WebGPU renderer swap — would silently disable
    @react-three/postprocessing (Bloom + Outline). Capability probe
    ships; activation needs separate visual QA + product judgement.
  • OffscreenCanvas brain worker — R3F doesn't have first-class
    OffscreenCanvas support; pointer-event raycasting would require
    postMessage round-trips on every click. Wait for upstream support.
  • Periodic Background Sync for daily-challenge corpus — the
    corpus is static in code today; no benefit until it's
    server-backed.

Verification

  • npm run build clean, 0 errors, 0 new warnings.
  • All 13 existing URL deep-links land on the correct workspace:
    ?scan=, ?scan-url=, ?r=, ?i=, ?q=, ?d=, ?a=, ?x=,
    ?t=, ?n=, ?v=, ?room=, #state=.
  • All 9 keyboard shortcuts from shortcuts.js preserved (Space, b,
    r, 1/2/3, s, e, q, ?). New: g + h/a/d/b/k/t/c for workspace
    switching; ArrowKeys on the tab rail.
  • OnboardingWalkthrough still highlights its 5 DOM anchors and now
    navigates between workspaces between steps.
  • Theme toggle (ThemePanel / Layer 98) reskins every panel including
    the 3D brain materials (via useThreeTokens) and broadcasts to
    open tabs.
  • Two tabs open: theme toggle, snapshot save, archive star, dict
    add, feedback record all sync across tabs via BroadcastChannel /
    Web Locks.
  • Offline DevTools toggle: Sync upload + Rooms submit show queued
    status; reconnect drains the queue.

Bot review fixes from earlier (6aa03ba)

  • Active rules propagation to firewall worker (scoreContentAsync
    was using default rules)
  • IDB connection onversionchange handler (cross-tab upgrade
    deadlock fix)
  • Firewall pool size lifted from 1 to default (multi-core)

Ready for review.


Generated by Claude Code

claude added 2 commits May 17, 2026 01:34
Adds src/styles/tokens.css as the canonical :root token sheet, imported
first from main.jsx. Replaces the dual teal+gold accent scheme with a
single rust accent (#c96442), kills drop shadows in favor of hairline
borders, swaps font-display to Source Serif 4 (now loaded), and lights
up the previously dormant light-mode + high-contrast theme branches
that utils/theme.js was already wired for.

- src/styles/tokens.css         new — :root + data-theme="light" + high-contrast
- src/main.jsx                  imports tokens.css before global.css
- src/styles/global.css         :root block removed (moved to tokens.css);
                                .backdrop loses dual radial; convo-bar.peak
                                gradient flattened to var(--danger)
- index.html                    Source Serif 4 added; theme-color → #1a1815

Legacy --primary / --gold aliases retained as var(--accent) / var(--warn)
so the 150+ inline hex / token references across panel JSX keep working
during the follow-up sweep PR. CI guard against raw rgba() and remaining
aliases lands with that sweep.
…IDB store

Lays the under-the-hood plumbing for the 100-layer engine to run heavy
work off the main thread and store at IndexedDB scale instead of the
5MB localStorage ceiling. Additive only — no existing caller changes
shape; every sync API still works.

Worker pool (src/utils/workerPool.js)
  Comlink-lite postMessage proxy. createPool(factory, {size, fallback})
  spawns N module workers, round-robins requests, resolves a Promise per
  request via reply ID. Graceful degrade: when Worker is unavailable
  (SSR, ancient browser, test) calls invoke the optional sync fallback.

Firewall worker (src/workers/firewall.worker.js)
  Hosts scoreContent / scoreContentWithRules / setRules off the main
  thread. Vite emits a separate chunk via new Worker(new URL(...)).
  Build verified: dist/assets/firewall.worker-*.js, 9.67kB.

scoreContentAsync (cognitiveFirewall.js)
  New export. Routes long text (≥500 chars) through the pool; short text
  uses the sync path because spin-up overhead exceeds the work. Guarded
  against recursive spawn inside the worker itself (typeof window).
  Panels opt in by switching scoreContent → scoreContentAsync; sync API
  is unchanged.

Persistence layer (src/utils/store.js)
  openStore(name) returns an IDB-backed get/set/delete/keys/values/clear
  API per collection. Lazy schema upgrades on first use of a new
  collection. localStorage fallback (namespaced brainsnn_store/<col>/<id>)
  when IDB unavailable. storageEstimate() surfaces quota for the Privacy
  Budget panel. Migration of the 30+ scattered brainsnn_*_v1 keys lands
  in a follow-up PR.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a design token system, a unified persistence layer using IndexedDB with localStorage fallback, and a Web Worker pool to offload heavy content scoring tasks. Feedback highlights a bug where asynchronous scoring ignores custom active rules and notes that the IndexedDB implementation lacks necessary versionchange handlers to prevent blocking upgrades across tabs. Additionally, it was suggested to increase the worker pool size for the firewall to fully leverage multi-core performance.

Comment on lines +264 to +266
try {
return await pool.call('score', { text });
} catch {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The scoreContentAsync function currently ignores the _activeRules state (e.g., evolved rules from Layer 31). Since workers maintain their own module-level state, they will default to DEFAULT_RULES unless explicitly updated. This results in a discrepancy between synchronous and asynchronous scoring when custom rules are active.

To fix this, you should use the scoreWithRules worker handler and pass the serialized active rules when they differ from the defaults.

  try {
    const rules = getActiveRules();
    if (rules === DEFAULT_RULES) {
      return await pool.call('score', { text });
    }
    return await pool.call('scoreWithRules', {
      text,
      rules: serializeRules(rules)
    });
  } catch {

Comment thread brainsnn-r3f-app/src/utils/store.js Outdated
probe.onsuccess = () => {
const db = probe.result;
const haveAll = neededCollections.every((c) => db.objectStoreNames.contains(c));
if (haveAll) { resolve(db); return; }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The IndexedDB connection should handle the versionchange event. Without this, if another tab or a dynamic store request triggers a database upgrade, this connection will block the upgrade indefinitely. Adding this listener also resolves potential deadlocks when getDb triggers an upgrade while a connection is still active.

      if (haveAll) {
        db.onversionchange = () => {
          db.close();
          dbPromise = null;
        };
        resolve(db);
        return;
      }

Comment thread brainsnn-r3f-app/src/utils/store.js Outdated
}
}
};
upgrade.onsuccess = () => resolve(upgrade.result);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Ensure the upgraded database connection also handles the versionchange event to prevent blocking future upgrades from other tabs or dynamic store additions.

      upgrade.onsuccess = () => {
        const db = upgrade.result;
        db.onversionchange = () => {
          db.close();
          dbPromise = null;
        };
        resolve(db);
      };

try {
_firewallPool = createPool(
() => new Worker(new URL('../workers/firewall.worker.js', import.meta.url), { type: 'module' }),
{ size: 1, fallback: (type, payload) => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The pool size is hardcoded to 1, which negates the performance benefits of the worker pool described in the PR (round-robinning across multiple cores). For heavy regex sweeps on long text, utilizing multiple workers would improve throughput when multiple scans are requested in quick succession.

Suggested change
{ size: 1, fallback: (type, payload) => {
{ fallback: (type, payload) => {

claude added 18 commits May 17, 2026 01:40
…lers

Addresses three issues from review on PR #46.

1. scoreContentAsync ignored non-default rules
   The worker keeps its own module-level _activeRules, so custom rules,
   evolved rulesets (Layer 31), and rule packs (Layer 83) set on the main
   thread never reached the worker — async results silently used
   DEFAULT_RULES while sync results used the promoted ruleset.

   Fix: when active rules differ from DEFAULT_RULES, scoreContentAsync
   serializes them and routes through the scoreWithRules handler. The
   worker temporarily swaps its active rules around the call so the full
   pipeline (language detection, language-pack routing, template
   detection, language decoration) runs through scoreContent and matches
   the main-thread output shape byte-for-byte. Workers process one
   message at a time so the swap-and-restore is race-free.

2. IDB connections lacked versionchange handlers
   Without onversionchange, a stale handle in tab A blocks any schema
   upgrade triggered by tab B (new collection registered, etc.), leaving
   writes hung indefinitely.

   Fix: attachVersionChange() wires db.onversionchange to close the
   handle and reset dbPromise so the next getDb() call rebuilds. Applied
   on both code paths (probe with all collections present + upgrade
   path). Also added an onblocked handler so the upgrade rejects cleanly
   instead of hanging when another tab holds an old handle.

3. Worker pool capped at size 1
   Hardcoded {size: 1} negated the multi-core point of the pool. Removed
   the override so the firewall pool uses the default (min(4, cores-1)),
   giving Red Team batch scans and inbox triage real parallelism.

Build verified: dist/assets/firewall.worker-*.js, 9.63kB, no new warnings.
…tokens

Shell PR #2 from the plan: migrate the 100+ raw brand-color references
scattered across JSX inline styles + global.css + the Three.js scene to
the new Claude-design tokens (--accent, --danger, --warn, --ok). Removes
the temporary --primary / --gold migration aliases.

Coverage:

  * src/components/*.jsx — 95 files swept (excludes BrainScene):
    - '#4fa8b3' / "#4fa8b3"  -> 'var(--accent)' / "var(--accent)"  (9 refs)
    - '#dd6974' / "#dd6974"  -> 'var(--danger)' / "var(--danger)"  (69 refs)
    - '#e8b934' / "#e8b934"  -> 'var(--warn)'   / "var(--warn)"    (1 ref)
    - '#6daa45' / "#6daa45"  -> 'var(--ok)'     / "var(--ok)"      (13 refs)
    Plus the 6 'borderLeft: ... #dd6974' template literals and the two
    ActivityCharts gradient strings.

  * src/components/{BrainScene,brain/NeuralFlowGrid,brain/PulseWave}.jsx
    Three.js parses CSS color strings but not CSS vars. Added
    src/utils/threeTokens.js with a tokens cache + theme-reactive
    useThreeTokens() hook that re-reads on data-theme / data-high-contrast
    mutations. BrainScene's background, fog, point light, signal
    particles, outline edge color, and floor disc now reskin live when
    the user toggles theme.

  * src/styles/global.css
    All rgba(79,168,179,...) / rgba(221,105,116,...) / rgba(232,185,52,...)
    / rgba(109,170,69,...) chrome escapes migrated to
    color-mix(in srgb, var(--*) X%, transparent). The .bar
    linear-gradient at L325 now uses var(--accent) / var(--accent-bright).

  * src/styles/tokens.css
    --primary / --gold aliases deleted. Anything that still references
    them would fail CSS resolution; the CI grep guard below catches it.

CI guards (all return 0):

  * grep -rE 'var\(--primary\)|var\(--gold\)' src/
  * grep -cE 'rgba\(\s*(79\s*,\s*168\s*,\s*179|...)' src/styles/global.css
  * grep -cE '#4fa8b3|#dd6974|#e8b934|#6daa45' src/styles/global.css
  * grep -rE same-set src/components/*.jsx | grep -v BrainScene

Deliberate scope cuts (follow-up PR):

  * src/utils/*.js severity-tier color tables (autopsyCard, immunityCard,
    quizCard, reactionCard, heatmap, diagnostic, hypothesis, oscillations,
    coverage, calendarHeatmap, diffMode, textAdventure, styleFingerprint,
    compliment, echoDetector, dailyCard) — these encode semantic state
    (high/med/low pressure) and need a JS-side cssToken() helper to read
    --danger / --warn / --ok at runtime since they're consumed by canvas
    / SVG renderers that don't auto-resolve CSS vars. Plan calls this out
    as step 3 of PR #2 and the natural follow-up.

  * src/data/network.js + src/data/knowledgeGraph.js region identity
    colors — semantic identity per the plan, intentionally left alone.

Build verified: CSS bundle 62.20kB -> 63.76kB (color-mix() is longer than
rgba()), JS bundle +1.7kB for threeTokens helper. No new warnings.
…new)

Boils Shell PRs #3 + #4 + #5 from the plan into one push. Lays down the
entire new front door without touching the legacy App.jsx — both shells
coexist via main.jsx and only one mounts at a time so MCP bridge / dream
monitor don't double-register.

What ships:

  * src/shell/AppShell.jsx + Topbar + WorkspaceTabs + Sidebar + Composer
    + BrainViewport. Layout is left-rail tabs (Home / Analyze / Defend /
    Brain / Knowledge / Training / Connect) · sticky persistent brain
    viewport · workspace body · right rail with InspectorPanel + lazy
    role-tour + milestone widgets.

  * src/shell/NewApp.jsx — parallel App with the same state, refs,
    mount effects, and ~16 onApply* handlers. Builds a single `session`
    object that all workspaces consume. Mirrors App.jsx exactly so
    behavior is identical when ?shell=new is set.

  * src/shell/workspaces/*.jsx — 7 workspace shells (Home, Analyze,
    Defend, Brain, Knowledge, Training, Connect). Each lazy-imports its
    drawer panels, error-boundaries every panel, and groups via
    <details class="shell-drawer"> sections so the page-load picks up
    only the anchor panels (~5-10) instead of all 97.

  * src/shell/bus.js — tiny global event bus (~50 LOC, zero deps).
    Replaces scattered window.dispatchEvent patterns. Drives shell:goto
    for workspace navigation from CommandPalette, hotkeys, onboarding.

  * src/shell/BrainViewport.jsx — wraps BrainScene with promoted /
    strip classes. CSS controls per-workspace size (compact strip
    everywhere, ~68vh on the Brain workspace). Mounted ONCE in
    AppShell so tab navigation never remounts the R3F Canvas.

  * src/utils/flashLayer.js — extracts the cyan-ring scroll helper
    shared by CommandPalette and hotkeys.js. Now ALSO dispatches
    shell:goto so jumping to a layer in another workspace switches
    tabs before scrolling. workspaceForLayer() maps every catalog
    layer id to its target workspace.

  * src/styles/shell.css — 350 lines, shell-only. Sticky topbar, left
    tab rail, sticky persistent viewport strip, sticky composer, drawer
    sections, sub-tabs for the Connect kitchen sink. Responsive
    breakpoints at 1280px (tighten), 1050px (collapse sidebar + tab
    labels), 760px (horizontal tab strip).

  * src/main.jsx — flag gate. ?shell=new mounts NewApp; ?shell=old or
    no flag mounts legacy App. localStorage.brainsnn_shell_pref pins
    preference across visits.

  * src/components/OnboardingWalkthrough.jsx — patched to emit
    shell:goto before highlighting each anchor. Legacy app is unaffected
    (bus has no shell:goto subscriber when AppShell isn't mounted).
    Logs a console.warn if a target class is missing post-rAF so silent
    breakage gets surfaced in dev.

Plan alignment:

  - PR #3 (Context providers): superseded by the `session` prop pattern
    which is simpler than 3 contexts and serves the same purpose.
    Workspaces never see App.jsx state directly — only the session
    bundle NewApp constructs. If selector-based subscriptions become
    necessary, splitting `session` into focused contexts later is a
    drop-in refactor.

  - PR #4 (Shell + 6 workspaces, gated): done, including the seventh
    Home workspace (landing).

  - PR #5 (Persistent BrainViewport): done. BrainScene mounts once
    inside AppShell and stays mounted across all tab switches.

How to test:

  npm run dev
  /?shell=new        -> new shell
  /                  -> legacy (unchanged)
  /?shell=new&w=defend  -> direct workspace link
  g + (h/a/d/b/k/t/c) -> keyboard workspace switch

  localStorage.removeItem('brainsnn_onboarded'); reload /?shell=new ->
  walkthrough should switch tabs at steps 2-5 as it highlights anchors.

Build verified: 706kB JS (+45kB for shell), 70kB CSS (+6kB for shell.css),
no errors. Vite warnings about dynamic-imports-also-statically-imported
are expected: legacy App.jsx still statically imports the same panels,
so chunks won't split until App.jsx is retired in a follow-up PR.

Out of scope for this PR (next):

  - Lazy-loading legacy App.jsx (current dual-import prevents proper
    code splitting; cleanup happens when default flips to new shell)
  - utils/*.js severity color tables -> cssToken() helper (PR #2
    follow-up; canvas/SVG renderers don't auto-resolve CSS vars)
  - Beast PR #8: transformers.js MiniLM into a worker + IDB cache
…ty panels

Shell PR #6 from the plan. The new Claude-design shell is now the default
landing experience; legacy shell kept reachable via ?shell=old as a
one-release escape hatch.

Removed (duplicate discoverability):

  * src/components/LayerExplorerPanel.jsx — exact functional duplicate
    of the ⌘K CommandPalette. Both iterated LAYER_CATALOG with fuzzy
    matching and a cyan-ring scroll. Sidebar's milestone widget plus
    the palette cover the remaining navigation need.

  * src/components/HotkeyMap.jsx — the cheat-sheet overlay. The actual
    2-letter chord dispatch lives in src/utils/hotkeys.js and stays
    fully alive; only the modal renderer dies. New shell exposes
    chord hints inline on every tab (e.g. 'gd' on the Defend tab).

Per user decision, RoleTourPanel and MilestonePanel are KEPT as
Connect-workspace entries plus collapsible sidebar widgets.

main.jsx flag flipped — default is NewApp; ?shell=old opts back into
legacy App. localStorage.brainsnn_shell_pref still honored.

App.jsx imports + references purged so the old shell builds clean
without the deleted panels.

Build verified, 0 references remain to either deleted file.
…→ IDB

Beast PR #8 from the plan. Moves the ~25MB Xenova/all-MiniLM-L6-v2
model load and inference off the main thread, and replaces the
500-entry localStorage cache with an IDB-backed LRU store that scales
to 5000+ vectors without quota errors.

New files:

  * src/workers/embeddings.worker.js — loads transformers.js from CDN
    inside the worker, exposes warmup / getStatus / embed / embedBatch
    over the existing workerPool envelope. Pool size 1 (single model
    instance is enough; embeds are sequential per worker anyway).

  * src/utils/embeddingsStore.js — IDB cache via openStore('embeddings').
    Stores { vec: Float32Array, t: timestamp }; refreshes `t` on read
    so LRU eviction stays accurate. Soft cap 5000 / prune-to 4500,
    amortized prune every ~100 writes. One-shot legacy migrator copies
    brainsnn_embeddings_v1 (cap-500 localStorage blob) into IDB on
    first read, sets brainsnn_embeddings_migrated_v1, leaves the old
    key in place for one release as rollback safety.

Rewritten:

  * src/utils/embeddings.js — thin facade over worker + IDB cache.
    Public API unchanged (initEmbeddings, embed, embedBatch, isReady,
    subscribeStatus, getEmbeddingStatus, clearEmbeddingCache,
    cosineSimilarity). New: cacheSize() helper for Privacy Budget.
    Status polling (600ms) only runs while loading; stops once ready
    or errored. Inline fallback for Worker-less environments (SSR,
    test env). Idle warm-prefetch on returning visits: marks the user
    "seen" once the model reaches ready state; next visit kicks off
    initEmbeddings() under requestIdleCallback so the first scan that
    needs embeddings doesn't pay the 2-3s cold-load tax.

Behavior parity: every caller (RAG, semantic templates, code search,
similarity search) continues to work without changes. The worker is
spawned lazily on first init/embed, so apps that never touch
embeddings still pay zero cost.

Build verified: embeddings.worker-*.js emits as a 1.24kB chunk; main
JS bundle unchanged (heavy CDN load happens at runtime inside worker).
Beast PR #10 from the plan. Decomposes the previously monolithic
~700kB index chunk into ~13 chunks loaded on-demand:

  Before (single chunk):
    index-*.js  701 kB  (everything: shell + 97 panels + react + utils)

  After:
    index-*.js          2.2 kB  (main.jsx + lazy loaders)
    NewApp-*.js        18.7 kB  (shell + workspace router)
    ws-home-*.js        1.0 kB  ┐
    ws-analyze-*.js     1.9 kB  │ each workspace's panels
    ws-defend-*.js      4.1 kB  │ lazy-load on tab activation
    ws-brain-*.js       3.7 kB  │ (and on hover via the inherent
    ws-knowledge-*.js   3.2 kB  │  React.lazy prefetch behaviour)
    ws-training-*.js    3.1 kB  │
    ws-connect-*.js     5.6 kB  ┘
    legacy-app-*.js   668.5 kB  (only fetched when ?shell=old)
    react-*.js        241.8 kB  (separate vendor chunk)
    three-*.js        827.3 kB  (separate; eager because viewport
                                 is always mounted)
    shared-utils-*.js   3.7 kB  (IDB store + workerPool, shared
                                 with workers)
    postprocessing-*  78.0 kB

Initial download for the default (new) shell drops ~40 % gzip; the
single biggest contributor was the static-import chain off App.jsx
which pulled in all 97 panels for every user — now isolated to
the legacy-app chunk and ignored unless ?shell=old is set.

Changes:

  * vite.config.js — manualChunks function:
    - vendor: three / postprocessing / react
    - per-workspace: each shell/workspaces/*Workspace.jsx into its
      own ws-{name} chunk; React.lazy imports inside the workspace
      naturally roll up into the same chunk via Rollup's analysis
    - legacy-app: src/App.jsx forced into its own chunk so its
      static panel imports don't pollute the shared graph
    - shared-utils: utils/store.js + utils/workerPool.js (consumed
      by both threads via worker imports)

  * src/main.jsx — both App and NewApp now load via React.lazy +
    Suspense, so the unused shell's chunk is never fetched. Cheap
    Suspense fallback ("Loading the brain…") shown for the few
    hundred ms while the chosen shell + react chunk land.

Build verified: 13 chunks emit cleanly; the only warning is the
pre-existing lobsterTrap dynamic-vs-static import note inherited
from cognitiveFirewall.js (deferred to a later refactor — it's a
soft warning that doesn't affect runtime correctness).
…apshots

Beast PR #11 from the plan. Brings the four parallel PWA upgrades into
the codebase as additive helpers + one concrete consumer (snapshots).

Service worker (public/sw.js) bumped to brainsnn-v2:

  * Precache shell + manifest on install
  * Hashed /assets/* now stale-while-revalidate (was cache-first only);
    updated chunks land on next nav without hard refresh
  * Cached API GET responses so /a/<hash>, /d/<hash>, etc. share cards
    still render offline if they were viewed online once
  * Non-GET requests to /api/* and /[rqidaxntvwb]/* are AUTOMATICALLY
    enqueued when offline:
      - request body + headers persisted to a small IDB store
      - sync event listener replays the queue when Background Sync fires
      - response to caller is 202 + { queued: true } so the UI can say
        "queued — will sync when online"
  * 'replayNow' postMessage handler lets the main thread force a flush
    without waiting for the browser's Background Sync trigger

Main-thread helpers (additive — every caller can keep using fetch()):

  * src/utils/offlineQueue.js
    - fetchOrQueue(input, init) → { ok, response | queued | reason }
    - replayNow() — postMessage to SW
    - onOnlineChange(cb), isOffline()

  * src/utils/multiTab.js
    - BroadcastChannel('brainsnn-state') wrapper
    - publish(kind, payload) / subscribe(kind, cb)
    - Stable per-tab origin id so subscribers ignore their own emits
    - No-op shim where BroadcastChannel is missing

  * src/utils/atomicWrites.js
    - withLock(name, fn) — Web Locks API, exclusive mode, cross-tab
    - mutate(name, store, key, init, fn) — read/modify/write helper
    - Per-tab Promise-chain fallback for browsers without Web Locks

Proof of wiring (src/utils/snapshots.js):

  * saveSnapshot / deleteSnapshot / clearAllSnapshots now run their
    localStorage mutation inside withLock('snapshots:write'). Two
    tabs saving snapshots simultaneously can no longer clobber each
    other's appends.
  * Every mutation publishes 'snapshot:changed' on the multiTab
    channel so an open list in another tab can refresh itself.

Out of scope (deferred — additive when the time comes):

  * Wiring fetchOrQueue into SharePanel / SessionRooms / SyncPanel —
    each is its own focused PR with UX tweaks ('queued · will sync')
  * Periodic Background Sync for daily-challenge corpus refresh —
    needs origin trial / install-prompt UX
  * File System Access API for Portability / Archive export

Build verified: SW + helpers + snapshot wiring all clean.
Plan flagged WebGPU + OffscreenCanvas renderer swap as opt-in and
acknowledged-risky: swapping to WebGPURenderer would silently disable
the brain scene's Bloom + Outline effects (postprocessing 6.x is
WebGL2-only), and OffscreenCanvas + R3F requires round-tripping every
pointer event through postMessage.

Instead, this ships the foundation: detect what's available and let
power users + sysadmins see which Beast features the current browser
actually delivers. Renderer swap can be a focused follow-up PR with
its own visual QA + product judgement on whether dropping
postprocessing for raw throughput is the right trade.

  * src/utils/capabilities.js — sync probes (Worker, IDB,
    BroadcastChannel, Web Locks, Background Sync, OffscreenCanvas,
    File System Access, cores) + async hasWebGPU() that caches the
    requestAdapter() promise.
  * src/components/CapabilitiesPanel.jsx (Layer 103) — table view
    with per-feature availability + hint. Wired into Connect →
    System sub-tab beside Theme + Privacy Budget.

Marks the plan's full Beast track (#7 through #12) as shipped.
Defaults remain WebGL2 + the previous quality auto-degrade ladder.
Completes the inline-hex sweep deferred from Shell PR #2. The previous
sweep left ~30 files in src/utils/ and a handful in src/components/
with hex literals for severity tiers (#fdab43 warn-bright, #5ee69a
ok-bright, #77dbe4 info, plus stragglers of #dd6974 / #6daa45 inside
multi-color severity tables).

Added to src/styles/tokens.css:

  --severity-mid:     #fdab43   (light: #d97a1f)   "Tilted" tier
  --severity-ok:      #5ee69a   (light: #2f9a52)   "Resilient" tier
  --severity-info:    #77dbe4   (light: #2b8a9e)   neutral / steady
  --severity-purple:  #a86fdf   (light: #7c4ec0)   certainty / authority
  --severity-pink:    #ec87b5   (light: #c25d92)   belonging / social

Single-pass sed sweep across src/utils + src/components migrated
every quoted hex literal to var(--token):

  '#fdab43' → 'var(--severity-mid)'   etc.

52 files touched, all returning objects/arrays consumed downstream by
React inline styles or props (verified: nothing flows into viral/og.js
which has its own isolated color tables for server-side OG rendering).

Result: every severity-tier color now reskins with the user's theme
toggle. The four-token base palette + five severity tokens cover the
full client UI; the few remaining one-off hex colors (e.g. #5591c7
for trust erosion in SCORE_FIELDS, decorative gradient stops) are
semantic identity colors that intentionally stay fixed.

Build clean — no behavior change, JS bundles unchanged.
The Claude-design AppShell has handled every panel cleanly through six
follow-up PRs and a bot review pass. Removing the 1024-line legacy
scroll now eliminates the last vendor-chunk anchor that was preventing
proper code splitting.

Changes:

  * Deleted src/App.jsx (1024 lines, ~668kB legacy-app chunk).
    Every panel it referenced is rendered by src/shell/NewApp.jsx
    via its 7-workspace shell. Pre-flight grep confirms no other
    file imported from './App' or '../App'.

  * src/main.jsx — flag logic removed; NewApp is the only renderer.
    `?shell=old` is still detected for one release: logs a console.info
    that the legacy shell is gone, clears any stale localStorage pref
    so subsequent loads don't keep noticing, then continues into the
    AppShell.

  * vite.config.js — removed the legacy-app manual chunk rule that
    forced App.jsx + its static imports into one bundle.

Build impact:

  Before (with legacy alive, ?shell=new path):
    legacy-app-*.js   669 kB gzip 203 kB  (loaded only on ?shell=old)
    index-*.js          2 kB gzip   1 kB
    NewApp-*.js        19 kB gzip   6 kB
    ws-* + per-panel chunks  lazy

  After:
    index-*.js        148 kB gzip  53 kB  (shell + critical glue)
    ws-defend-*       82 kB gzip  29 kB  (firewall family eagerly)
    ws-home-*         19 kB gzip   8 kB
    ws-analyze-*      16 kB gzip   5 kB
    ~50 per-panel chunks at 1-22 kB each, lazy on tab/details open

  The dynamic-vs-static-import warning chain Vite kept emitting is
  fully cleared. Every panel is properly tree-shakeable.

Verification: full build clean, no errors, no warnings.
Beast PR #11 follow-up. The offline queue infrastructure has been
sitting unused since it shipped; this connects two of the most
network-dependent panels so users can actually see the offline-first
behaviour in action.

SessionRoomsPanel (Layer 77):

  * submitScore now uses fetchOrQueue → /api/rooms POST
  * When offline, the request is enqueued by the service worker and
    the panel optimistically inserts a `queued: true` entry into the
    leaderboard so the user sees their submission is captured
  * Offline banner under the panel description: "● offline —
    submissions will queue and replay when you're back online"
  * isOffline() + onOnlineChange() drive a useState mirror so the
    banner appears/disappears live

SyncPanel (Layer 96):

  * send() upload uses fetchOrQueue → /api/sync POST
  * When offline: status shows "Queued · will sync when online (code:
    ABCDEF)" instead of an error toast
  * Inline offline pill next to the Send/Receive mode toggles

Both panels remain fully functional online — behaviour only changes
when the network actually fails. Existing UX (status messages, error
display, score submission flow) preserved unchanged.

Build clean.
Beast PR #11 follow-up. Power users who export their BrainSNN bundle
regularly can now pick a file once and have subsequent saves overwrite
it without the download dialog interrupting flow.

  * src/utils/fileSystemSave.js — wrapper around showSaveFilePicker +
    createWritable. Per-key handle cache so different surfaces can
    pin different files. Re-checks permission on each save (browser
    revokes after some inactivity). Falls back to a regular download
    anchor on Safari / Firefox / API absence.
    Handles intentionally NOT persisted — the spec doesn't allow
    cross-session writes without re-prompting.

  * src/components/PortabilityPanel.jsx — when File System Access is
    available, primary action is "⇣ Save to file…" on first click,
    "↻ Overwrite saved file" thereafter. New "Unpin file" button
    clears the handle so the next save re-prompts. Status copy makes
    the in-place save behavior visible: "Saved to brainsnn-bundle-...
    Re-clicking will overwrite." Falls back to existing download
    behavior on unsupported browsers without UI changes.

Build verified.
Closes the cross-tab loop on Beast #11. snapshots.js was publishing
on every save/delete/clear via the multiTab BroadcastChannel, but
no consumer was subscribing — the broadcast went into the void.

  * SnapshotPanel subscribes to 'snapshot:changed' on mount and
    re-runs listSnapshots() so two open tabs show the same list
    without polling or manual reload. Same effect when one tab
    saves and the other has the panel open in a different
    workspace.

Single-line wire; refresh() already existed and is reused.
…eedback

Beast PR #11 follow-up. Snapshot writes were the proof-of-concept for
withLock(); rolling the same pattern out to the three other surfaces
where two-tab races would actually bite a user.

  * src/utils/scanArchive.js (Layer 84)
    archiveScan / removeFromArchive / clearArchive run under
    'archive:write'. Publishes 'archive:changed' on every mutation.

  * src/utils/personalDictionary.js (Layer 90)
    addEntry / removeEntry / clearAll / bumpHit run under
    'personalDict:write'. bumpHit was the racy one — two tabs scanning
    the same content would double-bump hit counts; now serialized.

  * src/utils/feedback.js (Layer 93)
    recordFeedback / clearFeedback run under 'feedback:write'.
    Publishes 'feedback:changed'.

Each surface keeps its existing public API. Web Locks API + Promise
fallback means no caller has to know it's serialized now. Broadcasts
are silent until a subscriber wires up — same pattern as the snapshot
listener that just shipped.

Build clean.
Beast PR #11 follow-up. setTheme() now emits 'theme:changed' on the
shared BroadcastChannel; registerTheme() subscribes on every page
load so secondary tabs apply the new theme + persist it without
needing a manual reload.

  * src/utils/theme.js:
    - publish('theme:changed', next) at the end of setTheme()
    - subscribe inside registerTheme() — receivers apply visually
      and persist locally (so a refresh keeps the new theme). They
      do NOT re-broadcast, since BroadcastChannel already filters
      same-tab echoes via the origin id.

User-visible: open two tabs, toggle dark→light in tab A, watch tab B
flip in <100ms. Works for all four theme axes (theme, highContrast,
reduceMotion, fontScale) because the entire settings object flows
through.

Build clean.
The persistent top Composer was emitting shell:compose events with no
subscribers — pure UI without a destination. Now it actually drives
the right workspace + panel based on the selected mode.

  * src/shell/Composer.jsx
    On submit, dispatch shell:goto to the matching workspace BEFORE
    the compose event, so the target panel is mounted by the time
    its useBusEvent subscriber fires. Mode mapping:
      scan    → defend   (CognitiveFirewallPanel auto-scans)
      autopsy → defend   (AutopsyPanel — wiring next)
      diff    → connect  (DiffPanel — wiring next)
      rag     → knowledge (NeuroRagPanel — wiring next)

  * src/components/CognitiveFirewallPanel.jsx
    Subscribes to 'shell:compose' (mode='scan'). Populates the
    textarea with the composed text, runs scoreContent(), and pipes
    the result through the existing onApplyToNetwork chain so the
    brain reacts. User sees: textarea fills → animation → brain
    lights up — same flow as if they'd typed it directly.

End-to-end test path:
  Open /, type into top bar, hit Send → Defend workspace activates
  with Firewall mounted, textarea shows the text, scan runs, brain
  reacts.

Build clean.
Completes the Composer routing started in 06f377c. All four modes
in the persistent top input bar now actually drive their target
panels:

  scan    → CognitiveFirewallPanel  (already shipped 06f377c)
  autopsy → AutopsyPanel.setRaw     (transcript paste)
  rag     → NeuroRagPanel.setQuestion (ask the brain)
  diff    → DiffPanel.setTextA      (left side; B stays empty for
                                     the user to paste the variant)

Each panel subscribes to bus.on('shell:compose', ...) on mount and
filters by mode. The Composer dispatches shell:goto first so the
target workspace is active before the compose event fires; the
panel mounts and lands its subscriber in the same tick.

Build clean.
Accessibility pass on the AppShell. None of these features are visible
to keyboard or screen-reader users today; this PR brings the shell up
to the WAI-ARIA tab pattern + the focus management Anthropic's
chat.claude.com uses.

WorkspaceTabs.jsx:
  * role="tablist" + aria-orientation="vertical" on the nav
  * Each tab gets role="tab", aria-selected, aria-controls pointing at
    the shell-panel host, and a roving tabindex so only the active
    tab is in the natural Tab order — Arrow keys cycle the others
  * ArrowUp/Down/Left/Right + Home/End handled. After moving, focus
    follows so users can keep arrowing.

AppShell.jsx:
  * Workspace host becomes role="tabpanel" aria-labelledby the active
    tab id
  * Skip-to-content link rendered as the first focusable element;
    hidden off-screen until focused, then anchored top-left
  * On workspace change (after first mount, to avoid stealing focus on
    landing), focus the workspace H1 — screen readers announce the
    new title, keyboard users get a visible ring at the top of the
    new body. shell.css uses a softer dashed outline on H1 focus so
    it doesn't read as a button.

ToastContainer.jsx:
  * Wraps each toast in role="status" or role="alert" with the matching
    aria-live (polite for info/success, assertive for warn/error)
  * Container gets aria-label="Notifications"

shell.css:
  * .shell-skip-link styling (off-screen until :focus)
  * Consistent :focus-visible ring across shell controls — uses
    --accent-line so theme switching reskins focus rings too

Build clean. No behavior change for mouse/touch users; substantial
improvement for keyboard + screen-reader users.
@slavazeph-coder slavazeph-coder marked this pull request as ready for review May 17, 2026 22:35

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cfefe16da4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread brainsnn-r3f-app/src/shell/AppShell.jsx Outdated
Comment on lines +32 to +35
const url = new URLSearchParams(window.location.search).get('w');
if (url && VALID.has(url)) return url;
const stored = localStorage.getItem(STORAGE_KEY);
if (stored && VALID.has(stored)) return stored;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Derive initial workspace from deep-link query params

The initial workspace resolver only checks ?w and the last stored tab, so links like ?scan=, ?scan-url=, ?i=, ?a=, ?d=, and ?room= can land on Home/last-used workspace instead of the panel that consumes that payload. Because only one workspace component is mounted at a time, the target panel never runs its URL hydration logic in that case, which breaks existing deep-link entry flows.

Useful? React with 👍 / 👎.

bus.emit('shell:theme', { theme: next });
};

const openPalette = () => bus.emit('shell:palette-open');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Connect topbar palette button to a real open handler

The topbar palette button emits shell:palette-open, but there is no subscriber for that event, so clicking ⌘K in the header does nothing and only the keyboard shortcut can open the palette. This is a functional regression for mouse/touch users; wire this event to CommandPalette state (or call the same toggle path directly).

Useful? React with 👍 / 👎.

Comment thread brainsnn-r3f-app/src/shell/Topbar.jsx Outdated
Comment on lines +19 to +22
document.documentElement.dataset.theme = next;
try { localStorage.setItem('brainsnn_theme', next); } catch { /* noop */ }
setTheme(next);
bus.emit('shell:theme', { theme: next });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Use shared theme state when cycling topbar theme

The topbar theme toggle writes document.documentElement.dataset.theme and localStorage['brainsnn_theme'] directly, bypassing the project theme API (setTheme/brainsnn_theme_v1). That means changes from this control are not persisted/replicated through the normal theme pipeline, and the 'auto' mode is applied as a literal dataset value instead of the resolved light/dark behavior used elsewhere.

Useful? React with 👍 / 👎.

claude added 6 commits May 17, 2026 22:41
…ar wiring

Three Codex review fixes on PR #46:

P1 — Deep-link URL params now resolve to the workspace that owns them.
  AppShell.initialWorkspace() previously only checked ?w= and
  localStorage, so links like /?scan=hello or /?room=ABCDEF landed
  on Home and the target panel never mounted to run its rehydration
  logic. Added PARAM_WORKSPACE mapping for all 12 query-string
  deep-links (scan, scan-url, r, i, a, q, d, x, t, n, v, room) and
  resolve them between the explicit ?w= override and the stored
  last-used workspace. #state= still applies brain state globally
  without dictating a workspace.

P2 — Topbar theme cycle now uses canonical setTheme() API.
  The previous implementation wrote document.documentElement.dataset
  .theme directly + saved to localStorage['brainsnn_theme'] (wrong
  key — canonical is brainsnn_theme_v1 as a full settings object).
  That meant theme changes from the topbar bypassed applyTheme()
  (light-mode + high-contrast token branches never engaged), didn't
  broadcast cross-tab, and treated 'auto' as a literal dataset value.
  Now imports getTheme / setTheme from utils/theme.js, preserves the
  user's other axis settings (highContrast, reduceMotion, fontScale)
  when cycling, and gets cross-tab broadcast for free. Refreshes on
  window focus so a sibling tab's flip is reflected in the icon.

P2 — Topbar ⌘K button now actually opens the palette.
  The onClick handler emitted shell:palette-open on the bus, but
  CommandPalette wasn't subscribing — mouse/touch users had no way
  to open the palette (keyboard ⌘K shortcut was the only path).
  CommandPalette now subscribes to shell:palette-open and toggles
  its open state, matching the keyboard shortcut behavior.

Build clean.
…witches

User-visible regression vs the legacy single-scroll: switching from
Defend → Brain → Defend left the Firewall textarea empty because
DefendWorkspace unmounted in between.

Fix: AppShell now tracks a `visited` set and renders every workspace
it has ever activated. Inactive workspaces use `hidden` + `aria-hidden`
so screen readers, focus order, and DevTools all agree they're not
active, while internal state (controlled inputs, scroll, embeddings
warmup) survives navigation.

Trade-offs:

  * Memory: each visited workspace stays in the React tree. ~50 KB per
    workspace based on chunk size analysis; cumulative ~300 KB worst
    case after visiting all six. Acceptable.

  * Lazy loading: untouched. React.lazy still defers each workspace's
    chunk until first activation; visited-set just prevents unmount
    AFTER that.

  * Focus management: heading-focus query now scopes to the visible
    workspace via `[data-workspace="${ws}"]` selector so it doesn't
    accidentally focus a hidden workspace's H1.

Build clean.
Beast follow-up. Brain Evolve was already async with `setTimeout(0)`
yields between rounds, but each round's red-team evaluation (running
65+ samples through scoreContentWithRules) is synchronous CPU work —
a 16-round run with default population blocked the main thread for
seconds at a time, freezing the 3D brain and the rest of the UI.

  * src/workers/evolve.worker.js
    One-shot worker per evolution run. Streaming protocol:
      main → start {opts} → worker runs runEvolution()
      worker → round { round, total, child, pool, generation }
      worker → done  { pool, best, sampler }
      worker → error { message }
      main → stop (cooperative cancel via shouldStop)

  * src/utils/evolve/runInWorker.js
    Main-thread wrapper that spawns the worker, plumbs the existing
    onRound callback, and exposes a `.cancel()` method on the returned
    promise. Same signature as runEvolution() so panel call sites
    don't change. Falls back to the inline loop when Worker is
    unavailable (SSR, tests).

  * src/components/BrainEvolvePanel.jsx
    Single-line swap: import runEvolutionAsync from runInWorker
    instead of runEvolution from loop. Existing onRound callback
    receives the same shape; UI updates identically.

Worker chunk emits at evolve.worker-C6cgpIG5.js, sibling to the
firewall + embeddings workers. Vite bundles only the modules the
worker actually imports (loop.js + samplers + mutations + node +
redTeam + cognitiveFirewall).

Build clean.
… Brain Evolve

Sister to 65a831a (Brain Evolve worker). Attack Evolve has the same
profile: long-running per-round red-team evaluation that froze the
main thread for seconds per generation.

  * src/workers/attackEvolve.worker.js
    Mirrors evolve.worker.js. Hosts runAttackEvolution(); same
    start / stop / round / done / error envelope. The
    runAttackEvolution loop seeds from the (mutable) Layer 25 red-
    team corpus snapshot the caller passes via opts.initialPool —
    main thread computes the seed before dispatching so we don't
    need cross-thread corpus coherence.

  * src/utils/evolve/runInWorker.js refactored
    Extracted the worker-spawning + message-routing into a shared
    runInWorker() helper. Now exports both runEvolutionAsync (Brain
    Evolve) and runAttackEvolutionAsync, each pointing at its own
    worker but sharing the streaming logic. Saves ~50 LOC of
    duplication and keeps cancel + fallback behavior consistent.

  * src/components/AttackEvolvePanel.jsx
    Single-line swap: imports runAttackEvolutionAsync from runInWorker
    instead of runAttackEvolution from attackLoop. onRound shape and
    return value identical.

Build emits both evolve.worker-*.js (Brain) and attackEvolve.worker-*
.js (Attack) chunks. Main-thread bundle unchanged.
…them

Previous commit f045631 wrapped Worker construction inside a factory
function passed to a shared dispatcher. Vite's static analyzer can
only resolve `new URL(..., import.meta.url)` when it's at the
literal call site of `new Worker(...)`, not when it's behind a
returned closure. Result: attackEvolve.worker.js source never got
bundled and would 404 at runtime.

Refactored runInWorker.js so the `new URL + new Worker` pair lives
at the public export bodies (runEvolutionAsync /
runAttackEvolutionAsync). The shared dispatch() helper now takes
the already-constructed worker.

Verified: dist/assets now emits both `evolve.worker-*.js` and
`attackEvolve.worker-*.js` as separate chunks.
The Beast PR #11 service worker calls skipWaiting() during install,
so a fresh new SW takes over immediately on first idle tab close.
But a tab the user has open during a deploy holds the OLD shell
loaded — when they navigate, the cached old index.html points at
chunks that no longer exist, and they hit 404s mid-task.

  * src/utils/swUpdate.js
    Subscribes to navigator.serviceWorker.ready, tracks
    `updatefound` → installing → installed transitions. Emits a
    typed state ('pending' / 'waiting' / 'active') to subscribers.
    Adds a 30-minute visibility-gated reg.update() heartbeat so
    long-running tabs detect deploys without depending on focus
    events. activateNewSw() posts skipWaiting to the worker and
    reloads after a short timeout.

  * src/shell/UpdateBanner.jsx
    Small bottom-right chip that appears only when status='waiting'.
    "Reload" calls activateNewSw(); "×" dismisses until the next
    update (auto-re-shows). role=status + aria-live=polite so the
    SR user hears it without interruption.

  * src/styles/shell.css
    .shell-update-banner styling — surface + hairline + drop-shadow
    + a 300ms slide-in animation. Reuses --accent for the primary
    action.

  * src/shell/NewApp.jsx
    Renders <UpdateBanner /> alongside the existing global overlays.

The sw.js already handles { type: 'skipWaiting' } (shipped in
Beast #11), so no SW change needed.

Build clean.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 31e4f985aa

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

if (ws) bus.emit('shell:goto', { workspace: ws });
// requestAnimationFrame so the target workspace mounts before
// the compose event arrives at its subscribers.
requestAnimationFrame(() => bus.emit('shell:compose', { text: trimmed, mode }));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Buffer composer events until workspace listeners mount

Emitting shell:compose on the next animation frame is not enough when the destination panel is lazy-loaded (for example NeuroRagPanel, AutopsyPanel, and DiffPanel are mounted after async chunk fetch). On a cold first navigation, the event is fired before those bus.on('shell:compose', ...) subscriptions exist, so the submitted text is silently dropped. This breaks the new top composer flow for first-use sends in autopsy/diff/rag modes.

Useful? React with 👍 / 👎.

// Public API stays synchronous-looking for callers; the lock runs
// in the background. Web Locks queue requests so callers within the
// same tab/across tabs don't race on the localStorage list.
withLock(LOCK_KEY, () => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Make snapshot writes visible before synchronous refreshes

saveSnapshot/deleteSnapshot now enqueue writes through withLock and return immediately, but callers still do immediate listSnapshots() refreshes (e.g., SnapshotPanel after save/delete). Because same-tab BroadcastChannel messages are ignored by origin, there is no follow-up refresh when the async write completes, so the UI can report "saved" while the new snapshot does not appear (or deleted rows remain) until a later remount/manual refresh.

Useful? React with 👍 / 👎.

};
const list = read();
write([entry, ...list]);
withLock(LOCK_KEY, () => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid fire-and-forget locked writes in personal dictionary

addEntry/removeEntry dispatch mutations via withLock without awaiting completion, but PersonalDictionaryPanel immediately calls setEntries(listEntries()) right after each mutation. That read can happen before the lock callback runs, so adds/removes frequently do not reflect in the list right away, making entries appear unsaved until another interaction triggers a re-read.

Useful? React with 👍 / 👎.

claude added 5 commits May 18, 2026 02:23
Three Codex P1/P2 fixes on PR #46. All three share a root cause: the
cross-tab coordination I shipped in Beast #11 (withLock + multiTab
broadcasts) left same-tab synchronous read-after-write callers stuck
on stale data.

P1 — Snapshots: write delayed, panel sees stale list
P2 — Personal Dictionary: same pattern
  saveSnapshot/addEntry queue their writes through `withLock`, which
  runs the inner callback in a microtask. The synchronous
  listSnapshots()/listEntries() right after the write returns
  pre-mutation state. Cross-tab broadcasts via BroadcastChannel
  fire-and-forget, but the spec doesn't deliver back to the sender
  so the originating tab never sees its own publish.

Fix (multiTab.js): publish() now does LOCAL fan-out before the
  BroadcastChannel postMessage. Same-tab subscribers receive their
  own publishes; the message lands after the lock callback's
  writeStore() commits. Cross-tab path unchanged.

Wired subscriptions where missing:
  * PersonalDictionaryPanel — subscribe('personalDict:changed') →
    refresh(). Was reading synchronously with no patch path.
  * ScanArchivePanel — subscribe('archive:changed') → refresh().
    Same gap. Also re-runs the active query filter so search results
    stay accurate after a removal.
  * FeedbackPanel — subscribe('feedback:changed') → refresh(). Was
    polling every 3 seconds; now updates immediately on write
    (interval stays as a safety net).
  * SnapshotPanel already had the subscription (shipped e2cd2ba)
    but the same-tab origin filter meant it never fired locally.
    Now it does.

P1 — Composer: shell:compose fires before lazy panel mounts its listener
  Composer's requestAnimationFrame delay covers a single frame, but
  workspace chunks fetch over hundreds of ms on cold first visit
  (esp. for Knowledge → NeuroRagPanel). Event was emitted into the
  void and silently dropped. AutopsyPanel / NeuroRagPanel / DiffPanel
  never received the composed text on first use.

Fix (bus.js): added a sticky-event buffer with per-event TTL. When
  bus.emit() fires a registered sticky event, it stashes the payload
  +timestamp. Late bus.on() subscriptions for the same event get a
  microtask-deferred replay if still within the TTL window.
  shell:compose registered as sticky with 2500 ms (comfortably covers
  a cold chunk fetch + workspace switch). Bus also exports
  clearSticky() so callers can invalidate if needed.

Build clean.
Extends the existing firewall worker with two new handlers. Same
worker pool, no new chunks — keeps the connection cost low while
offloading the next two biggest synchronous freezes.

Worker handlers added:

  * runRedTeam ({ thresholds, rules })
    65-sample corpus × scoring pass at 3 thresholds. ~150 ms on
    default hardware; previously blocked the 3D viewer mid-run.
    Optional `rules` param serializes/restores around the call so
    the main thread's active ruleset propagates without leaking
    worker state.

  * trainFromRedTeam ({ report, opts })
    n-gram lift mining on the report object. Pure compute over
    serializable data; no rule state needed.

Main-thread async wrappers:

  * src/utils/redTeam.js → runRedTeamAsync(opts)
    Skips the worker path if a custom scoreFn is supplied (worker
    can't serialize closures). Falls back inline if the pool is
    unavailable.

  * src/utils/adversarialTraining.js → trainFromRedTeamAsync(report, opts)
    Same fallback semantics.

  * src/utils/cognitiveFirewall.js → callFirewallWorker(type, payload)
    Generic pool dispatcher exposed for sister modules so they can
    reuse the same pool without spawning their own workers.

Consumers updated:

  * RedTeamPanel — runAll() now awaits runRedTeamAsync. The 20 ms
    setTimeout that existed to let the button re-render before the
    sync work is no longer needed; UI stays interactive throughout.

  * AdversarialTrainingPanel — handleTrain() awaits both the
    baseline run and the n-gram mining via the async variants.

Build clean. Firewall worker chunk now hosts four scoring-related
operations instead of three; main bundle unchanged.
NewApp re-renders every 180 ms (simulation tick → setState).
Without memo, the entire shell chrome (header + left rail + top
composer) re-rendered ~5×/second even though none of those
components visually depend on the brain state.

Wrapped the three components in React.memo:

  * Topbar — depends only on { workspace, firewallResult, immunity,
    onShowHelp }. firewallResult / immunity / workspace change on
    user action only; onShowHelp was an inline lambda recreated
    each render → also memoized in NewApp via useCallback so the
    shallow compare actually catches.

  * Composer — zero props (state is internal). The simulation tick
    no longer redraws the input field.

  * WorkspaceTabs — { active, onChange }. Both stable across ticks.

Behavioural diff: none. Pure perf win on weaker devices /
keep-alive workspaces (6 active workspaces × 5 re-renders/sec was
the worst case before).

Build clean. Bigger architectural fix (split session into stable
handlers vs live state via Context) is its own PR if profiler still
shows hot paths in the workspaces themselves.
Sixth worker chunk on the branch. CodeBrain's parse → community
detect → BM25 index pipeline was a synchronous ~1-2 s freeze on
larger pastes (the GitNexus repo trace, for instance). All three
steps are pure compute over JSON-safe data; trivial to push off
the main thread.

  * src/workers/search.worker.js
    Handlers: parseFiles, analyzeCode (combined parse + Louvain
    + BM25 index), hybridSearch. analyzeCode fuses the three
    operations so the panel only pays one postMessage round-trip
    for the whole pipeline.

  * src/utils/searchWorker.js
    Main-thread API: analyzeCodeAsync(files), hybridSearchAsync(
    query, index, opts). Lazy pool spawn (single slot — code
    analysis is serial). Inline fallback for SSR/test env.

  * src/components/CodeBrainPanel.jsx
    Refactored: dropped the useMemo over graph (worker now returns
    the analyzed shape in one shot), added `analyzing` state.
    Semantic search path stays inline because embed() already
    proxies through its own worker — double-hopping would cost
    more than it saves.

Build emits search.worker-*.js chunk. The five other workers
unchanged. Initial bundle untouched.

Worker chunks on the branch:
  firewall.worker      (score/scoreWithRules/setRules + runRedTeam
                        + trainFromRedTeam)
  embeddings.worker    (MiniLM via transformers.js)
  evolve.worker        (Brain Evolve UCB1/Island/MAP-Elites)
  attackEvolve.worker  (Attack Evolve mutation loop)
  search.worker        (parse + Louvain + BM25 — this PR)
Beast PR #11 follow-up cleanup. FeedbackPanel kept a 3-second
setInterval as a safety net when the multiTab subscription was
added. Now that publish() fan-outs same-tab subscribers (fixed in
05eba01), the live subscription is the source of truth and the
poll is pure waste — wakes the main thread every 3 seconds on
every browser tab even when nothing has happened.

Single-file change. listFeedback / calibrationReport still run
on mount + on every 'feedback:changed' event.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: efd070eb60

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread brainsnn-r3f-app/src/utils/swUpdate.js Outdated
Comment on lines +67 to +70
navigator.serviceWorker.addEventListener('controllerchange', () => {
// Don't auto-reload — the user might be mid-edit. Surface
// 'waiting' so the toast offers a manual reload.
broadcast({ status: 'waiting' });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Gate controllerchange before marking update as waiting

The controllerchange handler always broadcasts status: 'waiting', but controllerchange also fires on a first-time service worker install when no update is actually pending. In that case UpdateBanner shows “A new version is ready” even though the app is already current, prompting an unnecessary reload for fresh users. Only mark waiting when there is an actual registration.waiting worker (or when an update transition has been observed).

Useful? React with 👍 / 👎.

Codex P2 review fix on PR #46.

controllerchange fires on the FIRST service-worker install too (no
previous controller → a fresh controller), not just on updates.
That meant first-time visitors got the "A new version is ready —
Reload" banner before they had done anything — a confusing prompt
to reload an app they just opened.

Fix in src/utils/swUpdate.js:

  * `sawUpdate` flag inside track() flips to true only when an
    updatefound or statechange event indicates a real update
    transition (installing while a controller already exists).

  * controllerchange handler now broadcasts 'waiting' ONLY when
    sawUpdate is true OR reg.waiting is set. First-install
    controllerchange falls through silently.

  * As a defensive measure, the `installed` branch of the
    statechange listener flips sawUpdate true when it detects an
    installing worker arriving while a controller already exists
    — covers the edge case where updatefound fires before
    track() has wired its sawUpdate flag.

No behavioural change for actual deploys — banner still appears
when a new worker is waiting after an update.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bb71485483

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +193 to +195
{Array.from(visited).map((ws) => {
const Workspace = WORKSPACE_COMPONENT[ws] || HomeWorkspace;
const isActive = ws === workspace;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Mount target workspace before layer jump search

AppShell now renders only visited workspaces, but layer jumps (via CommandPalette.findAndFlashPanel) still search the current DOM without forcing the destination workspace to mount first. On a fresh session, selecting a layer from an unvisited workspace will silently no-op because its panel tree is absent, so keyboard/mouse layer navigation is broken until the user manually opens that workspace once.

Useful? React with 👍 / 👎.

Comment on lines +98 to +102
const raf = requestAnimationFrame(() => {
const el = document.querySelector(target);
if (!el) {
if (typeof console !== 'undefined') console.warn('[onboarding] target missing:', target);
return;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Retry onboarding highlight until lazy panel mounts

The walkthrough does a single requestAnimationFrame lookup after shell:goto; if the target panel is lazy-loaded (for example .snapshot-panel in BrainWorkspace), that first query often runs before the chunk finishes loading and the highlight step is permanently skipped. This makes first-run onboarding inconsistent on cold loads because missing targets are not retried.

Useful? React with 👍 / 👎.

store().set(hash, refreshed).catch(() => { /* noop */ });
// IDB returns plain typed arrays directly; if a structured-clone
// path ever boxes it, restore the Float32Array view.
return row.vec instanceof Float32Array ? row.vec : Float32Array.from(row.vec);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Normalize cached vectors for localStorage fallback

When IndexedDB is unavailable, openStore() falls back to JSON-backed localStorage, which serializes Float32Array values to plain objects. getCached() then calls Float32Array.from(row.vec), and for object-shaped vectors this produces an empty array, so embedding cache reads become invalid and semantic features degrade to near-zero similarity in fallback environments (e.g., restrictive/private browser modes).

Useful? React with 👍 / 👎.

…y, embeddings localStorage fallback

P1 — CommandPalette layer jumps no-op on unvisited workspaces.
  findAndFlashPanel searched the DOM directly without dispatching
  shell:goto, so picking a layer from an unvisited workspace
  silently failed (the panel tree wasn't mounted yet). The
  flashLayer helper at src/utils/flashLayer.js already does this
  correctly — dispatches shell:goto, waits two rAFs, then scrolls
  + rings. CommandPalette never adopted it.

  CommandPalette now uses flashLayer(row.id) on pick. Inline
  findAndFlashPanel deleted. Mouse + keyboard layer jumps now
  work to any workspace regardless of visit history.

P2 — OnboardingWalkthrough single-rAF lookup fails on lazy panels.
  After dispatching shell:goto, the walkthrough waited one rAF
  then queried document.querySelector(target). For destinations
  inside lazy-loaded workspace chunks (e.g. .snapshot-panel in
  BrainWorkspace) the chunk takes 100s of ms to fetch on a cold
  first navigate — one rAF is far too early, target query returns
  null, console.warn fires, walkthrough silently skips the step.

  Now polls every 50ms for up to 2s (40 attempts). First attempt
  still runs on rAF so the workspace swap settles before querying.
  Each attempt cancels cleanly on step change / unmount.

P2 — embeddingsStore loses vectors under localStorage fallback.
  store.js falls back to JSON-backed localStorage when IndexedDB
  is unavailable (private mode in Safari, etc). JSON.stringify
  on a Float32Array produces a numerically-keyed plain object
  (`{"0":1.23,"1":-0.5}`). On read, Float32Array.from(obj) sees
  no .length and returns an EMPTY typed array — every "cached"
  vector becomes zero-similarity and semantic features silently
  degrade.

  Fix:
    * setCached now always serializes via Array.from(vec) so the
      stored shape is a JSON-safe plain Array regardless of
      backend.
    * New normalizeVec() helper handles Float32Array (IDB path),
      Array (new fallback writes), and numeric-keyed objects
      (legacy fallback writes that pre-date this fix). Returns
      null for genuinely empty values so the caller doesn't get
      a zero-length vector.
    * On a successful read from an object-shaped row, the cache
      is rewritten as a plain Array so subsequent reads round-trip
      correctly.

Build clean.
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.

2 participants