Verdict: ~3-5 days. pcbnew undo already UUID-guards stale pointers; core work = porting that guard to eeschema, splitting connectivity out of the SCH SKIP_UNDO gate, plugging the SKIP_UNDO removed-item leak, silencing the incomplete-undo modal. Cross-linked from 09 and the overview index. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019ejJEvS7ogef2o9gVTXjmp
7.3 KiB
Research 19 — Feasibility of miss-09 option 1: exclude remote applies from undo
Status: research (2026-07-07), not implemented
Verdict: moderately hard but tractable — ~3–5 days. The pointer-invalidation risk
09 flagged is already solved on the pcbnew side;
the core work is porting that resilience to eeschema, plus two small fork fixes to
make SKIP_UNDO safe for our use.
Question
Miss 09's option 1 is "local-ops-only undo": remote applies stop landing on the
receiving editor's undo stack, so Ctrl+Z can never revert a peer's work. Both commit
classes support a SKIP_UNDO push flag — but 09 warned that KiCad's undo machinery
assumes the stack mirrors model history, and that PICKED_ITEMS_LIST holds raw
pointers which remote applies could strand. How bad is that really?
What makes it easier than 09 feared
The flag exists on both sides and skips only what we want (almost — see fix 2).
SKIP_UNDO is honored by both commit classes (pcbnew/board_commit.h:40,
eeschema/sch_commit.h:40; base signature include/commit.h:132). Under it the
commit still does everything remote applies go through a real commit for:
board/screen add/remove, view->Add/Remove/Update, connectivity dirty-marking and
ratsnest (board_commit.cpp:463-467,516), teardrops, listener callbacks
(OnItemsCompositeUpdate / OnSchItems*), and the model-change events. Only the
undo-picker creation and the final SaveCopyInUndoList are gated
(board_commit.cpp:325,356,455,606; sch_commit.cpp:266,295,363,448).
Exactly four Push sites to change, each already remote-tagged. All remote data
funnels through doApply / doApplyItems per editor
(wasm/bindings/eeschema_embind.cpp:1093,1218;
wasm/bindings/pcbnew_embind.cpp:965,1061), every one bracketed by the
s_applyingRemote guard — the exact local/remote signal, available at the Push call.
Deletes, adopt, and the parked-dirty catch-up all funnel through doApplyItems;
there is no fifth production path.
Change detection is unaffected. The local-edit→yjs emit path is the
listener-trigger() + post-settle flushDiff, suppressed during remote applies by
s_applyingRemote (eeschema_embind.cpp:652, pcbnew_embind.cpp:871) — entirely
independent of whether the commit is undoable. Skipping undo disturbs neither the
suppression during applies nor the emit on genuine local edits (including the emit of
a local undo itself, which is what makes undo propagate today).
Pcbnew undo already survives items vanishing behind its back. Our fork's
PutDataInPreviousState checks every picker against BOARD::ResolveItem( uuid ) and
drops entries whose items no longer exist (pcbnew/undo_redo.cpp:428-437), and even
re-resolves stale CHANGED pointers by UUID before swapping the clone image back in
(:504, comment cites ExchangeFootprint-style swaps). Group parent/member pointers
are rebuilt from KIIDs cached in the picker itself
(undo_redo_container.h:114-115, rebuilt at undo_redo.cpp:633-658). So on the PCB
side, a remote apply deleting or replacing an item referenced by a local undo entry
degrades gracefully (entry dropped) instead of crashing.
The actual work
-
Port the UUID guard to eeschema — the core task.
eeschema/schematic_undo_redo.cpp::PutDataInPreviousState(:246) has no existence or re-resolution check: it dereferences the stored rawEDA_ITEM*directly (:270,399,431); only the group-relink post-pass usesSchematic().ResolveItem(:485,504). SincedoApplyItemsupserts full item subtrees as remove-by-uuid + re-add, a remote change (not just a delete) to an item referenced by a local undo entry means a dangling pointer → crash on Ctrl+Z. Fix: mirror pcbnew's pattern — existence-check each picker viaSCHEMATIC::ResolveItem(eeschema/schematic.h:129), drop missing entries, re-resolveCHANGEDpointers beforeSwapItemData. ~50–100 lines, follows an in-tree precedent, plausibly upstreamable. This is the bulk of the effort. -
Un-bundle connectivity from
SKIP_UNDOon the SCH side. Unlike pcbnew,SCH_COMMIT::pushSchEditgatesRecalculateConnectionsinside the sameif( !( aCommitFlags & SKIP_UNDO ) )block asSaveCopyInUndoList(sch_commit.cpp:448-463). Remote applies need that recalc — it is the reason they go through real commits. Small fork change: split the gate (or run the recalc from the binding after Push). -
Plug the removed-item leak under
SKIP_UNDO. In the REMOVE branch the live item is flaggedUR_TRANSIENT(board_commit.cpp:447) but withSKIP_UNDOno picker takes ownership and the clone is deleted (:490), so the removed item leaks; same shape on the SCH side (sch_commit.cpp:409-410). ExistingSKIP_UNDOcallers (DRC-marker flows,drc_tool.cpp:208etc.) never remove user items, so upstream never hit this. Fix: delete removed items in the commit when the flag is set (or in the binding, which knows the removed set). -
Silence the "incomplete undo" modal. When pcbnew drops a stranded entry it pops a blocking
wxMessageBox( "Incomplete undo/redo operation: some items not found" )(pcbnew/undo_redo.cpp:628). In a collab session that is routine, not exceptional — downgrade to a log line (or toast) in the fork, and use the same quiet path in the eeschema port.
Plus wasm-side flag changes at the four Push sites (~one line each) and multi-client e2e coverage for undo-after-remote scenarios (delete-under-undo, change-under-undo, adopt-while-history-nonempty), which given the two-tab harness is roughly another day.
Residual semantic gaps (accepted, not blockers)
- Full-clone restore clobbers concurrent field edits. Undoing a local move of an
item restores the whole old item image (
CHANGEDswaps the full clone), so a peer's concurrent edit to another property of the same item is reverted too. Last-writer-wins collab undo — same as Figma; converges; not an operational rebase. Same granularity trade-off as 14. - Duplicate-KIID re-add edge. Local delete parks the item on the local undo stack
(owned,
DELETEDpickers skip the existence check —undo_redo.cpp:423); if a peer re-adds the same uuid and the local user then undoes the delete, the stored item is re-added alongside the peer's copy with the same KIID (:569-572). Rare; the differ keys by uuid so it likely reconciles to one, but needs a test. - Pre-existing local entries at adopt time mostly reference replaced items and get dropped by the UUID guard on first undo — acceptable, and strictly better than the current adopt undo bomb.
Effort summary
| Piece | Where | Size |
|---|---|---|
SKIP_UNDO at the 4 remote Push sites |
wasm bindings | trivial |
SCH connectivity/SKIP_UNDO split |
kicad fork (sch_commit) | small |
| Removed-item leak fix | kicad fork (both commits) | small |
| eeschema UUID guard port | kicad fork (schematic_undo_redo) | 1–2 days, core |
| Modal → quiet drop | kicad fork (undo_redo) | trivial |
| Two-client undo e2e | tests/ | ~1 day |
All fork touches are small and upstream-plausible, consistent with keeping the fork
close to upstream. Miss 09's option 3 (clear undo stack on adopt) becomes largely
moot: with SKIP_UNDO, adopt creates no undo entry at all, and stranded pre-adopt
entries are handled by the UUID guard.