M9: collaboration: per-user undo, rejections, reconnect (#105)

* M9: collaboration: per-user undo, rejections, reconnect

Step 3 of docs/collab-protocol.md, the parts that don't need the transport:

- Per-user undo and redo (figcraft_collab::Client::{undo, redo}): each undoable batch is a step
  (values before/after by target, nodes created/deleted). Others' batches ordered after the step
  take its targets for good; creates invert to deletes (not when others built inside), deletes
  re-create the subtree. An undo or redo waits for every earlier batch and is remade from the
  confirmed state when it goes out; derived data follows as a follow-up batch.
- Rejections: refused batches leave the history (with an undo waiting on them) and queue a
  Notice; the engine shows it through Session::message (the UI's toast).
- Offline queue and reconnect: Hello.resume {session, client_id, seq}; the server keeps a
  bounded log and answers with the missed batches or a snapshot, plus what it did with the
  client's batches (processed {last_batch, refused}); queued batches then go out.
  Server::state / restore persist the session (client-id counter, log, session id).
- Presence carries editing_text {node, caret, selection}.
- Engine: documents in a live session (LiveDoc, Session::collab_*): commands, gestures and
  groups sync as one step each; edit.undo / edit.redo route to the per-user history; remote
  edits show under a gesture in progress. Outside a session snapshot undo is unchanged.
- Sim: dropped connections, undo/redo, and a checker that no undo reverts a change by someone
  else its author had seen. New tests in undo_tests, reconnect_tests, collab_live_tests,
  collab_undo_tests.
- CLAUDE.md / AGENTS.md: collab in the L2 layer list.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* M9: collaboration wording in the progress docs after per-user undo and reconnect

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* M9: protocol doc: what the engine's live-session tests cover

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
BFlat
2026-10-11 03:19:24 +07:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 4d7a9187ff
commit 94ff3d4df6
38 changed files with 2890 additions and 273 deletions
+124 -28
View File
@@ -5,8 +5,10 @@ identifiers, the object/property model, change sets, ordering and validation, cl
reconciliation, derived data and the message encoding. **Step 2 is implemented** in
`crates/collab-net` and the engine: the WebSocket transport, the session host the desktop app
embeds, invite links, document sync end to end (several apps edit one file), live cursors,
selections, avatars and follow. Undo is not per person yet (step 3). The rest is the plan the next
implementation PRs follow and update; sections say what is built.
selections, avatars and follow. **Step 3 is implemented**: per-user undo and redo, rejection
handling and offline queueing with reconnect (`crates/collab`, wired into the engine's live
session and the transport). The rest is the plan the next implementation PRs follow and update;
sections say what is built.
Goal: several people edit the same `.figcraft` file at the same time and see each other's edits,
cursors and selections live, on the desktop app and in the browser, without a central cloud
@@ -54,7 +56,9 @@ fit FigCraft's architecture:
- **Client id**: a 32-bit id the server assigns to each connection on join. The server starts
above every client id space already used by node ids in the file, so ids from earlier sessions
are never handed out again (a persistent server will also store its counter with the file).
are never handed out again. The counter is part of the session state a server saves
(`Server::state` / `Server::restore`), so a restarted server doesn't hand ids out twice either;
a client that reconnects keeps its id.
- **Node ids** are unique across clients without coordination: with a client id set
(`Document::set_client_id`), `Document::alloc_id` returns `client_id << 32 | counter`,
continuing after the highest id of that space already in the document. Ids in existing files
@@ -65,7 +69,8 @@ fit FigCraft's architecture:
created again whole (what undo does): the server accepts creating an id that doesn't exist only
if it was deleted before or lies in the sender's own id space.
- Styles, variables, collections and comments keep their existing string or numeric ids (made
unique by the engine); a client-scoped scheme for them comes with per-user undo.
unique by the engine); a client-scoped scheme for them is still to come (until then two people
creating one at the same moment can get the same id, and the later write wins).
## The document as objects and properties
@@ -140,9 +145,9 @@ Client → server:
| Message | Meaning |
|---|---|
| `Hello {protocol, file, token, name, color, client_version}` | Join a file's session. |
| `Hello {protocol, file, token, name, color, client_version, resume?}` | Join a file's session; `resume {session, client_id, seq}` when coming back after a dropped connection. |
| `Changes {base_seq, batch_id, changes: [Change]}` | One user action's changes (one undo step). |
| `Presence {cursor?, selection?, viewport?, editing_text?}` | Ephemeral; throttled to ~30 Hz, latest wins. |
| `Presence {cursor?, selection?, viewport?, editing_text? {node, caret, selection?}}` | Ephemeral; throttled to ~30 Hz, latest wins. |
| `Ack {seq}` | Highest sequence applied (lets the server trim per-client buffers). |
| `Ping` | Keep-alive. |
@@ -150,7 +155,7 @@ Server → client:
| Message | Meaning |
|---|---|
| `Welcome {client_id, seq, snapshot or delta, peers}` | Join accepted: the full document (or changes since a cached `seq`). |
| `Welcome {session, client_id, seq, catch_up: Snapshot or Delta, processed, peers}` | Join accepted: the full document, or for a reconnecting client the batches it missed; what the server did with that client's batches (`processed {last_batch, refused}`). |
| `Changes {seq, author, batch_id, changes}` | Accepted changes, in order, to everyone (the author gets its own back as confirmation). |
| `Reject {batch_id, reason, fixes: [Change]}` | A batch (or part of it) was invalid: the current values to roll back to. |
| `Presence {client_id, …}` / `PeerJoined` / `PeerLeft` | Other people's cursors, selections and arrivals. |
@@ -160,8 +165,8 @@ Server → client:
`Change` is one of: `Create {id, type, props}`, `Set {id, prop, value}`, `Delete {id}` (deleting a
node deletes its subtree; ids are never allocated twice), plus the document-level `SetDoc`, `Put`
and `Remove` (see above). A snapshot is the list of changes that builds the document from nothing;
`Welcome` carries one (deltas since a cached `seq` come with reconnect, step 3). A whole batch is
accepted or rejected.
`Welcome` carries one, or the missed batches when a client reconnects (see Offline and
reconnect). A whole batch is accepted or rejected.
## Ordering, optimism and convergence
@@ -190,7 +195,19 @@ or the JSON encoding. Tests drive it with synthetic edits and, in the engine, wi
commands (creating, moving and reparenting layers including cycle attempts, deleting, grouping,
auto layout, components and instances, styles, variables, text, pages, comments): after quiescence
every client's state equals the server's, for every seed (more seeds run as an ignored test).
Undo is not part of these runs (step 3).
The same runs with undo, redo and dropped connections are described below.
### Rejections
*Built.* When the server refuses a batch (a layer it changes was deleted by someone else, a move
would make a cycle, a value is out of range…), the `Reject` carries a short reason (a deleted
layer reads "a layer it changes was deleted by someone else") and the current values of what the
batch touched. The client drops the batch, applies the fixes to its confirmed copy (a no-op there:
every batch ordered before the rejection arrived first; they matter to clients that keep no
confirmed copy), rebuilds its view from the confirmed state and its remaining batches, and takes
the batch's step out of the undo history, together with any undo or redo of it still waiting to be
sent. It then queues a notice (`Client::take_notices`, `Notice::message`); the engine shows it as
the UI's toast (`Session::message`).
### Derived data
@@ -215,22 +232,61 @@ Undo is not part of these runs (step 3).
## Undo and redo
*Step 3.* Until then the engine's snapshot undo must not be used in a live session: it restores a
whole earlier document and would revert other people's changes made in between. The server already
accepts re-creating deleted nodes with their ids, which undoing a delete needs.
*Built* (`Client::{undo, redo}`; in the engine, `edit.undo` / `edit.redo` and the UI's undo route
there while the document is in a live session). Outside a live session the engine's snapshot undo
works exactly as before; a document entering or leaving a session starts with an empty history.
Undo stays per user and undoes only **your own** actions: an undo turns your last batch into a new
batch that writes back the values your batch replaced, for the properties nobody has changed since
(a property someone else changed afterwards is left alone). Redo is the inverse. Remote changes
never enter your undo history.
Decisions, as built:
- **A step per batch.** Every undoable batch of the user (one command, one gesture, one
`command.batch` / `Session::grouped` group) becomes a step: per target (a node property, a
document-level property or entry) the value before and after, plus the nodes it created and
deleted, each as the change that creates it as it was. Batches that aren't commands' edits
(derived-data follow-ups) never become steps.
- **"Nobody has changed it since"** is decided by server order: a batch from someone else that the
server ordered after the step's last confirmed batch takes the targets it writes away from the
step for good (a write to a list as a whole takes every entry of that list; creating a node again
takes all its properties; a taken parent takes the position too). Batches ordered before it are
what the step replaced: the step learns their values when its batch is confirmed or replayed, so
an undo restores the value that was there in server order, not the one the user happened to see.
- **Creates become deletes, deletes create the subtree again** with the same ids, positions and
values (the server accepts re-creating deleted ids). A created layer someone else has since put
layers into is not deleted (and if nothing else of the step is left, the step is skipped); a
layer whose parent is gone is not brought back. Changes are checked in order against the current
state, so an undo is never refused for what the client can see.
- **An undo or redo waits for what came before it.** It shows at once, but goes out only when
every earlier batch of the client is confirmed or refused, and is then made again from the
confirmed state. So it never rests on an edit the server refused (the step and its undo are
dropped with the edit) or on values the client hadn't seen yet. Undoing right after an edit
costs one round trip before it reaches the others; edits made meanwhile queue behind it.
- **Derived data** that the restored values imply (auto layout, groups…) is recomputed with the
recompute hook and sent as a follow-up batch, like the repairs after concurrent edits.
- **A step that no longer changes anything is skipped** (the next undo goes one step further).
- Concurrent edits the undoing client couldn't have seen (made while its undo was on the way, or
while it was offline) are ordered last-writer-wins like any other edits.
Tests: the in-memory simulation checks every undo and redo the server accepts against the log of
accepted batches: a value it changes was last written by its author (or after the client's last
seen sequence number), and every layer it deletes was created by its author or moved into the
deleted layer after that. Random sessions of synthetic edits and of real engine commands, with
undo, redo and dropped connections, converge with no violation (thousands of seeds were run while
building it; an ignored test runs more). The engine's tests (its live session with a scripted
link) also cover a gesture staying one step while others edit, a group of commands syncing as one
step, an undo right after an edit waiting for it, a refused edit's toast, a dropped connection
resuming with its offline edits, and leaving the session going back to snapshot undo.
## Presence
*Built* (`figcraft_engine::live`, `crates/collab-net`, `crates/ui-egui/src/live.rs`).
- Cursor position in page coordinates plus the page id; selection ids; viewport rectangle; whether
the user is typing in a text layer (to show their caret and selection inside it). The message is
the core's `Presence`; the engine builds yours from the active document (page, selection, view,
the user is typing in a text layer, with their caret and selection inside it
(`editing_text {node, caret, selection}`, in characters). The message is the core's `Presence`; the engine builds yours from the active document (page, selection, view,
edited text) and the pointer over the canvas, and sends it only when it changed.
- Shown as named, coloured cursors on the canvas (a pointer drawn in code with a name tag in the
person's colour, gliding to each reported position; reduced motion makes it jump), selection
@@ -284,8 +340,9 @@ never enter your undo history.
registered with the operating system yet: links are pasted into the Join dialog.
- Reconnect: a connection that drops after it was live reconnects with growing pauses (0.5 s,
doubling up to 10 s, 12 attempts); a refusal (wrong token, full) or the host ending the session
is final; a first join that can't reach the host fails at once. A reconnect is a new client id
(resuming with the last confirmed `seq` is step 3).
is final; a first join that can't reach the host fails at once. A reconnect resumes as the same
client: the guest's connection keeps the session id, client id and last sequence number from
the messages it read and sends them in its next `Hello` (see Offline and reconnect).
- The browser build has the commands but says live sessions need the desktop app (its WebSocket
client is step 4).
@@ -296,26 +353,64 @@ never enter your undo history.
its own edits go through the same ordering. A guest's welcome carries the whole document
(pages, layers, styles, variables, images): it opens as a new tab, the guest's own files stay
open, and leaving keeps it as a local copy.
- The engine syncs one open document per session (`Live::doc_uid`). After every local action it
diffs that document against the client's view into one batch (`ClientMsg::Changes`); a gesture
- The engine syncs one open document per session (`Live::doc_uid`). Every command, group of
commands or gesture on it is diffed against the client's view into one batch
(`ClientMsg::Changes`) that is one step of the user's undo history (`Session::record`); a gesture
(a drag, a resize) is sent once, when it ends, and nothing is sent while a version or a live
text preview shows. Batches the server orders (`Changes`, `Reject`) go through
text preview shows (anything changed outside a command goes out at the next frame, not
undoable). Batches the server orders (`Changes`, `Reject`) go through
`Client::receive` with the engine's recompute hook, and the resulting document is installed with
`Session::apply_remote`, which keeps your selection, page and view and never adds an undo step.
Batches that arrive during a gesture wait for it to end. New layers take ids in the client's
space (`client_id << 32 | counter`). Closing the session's document leaves the session.
- Each applied batch wakes the window (the inbox calls `request_repaint`); nothing runs while
nobody edits or moves.
- Limits of this step: undo restores whole earlier states, so it can also revert others' recent
edits until per-user undo (step 3), and the Share popover says so; a reconnect is a new client
whose welcome replaces the document, so edits made while disconnected are lost (resuming from
the last confirmed `seq` with queued batches is step 3).
## Offline and reconnect
On a dropped connection the client keeps editing locally, queues batches, and on reconnect sends
`Hello` with its last confirmed `seq`; the server replies with the missed changes, then the queued
batches are sent (they may be partly rejected if their objects were deleted meanwhile).
*Built* (`Client::{disconnect, reconnect}`, the server's log, the guest's connection in
`crates/collab-net`, the engine's live session). On a dropped connection the client keeps
editing locally, queues batches, and on reconnect sends `Hello` with its last confirmed `seq`; the
server replies with the missed changes, then the queued batches are sent (they may be partly
rejected if their objects were deleted meanwhile).
Decisions, as built:
- **Same identity.** `Hello.resume` carries the session id (random per server, kept in its saved
state), the client id and the last sequence number applied. A client this session gave its id to
keeps it (a stale connection with that id is taken over); anyone else joins as new. A client
welcomed as new after all (the server restarted without its state) starts over from the
snapshot and is told how many unsent changes couldn't be kept.
- **Catch-up.** The server keeps a log of accepted batches (`Limits::max_log_batches`, default
10 000, and `max_log_changes`, default 1 000 000). If every batch after the client's sequence
number is still in it, the welcome carries them (`CatchUp::Delta`); otherwise the whole document
(`CatchUp::Snapshot`).
- **What happened to the batches in flight.** The server remembers per client the last batch id it
processed and the latest refused ids (at most 1 024). Batches up to that id that the catch-up
doesn't confirm were refused (the user is told); the others are sent again, in order, keeping
the sequence number they were made on. Batches the server confirmed but whose echo was lost are
confirmed by the catch-up and never sent twice.
- **Undo across a gap.** With a delta, the missed batches mark the steps they conflict with exactly
as they would have live. With a snapshot, which batches came from others isn't known: a step
keeps a value only if it is still the one the step wrote (someone else setting exactly the same
value can't be told apart). An undo or redo lost in flight is made again from the caught-up
state, treating what others wrote meanwhile as later than the step.
- **Over the network.** The guest's connection keeps what it needs to resume from the messages it
reads (the welcome's session and client id, the last sequence number) and sends it in the next
`Hello`; the engine takes the connection's "reconnecting" status as the client going offline and
the next welcome as its catch-up, in order with the batches before it. What the engine sent for
a dropped connection is not carried over (the client sends again what the server didn't get),
the server ignores a batch id it already processed (a copy sent twice around a reconnect), and
the host's hub hands a resumed client id from its stale connection to the new one.
- **Session state.** `Server::state` returns what a server needs to carry on after a restart
(`SessionState`: snapshot, sequence number, session id, client-id counter, the clients that can
reconnect and what was done with their batches, deleted ids, the log), as one serde value; the
standalone server will store it.
Tests: random sessions where clients drop and come back mid-session converge, with the default log
and with logs short enough that long absences fall back to snapshots; targeted tests cover a lost
echo, a batch refused while offline, a long absence, an unknown client and a restart from the
saved state.
## Agents, MCP and the CLI
@@ -376,6 +471,7 @@ chooses to share on the network; the default is the invite link with a random to
a writer thread per connection, no async runtime; the engine holds the live state and the sync
client and reaches the network only through the `LiveLink` the app installs).
3. **Per-user undo**, reconnect and offline queueing, rejection handling, presence for text
editing.
editing. **Done** (the core's client and server, the engine's live session, the guest's
resuming connection).
4. **Browser client** (WebSocket through `web-sys`).
5. **Standalone `figcraft-server`** with persistence, compaction and accounts.
+1 -1
View File
@@ -10,7 +10,7 @@ nobody can co-edit yet ([`collab-protocol.md`](collab-protocol.md)). Remaining:
| Feature | Figma | FigCraft | Hours |
|---|---|---|---|
| Real-time co-editing | yes | **core built, no networking** (#103): client-scoped ids, per-property change sets with fractional positions, server ordering and validation, optimistic clients, binary messages; randomized multi-client convergence tests over real commands. Live sessions over the network landed (`crates/collab-net`): the WebSocket transport (keep-alive, size and rate limits, reconnect with backoff), hosting from the app (this machine or the local network) with invite links, live cursors with name tags, selections, avatars and follow, `collab.*` commands. Document sync between apps works over them (the host's server orders every batch; guests open the host's file as a new tab). Left: per-user undo, resuming after a reconnect, browser client, standalone server | 25–55 |
| Real-time co-editing | yes | **core built, no networking** (#103): client-scoped ids, per-property change sets with fractional positions, server ordering and validation, optimistic clients, binary messages; randomized multi-client convergence tests over real commands. Live sessions over the network landed (`crates/collab-net`): the WebSocket transport (keep-alive, size and rate limits, reconnect with backoff), hosting from the app (this machine or the local network) with invite links, live cursors with name tags, selections, avatars and follow, `collab.*` commands. Document sync between apps works over them (the host's server orders every batch; guests open the host's file as a new tab). Undo is per person (only your own steps, others' later writes left alone), refused edits roll back with a toast, and a reconnect resumes as the same client with what it missed (or a snapshot) and sends what was made offline. Left: browser client, standalone server | 25–55 |
| Live cursors, selections, viewports; follow / observation mode; spotlight | yes | no (presence messages encoded; no UI or transport) | 8–15 |
| Cursor chat, audio, high-fives | yes | no | 4–8 |
| Share links, permissions (view / edit), teams and projects | yes | no | 8–17 |
+6 -5
View File
@@ -60,13 +60,14 @@ No alpha blockers: the alpha gate in [`roadmap.md`](roadmap.md#alpha-gate) passe
- **Evidence:** the core landed in #103 (`crates/collab`: client-scoped ids, change sets, server
ordering and validation, client reconciliation, binary messages, randomized convergence tests over
real commands), and live sessions run over the network (`crates/collab-net`: hosting from the
app, invite links, document sync between apps, live cursors, selections, avatars and follow),
but undo isn't per person, a reconnect loses offline edits, the browser can't join, and there
are no share links or permissions (`docs/collab-protocol.md`).
app, invite links, document sync between apps, live cursors, selections, avatars and follow)
with per-person undo, refused edits rolled back with a reason, and reconnects that resume and
send what was made offline; but the browser can't join, and there are no share links or
permissions (`docs/collab-protocol.md`).
- **Impact:** Figma's defining feature; most teams use it every day. Solo designers can work
without it.
- **Estimate:** 45–90 h: per-user undo, resuming after a reconnect, then sharing and
permissions and a standalone server; the browser build needs its own WebSocket client.
- **Estimate:** 45–90 h: sharing and permissions and a standalone server; the browser build
needs its own WebSocket client.
### 3. Nothing checked against real Figma files
- **Missing:** a corpus of real documents (design systems, app screens, prototypes) imported and
+9 -2
View File
@@ -482,7 +482,9 @@ and a file keeps the newest 20 autosaves (user versions are never dropped for th
`collab.diff {against? (undo: the last undoable action, the default | saved: since the last
save)}` returns `{changes, count, bytes}`: the change batch a live session would send for that edit
(per-property `create` / `set` / `delete` and document-level changes, see
`docs/collab-protocol.md`) and its binary size. It changes nothing.
`docs/collab-protocol.md`) and its binary size. It changes nothing. In a live session, where undo
is per user (`edit.undo` / `edit.redo` take back only your own steps, see
`docs/collab-protocol.md`), only `saved` applies.
### Live sessions
@@ -502,11 +504,16 @@ what happened since the last one and sends your edits (a joined file opens as a
- `collab.peers {}` → `{available, status (off|connecting|live|reconnecting|failed), reason?,
attempt?, role (host|guest), me, name, color, fileName, pages, link, url, address, network,
follow, syncing (the document arrived), document (the index of the synced open document), peers: [{id, name, color, presence: {cursor: [page, x, y], selection: [ids], viewport:
[page, x, y, w, h], editing_text}}]}`.
[page, x, y, w, h], editing_text: {node, caret, selection?}}}]}`.
- `collab.cursor {x, y}` puts your cursor at a page position (omit both to hide it): an agent's
pointer. Your selection, page and view go out by themselves when they change.
- `collab.follow {client}` follows that person's viewport; `collab.follow {}` stops (so does any
pan or zoom of your own, or Escape).
- On the session's document every command (or `batch`, or gesture) is one step of your own undo
history: `edit.undo` / `edit.redo` take back only your steps and leave what others changed since.
An edit the host refuses (someone deleted the layer) is rolled back and the reason is the
session's message. While the connection is reconnecting, editing goes on; the edits go out when
it is back (the same client, catching up with what it missed).
### Plugins
+2 -2
View File
@@ -17,7 +17,7 @@ are one Opus 5.5 agent; most milestones split across 4–6 agents on disjoint cr
40–60 h, parallel by tool.
4. **Localization layer**, then Figma's own languages first (ja, ko, es, pt-BR, fr, de): 15–25 h
for the layer, 3–5 h per language plus native review.
5. **Multiplayer M9**: per-user undo, reconnect with offline edits, the browser client, a standalone server and sharing on top of the live sessions (the WebSocket transport, hosting from the app, invite links, document sync, live cursors, selections, avatars and follow landed), per [`collab-protocol.md`](collab-protocol.md): 45–90 h.
5. **Multiplayer M9**: the browser client, a standalone server and sharing on top of the live sessions (the WebSocket transport, hosting from the app, invite links, document sync, live cursors, selections, avatars and follow, per-user undo and reconnect with offline edits landed), per [`collab-protocol.md`](collab-protocol.md): 45–90 h.
## Alpha gate
@@ -55,7 +55,7 @@ blockers.
| M6 Interop & Dev Mode | ~55% | `.fig` import, video/HEIC/TIFF, CMYK intent, Code Connect | 40–80 |
| M7 Release & fidelity (signed builds, look & feel, shortcuts) | builds done; fidelity pass started | tool-by-tool pass | 40–60 |
| M8 Hardening & performance (bench, retained canvas, accessibility, idle) | ~60% | real-file corpus, platform QA, GPU canvas, screen readers | 60–110 |
| M9 Real-time collaboration | core done (#103: ids, change sets, server ordering, convergence); live sessions (WebSocket transport with limits and reconnect, hosting from the app, invite links, document sync between apps, cursors, selections, avatars, follow, `collab.*` commands) | per-user undo, resuming after a reconnect, `figcraft://` link handler, browser client, standalone server, sharing | 45–90 |
| M9 Real-time collaboration | core done (#103: ids, change sets, server ordering, convergence); live sessions (WebSocket transport with limits and reconnect, hosting from the app, invite links, document sync between apps, cursors, selections, avatars, follow, `collab.*` commands); per-user undo, rejection handling, offline queue and resuming after a reconnect | `figcraft://` link handler, browser client, standalone server, sharing | 45–90 |
| M10 Localization (new) | not started | layer + 11 languages | 60–100 |
| M11 Plugins | runtime, API, menu, manager and SDK done (#101) | run-time plugin UI, settings storage, distribution, widgets; Figma plugin compatibility is an owner decision | 25–55 |
| M12 AI features (new, owner decision) | not started | local models | 30–60 |
+9 -3
View File
@@ -441,7 +441,14 @@ Hours in this narrative are the builders' estimates; the tables above supersede
and a client that shows its own edits at once and replays them over others'; derived data
(auto layout, instances, bindings) is repaired by the author after concurrent edits; binary
messages (a moved layer is 43 bytes, a cursor 23) with a JSON form; a randomized multi-client
test over real commands converges.
test over real commands converges. Live sessions run over the network (hosting from the app,
invite links, document sync, live cursors, selections, avatars, follow). Undo is per person in
a session (`edit.undo` takes back only your own steps, leaving what others changed since;
creations and deletions invert; a gesture or command group stays one step); refused edits roll
back with a toast; a dropped connection keeps editing and resumes as the same client with what
it missed (or a snapshot), then sends what was made offline. Randomized sessions with undo,
redo and dropped connections converge, and a checker confirms no undo reverts someone else's
change.
Plugins landed: sandboxed WebAssembly plugins (a metered wasmi
interpreter with a memory cap and no WASI, also in the browser) whose API is the command
registry; each run is one undo step and is rolled back when it fails (error, trap, endless loop,
@@ -453,8 +460,7 @@ Hours in this narrative are the builders' estimates; the tables above supersede
plugins, and `plugin.*` commands for scripts, the CLI and MCP; a Rust SDK and two example
plugins (Tidy names, Swatch grid) in `plugins/`, built by `cargo xtask plugins`. *Left:*
plugin-defined UI at run time (forms or panels a plugin returns), plugin settings storage,
distribution; for collaboration, the WebSocket transport, hosting and joining from the app,
live cursors, per-user undo, reconnect, the browser client and a standalone server. 18–45 h.
distribution; for collaboration, the browser client and a standalone server. 18–45 h.
9. **Hardening and performance**: *landed:* `cargo xtask bench` (synthetic 1k/10k/50k-layer
pages: engine, render and headless UI-frame timings). The canvas keeps its last image and
re-renders only what a pan uncovers or an edit touches: moving one layer at 10k layers re-renders