- presence.ts: colors claimed by ARRIVAL ORDER — each client takes the lowest
free palette slot (no birthday-problem hash collisions; the best palette
colors go first; N ≤ palette-size editors always distinct). Coordination-
free: claims ride awareness; simultaneous-join collisions converge (lower
clientID keeps, the other re-claims), same-user tabs adopt one color,
claims are sticky across eeschema sheet rebinds (skeletons reuse them).
colorOf(userId) resolves live colors; comment pins/popovers follow it
(offline authors fall back to the hash). +2 unit tests (verified live:
alice=slot0, bob=slot1, roster/pins consistent).
- CommentLayer: single comment icon expanding into a horizontal bar — new
comment · list · show/hide all (eye empties the GAL pin set + DOM targets;
re-shown on new-comment/panel-jump). Pins are DRAGGABLE: live LWW anchor
writes while dragging (peers + the GAL dot follow), nearest-item re-snap on
drop, click-vs-drag by 4px threshold (verified live: drag synced to the
peer tab exactly). Shared setThreadAnchor + controller moveThread/
setPinsVisible/colorFor.
- collab_presence_style.h: chip text color by background luminance (dark on
light chips, white on dark — BitmapText draws with the stroke color).
- comments/presence-roster e2e updated for the bar + passing; collab units
56/56; shared 122/122; shared pointer bump.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CvqUd4QsJSGHN28aunJRTq
- collab_presence_style.h: GAL BitmapText CENTERS on its position (confirmed
in GAL::ResetTextAttributes — the mispositioned nameplates); labels/chips
now hand GAL the block center. New selection shape 5 'exact outline':
pcbnew hugs real geometry (footprint bounding hull, TransformShapeToPolygon
for the rest, padding inflates the polygon); eeschema falls back to rect.
- kicadCollabTestDemoSet (both TUs + merged): labeled demo groups — smallest
+ largest footprint and the two busiest nets' segments (symbols + wire
bundles on sch) — so the style preview covers the real range of shapes.
- PresenceTuner: Colors section rebuilt as explicit modes (per-user / fixed /
palette) with preset palettes (default, pastel, vivid, okabe-ito), buffered
hex editing + Apply (the old always-filtering textarea ate keystrokes), an
'overlay only' hint; demo injection consumes the varied demo set; 'exact
outline (pcb)' in the shape list.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CvqUd4QsJSGHN28aunJRTq
Parametrizes every visual knob of the presence overlay so we can pick the
shipped look live, then wire the winners into the defaults:
- collab_presence_style.h: shared STYLE struct + drawing (now used by BOTH
editor TUs — no more duplicated overlay code): selection shape (rect /
corner brackets / underline / rounded rect / filled-only), border width +
alpha, infill alpha, padding, corner radius; name tag show/size/chip-
background/inside-outside/top-bottom/start-end-center/offset; cursor shape
(cross / pointer / circle+dot), size/width/alpha + label knobs; fixed-color
and palette-by-name-hash overrides (try palettes without changing what
senders publish); pin radius/ring/alphas. Defaults == shipped look.
- kicadCollabSetStyle(json) live-patch export + kicadCollabTestListItems(n)
(real KIIDs for synthetic previews); merged dispatch; pins now carry the
author name so palette overrides recolor them consistently.
- PresenceTuner.tsx: floating dev panel (env-gated, tree-shaken otherwise) —
grouped sliders/selects, demo peers+pins injection for SOLO tuning,
localStorage persistence across reloads, Copy JSON export, reset.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CvqUd4QsJSGHN28aunJRTq
Hybrid pins: the wasm draws the dot (kicadCollabSetPins rides the presence
VIEW_OVERLAY, author color + white ring, drawn above selections; zero
kicad-fork changes), the DOM owns interaction —
- comments.ts: controller gluing the MIT kdoc_comments helpers to the editor:
anchor resolution per tool IU (pins track item moves via kdoc_items
observation), throttled pin snapshots, anchorAt nearest-item snap, jumpTo
via new kicadCollabSetViewport; rebinds per sheet like presence.
- CommentLayer.tsx: comment mode (click catcher + composer), pin hit targets
over the GAL dots, thread popover (reply/edit/delete own, resolve/reopen,
delete thread), panel with resolved filter + jump-to (popover centers when
the pin is off-screen). Resolved pins drop figma-style.
- WasmTool: controller lifecycle beside presence; live viewport feed;
window.__pcbjamComments test handle (threads persist in the room ydoc).
- e2e tests/web/comments.spec.ts: two-tab create → reply → resolve → panel
filter → delete, passing vs real partykit; presence suites + collab units
stay green; shared pointer bump (0004 model).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CvqUd4QsJSGHN28aunJRTq
Zero kicad-fork changes — all in the embind layer:
- pcbnew_embind.cpp: presence section — canvas wx Bind() triggers (motion/
leave/up/key/wheel) + COLLAB_LISTENER piggyback → post-settle selection
check (dedupe) → onSelection; throttled cursor emit via VIEW::ToWorld;
viewport push/pull (px-per-IU via the GAL matrix — GetScale() is the zoom
and sized the first cut's overlay nm-small); kicadCollabSetRemote renders
peers' cursors (cross + name) and selection bbox outlines into one
per-user-colored VIEW_OVERLAY (CallAfter+COROUTINE), never touching local
selection; PresenceStart/GetSelection/TestSelectFirst/TestClearSelection.
- kicad_editor_embind.cpp: merged-image dispatch (pcb-only until 0003).
- presence-kicad.ts: bindKicadPresence — routes emits into awareness (0001)
and pushes trailing-throttled peer snapshots into the wasm; wired from
WasmTool.startPresence (pcbnew-gated). +5 unit tests.
- tests/kicad/presence-pcbnew.spec.ts: 5 e2e — programmatic + real box-select
emit, throttled cursor, remote render with no-leak + pixel restore,
viewport unit band. Existing pcbnew-collab/items-bridge suites stay green.
Verified live: two tabs over partykit — peer cursor cross + label + selection
outline visible on the other tab's canvas.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CvqUd4QsJSGHN28aunJRTq
- provider.ts: expose awareness on YjsProvider (partykit/hocuspocus surface
their own; BroadcastChannel gets a sibling-channel relay via awareness-bc.ts)
- presence.ts: createPresence — publish local PresenceState, subscribable
deduped peers() roster, setCursor/setSelection for P2, pagehide fast removal
- PresenceRoster.tsx + WasmTool wiring: facepile chip next to SourceChip;
eeschema rebinds per active sheet (sheet-manager ActiveSheet now carries the
room's provider); config presenceUser() = slug + colorForUser
- new dep y-protocols; unit tests (presence.test.ts) + e2e
tests/web/presence-roster.spec.ts (two tabs, verified vs real partykit)
- bump pcbjam-shared: presence-wire schema (MIT)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CvqUd4QsJSGHN28aunJRTq
Miss 08A: the binding stores wire-carried (lib_symbols …) definitions in
kdoc_libsymbols and prefixes them on apply wires — a joiner that never saw a
symbol adopts it WITH its definition (new e2e ysync-libsymbols.spec.ts).
Miss 08B: registerSaveHook gains onSavedText; WasmTool routes saved-file text
to syncLayoutToY (per sheet room via the manager's syncLayoutFromSave, or the
single-room doc) so title block / paper / setup edits converge instead of
drifting. Opt 12 (TS half): zod off the observer hot path (yToItemUnchecked),
children index built once per conversion. Opt 13: seed()'s adopt diffs the
editor snapshot against the doc view and applies only the doc-authoritative
difference — clean rebinds apply nothing, the adopt undo entry shrinks to the
real changed set. Opt 14 deliberately deferred (doc 18). All TS-side; no wasm
rebuild (the C++ blob/findLib sides already carried definitions).
Verified: shared 107, standalone 79 (+2 known pre-existing wasm-assets),
ysync e2e 21/21 chromium, collab regression 21/3-skip firefox.
Bumps: web/pcbjam-shared (lib_symbols channel + syncLayoutToY + opts).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JgThWXtdvrYLK47EDFoGdq
Trim the waitlist microcopy on the standalone demo and the marketing site's
CTA/hero forms to just the value prop, removing the "no spam, unsubscribe
anytime" reassurance line.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01REwWZGkAz1AZUJw8yqxkCN
The footprint and symbol editors are no longer separate WASM bundles: the frontend loads the parent pcbnew/eeschema bundle and passes --frame=fpedit / --frame=symedit (TOOL_BUNDLE + TOOL_FRAME -> Module.arguments in boot). Drops the two duplicate build+deploy targets and their wrapper scripts + vestigial embind; adds low-level harnesses (footprint_editor.html, symbol_editor.html) and a runtime-frame spec. Bumps the kicad submodule to the runtime --frame launcher.
The frame-runtime spec is listed in PCBNEW_FAMILY_SPECS so CI routes it to the chromium-ci (V8) project — its footprint case boots the pcbnew module, which OOMs SpiderMonkey on x86 CI. Includes the editor-unification dossier (research docs 01-04 + the as-built implementation record 05).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Waitlist signup form on the home page (between Projects and Tools),
cross-posting to the landing site's /api/waitlist (CORS + OPTIONS added there,
gated by WAITLIST_ALLOWED_ORIGINS).
- Version badge gains a pcbjam.com landing-page link (VITE_LANDING_URL).
- Optional Plausible analytics from VITE_PLAUSIBLE_DOMAIN (off by default);
wired into build-demo + both deploy workflows via vars.PLAUSIBLE_DOMAIN.
- Opening a read-only gallery project auto-"moves to local": it forks into a
writable browser-local project at the same slug, shadowing the gallery row.
- Project + local-folder views use the same iconed tool launcher (ToolGrid)
and a navigable directory FileTree instead of a flat file list.
- Boot overlay shows real wasm download progress (Module.instantiateWasm +
streaming byte counter) and a "taking too long" state after 60s.
- Home Libraries section lists the active libs source (the read-only R2/CDN
set in the demo) with a name filter; useLibs now reads libsSourceConfig.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPJYx4urFVhdMjEmYfMCMD
Mirrors the closed app: create a templated file then open it, so you always edit a real file that saves back.
- lib/new-file.ts: minimal valid KiCad templates (pcbnew/eeschema/pl_editor, from the e2e fixtures) + document-tool helpers.
- NewFileDialog: in-project mode (file name) and home mode (pick/create a browser-local project + file name); writes via the active source uploadFileBytes (local IDB or remote-rw backend), then opens by path.
- HomePage: a document tool from the grid (local store on) opens the dialog instead of booting a throwaway blank doc.
- ProjectView: project-centric New-file row (writable projects) + Save-a-copy-to-browser fork for read-only gallery projects (copies into editable IDB), alongside files/export.
Also replaces a stray NUL byte that had been written into idb-project-store.ts in place of a space (made the file parse as binary) with an explicit unicode escape for the key separator (NUL, sorts below every slug char so project key ranges cannot collide) and the range upper bound. Runtime behavior unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Home page feedback: merge "Your projects" (local) and the gallery/backend list
into one "Projects" section where each ROW carries its own source chip (instead
of a per-section header chip). Sections are now Open → Projects → Tools →
Libraries. Local rows keep open/export/rename/delete; remote rows just open.
LocalProjectsSection → ProjectsSection.
Source chips were too pale on the editor canvas — switch from translucent pastel
to solid fills with white text + an inset ring, so they read on any backdrop
(home, dark boot screen, light editor canvas).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Loading a folder now imports a COPY into a browser-local IndexedDB project with
its own /p/:slug URL (original disk files untouched): edits persist to IDB across
visits, and the project exports via Download .zip or per-file. Gated by
VITE_LOCAL_PROJECTS=idb (on for the demo; off keeps the File System Access
write-back flow), so it's configurable per deployment.
Modular, swappable project sources behind the shared ProjectSource interface,
each self-describing via a SourceDescriptor whose kind is shown verbatim in the
UI — "Local (this browser)", "Remote · read-only", "Remote · editable" — on the
home page, the project view, and inside the editor, so the user always knows
whether/how Save persists:
- remote → REST backend (remote-rw)
- static → CDN gallery (remote-ro), saves download
- local (new) → idbProjectStore, writable IDB store
A composite layers the local store over the configured remote/gallery source,
routing per slug so imported/saved projects and the gallery share one namespace.
New, dependency-free (matching sync-client's raw-IDB ethos):
- lib/idb-project-store.ts raw IndexedDB store + create/delete/rename/export
- lib/zip.ts store-only ZIP writer (verified via system unzip)
- lib/import-folder.ts FSA/webkitdirectory folder → in-memory bytes
- lib/project-source-shared.ts source descriptors + deterministic uuid
- components/SourceChip, components/LocalProjectsSection
Home lists local projects (open/export/rename/delete). Verified: typecheck,
project-source tests, prod build, and IDB key-range project isolation in-browser
(prefix-colliding slugs don't leak).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A small fixed bottom-right overlay (VersionBadge) shown app-wide — on the home
page AND inside the loaded editor — surfacing this build's release tag and a
link to the source. The tag links to the exact commit when known (the pcbjam
commit pins the kicad + wxwidgets submodule revisions → our GPLv3
corresponding-source pointer, mirroring site Footer's BUILD_SHA), else the tag's
release page, else the repo root.
build-demo.mjs injects VITE_APP_TAG / VITE_GIT_SHA / VITE_REPO_URL (new --repo
flag, defaults to github.com/emergence-engineering/pcbjam).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- drift-detect.ts: every-N-edits + beforeunload check via kicadSave* (no save-hook), reports drift
- WasmTool: start drift detection after collab; return collab handle; stop on cleanup
- fix uuid-less docs (pl_editor): use ydocHasState in maybeConnectDocSession + seed() so a 2nd tab materializes the room instead of refetching the stale file
- api: reportDrift + reportDriftBeacon (sendBeacon/keepalive)
- global.d.ts: FS.unlink
- bump web/pcbjam-shared pointer
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The WASM tools boot + fetch lib items with no visible feedback, so a big tool
download looks like a freeze and a failed body fetch (e.g. a backend 404) looks
like nothing happened. Add a full-screen boot overlay that stays until the wx UI
has actually built (waitForWxUi, gated on wxElementRegistry — not just boot
resolve, which flashes a blank editor), a debounced spinner while an item is
being opened/saved, and an auto-dismissing error toast when a body can't be
loaded. Driven by events the libs bridge now dispatches (LIB_BUSY/LIB_ERROR).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Open a single library scoped to itself in its editor: a backend lib (HomePage
chip → scopedLibsSource) or a local .kicad_sym/.kicad_mod file (LocalProjectView
→ localFileLibsSource + a browser-side multi-symbol .kicad_sym parser). WasmTool
gains an optional libsSource override (transparent default). All via the existing
lib bridge — no MEMFS doc open, no fork changes.
Also fix a systemic editor boot failure: the build split the wx glue into
wx.js + wx-dom.js, but boot.ts only injected wx.js + <tool>.js. wx-dom.js defines
window.wxDomCreateControl (used by every tool to build its UI), so without it the
canvas rendered but no wx controls did (ReferenceError: wxDomCreateControl is not
defined). Inject wx-dom.js between wx.js and the tool, best-effort (older non-DOM
builds tolerated). Bump pcbjam-shared for LIB_EXTENSION_TOOL.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rework HomePage into KiCad-launcher-style zones: local file-load, a Tools grid,
backend projects, and a backend libraries listing (useLibs over the shared
listLibs). The Tools grid shows all seven tools in KiCad's standalone order
(Schematic/Symbol/PCB/Footprint editors, Gerber Viewer, Calculator, Drawing
Sheet); clicking a tool or a library launches the editor with no project
(launchedTool → WasmTool slug=local) — file-less editors browse backend libs,
document editors open blank. Harden the web e2e global-setup to assert the
self-provisioned origin libs (Device, Resistor_SMD) are present, failing fast.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
standalone: LibsSource abstraction (remote over the contract + static fallback),
generic window.kicadLibs provider, sym-lib-table generated from the backend's
lib list at boot (replaces the spike). backend: serve pre-built self-contained
symbol bodies from LIBS_DIR (listLibs/listLibItems + raw item-body route).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- WasmTool: hard cutover to startKicadCollab (gates on
kicadCollabSnapshotItems; scalar reconciler no longer runs in the app)
- file-seed (ysync 0005 tie-in): empty rooms are seeded from the opened
file via fileToDoc/docToY (meta+layout+items — file recoverable from the
Y.Doc alone); populated rooms still adopt (seed-once unchanged)
- binding: gate UP applies until seed() — the provider's initial state
sync otherwise streams the full doc into an editor that already holds
the file (trapped eeschema's paste path in the real app)
- pcbnew blobForItem: footprints Format a uuid-corrected copy directly —
SaveSelection's copy regenerated mandatory-field uuids (FOOTPRINT copy
ctor assigns into fresh fields), breaking wire identity (rebuild)
- roundtrip.spec on the v2 items wire; new passing pcbnew
footprint-containment round trip (the 0004 containment win); full
fixture stays fixme on the tracked envelope-parse limit
- real-app two-tab emit verified for eeschema + pcbnew (see 0008 doc)
Full kicad suite: 45 passed, 3 skipped (known), 0 failed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The automatic recovery uses an in-place location.replace reload, which does NOT
reliably release the OOM'd wasm heap in every browser (notably Firefox), so the
respawn can land right back in an out-of-memory state.
Add a user-gesture escape hatch to the terminal MemoryExhaustedDialog: an "Open
in a new tab" button that opens the editor in a fresh top-level browsing context
(clean retry chain) and closes the current tab. Because it runs from a real
click, window.open is not popup-blocked; window.close() may still be refused on
the original user-opened tab, which is fine — the new tab is the survivor. Copy
tells the user this sometimes helps where the in-place reload didn't.
- oom-watch.ts: export respawnInNewTab(win) — fresh-tab respawn with ?oomRetry
cleared.
- MemoryExhaustedDialog: "Open in a new tab" (primary) + "Reload this tab"
(secondary), with honest copy about Firefox + unsaved edits.
- WasmTool: wire onOpenNewTab to respawnInNewTab.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Automatically recover from an out-of-memory by respawning a fresh browsing
context (fresh wasm heap) and discarding the dead one — the manual "duplicate
tab, close the OOM'd one" fix, automated and capped at 2 retries so a too-small
machine stops instead of looping.
- recovery/oom-watch.ts: the recover() state machine with the retry counter in
the URL (?oomRetry=N, cap 2). Mode A soft detection — Module.onAbort plus
window error/unhandledrejection matched against an OOM signature, behind an
idempotent latch. Mode B hard-kill detection — a per-tab localStorage
heartbeat sentinel; a stale sentinel on the next load continues the retry
chain (heartbeat keys avoid mistaking a second live tab for a crash). Healthy
session (alive HEALTHY_RESET_MS) resets the chain. Default strategy is
location.replace; window.open+close is behind ?oomStrategy=newtab. Platform
access goes through injectable win/storage seams for testability.
- recovery/MemoryExhaustedDialog.tsx: terminal "out of memory" UI, reusing
0001's BlockingDialog; copy is honest about lost unsaved edits.
- wasm/boot.ts: forward emscripten Module.onAbort to the watcher (optional
BootOptions.onAbort).
- WasmTool: install the watcher, gate boot on its proceed flag (skip when the
chain is already exhausted), and render the terminal dialog.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Generalize the collab transport into a provider registry (none |
broadcastchannel | partykit | hocuspocus), one active per env. startCollab
takes { provider, room } and awaits provider.whenSynced() before seed/adopt.
Network libs lazily imported; room id from @pcbjam/shared collabRoomId.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reduce web/ to a single generic editor app plus a minimal example backend,
moving all project-specific/app code out to the closed root repo:
- apps/frontend -> standalone (@pcbjam/standalone): the GPL editor. Strips the
project-management UI (ProjectsPage/ProjectDetailPage/UploadDropzone and the
write half of lib/api.ts). WasmTool now takes fetchBytes + assetBaseUrl as
injected props (decoupled from the API client) so it can be driven by either
a backend or a local folder. New HomePage (local-folder loader + backend
project list) and read-only ProjectView. BroadcastChannel collab unchanged.
- backend (@pcbjam/backend-example): thin Fastify+ts-rest reference impl of the
@pcbjam/shared contract — serves a single project off the local filesystem
(PROJECT_DIR), no DB/auth/uploads.
- packages/contract -> web/pcbjam-shared (git submodule, MIT @pcbjam/shared).
- Remove apps/server, packages/storage, packages/contract, docker-compose
(server + storage move to the closed root repo; preserved via subtree
branches). Rewrite pnpm-workspace/turbo/.env for the frontend-only layout.
- Add tests/fixtures/demo so the editor + example backend run out of the box.
Standalone typechecks + builds; backend serves the contract end-to-end.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>