Ctrl+Z after a peer's edit no longer reverts (and re-broadcasts) the
peer's work, and the adopt undo-bomb is gone:
- doApply/doApplyItems (both editors) Push with SKIP_UNDO; the emit path
is unaffected (suppression keys off s_applyingRemote, not undo).
- With SKIP_UNDO no picker owns removed items — the bindings free them
after Push (explicit removals + upsert's remove-before-re-add; fields
excluded: CHT_REMOVE hides them, parent keeps ownership). Freeing stays
out of the fork commit classes so DRC's SKIP_UNDO callers can't
double-free.
- Test hooks kicadCollabTestUndo/UndoDepth, registered per-editor AND in
the kicad_editor dispatcher (merged image compiles out per-app
registrations).
- kicad pointer: eeschema UUID undo guard + SKIP_UNDO connectivity split
+ quiet stale-entry drop (ca8877324c).
- tests/kicad/collab-undo.spec.ts: 5 scenarios (no undo entry from remote
applies; selective undo; stranded replaced/deleted entries) — 5/5, plus
collab/ysync regression 30 pass.
- docs: ysync-review 20 fix record; 09 marked FIXED; overview indexed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ejJEvS7ogef2o9gVTXjmp
While a peer has an item selected, local users can still select it for
inspection but move/drag/rotate/delete skip it with an infobar naming the
holder (native locked-item UX; enforced via the fork's PCBJAM_REMOTE_LOCK
query — kicad 81f9cd80fd, the epic's first fork-touching phase). Overlapping
holds (both grabbed inside the awareness propagation window) tie-break
deterministically: lowest (user.id, clientID) keeps the item, every losing
client auto-releases it.
- lock-tiebreak.ts: pure policy — beats(), remoteLocks() (union of ALL other
clients' selections incl. own user's other tabs, minus own-held-and-winning
uuids so the winner isn't blocked mid-release), contestedReleases()
- presence.ts: clients() (per-client view, no user dedupe) + self(); FIX for
a pre-existing flaky stack overflow — resolveCollision re-entered itself
synchronously via its own patch's awareness 'change' and could ping-pong on
stale same-user states (~1-in-3 unit runs); re-entrancy guard defers
re-resolution to the next genuine delivery
- presence-kicad.ts: locks ride the kicadCollabSetRemote snapshot
(`locks:[{uuid,name}]`); losing overlaps call kicadCollabReleaseSelection
- wasm bindings (both TUs + merged dispatch): g_locks map + fork query
install; kicadCollabReleaseSelection (cancelInteractive only when a tool
stack is live — bare ESC would clear the whole selection — then selective
RemoveItemFromSel + infobar + forced re-emit); kicadCollabTestGetLocked
- tests: lock-tiebreak unit suite; presence-locks e2e for both editors (real
move veto — pcbnew click+M hotkey since its default left-drag is
rubber-band select, eeschema real drag — each with an unlocked control);
two-tab tests/web/locks.spec.ts (lock propagation + deterministic tiebreak
release + unlock on clear, passing vs real partykit)
Spec: docs/features/collab-presence/0007 (closed repo).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lg5jwWuhFH5dL8hEcBDuP2
Selecting a symbol in eeschema ghost-highlights the linked footprint(s) in
every pcbnew tab of the project, and vice versa — across users AND one
user's own two tabs. Native KIWAY cross-probe is inert in WASM (one frame
per page); this rides the presence layer instead.
- cross-app.ts: project-wide awareness-only room (presenceRoomId), publishes
full PresenceState at selection rate (cursor always null); peers() = other-
TOOL clients incl. own user's other tabs; window.__pcbjamCrossApp test handle
- presence-kicad.ts: parseSelectionEmit (bare array | {uuids,fpPaths}),
xselFromPeerState (pcbnew paths → symbol uuids; eeschema uuids verbatim),
cross peers appended to the kicadCollabSetRemote snapshot as
{id "<user>#x<client>", name "<user> · sch|pcb", xsel}
- C++ (zero fork changes): pcbnew emits {uuids, fpPaths} (FOOTPRINT::GetPath)
and ghost-renders xsel via path-tail suffix scan; eeschema resolves xsel via
ResolveItem gated to the CURRENT sheet (xsel arrives project-wide, unlike
room-scoped selections); ghostStyle = alphas × xselAlphaScale (0.55, tuner-
patchable); new exports kicadCollabGetSelectionFull / TestGetCrossMapped /
TestSelectComponent (skips power symbols — PWR_FLAG has no footprint) +
merged-image dispatch
- tests: presence suites extended (payload shape, ghost render pixel tests,
13/13) + new two-tab tests/web/cross-probe.spec.ts (passing vs real
partykit); eeschema pixel compares now target the #glcanvas-* GAL panel
(the whole-window #canvas compare flaked on the auto-dismissing version
infobar — also fixes the long-known presence-eeschema restore flake);
cross-app + presence-kicad vitest suites
Spec: docs/features/collab-presence/0006 (closed repo).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lg5jwWuhFH5dL8hEcBDuP2
The full root-cause chain of the empty/mangled badges:
- GAL text justify is painter-residue → PRESENCE_TEXT_OVERLAY pins TOP-LEFT.
- VIEW_OVERLAY::ViewDraw hard-sets EVERY overlay to GetMinDepth(), so the
shapes and text overlays always collided at one depth, where later-drawn
fragments lose (and bitmap glyphs are textured quads whose transparent
cells also write depth — punch-through produced cell-shaped holes instead).
Fix rides the new fork VIEW_OVERLAY::SetDepthOffset: shapes at min+1,
labels at min — 'rect first, text on top' now holds regardless of paint
order. Verified: chips contain crisp names (dark-on-light, white-on-dark)
for selection tags and cursor labels; presence suites 10/10.
- kicad pointer bump (fork 24c5854d5b).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CvqUd4QsJSGHN28aunJRTq
Two root causes of the wandering/invisible badge text:
- A plain VIEW_OVERLAY draws with whatever text justify the LAST painter left
in the GAL (CENTER is only the reset default) — anchoring was
nondeterministic. PRESENCE_OVERLAY (VIEW_OVERLAY subclass, replaces
MakeOverlay) pins TOP-LEFT justify before executing its commands; the label
math is written against that.
- The whole overlay draws at ONE depth and same-depth fragments drawn later
LOSE the depth test — a chip rect over its text erased the text. Draw the
text FIRST, the chip rect AFTER: the rect is rejected exactly on the glyph
pixels, punching the text through.
Verified live: chips contain their names on light (dark text) and dark
(white text) user colors, for both selection tags and cursor labels.
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
All seven ysync-review bugs fixed and verified; every repro's expected-fail
marker removed (they now run as regression tests). Also fixes two bugs found
while verifying (doc 17 F5/F6): file-seeded Y bodies are re-upserted in the
editor's serialization (doc-16 F4 was an artifact of bug 01), and the
0008-era "asyncify-fragile envelope parse" was really wrapInBoardEnvelope
emitting display layer names — canonical LSET::Name() fixes track/via/zone
v2 applies; makeFromBlob now logs parse errors instead of swallowing them.
Verified: shared 98, standalone collab 37, ysync e2e 20/20 (chromium),
collab regression set 21 passed / 3 pre-existing skips (firefox).
Bumps: kicad (board_commit child-removal listener notification),
web/pcbjam-shared (slot prune + arbitrated seed).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JgThWXtdvrYLK47EDFoGdq
The 2026-07-02 sync review (docs/features/ysync-review, ysync-review branch)
found 7 bugs and that the two-tab e2e only exercised the DEAD legacy scalar
wire. This lands plan doc 15 in full; results + empirical findings in doc 16.
- tests/collab/browser-entry-v2.ts (+build.mjs): the PRODUCTION v2 stack
bundled for e2e (connectKicadDoc + attachKicadCollab, kdoc_* keys), with
in-page renderActiveDoc/singleSeedRender/driftReport helpers and yjs forced
to ONE copy (the two web pnpm workspaces otherwise bundle two
instanceof-incompatible instances).
- tests/kicad/ysync-two-tab.spec.ts: pl_editor green baseline (A↔B edits,
ITEM-level drift silence) + divergent-uuid adopt; bug-01 pcb/ee fresh-room
repros (Chromium-only: two kicad_editor tabs exceed Firefox's per-process
wasm budget); bug-06 concurrent-seed race; bug-03 Y-half.
- tests/kicad/ysync-repros-{pcbnew,eeschema}.spec.ts: bugs 02/03/05 + the
bug-04 matrix (anchor-centred fp rotation, pad resize, endpoint drag,
symbol rotation, Value-field edit), each with green landed-preconditions;
the "local move emits" controls double as headless-emit probes — GREEN on
both tools, so every emit-dependent repro is a live test.fail.
- wasm/bindings: 7 local-edit test hooks via real commits
(CallAfter+COROUTINE) — TestRemoveItem/TestRotateItem (both tools,
dispatched in the merged image), TestSetPadSize/TestMoveEndpoint (pcbnew),
TestSetFieldText (eeschema).
- web/standalone ysync-repros.test.ts: bug-01 units (C++-faithful fake gating
emit on ensureBridge) + bug-07a/b (stale DOWN hook, real sheet-manager gap).
- web/pcbjam-shared bump: bug-03/06 unit repros.
Convention: every repro asserts the CORRECT behavior and is expected-fail
(test.fail/it.fails) naming its bug doc; a fix flips it to "unexpected pass",
forcing marker removal — the repro becomes the regression test. Every
expected failure verified (JSON reporter) to fail at its documented assert.
Suite state: 39 passed / 0 failed / 0 flaky / 5 skipped (2 firefox guards,
2 pre-existing legacy two-tab skips, 1 pre-existing roundtrip fixme).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DPfrVhfYgPPgtawjSssZfn
All four editors (PCB / Footprint / Schematic / Symbol) are now runtime --frame
choices of a single kicad_editor.wasm (178 MB at -O1 vs 147+82 separate; shared
wx/common/boost linked once). One editor per page load, as before; frames pcb /
fpedit / sch / symedit.
- wasm/editor/: the merged executable target (single_top + both kiface library sets,
whole-archive pcbcommon) + the safety-net focus-walk Kiface() dispatch TU. Gated by
KICAD_WASM_MERGED_EDITOR (kicad submodule bump carries the fork side: per-engine
Kiface/getter binding + ODR renames + dual-kiface launcher).
- wasm/bindings/: per-editor collab entries renamed pcbCollab*/schCollab* (JS names
unchanged); duplicate kicadOpenFile/kicadCollabOnSave + shared-name registrations
guarded behind KICAD_MERGED_EMBIND; new kicad_editor_embind.cpp registers each
shared JS name once, dispatching on the live frame.
- Build: kicad_editor app (build wrapper, target case arms, 3-object embind compile
with the ABI-critical flags, STUB_APP=pcbnew); docker/build.sh "all" =
kicad_editor calculator pl_editor gerbview (pcbnew/eeschema stay as explicit debug
apps); scripts/kicad/audit-merged-symbols.sh = repeatable ODR-collision audit (run
on kicad bumps).
- Frontend: Bundle type (bundle ≠ tool); TOOL_BUNDLE maps all four editors to
kicad_editor; explicit --frame tokens for pcbnew (pcb) and eeschema (sch); publish
list = the 4 real bundles.
- Tests/CI: five harnesses load kicad_editor.js with explicit frame tokens;
PCBNEW_FAMILY_SPECS renamed BIG_MODULE_SPECS + the 8 eeschema-family specs (they
now boot the merged module — SpiderMonkey x86 CI OOM routing); frame-runtime spec
covers all four frames from the one bundle.
Validated so far: frame-runtime 4/4 (each frame boots with the right title, no
aborts, no duplicate embind registration); 24-spec merged-module regression green;
3D raytracer renders. Known pre-existing failure: 3d-viewer title-bar drag deadlock,
fixed on main by 7630c7e (2N+8 pthread pre-warm) — picked up by the follow-up rebase.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add kicadCollabSnapshotItems / kicadCollabApplyItems / window.kicadCollab.onItems
to pl_editor, eeschema, pcbnew — per-item native-blob payloads
({added/changed:[{sexpr,parent}], removed:[uuid]}) alongside the untouched scalar
wire (legacy collab specs stay green).
- pl_editor: itemBlob per item; apply = SetPageLayout(append) + replace-by-uuid
(pointer-snapshot safe); bare payloads get the kicad_wks envelope. Snapshot +
apply + emit verified headless.
- eeschema: clipboard Format per item (symbols carry their lib_symbols); apply
mirrors the native paste — LoadContent into a throwaway sheet → detach →
replace-by-uuid → symbol lib relink (blob's lib_symbols first, live screen's
second) → SCH_COMMIT, in the CallAfter+COROUTINE context. Snapshot + apply
verified headless (a "lost" lone junction turned out to be correct connection
cleanup — test uses text).
- pcbnew: blobForItem per ROOT item with child→footprint lifting in flushDiff;
apply = makeFromBlob + commit replace-by-uuid on the fiber; bare non-footprint
payloads get wrapInBoardEnvelope (live board layer table). Snapshot + footprint
replace/add WITH children (the 0004 containment gap, closed) + removal verified
headless. Track/via/zone/text blob-apply hits the documented asyncify-fragile
envelope parse (reconfirmed empirically — a verbatim SaveSelection segment
envelope dies silently in the commit) and stays on the legacy scalar apply;
tracked in ysync 0008 status.
- eeschema/pcbnew scheduleFlush now runs flushDiff inside a COROUTINE: the
per-item Format in the v2 emit needs the fiber stack (0007 lesson). Their emit
remains unverifiable headless (both legacy two-tab tests are test.skip:
"open=false → SCH_COMMIT no-ops" / "harness can't PAINT") — verify in the real
app at Stage D; pl_editor's emit IS verified.
- tests/kicad/items-bridge.spec.ts: per-tool suite (snapshot uuids → local-edit
emit (where drivable) → apply changed/added/removed via save-readback → no
apply echo). 3/3 pass; roundtrip + collab suites unaffected.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Block A save embind exports + the round-trip integration harness.
Save exports (wasm/bindings, GPL):
- pcbnew: kicadSaveBoard(path) via PCB_IO_KICAD_SEXPR::SaveBoard(GetBoard()).
- eeschema: kicadSaveSchematic(path) via SCH_IO_KICAD_SEXPR::SaveSchematicFile.
Saves GetCurrentSheet().Last() (NOT Schematic().Root()): the wasm open-flow
nests the opened doc under an auto-created project root, so Root()'s screen
holds only a child-sheet symbol, not the loaded items.
(pl_editor already had kicadSaveDrawingSheet.)
Round-trip harness (tests/kicad/roundtrip.spec.ts):
- load fixture → save (ORIG) → kicadCollabSnapshot → reload (fresh wasm) →
open empty → kicadCollabApply → save (REGEN) → assert sexprDiff(ORIG,REGEN).equal.
- Two separate pages (extract closed before rebuild opens) so the process-global
wasm heap frees between boots (pcbnew ~190MB). Open both sides from the same
filename (eeschema embeds it as the root Sheetfile property). pcbnew boots the
seeded pcbnew-collab.html (plain pcbnew.html's first-run wizard blocks boot).
- pl_editor + eeschema round trips PASS (lossless). pcbnew is test.fixme with
tracked apply-coverage findings (footprints/zones not reconstructed; segment
width + via size lost; fp_text→gr_text) — bridge gaps for follow-up, not test bugs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The batched-emit fix still lost segments on a large connected drag (peer
dropped the P3-C1 wire). Deeper cause: the SCHEMATIC_LISTENER fires in
pushSchEdit BEFORE RecalculateConnections (sch_commit.cpp ~402 vs ~430), so
every emit was pre-cleanup RAW geometry; the cleanup that follows (merge
collinear wires, drop/split junctions) was never broadcast. The peer rebuilt
the raw edit and ran its own cleanup over a different dirty scope, so the two
peers cleaned up differently and the peer lost segments.
Replace the listener-list emit with a post-settle full-model snapshot DIFF
(snapshotByUuid), flushed via CallAfter once Push (cleanup included) returns —
capturing tab A's final, already-clean geometry. The peer applies that and
re-cleaning already-clean geometry is idempotent, so they converge. The native
listener is now just a change trigger. g_baseline holds the last-broadcast
state; doApply and kicadCollabSnapshot rebaseline so applied/seed items aren't
re-broadcast (echo). Mirrors pl_editor's snapshot-differ; no kicad-fork change.
Verified two-tab, rigorously (real edit: tabA state changed AND tabA===tabB
byte-for-byte): a wire reroute plus U1A/U1B/C2 symbol drags all converge
exactly. eeschema-collab + eeschema-ui suites green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A single SCH_COMMIT::Push fires OnItemsAdded/Removed/Changed separately and
synchronously, then RecalculateConnections once. COLLAB_LISTENER emitted each
category as its own delta, so the peer applied one atomic edit as three separate
commits, each with its own connectivity recompute. On a large connected drag the
junction at the wires' new crossing (added) was applied before the wires moved
(changed) -> dangling junction -> the peer's cleanup deleted it (lost segments).
COLLAB_LISTENER now buffers added/changed/removed (serialized in each synchronous
callback) and flushes one combined delta after Push returns, coalesced via
CallAfter. doApply applies it atomically removed->changed->added in a single
commit with one recompute, so the junction survives. Verified two-tab: a G-drag
of U1A that previously left the peer at 74 items / 7 junctions now emits one
delta {a:1,c:4,r:1} and both tabs converge at 75 / 8.
Root-repo only; kicad and wxwidgets untouched.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Thrust A — dialogs render top-left with OK clipped in the WEB app (not the
test harness): root cause was the React shell missing the .window /
.window-canvas CSS that wx.js relies on (it positions each dialog div via
inline left/top, which need position:absolute). Added the rules to
web/apps/frontend/src/index.css. Native draw-text now works end-to-end;
symbol/power choosers render (placing still blocked by absent libraries).
Thrust B — collab apply of a newly-added SCH_SHAPE trapped in KiCad core
(SCH_COMMIT::Push CHT_ADD -> GAL view->Add, an asyncify invoke_* mis-dispatch)
because doApply ran off a fiber stack. doApply now runs inside a COROUTINE so
it executes on a libcontext fiber, the same context native draws use; the add
dispatches correctly. Re-enabled the SCH_SHAPE converter (rect/circle). Added
thirdparty/libcontext to the embind include path (tool/coroutine.h needs it).
Verified two-tab: rectangle + circle drawn in tab A sync + render in tab B.
Extended eeschema-collab.spec.ts apply test with a SCH_SHAPE add.
All changes root-repo only; kicad and wxwidgets forks untouched.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
After the body-move devirtualization, moving a symbol synced the body to the peer but
left its reference/value text behind: SCH_SYMBOL::Move()/SCH_LABEL_BASE::Move() move the
child fields via an inner virtual field.Move() that also mis-dispatches in the apply
context. Move the fields explicitly with a devirtualized SCH_FIELD::Move (moveFields).
Verified: a moved symbol's text label now follows the body on the peer.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The changed-path used the virtual SCH_ITEM::Move(), which silently no-ops from the
apply/CallAfter context (asyncify call_indirect mis-dispatch) for every non-wire item —
so moving a symbol synced on the sender but not the peer. Devirtualize Move() with an
explicit class-qualified call (moveItemTo), which is statically bound (a plain call, not
call_indirect) and executes. Verified: a symbol move now propagates. Also bumps wxwidgets
to ea599f7 (toolbar clicks no longer steal canvas keyboard focus).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extend doApply added-item construction beyond wires: SCH_TEXT, SCH_LABEL /
SCH_GLOBALLABEL / SCH_HIERLABEL (position + text + label shape), and SCH_NO_CONNECT.
Serialize text (any EDA_TEXT) and label shape in itemToJson. Moving/deleting existing
items of any type already worked (generic changed->Move and removed->Remove); this adds
their reconstruction on add.
Still uncovered: SCH_SYMBOL (needs lib-symbol + fields/orientation) and graphic shapes
(SCH_SHAPE). Known issues to fix next: adding text freezes the app; circles (SCH_SHAPE)
don't sync; deletes don't apply; wire moves only partially converge.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two things, both verified in the real web app (two-tab eeschema collab).
1. dynCall crash fix (all apps) — scripts/common/shims/dyncall-binding.js.tmpl.
Programmatic editor edits trapped with 'indirect call signature mismatch': the
asyncify-instrumented wasmExports[dynCall_<sig>] trampoline does call_indirect with a
stale type for some table indices (post-asyncify+O2) even though the table entry is
valid. Proven by patching the built js: at the trap getWasmTableEntry(index) SUCCEEDS
where the trampoline fails. Fix: the shim now catches the 'signature mismatch'
RuntimeError and falls back to getWasmTableEntry; the Asyncify unwind sentinel and real
exceptions re-throw, so instrumentation/unwind is untouched for normal calls. This
unblocks ALL programmatic edits, not just collab (e.g. eeschema SCH_ITEM::Move).
2. eeschema collab apply converters (wasm/bindings/eeschema_embind.cpp).
doApply now handles added-item construction (build the SCH_ITEM with the delta's uuid
via const_cast — as the s-expr parser does — + commit.Add) and richer SCH_LINE
serialization (start/end/layer) so wire edits reconstruct on the peer. Implemented for
SCH_LINE (wires) + SCH_JUNCTION; other types log 'no converter for added type' and are
skipped (next batch). eeschema re-enabled in the web app collab gate.
Tests: eeschema-collab.spec snapshot (green); apply/two-tab skipped — they no-op headless
because the e2e harness's kicadOpenFile returns false (OpenProjectFiles bails before
building the connectivity graph), so SCH_COMMIT::Push doesn't persist. Verified in-app.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
eeschema's half of the Yjs collaborative bridge, reusing the generic reconciler /
BroadcastChannel transport unchanged. Zero kicad-fork change: native SCH_ITEM uuid +
native SCHEMATIC_LISTENER. All in the wasm layer (wasm/bindings/eeschema_embind.cpp).
Working (verified in the web app):
- kicadCollabSnapshot(): enumerate sch.Hierarchy() -> LastScreen()->Items() as
{id,type,x,y}; registers the listener on first call
- emit: SCHEMATIC_LISTENER subclass -> per-item delta via window.kicadCollab.onDelta;
fires on real SCH_COMMIT::Push (a real wire move broadcasts added/removed/changed)
Apply is a documented follow-up (gated off so a peer tab can't crash): SCH_ITEM::Move
traps with 'indirect call signature mismatch' when invoked outside a KiCad tool
coroutine (Asyncify+fiber+exception-trampoline). Modify/Clone/GetPosition all work;
only the virtual Move write traps. Fix direction: route apply through TOOL_MANAGER.
Also: build-kicad-target.sh now force-relinks when only <app>_embind.cpp changed (the
embind .o isn't a make dep, so new bindings silently vanished), and adds the
expected/rtree/fmt thirdparty includes the eeschema bindings need.
Tests: eeschema-collab.spec.ts covers snapshot (green); apply/two-tab skipped with the
blocker noted. WasmTool gates collab to pl_editor only until eeschema apply works.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>