- A branch keeps its own version history. On a branch tab, `file.saveVersion`, `version.autosave` and the `version.*` commands (list, preview, restore, rename, delete, duplicate) act on the branch's versions; the main file's history is untouched. The Version history panel on a branch tab lists the branch's versions (its banner gives way to the version banner while one is viewed).
- Branch versions are saved in the `.figcraft` file with the branch (`branches/<id>/versions/index.json` plus one snapshot each, the same layout as the file's own history; images shared; damaged entries skipped and reported; the snapshot byte budget is shared across branches).
- Merging adds a version to the main file's history (`Merged "<name>"`, after an autosave of main when it had changed); the merged or archived branch keeps its own history and opens with it.
- Fills, strokes and effects merge item by item in `figcraft_collab::merge`: different items changed on each side, or items added at the end on one side (or the same ones on both), merge without a conflict; the same item changed both ways, an item removed on one side, or different items added on both sides still conflict as the whole list. Both sides' writes become the merged list, so merging and Update from main write the same result.
A behaviour suite for comments and version history written from the public documentation
(`crates/engine/src/history_behaviour_tests.rs`, 15 tests, about 80 table rows plus stepwise checks),
each row with a one-line derivation; where the documentation is silent the row says so and pins
today's behaviour. 4 of the 15 tests failed on the old code. Fixes, all in shared engine code:
- **Live sessions: comment threads made in the session couldn't be used.** Their ids come from the
creator's id space (e.g. 4294967297) and every thread command (`comment.reply`, `edit`, `react`,
`resolve`, `move`, `delete`, `markRead`, `open`, `link`) refused them as "out of range".
Comment ids are now parsed as ids, not small indexes.
- **Restoring a version lost newer comments.** Comments are a conversation about the file, kept
apart from its design history: restoring now keeps the current comments; a comment on a layer
the version doesn't have stays where it was (on the nearest surviving parent, or the canvas).
- **Restoring now keeps the replaced design in the history** as an autosave first, unless a
version of it was just saved or restored (so work isn't only in the session's undo history).
The checkpoint marker is now "none yet" for a freshly opened file and compares contents when
normalising copied the document.
- **A titled autosave becomes a named version** (`version.rename` clears `auto`), so it is no longer
dropped when autosaves make room.
- **Merging a branch adds to the main file's history:** the main file as it was (an autosave,
when it changed since its last version), then `Merged “<branch>”`.
- `version.restore` refuses up front for a view-only live session (nothing goes into the history).
Pinned where the documentation is silent: pins go to the innermost frame under them (not to a
leaf layer); copies, duplicates and duplicated pages come without comments; replying reopens a
resolved thread; only authors delete their messages (no accounts); stepping past the last thread
stays on it; comments show on the canvas by default; versions stay with whoever saved them in a
live session.
`crates/engine/src/collab_live_tests.rs`: the test `Room` is `pub(crate)` so the suite can run a
live session. `crates/engine/src/cmd/props.rs`: one line reformatted by `cargo fmt` (it was
unformatted on the base, which failed the fmt check). `crates/render/src/effects_behaviour_tests.rs`:
the paint suite expected an offset drop shadow to grow the export on every side, but the later export
suite made exports grow only where the shadow falls (40 x 50 for a shadow 10 down); the base failed
the gate on it, so the expectation now says "grows" (wider or taller). No new commands.
Docs: collaboration-parity, target-app-parity (area 16 85%/60% -> 85%/62%, 2-5 h -> 2-4 h; full
parity ~306-595 h -> ~306-594 h; percentages unchanged at this rounding), ROADMAP (also corrects the
stale area 16 row, 80%/58%/4-8 h), gaps (gap 4's landed list), mcp.md.
The desktop app joins wss:// sessions (rustls with webpki roots, certificates always checked, clear errors; FIGCRAFT_CA_FILE for private authorities), with one TLS session shared by the reader and writer threads. figcraft-server stops gracefully on SIGTERM/SIGINT/SIGHUP/Ctrl+C, ships in Linux x86_64/aarch64 release archives, and docs/server.md gains a Deploy on a Linux server guide with a hardened systemd unit, Caddyfile, nginx config and Dockerfile in packaging/server.
A file's library can be published to figcraft-server and enabled in other files by a share link (view access is enough); placing, styles, variables and library updates with overrides kept work as for local libraries, with a background check while the Assets panel is open. library.swap re-points instances, styles and variables to another library by name (one undo step, unmatched items listed). The server keeps per-file usage reports (library.usage).
Branches live inside the .figcraft file (base and head snapshots, own id space). Update from main and merge are three-way merges over the collaboration change sets; a conflict (same property changed on both sides, or delete vs edit) must be resolved branch-or-main before merging, which is one undo step on main. The review dialog reuses Dev Mode's compare view; branch tabs show a banner; File > Create branch; branch.* commands for CLI and MCP.
figcraft-server hosts many files over the same hub the app uses for sessions, persists each as a checksummed snapshot plus an append-only log (synced before replies, compacted, damaged files skipped and reported) so restarts lose nothing confirmed, and gives per-file share links with edit or view access (hashed 128-bit tokens, revocable). The app can publish a file to a server and join server-hosted files; view-only tabs refuse edits. Protocol version 2.
- Styles, variables, collections, modes and comment threads created in a live
session take ids from the creator's client space (figcraft_doc::ids: a
number from the node id allocator, client_id << 32 | counter); outside a
session ids are made exactly as before. The server refuses a new entry in
another connected client's space (removed entries can be put back by undo),
keeps removed entries in its saved state, and starts client ids above every
space used by any kind of id.
- Others' text carets and selected characters show on the canvas in their
colour at the glyph positions, with their name above the caret (no blinking,
no frames of their own). Typing in a layer someone else is typing in says so
(toast and tag); collab.peers reports who types where.
- Toasts wrap at a readable width.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
- Browser client (step 4): the web build joins sessions a desktop app hosts
through the browser's WebSocket API (web-sys, binary frames, the same
message encoding). Its connection logic is a target-independent state
machine (collab-net driver.rs): hello on open, changes and presence held
until the welcome, throttled presence, keep-alive pings, a silent host
counted as dropped, reconnect with backoff, final refusals. Tested natively
against a scripted socket and a real host over loopback. Hosting stays
desktop-only (LiveLink::can_host; collab.peers canHost). https pages are
told up front that they can only reach ws:// sessions on this computer.
- Shared join logic (join.rs: GuestConfig, outcomes, taking in host messages)
and transport limits (limits.rs) used by the native guest and the browser.
- Invite links: `figcraft figcraft://join?...` joins on start (a bad link
opens the Join dialog with it); file:// arguments are paths. Windows
registers a per-user figcraft:// opener (HKCU\Software\Classes\figcraft)
behind a Registry trait (tests use a fake): collab.registerLinks {on?} and a
Preferences checkbox, default on for installed builds, never for builds
from source. macOS Info.plist declares CFBundleURLTypes; the Linux desktop
entry declares x-scheme-handler/figcraft and passes %U.
- Join dialog: Escape closes it and no longer reaches the canvas (selection
kept); its field has the keyboard when it opens.
- Docs: collab-protocol (browser client, links, mixed content), mcp,
control-protocol, README, web hosting notes, collaboration wording in the
progress docs (numbers unchanged).
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* 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>
* M9: real-time collaboration protocol (design)
docs/collab-protocol.md: WebSocket sessions with a server-ordered,
per-property last-writer-wins document sync, client-scoped ids,
fractional positions for children, ephemeral presence (cursors,
selections, follow), per-user undo, hosting from the app first and a
self-hosted server next, and the implementation plan. ROADMAP gains M9.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M9: collaboration core: ids, change sets, server ordering, convergence
Step 1 of docs/collab-protocol.md, no networking:
- Client-scoped node ids: Document::set_client_id makes alloc_id hand out
client << 32 | counter; single-user files allocate as before; the id
counter is no longer part of document equality.
- crates/collab (layer 2): the synced object/property model (every saved
Node field, plus document-level styles, comments, variables, images,
dev snapshots, library data), Change batches, an O(changed) diff, strict
and lenient apply, fractional sibling positions, snapshots.
- Server (ordering, validation, rejects with fixes) and Client (optimistic
view, replay under remote batches, author-side repair of derived data)
as pure state machines; binary (version byte + postcard, JSON values) and
JSON message forms, fuzzed decoders.
- Engine: normalize as the recompute hook, collab.diff.
- Tests: round trips over random real commands, an N-client convergence
fuzz test over delayed in-order links, junk batches.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M9: fmt after merge
* M9: live sessions: host from the app, invite links, live cursors and presence
Step 2 of docs/collab-protocol.md, networking and presence half:
- crates/collab-net (layer 6): WebSocket transport (sync tungstenite, a
reader and a writer thread per connection, no async runtime), the
session host the app embeds (runs figcraft_collab::Server as the
authority, carries its messages), invite links, presence throttling
(~30 Hz, latest wins), limits (sockets, hello/idle timeouts, message
size, rate, slow clients), keep-alive, clean close, reconnect with
backoff; a browser stand-in until the web-sys client (step 4).
- engine: live session state (status, peers, presence, follow) and the
collab.host/stop/join/leave/peers/follow/cursor commands; the network
is reached only through the LiveLink the app installs.
- ui-egui: remote cursors (pointer drawn in code, name tag, gliding),
selection outlines (once documents sync), avatars in the right panel
with follow, the Share popover (start, invite link + URL with Copy,
stop), File > Join live session.
- desktop app and MCP headless session install the link; xtask layers
keeps networking crates out of L0-L5.
- Documents don't sync between apps yet (follow-up).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M9: live sessions: document sync between apps
- The host's session runs figcraft_collab::Server seeded with the shared
document; every app (the host's too, through the hub without a socket)
is a collab Client. Guests open the host's file from the welcome as a
new tab; leaving keeps it as a local copy.
- Engine: after each local action (a gesture once it ends; nothing during
version or text previews) the session document is diffed into one
Changes batch; server batches go through Client::receive with
EngineRecompute and are installed with Session::apply_remote, which
never touches the undo history. Batches arriving mid-gesture wait.
- Transport: Changes/Reject/Welcome reach the engine as Event::Sync;
LiveLink::send carries batches in order; hellos are limited to 64 KiB,
joined clients to the core's message size.
- Tests: engine sync against a real Server (ids in the client's space,
one batch per gesture, remote edits skip undo, deferral, closing the
file leaves), three apps over loopback converging (create, move,
recolour, delete, rename) and a late joiner getting the server's state.
- Docs: collab-protocol, mcp, README, roadmap docs; the UI's
"cursors only" note is gone, replaced by the per-person undo caveat.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Progress docs per craftrules standards/progress-docs.md: target-app parity, gaps, roadmap with alpha gate, readiness by audience (full / mainstream / essentials) with Opus 5.5 hours, and auto layout, components, prototyping, dev mode, collaboration, format, hardware, UI and localization parity docs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>