Skip to content

Add maintainer skills for IPC, Pyodide, and redux-observable epochs#218

Open
jdpigeon wants to merge 25 commits into
mainfrom
add-maintainer-skills
Open

Add maintainer skills for IPC, Pyodide, and redux-observable epochs#218
jdpigeon wants to merge 25 commits into
mainfrom
add-maintainer-skills

Conversation

@jdpigeon

Copy link
Copy Markdown
Contributor

What

Adds four agent-facing skills under .claude/skills/ that document the non-obvious, cross-cutting architecture a maintainer needs before touching it. They consolidate and upgrade scattered context (and point to .llms/learnings.md for deep detail rather than duplicating it).

  • electron-ipc-architecture — the main/preload/renderer process-separation mental model: the no-Node-in-renderer rule, the three message directions, lazy native-module loading.
  • electron-ipc-channel — step-by-step procedure for adding/editing/removing an IPC channel without leaving it half-wired (the four files that must stay in sync, invoke vs send vs webContents.send).
  • pyodide-mne — how MNE runs in the Pyodide web worker: the JS↔worker↔Python message protocol, async plot routing, package install via InstallMNE.mjs, and prod-only load gotchas.
  • redux-observable-epochs — epic anatomy and the numeric marker-code contract linking live collection to MNE analysis (the source of empty-ERP bugs).

Notes

Docs only — no runtime code touched. Grounded in actual file paths/patterns (pyodideEpics.ts, lslBridge.ts, markerRegistry.ts, native.ts, etc.).

🤖 Generated with Claude Code

jdpigeon and others added 25 commits June 22, 2026 10:23
Deletes cortex.js and emotiv.ts entirely. Removes all Emotiv branches
from device epics, experiment epics, pyodide epics, components, and
constants. DEVICES.EMOTIV, EMOTIV_CHANNELS, parseEmotivSignalQuality,
and Cortex credential env vars are all gone. Muse is now the only
supported device, laying the groundwork for LSL-based connectivity.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Electron 22+ no longer shows a native Bluetooth picker automatically.
Instead it fires select-bluetooth-device on webContents, requiring the
main process to call the callback with a deviceId. Without this handler
requestDevice() hung silently, leaving the search in a perpetual
SEARCHING state.

Changes:
- main/index.ts: register select-bluetooth-device handler that auto-selects
  the first Muse headset as BLE discovery progresses; add bluetooth:cancelSearch
  IPC handler so the renderer can reject a pending requestDevice() on timeout
- preload/index.ts: expose cancelBluetoothSearch() to renderer
- muse.ts: cache BluetoothDevice from getMuse() so connectToMuse() reuses
  it instead of firing a redundant requestDevice() call; add cancelMuseScan()
- deviceEpics.ts: call cancelMuseScan() in searchTimerEpic so the pending
  requestDevice() promise is cleaned up when the 3s search window expires
- docs/device-connectivity.md: full connectivity flow diagram and bug analysis

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
In React 18, setState is always batched — calling setState in an async
componentDidMount continuation schedules a re-render but does not
immediately commit the DOM change. The subsequent querySelector('webview')
therefore returned null, the dom-ready listener was never attached, and
subscribeToObservable was never called.

Fix: defer webview setup to componentDidUpdate, triggered when viewerUrl
transitions from empty to set. At that point React has already committed
the DOM update, so the webview element exists. Because componentDidUpdate
runs synchronously before the browser event loop can process the webview
load, the dom-ready listener is in place before it fires.

This fixes signal not flowing on the Explore EEG screen when navigating
to it while already connected.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds Lab Streaming Layer support so BrainWaves can publish EEG and
stimulus markers as LSL streams and ingest data from external LSL
devices. Enables LabRecorder integration and multi-device experiments.

Phase 1: Main-process LSLOutletManager + IPC bridge forwards batched
Muse EEG samples as an LSL outlet. Adds @shared alias, asarUnpack for
native bindings, and fixes MUSE_CHANNELS hardcoding in experimentEpics.

Phase 2: Neurosity Crown SDK support — getNeurosity/connectToNeurosity
mirror the Muse driver; deviceEpics route by deviceType.

Phase 3: LSLInletManager + UI to discover and connect to external LSL
streams. lslForwardEpic skips LSL inlet sources to avoid feedback loops.

Phase 4: RunComponent emits stimulus markers via sendLSLMarker alongside
the existing injectMuseMarker call, preserving the CSV-embedded marker
path used by the Pyodide analysis pipeline.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Subscribes to lsl:status IPC at the App level and surfaces errors
via react-toastify. Completes Phase 5 production hardening (decimation,
BLE disconnect detection, error surfacing).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
node-labstreaminglayer 0.3.0 only ships an x86_64 liblsl.dylib in its
prebuild dir, which fails to load on arm64 Macs. patchDeps.mjs now
detects darwin-arm64 and symlinks the Homebrew-installed framework
binary over the bundled stub. No-op on x64 macs, Linux, and Windows.

Requires: brew install labstreaminglayer/tap/lsl

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Two related fixes that together unbreak Pyodide in production:

1. Protocol handler in main was looking at resources/webworker/src/ but
   electron-builder copies pyodide assets to resources/pyodide/. Update
   pyodideRoot to match the actual extraResources destination.

2. Worker was relying on import.meta.url to find pyodide.asm.wasm and
   python_stdlib.zip relative to pyodide.mjs. That works in dev (Vite
   middleware serves siblings from node_modules) but fails in prod where
   the bundled .mjs has no siblings. Set indexURL so pyodide fetches
   runtime files through the pyodide:// protocol handler — works in both.

Verified by installing the packaged dmg and running test plot.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Replace stale port-17173 http-server section with current
  pyodide:// protocol handler reality
- Document the prod resourcesPath/pyodide/ extraResources destination
- Add indexURL requirement for prod (siblings of pyodide.mjs aren't
  bundled, so import.meta.url resolution fails) — gotcha hit during
  packaging verification

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The .worktrees/modernization entry was an accidental submodule-style
gitlink (mode 160000) pointing into a local git worktree. There is no
.gitmodules and the worktree is not part of the repo tree, so it only
produced perpetual 'modified' noise in git status. Untrack it and
ignore .worktrees/ going forward.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
node-labstreaminglayer dlopen's liblsl at require time, so a static
import crashed the whole app on startup when liblsl was missing/
incompatible (e.g. Apple Silicon without the Homebrew build) — even for
Muse-only users who never need LSL.

Load the native bindings lazily and fail soft so LSL becomes a true
advanced, opt-in feature:
- src/main/lsl/native.ts: guarded require() in try/catch (memoized),
  exposing loadLSL() and isLSLAvailable()
- outlets.ts/inlets.ts: type-only imports + loadLSL() at call time;
  all ops no-op gracefully when liblsl is unavailable
- lsl:isAvailable IPC + preload bridge for renderer feature detection
- ConnectModal hides 'External LSL stream' when unavailable
- lslBridge no-ops sendEpoch/sendMarker when unavailable (no IPC spam)

Result: Muse/Neurosity work with zero LSL/LabRecorder dependency; LSL
features appear only where liblsl loads.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- behavior accuracy: count correct no-go/withhold trials (drop the
  response_given==='yes' filter from accuracy numerators; RT keeps it)
- balanceStimuliByCondition: iterate bucket keys so a stimulus with no
  condition no longer bails out and yields an empty experiment loop
- ExperimentCleanup: also reset topoPlot (stale map persisted across experiments)
- ParamSlider: map(Number) instead of map(parseInt) (index-as-radix -> NaN bounds)
- EEGViewer.findExtreme: fix nested slice that scanned the whole buffer
- multitasking: divide trial count by blocks.length*4 to stop ~2x over-generation
- MNE drop %: use drop_log_stats() so IGNORED/NO_DATA entries aren't counted

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Four agent-facing skills documenting the non-obvious cross-cutting
architecture:
- electron-ipc-architecture: process separation mental model
- electron-ipc-channel: procedure for wiring an IPC channel end-to-end
- pyodide-mne: JS<->worker<->Python protocol, plot routing, package install
- redux-observable-epochs: epic anatomy + the numeric marker-code contract

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant