Redesign interface — Claude-design shell + beast-mode engine foundations#46
Redesign interface — Claude-design shell + beast-mode engine foundations#46slavazeph-coder wants to merge 33 commits into
Conversation
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.
There was a problem hiding this comment.
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.
| try { | ||
| return await pool.call('score', { text }); | ||
| } catch { |
There was a problem hiding this comment.
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 {| probe.onsuccess = () => { | ||
| const db = probe.result; | ||
| const haveAll = neededCollections.every((c) => db.objectStoreNames.contains(c)); | ||
| if (haveAll) { resolve(db); return; } |
There was a problem hiding this comment.
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;
}| } | ||
| } | ||
| }; | ||
| upgrade.onsuccess = () => resolve(upgrade.result); |
There was a problem hiding this comment.
| try { | ||
| _firewallPool = createPool( | ||
| () => new Worker(new URL('../workers/firewall.worker.js', import.meta.url), { type: 'module' }), | ||
| { size: 1, fallback: (type, payload) => { |
There was a problem hiding this comment.
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.
| { size: 1, fallback: (type, payload) => { | |
| { fallback: (type, payload) => { |
…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.
There was a problem hiding this comment.
💡 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".
| 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; |
There was a problem hiding this comment.
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'); |
There was a problem hiding this comment.
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 👍 / 👎.
| document.documentElement.dataset.theme = next; | ||
| try { localStorage.setItem('brainsnn_theme', next); } catch { /* noop */ } | ||
| setTheme(next); | ||
| bus.emit('shell:theme', { theme: next }); |
There was a problem hiding this comment.
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 👍 / 👎.
…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.
There was a problem hiding this comment.
💡 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 })); |
There was a problem hiding this comment.
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, () => { |
There was a problem hiding this comment.
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, () => { |
There was a problem hiding this comment.
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 👍 / 👎.
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.
There was a problem hiding this comment.
💡 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".
| 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' }); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
💡 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".
| {Array.from(visited).map((ws) => { | ||
| const Workspace = WORKSPACE_COMPONENT[ws] || HomeWorkspace; | ||
| const isActive = ws === workspace; |
There was a problem hiding this comment.
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 👍 / 👎.
| const raf = requestAnimationFrame(() => { | ||
| const el = document.querySelector(target); | ||
| if (!el) { | ||
| if (typeof console !== 'undefined') console.warn('[onboarding] target missing:', target); | ||
| return; |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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.
Why
BrainSNN had 97 panel components stacked into a 1024-line
App.jsxwith nonavigation. 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)
src/styles/tokens.css— canonical:rootsheet, imported first.#c96442), warm-dark surfaces, hairline borders(
--shadow: none). Source Serif 4 added for headlines (was referencedbut never loaded); Inter retained for body.
utils/theme.jswas wired for them but the CSS overrides were dormant.
(
BrainScene+brain/*.jsxvia newutils/threeTokens.jswith atheme-reactive
useThreeTokens()hook).src/utils/andsrc/components/: five new semantic tokens (--severity-mid / -ok / -info / -purple / -pink) replace#fdab43,#5ee69a,#77dbe4, etc.--primary/--gold) removed at the end of thesweep; 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.jsxmirrors all state/effects from the old App.jsxand exposes a single
sessionprop to every workspace; ~16onApply*handlers memoized via
useCallback.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 scatteredwindow.dispatchEventpatterns. Drivesshell:goto,shell:flash,shell:compose.src/utils/flashLayer.js— extracts the cyan-ring scroll fromCommandPalette + hotkeys.js, extends it to dispatch a workspace
switch before scrolling so cross-workspace jumps work.
navigation. The R3F Canvas would otherwise pay ~400-800 ms remount
cost per workspace switch.
?shell=newflag added then flipped to default; legacy App.jsxretired entirely.
?shell=oldstill recognized for one release —logs a console.info and continues into the AppShell.
LayerExplorerPanel.jsx+HotkeyMap.jsx(the overlay; thehotkeys.jschord dispatch stays live).Composer wiring + accessibility (06f377c, 15a38a0, cfefe16)
and panel:
scan→ Defend → CognitiveFirewallPanel auto-scansautopsy→ Defend → AutopsyPanel.setRawrag→ Knowledge → NeuroRagPanel.setQuestiondiff→ Connect → DiffPanel.setTextArole="tablist"with arrow-keynav, roving
tabindex, andaria-selected.role="tabpanel", and moves focus to the workspace heading onevery tab change (after first mount).
role="status"(polite info / success)and
role="alert"(assertive warnings / errors). Container labeledaria-label="Notifications".shell:gotofor each ofits 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 forSSR / test env.
src/workers/firewall.worker.js— hostsscoreContent/scoreContentWithRules/setRulesoff the main thread.scoreContentAsyncis the opt-in async API: threshold-gated (≥ 500chars routes to worker, short text stays sync). Active rules are
serialized + sent with each
scoreWithRulescall 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— loadsXenova/all-MiniLM-L6-v2in a dedicated worker. Public API (
embed,embedBatch,getStatus,warmup) over the standard pool envelope.src/utils/embeddingsStore.js— IDB-backed cache viaopenStore('embeddings'). Stores{ vec: Float32Array, t };refreshes
ton read for LRU accuracy. Soft cap 5000 / prune-to4500, amortized prune every ~100 writes.
src/utils/embeddings.jsrewritten as a thin facade. Existing API(
initEmbeddings,embed,embedBatch,isReady,subscribeStatus,getEmbeddingStatus,clearEmbeddingCache)preserved. Idle warm-prefetch (
requestIdleCallback) on returnvisits.
brainsnn_embeddings_v1localStorage blob into IDB on first read, with rollback safety.
Unified IDB persistence (91183fd + 6aa03ba)
src/utils/store.js—openStore(name)IDB wrapper with lazyschema upgrades,
get/set/delete/keys/values/clear/storageEstimate.brainsnn_store/<collection>/<id>) when IDBunavailable.
attachVersionChange(db)releases handles on schema upgrades socross-tab upgrades don't deadlock;
onblockedrejects cleanly.Per-workspace bundle splits (548e4dd, 4f4b8cf)
vite.config.jsmanual chunks: per-workspacews-<name>chunks,vendor splits (react / three / postprocessing), shared utils.
React.lazy.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.jsbumped tobrainsnn-v2: precache shell on install,hashed
/assets/*stale-while-revalidate, cached API GET responsesso share cards render offline.
/api/*and/[rqidaxntvwb]/*auto-enqueuedvia Background Sync API when offline; response is
202 + { queued: true }. Replay handler drains the IDB queue onsyncevent.src/utils/offlineQueue.js—fetchOrQueue(),replayNow(),onOnlineChange(). Wired into SyncPanel + SessionRoomsPanel withvisible "● offline — submissions will queue" indicators.
src/utils/multiTab.js—BroadcastChannel('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 intosnapshots.js,scanArchive.js,personalDictionary.js(incl.the racy
bumpHit),feedback.js. Per-tab Promise-chain fallbackwhere Web Locks unavailable.
src/utils/fileSystemSave.js— File System Access API wrapper forthe 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).
disable Bloom + Outline (postprocessing 6.x is WebGL2-only). Lays
the foundation for a focused follow-up PR.
Build impact
Initial JS for default load dropped ~40 % gzip vs main.
What's deliberately deferred
@react-three/postprocessing(Bloom + Outline). Capability probeships; activation needs separate visual QA + product judgement.
OffscreenCanvas support; pointer-event raycasting would require
postMessage round-trips on every click. Wait for upstream support.
corpus is static in code today; no benefit until it's
server-backed.
Verification
npm run buildclean, 0 errors, 0 new warnings.?scan=,?scan-url=,?r=,?i=,?q=,?d=,?a=,?x=,?t=,?n=,?v=,?room=,#state=.shortcuts.jspreserved (Space, b,r, 1/2/3, s, e, q, ?). New:
g + h/a/d/b/k/t/cfor workspaceswitching; ArrowKeys on the tab rail.
navigates between workspaces between steps.
the 3D brain materials (via
useThreeTokens) and broadcasts toopen tabs.
add, feedback record all sync across tabs via BroadcastChannel /
Web Locks.
status; reconnect drains the queue.
Bot review fixes from earlier (6aa03ba)
scoreContentAsyncwas using default rules)
onversionchangehandler (cross-tab upgradedeadlock fix)
1to default (multi-core)Ready for review.
Generated by Claude Code