Collaboration in Moonkale grows out of something that already ships: the session bus. Every window or tab of one user is a peer; a user on another machine is a peer behind the server. Same messages, different transport — so the multi-window feature built in Milestone 1 is the first, zero-latency, single-actor form of collaboration. This note extends it to many people. Related: Version Management (the entity log is the shared history), LLM and RAG (agents are peers too).
Layers
flowchart TB subgraph now["shipped (Milestone 1)"] BUS["session bus: Hello · SourceOpened · DragStarted/Ended · Moved"] T1["desktop: in-process channels"] --> BUS T2["web: BroadcastChannel (same browser)"] --> BUS end subgraph next["presence (Phase 3)"] AW["awareness: cursors, selections, open documents, who is here"] T3["server: websocket hub per workspace, fan-out to all peers"] --> AW end subgraph later["shared editing (Phase 4–5)"] CRDT["text CRDT per document (Loro) + entity log for structure"] end BUS --> AW --> CRDT
1. Session bus (exists)
moonkale_ext_api::session: WindowId, SessionMessage, trait SessionBus { send }. A window announces itself (Hello), peers reply with their open sources, and documents can be dragged between windows (DragStarted → drop target in every other peer → Moved → origin closes). The transport is per platform; the messages are serde JSON so they already survive a browser hop (which is why Version is serialised as a string — 64-bit hashes don’t fit in a JavaScript number).
2. Presence / awareness
Small, frequent, ephemeral state that is never stored in the entity log: where each peer’s cursor and selection are, which document is active, a display name and colour, “is typing”. Modelled on Yjs’s awareness protocol:
Presence {
peer: PeerId, // (user, window) — a user with two windows is two peers
user: UserId, name, colour,
focus: Option<NodeId>, // active document
cursor: Option<{ node: NodeId, anchor: usize, head: usize }>, // char offsets
seen_at: Timestamp, // peers expire when it goes stale
}- Rendering: other users’ carets and selections as decorations in the Code Editor (the decoration channel already exists for highlights/diagnostics) and as coloured dots on Explorer entries and tabs; a “who’s here” list in the status bar.
- Transport: the same session bus, extended with
Presence(Presence); on desktop within one process it is free, across machines it goes through the server hub. - Cost model: presence is sent on change, throttled (~20 Hz cap), last-write-wins per peer, dropped when stale. Nothing here needs consistency.
3. The server as synchronisation hub
ADR-0005 Server functions as the remote backend already makes api the place where sources live for web clients. Collaboration adds one websocket per (client, workspace):
- Fan-out: every
SessionMessage/Presencea client sends is relayed to the other clients of that workspace. Desktop clients connect to the same hub when they open a shared workspace (“connect to team server”), so desktop and web users share one session. - Authority: the server owns the sources (folder or database), so it is naturally the serialisation point for writes; today’s optimistic
Versioncheck becomes “the server applies transactions in arrival order and broadcasts the resulting events” — which is exactly the entity log of Version Management being appended by many actors. - Auth and rooms: workspace = room; users authenticate to the hub; permissions per source (read/write) — the same permission model extensions get (Host API Reference).
- Offline/desktop-first: the local entity log keeps working without a hub; on reconnect, logs merge (structural events commute; content goes through the CRDT).
4. Shared editing
Two people typing in the same document need more than versions: a text CRDT per document (loro or automerge, both Rust; Loro’s rich-text and tree types map well onto Markdown and flow graphs). Plan:
Document.textbecomes a CRDT text; the editor backend applies remote ops assetText/splices, local edits become ops. The current whole-documentchangeevents would be replaced by real splices at this point (P-037).- The entity log records a
Contentevent per CRDT transaction — history stays one log. - Saving to a folder source writes the merged text; git sees ordinary commits.
- Conflict-free structure (adds/removes) is already given by the entity log’s set semantics.
What the multi-window work decided for collaboration
- Messages are typed Rust enums with serde, never ad-hoc JSON — so the server hub can be written in the same types.
- A window is identified by a random
WindowId, a user later byUserId; presence uses the pair. Nothing assumes one window per user. - Sources are attached by descriptor (
AttachSource): a peer that learns of a source can open it without a path — the exact shape a hub needs to hand a newcomer the room’s sources. - Drag-and-drop between windows is a move (
Movedcloses the origin) — not a shared edit. The gesture is the workbench tab itself (already an HTML5 drag with its id as data). The OS-level drop is used where the platform delivers it; otherwise the offer persists as a banner in every other window afterdragend(“README.md was dragged from another window — Move it here”), so the gesture degrades to grab-then-click instead of failing (P-044). Two windows editing the same document is deliberately unsupported until the CRDT exists; the conflict is caught by the version check on save.
Milestone 8: presence exists —
moonkale_server::presenceis a hub with a room per folder id; members carry{window, name, active document}; the client publishes on every active-document change and shows others as initials badges (status bar, tabs, Explorer). No cursors yet, web only (desktop's session bus stays local). Milestone 8 - Implementation Log.
Open questions (→ Problem Ranking R-34, R-30)
- Presence for the Graph View: show other users’ viewports/selections on the graph? Probably yes, same channel.
- Should agents publish presence (“agent X is reading main.rs”)? Useful for trust; cheap.
- Hub scale: one Axum task per room with a broadcast channel is enough for teams; beyond that, a message broker.
Where a shared server stands (2026-09-20)
Modes, what exists and the security model for several people on one server (accounts, roles, the terminal): Remote and Server Modes.