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.
Three related fixes so the GTK desktop notifications only fire
when they're actually useful.
- CollaborationSession grows an isSelf(userID:) helper so the
"don't notify myself" check reads cleanly at call sites.
Engine now stamps NotebookEntry.author with the local user on
addNotebookEntry so the check works for entries the user
authored before the server has had a chance to echo one back.
- NotebookChange gains a .snapshot([NotebookEntry]) case that
Engine fires once for the batch delivered on .join. Previously
every entry in the initial snapshot went out as .added, which
would have surfaced as a notification per historical entry
when the user joined a lab. NotebookPane routes it to
rebuildEntries.
- DesktopNotifier holds the main window and skips delivery
whenever that window is the active toplevel, so users looking
at Luma don't get a bubble over the app showing them what
they just watched happen.
The MainWindow / CollaborationPanel callback chains are also
tidied up to go via self.engine once and use the new isSelf
helper, so the optional soup shrinks. Not yet addressed:
macOS's equivalent blind-spot when the app is minimised but
still connected — APNs won't fire because the user looks
"online" to the server. Covering that wants a mirror of this
with UNUserNotificationCenter, next pass.
Replace snapshot-and-rebuild patterns with granular change
events throughout the app.
REPL cells: Engine emits onREPLCellAdded; REPLPane appends
individual cell widgets instead of clearing and rebuilding.
Notebook entries: Add NotebookChange enum with added/updated/
removed/reordered cases. Engine emits from addNotebookEntry,
updateNotebookEntry, deleteNotebookEntry, and collaboration
callbacks. NotebookPane tracks rows by ID and handles each
event incrementally.
Event stream: EventLog emits onEventsAppended with just the
new events from each flush. EventStreamPane appends matching
rows inline. Full rebuild only on filter/search change or
resume from pause.
Device list: Add DeviceChange enum to frida-swift with
appeared/disappeared cases and a changes() stream alongside
existing snapshots(). TargetPicker inserts and removes
individual device rows.
Collaboration: CollaborationSession emits onParticipantJoined,
onParticipantLeft, onChatMessageReceived. CollaborationPanel
appends participant rows and chat bubbles incrementally.