A notebook cell holds a Pharo snippet and what it produced, updated
field by field and synced back when it is re-run. Trace blobs move
to a store of their own, read through one loader.
The method mark's editor keeps its own keys and clicks, so editing
inside an opened body works like editing anywhere else.
Replace the four per-mutation notification types (+add/+update/
+remove/+reorder) with a single +op envelope carrying {op_id,
kind, ...}. Pending mutations persist to a `notebook_outbox`
table on the client and replay on every .joined transition, so
an offline user's work propagates as soon as they're back on the
bus. Projects that never enable collaboration keep a zero-row
outbox — we only touch it once `collaboration_state.lab_id` is
set, and bulk-enqueue existing entries on createLab's success.
Model changes, in the same pass:
- NotebookEntry swaps `author: Author?` for `editors: [Author]`
(creator at index 0, each editor appended as the server applies
their update op). The boolean `isUserNote` is gone; callers now
read `entry.kind == .note` directly.
- Entries carry a `position: Double` — server-authoritative on
add (`max + 1000`), client-computed midpoint on drag. Clients
sort by `(position, id)` so concurrent reorders converge.
- Schema folds the new columns into v1 (no migration — Luma isn't
shipped).
UI:
- Mac: header renders a stack of editor avatars (tooltip = name,
tap opens the GitHub profile). Entry rows are draggable and
accept drops on top/bottom halves to compute the insertion slot.
- GTK: same avatar stack; reordering lives in the right-click
menu (Move Up / Move Down) since SwiftGtk's DragSource binding
isn't verified yet on this branch.