loom v2
Loom v2: Mobile Multiplayer Interface for LLMs
Section titled “Loom v2: Mobile Multiplayer Interface for LLMs”Mobile Loom Vision
Section titled “Mobile Loom Vision”Mental Model
Section titled “Mental Model”Each rollout appends candidate futures along a path, and the user can splice branches into a shared continuation. On mobile, treat this as a DAG that is locally tree-like. The user sees one path as a strip, with cross-links revealed on demand through zoom or peeking. Moire — Language models are multiverse generators Moire — Block multiverse visualizations.
Primary Canvas
Section titled “Primary Canvas”Use an infinite canvas with kinetic panning and pinch zoom. The “sheet” is a vertically oriented strip for the active lineage, while side branches live in gutters. Semantic zoom reveals topology as you zoom out, and reveals text and controls as you zoom in. Miro — Infinite canvas Bederson — Pad++ ZUI Microsoft — Semantic zoom.
Generation Gesture
Section titled “Generation Gesture”Scroll down to the end of the current strip and pull past a threshold to trigger N candidates, like pull-to-refresh but grounded in generation. A small probability band displays a “block multiverse” miniature that previews near-future mass and lets you tap to lock a candidate before it appears as a full child. Moire — Block multiverse visualizations Moire — Loom: interface to the multiverse.
Discard Gesture
Section titled “Discard Gesture”Swipe a paragraph card horizontally to “rip” it off the sheet. Provide a visible affordance and an alternate action button so the gesture is not hidden-only. Play a brief success haptic and show an accessible undo. On Android, map to swipe-to-dismiss patterns and predictive back where appropriate. Apple HIG — Gestures Apple HIG — Accessibility Apple HIG — Playing haptics Material 3 — Gestures Jetpack Compose — SwipeToDismissBox.
Branch Reveal and Splice
Section titled “Branch Reveal and Splice”Two-finger press-and-hold at any paragraph opens a “branch rail” that lists sibling continuations. Drag a sibling chip into the strip to splice it, which adds an edge into the current path rather than cloning text. A dotted join marker communicates that the node has multiple parents. Moire — Loom: interface to the multiverse Moire — Language models are multiverse generators.
Focus + Context on Phones
Section titled “Focus + Context on Phones”Use a peek panel for the block multiverse. Tapping the panel zooms to a token-level preview, then renormalizes and pins the chosen partial sequence. This keeps the probability sidecar usable on a small screen. Moire — Block multiverse visualizations.
Viewport and Layout Rules
Section titled “Viewport and Layout Rules”The active strip always anchors center. Side branches compress into chips when zoomed in, and expand to node-link when zoomed out. Respect Fitts-like constraints by keeping touch targets at least platform minimums and bundling controls near thumbs. Miro — App wireframe on infinite canvas Apple HIG — Accessibility Material 3 — Gestures.
Empty State and Recovery
Section titled “Empty State and Recovery”A blank sheet invites you to type a prompt. If a rip removes the last paragraph, show a ghost node and an undo CTA. Offer a history timeline that replays edges added or removed. This mirrors DAG provenance while keeping the phone interaction simple. Moire — Loom: interface to the multiverse.
Authoring Flow
Section titled “Authoring Flow”- Type or paste context.
- Pull to generate.
- Swipe away weak continuations.
- Long-press to reveal sibling rail, drag to splice.
- Pin key nodes as bookmarks for Read mode.
This keeps human steering central, consistent with cyborgism. LessWrong — Cyborgism Moire — Loom: interface to the multiverse.
Technical Sketch: Data
Section titled “Technical Sketch: Data”Store a DAG with nodes keyed by canonical state, and edges labeled by op and params. On mobile, cache only the active strip, sibling rail, and a small ring buffer for recent branches. Persist to a compact JSON with an edges array. Bederson — Pad++ ZUI Moire — Loom: interface to the multiverse.
// React Native style types for a DAG-first mobile Loomtype NodeID = string;type EdgeID = string;
interface Node { id: NodeID; text: string; // current prefix chunk meta?: { modelId?: string; logprobs?: number[]; bookmark?: boolean; };}
interface Edge { id: EdgeID; parent: NodeID; child: NodeID; op: "generate" | "splice" | "rip"; params?: Record<string, unknown>; // e.g., n, temperature, top_p ts: number;}
interface Doc { nodes: Record<NodeID, Node>; edges: Record<EdgeID, Edge>; root: NodeID; activePath: NodeID[]; // linearization for Read mode}Technical Sketch: Rendering
Section titled “Technical Sketch: Rendering”Use a Skia or Canvas layer for the strip and chips to keep 60 fps with gesture compositing. Virtualize paragraphs and lazy-load side-branches. Clamp zoom levels and use semantic zoom thresholds to switch between text, chips, and node-link. Microsoft — Semantic zoom Bederson — Pad++ ZUI.
Apple HIG — Gestures Material 3 — Gestures.
Haptics and Accessibility
Section titled “Haptics and Accessibility”Use light impact on generate, success impact on splice, and warning on rip. Provide visible buttons that mirror gestures, larger hit targets, and VoiceOver labels for edges and nodes. Respect user settings for reduced motion and haptics. Apple HIG — Playing haptics Apple HIG — Accessibility.
Why This Fits Loom
Section titled “Why This Fits Loom”The infinite sheet preserves the narrative feel, while adaptive zoom and rails keep the DAG tractable on a phone. The user steers selection and merging rather than delegating to an autonomous agent, consistent with cyborgism. Moire — Language models are multiverse generators LessWrong — Cyborgism.
Multiplayer Architecture
Section titled “Multiplayer Architecture”Make Mobile Loom multiplayer: real-time coauthoring on an infinite sheet where scroll spawns generations and swipe rips context, all while the underlying state is a DAG that converges under concurrency. Yjs docs Yjs Awareness Shapiro CRDTs.
Architecture at a Glance
Section titled “Architecture at a Glance”- Local-first CRDT for the shared state (text blocks, nodes, edges).
- Awareness channel for presence, cursors, selections.
- A coordination service that serializes expensive LLM calls and caches results.
- Optional end-to-end encryption for content.
Pick Yjs for speed and mobile memory footprint or Automerge v2 for JSON-native ergonomics. Both converge without locks. Yjs docs Automerge v2 Preguiça CRDTs.
Data Model (CRDT)
Section titled “Data Model (CRDT)”Represent the sheet as two CRDT collections: nodes (text blocks with metadata) and edges (parent→child links). Allow multiple incoming edges per node to permit DAG rejoin. Use a move-capable tree or graph strategy so reparent and splice converge even under conflict. Shapiro CRDTs Kleppmann tree-move.
// CRDT-friendly schema (Yjs or Automerge-style)type NodeID = string;type EdgeID = string;
interface Node { id: NodeID text: string // block text attrs?: { modelId?: string; logprobs?: number[]; author?: string } tombstone?: boolean // rip without physical delete}
interface Edge { id: EdgeID parent: NodeID child: NodeID op: "generate" | "splice" | "rip" params?: Record<string, any> // n, temperature, etc. ts: number}Operations and Conflict Rules
Section titled “Operations and Conflict Rules”- generate(parent, k, params) - adds k edges from parent to new or de-duplicated children.
- splice(child, newParent) - adds an additional incoming edge to child; never loses history.
- rip(node or edge) - sets tombstone remove-wins on the element; readers hide it, but history persists.
Use remove-wins sets for rips and Kleppmann’s move for safe reparenting. Shapiro CRDTs Kleppmann tree-move.
Transport Choice
Section titled “Transport Choice”Default to client-server with y-websocket for reliability, offline buffering, and easy horizontal scale. For small private rooms or local networks, you can add a WebRTC path, but full-mesh topologies do not scale with many peers. y-websocket y-webrtc README MDN DataChannel.
Presence and Awareness
Section titled “Presence and Awareness”Use Yjs Awareness to broadcast lightweight presence: cursors, current focus node, and a “branch rail” selection. Keep awareness out of the document history to prevent log bloat. Yjs Awareness.
Coordination for LLM Calls
Section titled “Coordination for LLM Calls”A single coordination actor per document prevents duplicate generations and races. Durable Objects work well: they are globally unique actors with built-in WebSockets. On generate requests, the DO takes a short lease on the parent node, fans out model calls, writes results as CRDT updates, and caches by (parent, params, model, seed). Cloudflare Durable Objects.
Job Queue Alternative
Section titled “Job Queue Alternative”If you prefer a pull-based broker, back the coordinator with Redis Streams. Use consumer groups per document to single-flight jobs and fanout results to subscribers. Redis Streams.
Security Model
Section titled “Security Model”- Baseline: TLS to the provider and auth tokens per room.
- E2EE option: encrypt node payloads client-side, leave edges in clear for topology. Matrix-style pattern: Olm for device-to-device key share, Megolm for group message ratchet. Presence can remain plaintext. Matrix E2EE guide Olm/Megolm tutorial.
Mobile UI Semantics for Multiplayer
Section titled “Mobile UI Semantics for Multiplayer”- Infinite sheet remains the primary view. Other users’ cursors appear as colored handles at paragraph edges. Tap a handle to peek that user’s lineage.
- “Rip” becomes a shared soft-delete. Conflicts resolve remove-wins, with undo available locally until synced.
- “Splice” offers a picker of suggested siblings from other users near your cursor.
- A small presence rail shows avatars currently “holding” generation leases, so you do not collide. Yjs Awareness Yjs docs.
Offline and Intermittent Connections
Section titled “Offline and Intermittent Connections”CRDTs allow edits offline. On reconnect, deltas merge and the coordinator revalidates leases. Awareness gracefully times out for absent peers without polluting history. Yjs Awareness Automerge sync.
P2P Note
Section titled “P2P Note”If you must go serverless in a tiny room, use y-webrtc with RTCDataChannel for updates. Expect O(n²) peer links, so cap room size or add a super-peer relay. y-webrtc README MDN DataChannel.
Cost, Attribution, and Provenance
Section titled “Cost, Attribution, and Provenance”Attach cost metadata and authorship to each edge: who requested, tokens used, model, params, and cache key. Your Read view can display per-branch cost totals. Store a signed hash of prompt+params to make results reproducible. Cloudflare Durable Objects Redis Streams.
Implementation Sketch - Providers
Section titled “Implementation Sketch - Providers”- Yjs + y-websocket for content and awareness.
- Coordinator: Durable Object (or a small Node/Go service) hosting a doc room, exposing a WS for CRDT delta broadcast plus an HTTP endpoint for generation jobs.
- Optional: Automerge Repo or Y-Sweet if you want a batteries-included CRDT backend. y-websocket Automerge repo Y-Sweet.
// Client: join room and request a generation leaseconst ydoc = new Y.Doc()const provider = new WebsocketProvider(URL, roomId, ydoc)const awareness = provider.awarenessawareness.setLocalState({ user: me, cursor: { node: activeNode } })
// Ask the coordinator to single-flight a generateawait fetch(`/rooms/${roomId}/generate`, { method: "POST", body: JSON.stringify({ parent: activeNode, k: 4, model: "gpt-4.1", seed: 42 })})Determinism and Caching
Section titled “Determinism and Caching”For reproducible branches, include model version and a fixed seed. The coordinator dedupes by cache key and returns existing children when available. This keeps rooms fast and cheap under load. Cloudflare Durable Objects.
Testing Checklist
Section titled “Testing Checklist”- Concurrent rip vs splice on the same child.
- Move storms: many users reparenting a subtree.
- Lease loss during mobile suspend.
- E2EE key share to a new device.
- P2P fallback with 6+ peers to validate mesh caps. Kleppmann tree-move y-webrtc README.
Tradeoffs Summary
Section titled “Tradeoffs Summary”- y-websocket scales and is easier to run; y-webrtc saves server cost but caps room size.
- Yjs is very fast; Automerge is JSON-native and simple to reason about.
- DOs simplify single-flight and room fanout; Streams are great when you already run Redis. y-websocket Automerge v2 Cloudflare Durable Objects Redis Streams.
Linear Editing Model
Section titled “Linear Editing Model”Objective
Section titled “Objective”Let the user edit a single scrolling “page” with no visible graph. The DAG should update itself behind the scenes so branches, splices, and reuse remain correct and convergent. The trick is to separate the linear editing model from the structural model and bridge them deterministically. Yjs Y.Text Automerge Text Shapiro CRDT survey.
Three-Layer Approach
Section titled “Three-Layer Approach”- Page layer: one collaborative text object holds the visible page.
- Projection layer: invisible anchors mark where DAG nodes begin and end on that text.
- Committer: a debounced worker translates recent page edits into minimal DAG changes.
Anchors use RelativePosition so they survive insertions and deletions without drifting, and the committer applies intent-preserving rules for edits that cross boundaries. Yjs RelativePosition Peritext.
Anchoring the DAG to the Page
Section titled “Anchoring the DAG to the Page”Maintain a sparse index of node-boundary anchors: each anchor stores {nodeId, edgeId?, side} and is attached to the text via RelativePosition. When the user types or deletes, anchors stay attached to their logical characters, so the projector can always map page spans back to nodes without rescanning the whole document. Yjs RelativePosition.
When to Update the DAG
Section titled “When to Update the DAG”Use a short debounce on idle or caret pause, plus explicit triggers on large structural edits like paste or block move. Conceptually this is like an interactive rebase of the active lineage: we rewrite the minimal suffix that keeps ancestry valid while preserving reachable historical nodes. Git Book — Rewriting History git-rebase docs.
Minimal Rewrite Rules
Section titled “Minimal Rewrite Rules”- Edit inside a single node span: create a new node with updated text, add edge from the original parent, and redirect the active path to the new node. Children of the old node are reattached if their prefixes still match, else they remain as alternate futures. CRDT survey.
- Edit that spans multiple node spans: merge the touched spans into one new node and add edges from all original parents. This is a multi-parent splice; mark a provenance list on the new node. Peritext Move-op paper.
- Cut or “rip”: do not hard-delete. Set a tombstone flag on edges or nodes with remove-wins semantics so concurrent undo stays well-defined. CRDT survey.
- Reparent via drag or paste: use a true move operation on the structural CRDT rather than delete+insert to avoid dupes and cycles. Move-op paper.
Data Model Notes
Section titled “Data Model Notes”Nodes remain content-addressed by a stable key such as hash(normalize(text) + modelId + memory). If the new text equals an existing node’s key, we reuse that node and only add an edge, which deduplicates repeated passages automatically. CAS evaluation.
Committer Algorithm
Section titled “Committer Algorithm”- Read changed ranges from the text CRDT transaction.
- Expand to covering node-boundary anchors.
- Produce a minimal set of node creations, edge insertions, and tombstones that makes the active path match the new text.
- Apply a move operation if spans were reordered.
This pipeline is deterministic for a given text delta and anchor map, which keeps replicas convergent. Yjs Y.Text Peritext Move-op paper.
// Pseudocode for the committerfunction commitFromTextDelta(tx: TextDelta, anchors: AnchorIndex, dag: Dag) { const ranges = inflateToAnchors(tx.changedRanges, anchors) for (const r of ranges) { const oldNodes = anchors.nodesCovering(r.before) const newText = pageText.slice(r.after.start, r.after.end) const reused = dag.findByContentKey(newText) const newNode = reused ?? dag.createNode({ text: newText }) dag.spliceMultiParent(newNode, parentsOf(oldNodes)) // move-op safe dag.tombstoneEdges(edgesSupersededBy(oldNodes, newNode)) // remove-wins anchors.rebindRangeToNode(r.after, newNode) // RelativePos }}Yjs RelativePosition Yjs Y.Text Move-op paper.
Read Mode Stays Simple
Section titled “Read Mode Stays Simple”The page is always a single linear CRDT string. Read mode just renders that string. The DAG is only consulted to show provenance, alternate branches on demand, or to reuse existing nodes when a user splices passages. This preserves seamless editing and mental simplicity. Automerge Text Yjs Y.Text.
Concurrency and Intent
Section titled “Concurrency and Intent”Because all users type into one text CRDT, character-level merges are handled there. The committer then derives structure, using Peritext-style intent rules to avoid surprising block merges when edits cross boundaries, and the move-op to keep reparenting safe under races. Peritext CSCW paper Move-op paper.
UX Polish
Section titled “UX Polish”- No “graph flicker”: only the text updates immediately. DAG updates happen off the main thread, then provenance badges and branch chips fade in.
- Undo and redo operate on the page first. Structural undo derives from the same history.
- If a rewrite would orphan a large subtree, show a non-blocking toast with an option to keep that subtree reachable via a labeled side edge.
These keep the DAG invisible unless the user asks for it. CRDT survey.
Testing Checklist
Section titled “Testing Checklist”- Cross-boundary paste produces one merged node and multi-parent edges.
- Concurrent rip vs splice resolves remove-wins but preserves an undo window.
- Reorder paragraphs triggers move-op, not delete+insert.
- Determinism: identical text states yield identical DAG states across replicas.
Use the tree move CRDT tests and text CRDT transaction logs to validate. Move-op paper Yjs Y.Text.
CRDT survey Yjs RelativePosition.
Implementation Details
Section titled “Implementation Details”Design Goal
Section titled “Design Goal”On mobile the user edits a simple scrolling page. Behind the scenes the app derives and updates a DAG without exposing graph mechanics. We achieve this with a page-first CRDT, stable anchors, and a deterministic committer that rewrites just the necessary graph edges. Yjs Docs.
Page Layer - What the User Edits
Section titled “Page Layer - What the User Edits”Use a single Y.Text as the visible page bound to CodeMirror 6 in a SolidStart client-only route. Lazy import CM6 on mount to shrink JS. This keeps SSR isolated and prevents hydration issues. CodeMirror 6 Docs SolidStart clientOnly.
Projection Layer - Invisible Anchors
Section titled “Projection Layer - Invisible Anchors”Mark logical node boundaries with Y.RelativePosition anchors stored in a Y.Map. Relative positions stick to content under concurrent inserts or deletes, so the projection always knows where nodes begin and end without rescanning the whole string. Yjs RelativePosition.
Graph Storage - Fragments or Subdocs
Section titled “Graph Storage - Fragments or Subdocs”Represent each DAG node as either a named fragment inside the root doc or a Y.Doc subdocument when you want lazy load or very large nodes. Subdocs are first class in Yjs and can be fetched on demand by the server layer. Hocuspocus guides also describe multi document patterns if you defer subdocs. Yjs Subdocuments Hocuspocus multi docs guide.
Committer - How Text Turns into DAG Updates
Section titled “Committer - How Text Turns into DAG Updates”Debounce on idle or paste. For each changed range, expand to covering anchors, then emit the minimal set of operations: create or reuse a node by content key, add edges from all parents that contributed text, tombstone superseded edges, and apply a move operation when paragraphs were reordered. Because the input is a Yjs transaction plus anchor map, replicas compute identical graph updates. Yjs Docs.
// Pseudocode - derive minimal DAG edits from a Yjs text transactionfunction commitFromTx(tx, anchors, dag) { const ranges = expandToAnchors(tx.changedRanges, anchors) for (const r of ranges) { const newText = page.slice(r.after.start, r.after.end) const node = dag.findByKey(hash(newText)) ?? dag.createNode({ text: newText }) dag.spliceMultiParent(node, parentsCovering(r.before)) // add incoming edges dag.tombstoneEdges(edgesSupersededBy(r.before, node)) // soft delete anchors.rebind(r.after, node.id) // move anchors }}Presence and Multiplayer
Section titled “Presence and Multiplayer”Keep the page collaborative and fast. Use Awareness for cursors and selection highlights, but do not store presence in the document history. Awareness is an ephemeral CRDT that broadcasts lightweight state. Awareness.
Server Transport and Scale
Section titled “Server Transport and Scale”Prefer a server-authoritative WebSocket with Redis pub or sub for horizontal scale and late-join reliability. y-websocket is the reference provider, with y-redis as a scalable backend. Hocuspocus offers an opinionated Yjs server with auth hooks and persistence. y-websocket docs y-redis Hocuspocus.
Persistence and Snapshots
Section titled “Persistence and Snapshots”Persist the root doc and subdocs periodically by encoding updates with Y.encodeStateAsUpdate, and keep point-in-time snapshots for recovery or migrations. This enables deterministic restore for the page and the derived graph. Yjs Document Updates.
Mobile Performance Tips
Section titled “Mobile Performance Tips”Anchor the editor route client-only, lazy load bindings, keep the Awareness payload small, and cache subdocs or fragments for the active strip plus a small ring buffer of nearby branches. These patterns come straight from the Yjs documentation and provider guidance. SolidStart clientOnly y-websocket docs Awareness.
Optional High Performance Server
Section titled “Optional High Performance Server”If you need a Rust path, the yrs-warp server implements the Yjs protocol on Warp Tokio, which pairs well with mobile workloads where you want low tail latency and tight memory control. yrs-warp.
How This Feels to the User
Section titled “How This Feels to the User”They type and swipe as if no graph exists. The page string is the source of truth for viewing and editing. The DAG only appears when they open Visualize or provenance. All concurrency is resolved at the text layer, and the committer deterministically keeps the graph in sync. Yjs Docs.
Drop-in Steps for Your SolidStart + Yjs Setup
Section titled “Drop-in Steps for Your SolidStart + Yjs Setup”- Make the route client-only and lazy load CM6 and y-bindings. 2. Add an anchor index using RelativePosition. 3. Implement the debounced committer shown above. 4. Store graph nodes as fragments now and migrate hot paths to subdocs when size dictates. 5. Switch your transport to a Redis-backed server with snapshots. SolidStart clientOnly Yjs RelativePosition y-websocket docs y-redis Yjs Document Updates.
One-Shot Build Brief for a GPT-5 Codex Agent: “Mobile PWA Multiplayer Loom”
Section titled “One-Shot Build Brief for a GPT-5 Codex Agent: “Mobile PWA Multiplayer Loom””You are an expert full-stack engineer. Generate a complete, production-ready mobile web (PWA) app named **Loom Mobile** that implements a multiplayer, human-in-the-loop interface for LLMs: an infinite, scrollable “sheet” where each scroll action can generate new branches; a swipe gesture can “rip and tear” away unwanted context; editing is seamless as if no DAG existed, while the underlying multiverse DAG stays consistent. [MDN PWA](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps) [W3C Manifest](https://www.w3.org/TR/appmanifest/) [Moire — Loom](https://generative.ink/posts/loom-interface-to-the-multiverse/)
## Deliverables- A SolidStart + TypeScript monorepo PWA with service worker, manifest, install prompt, offline support, and Lighthouse-ready performance. Include Workbox for routing/caching. [MDN Service Worker](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API) [Workbox](https://developer.chrome.com/docs/workbox/service-worker-overview/) [SolidStart routing](https://docs.solidjs.com/solid-start/building-your-application/routing)- Real-time multiplayer via **Yjs** using **y-websocket** on the client and **Hocuspocus** (+ **Redis** fanout) on the server; **Awareness** for presence; **y-indexeddb** for offline replays; graceful reconnect and conflict-free merges. [Yjs](https://github.com/yjs/yjs) [y-websocket](https://docs.yjs.dev/ecosystem/connection-provider/y-websocket) [Hocuspocus](https://tiptap.dev/docs/hocuspocus/introduction) [Awareness](https://docs.yjs.dev/api/about-awareness) [y-indexeddb](https://github.com/yjs/y-indexeddb) [y-redis](https://github.com/yjs/y-redis)- A touch-first UI using Pointer Events: pull to generate, swipe to rip, pinch to zoom the block-multiverse preview, long-press to fork at token, two-finger pan to scrub history. Respect iOS/Android system gestures. [MDN Pointer Events](https://developer.mozilla.org/en-US/docs/Web/API/Pointer_events) [Apple HIG Gestures](https://developer.apple.com/design/human-interface-guidelines/gestures) [Material Gestures](https://m3.material.io/foundations/interaction/gestures)- Editor: CodeMirror 6 bound to a Y.Text; live cursors/selections; per-user color/labels; undo isolated to user intent. [CodeMirror 6](https://codemirror.net/docs/) [y-codemirror.next](https://github.com/yjs/y-codemirror.next) [Awareness](https://docs.yjs.dev/api/about-awareness)- Block-multiverse panel that visualizes near-future token paths with probability mass; renormalize on zoom; tap to branch. [Moire — Block multiverse](https://generative.ink/meta/block-multiverse/) [Moire — Loom](https://generative.ink/posts/loom-interface-to-the-multiverse/)- Optional: client-side E2EE of Y updates via Web Crypto AES-GCM; room key bootstrap assumed out-of-band. [MDN SubtleCrypto](https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API)
## Functional Spec1. **Infinite Sheet** - Vertical scroll paginates “pages.” Crossing a generation waypoint triggers “roll-out”: request N continuations and render immediately; the DAG stores branches even if the user sees only the active projection. [Moire — Loom](https://generative.ink/posts/loom-interface-to-the-multiverse/) [MDN SW offline](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API/Using_Service_Workers)2. **Rip gesture** - Horizontal swipe over a span creates a **Mask** that excludes selected nodes/edges from the projection; underlying DAG keeps history for undo/restore. Use physics-informed thresholds and elastic overscroll. Avoid edge-swipe conflicts per HIG. [Apple HIG](https://developer.apple.com/design/human-interface-guidelines/gestures) [Pointer Events](https://developer.mozilla.org/en-US/docs/Web/API/Pointer_events)3. **Seamless editing** - User edits the visible page as plain text. A **Projection Lens** maps page edits back to the DAG: split, merge, and annotate affected nodes; anchors use Yjs **RelativePosition** so selections survive concurrent edits. [Yjs docs](https://docs.yjs.dev/)4. **Multiplayer** - Presence cursors with names/colors via Awareness; offline edits persist to IndexedDB and sync on reconnect; Hocuspocus with Redis ensures horizontal scale and late-join catch-up. [Awareness](https://docs.yjs.dev/api/about-awareness) [y-indexeddb](https://github.com/yjs/y-indexeddb) [Hocuspocus](https://tiptap.dev/docs/hocuspocus/introduction) [y-redis](https://github.com/yjs/y-redis)5. **Block multiverse** - Inline panel: show tree of token prefixes with cumulative probability bars; renormalize when zooming; tap path → create branch at that prefix. [Moire — Block multiverse](https://generative.ink/meta/block-multiverse/)6. **PWA** - Manifest, icons, shortcuts, offline shell; SW caches app shell + doc snapshots; background sync for queued generations; install banner UX. [W3C Manifest](https://www.w3.org/TR/appmanifest/) [MDN Manifest](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Manifest) [Workbox](https://developer.chrome.com/docs/workbox/service-worker-overview/)
## Data Model- **Doc**: Y.Map with `{ nodes: Y.Array<NodeId>, edges: Y.Map<EdgeId, Edge>, masks: Y.Array<Mask>, meta: Y.Map }`. Node text in per-node Y.Text. [Yjs](https://github.com/yjs/yjs)- **Projection**: deterministic fold over DAG minus masks → linear page; stores mapping table from page ranges → {nodeId, nodeOffset..}. [Moire — Loom](https://generative.ink/posts/loom-interface-to-the-multiverse/)
```typescript// types/shared.tsexport type NodeID = string;export type EdgeID = string;
export interface LoomNodeMeta { model: string; prompt?: string; logprobs?: number[]; // optional for block multiverse author?: string; // user/device createdAt: number;}
export interface LoomNode { id: NodeID; textKey: string; // key to Y.Text in ydoc meta: LoomNodeMeta;}
export interface LoomEdge { from: NodeID; to: NodeID; weight?: number }
export interface Mask { range: [number, number]; reason: 'rip'|'hide'|'conflict'; createdAt: number }
export interface ProjectionSpan { pageFrom: number; pageTo: number; node: NodeID; nodeFrom: number; nodeTo: number }
export interface GenerationRequest { parent: NodeID; promptSuffix: string; n: number; maxTokens: number; temperature: number }Projection Lens Algorithm (Edit-as-if-No-DAG)
Section titled “Projection Lens Algorithm (Edit-as-if-No-DAG)”- Build
spans: ProjectionSpan[]for current page. On text insert/delete:- Map page offsets →
{nodeId, nodeOffset}via spans. - If edit crosses node boundary, split nodes and insert a merge node capturing the edited text; retarget incoming/outgoing edges to preserve structure; add an implicit merge edge.
- Use Yjs RelativePosition for anchors to remain stable under concurrent edits. Yjs RelativePosition
- Map page offsets →
Gesture Semantics
Section titled “Gesture Semantics”- Pull to generate: vertical overscroll at waypoints issues
GenerationRequestand paints ghost blocks while awaiting tokens. Pointer Events - Swipe to rip: horizontal swipe over selection adds
Maskand animates a tear; safe zones avoid iOS system edges. Apple HIG - Pinch to zoom: toggles block-multiverse density and renormalizes probabilities. Moire — Block multiverse
Networking & Storage
Section titled “Networking & Storage”- Client:
y-websocketprovider with JWT param;y-indexeddbfor offline; Awareness presence (name, color, selection). y-websocket y-indexeddb Awareness - Server: Hocuspocus with Redis extension; persist snapshots to object store; backpressure + rate limits. Hocuspocus y-redis
- Optional backend alt: Cloudflare Durable Objects with WebSocket hibernation for room-affinity scaling. Cloudflare DO
import { Server } from '@hocuspocus/server';import { Redis } from '@hocuspocus/extension-redis';
export const server = Server.configure({ port: Number(process.env.PORT || 1234), extensions: [ new Redis({ host: process.env.REDIS_HOST || '127.0.0.1', port: 6379 }) ], async onAuthenticate({ token, documentName }) { // verify JWT; return user identity & room permissions return { userId: token?.sub, room: documentName }; }, async onStoreDocument({ documentName, state }) { // persist Uint8Array state to object store (e.g., S3/R2) }, async onLoadDocument({ documentName }) { // return latest persisted Uint8Array or undefined return undefined; },});server.listen();PWA Scaffolding
Section titled “PWA Scaffolding”- Manifest with
display: standalone, icons, shortcuts, categories; route/newand last-room deep-link. W3C Manifest MDN Manifest
{ "name": "Loom Mobile", "short_name": "Loom", "start_url": "/", "display": "standalone", "background_color": "#000000", "theme_color": "#000000", "icons": [{ "src": "/icons/icon-192.png", "sizes": "192x192", "type": "image/png" }], "shortcuts": [ { "name": "New Loom", "short_name": "New", "url": "/new" }, { "name": "Last Room", "short_name": "Resume", "url": "/r/last" } ]}- Service worker with Workbox: cache app shell, fonts, editor wasm; background sync queue for generation calls; network-first for WebSocket, stale-while-revalidate for docs. Workbox MDN SW
// sw.js (Workbox)import { clientsClaim } from 'workbox-core';import { setCacheNameDetails } from 'workbox-core';import { registerRoute } from 'workbox-routing';import { StaleWhileRevalidate, CacheFirst } from 'workbox-strategies';
self.skipWaiting(); clientsClaim();setCacheNameDetails({ prefix: 'loom-mobile' });
registerRoute(({request}) => request.destination === 'document', new StaleWhileRevalidate({ cacheName: 'app-shell' }));
registerRoute(({url}) => url.pathname.startsWith('/api/gen'), new StaleWhileRevalidate({ cacheName: 'gen-api' }));
// ...add background sync, fonts, wasm caches...Editor Integration
Section titled “Editor Integration”- SolidStart client-only mount, lazy import CM6; Yjs bindings with Awareness cursors; UndoManager scoped to text ops. SolidStart clientOnly CodeMirror 6 y-codemirror.next Awareness
import { clientOnly } from '@solidjs/start/client';const LoomEditor = clientOnly(() => import('../../widgets/LoomEditor'));
export default function Room() { return <LoomEditor />; }import * as Y from 'yjs';import { WebsocketProvider } from 'y-websocket';import { IndexeddbPersistence } from 'y-indexeddb';import { yCollab } from 'y-codemirror.next';import { Awareness } from 'y-protocols/awareness';
const ydoc = new Y.Doc();new IndexeddbPersistence(`loom:${roomId}`, ydoc);const provider = new WebsocketProvider(import.meta.env.VITE_YWS_URL, roomId, ydoc, { params: { token: await getJWT() },});const awareness = provider.awareness;// set name/color/selection on awareness.localState// bind CodeMirror 6 to a Y.Text for the active nodey-websocket y-indexeddb y-codemirror.next.
Block Multiverse Rendering
Section titled “Block Multiverse Rendering”- Request top-k with logprobs from LLM; render prefix DAG as blocks with heights ∝ cumulative probability; pinch to change depth; tap to branch. Moire — Block multiverse
Optional Client-Side E2EE of Y Updates
Section titled “Optional Client-Side E2EE of Y Updates”- Wrap Y updates in AES-GCM before transport; rotate per-room keys; store ciphertext server-side. MDN Web Crypto
// e2ee.ts (concept)export async function encryptUpdate(update: Uint8Array, key: CryptoKey) { const iv = crypto.getRandomValues(new Uint8Array(12)); const ct = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, update); return { iv, ct: new Uint8Array(ct) };}Acceptance Criteria
Section titled “Acceptance Criteria”- PWA: manifest present, SW registered, installable, offline shell works; ≥90 Lighthouse PWA. MDN PWA web.dev PWA checklist
- Multiplayer: two simulated clients show presence, convergent edits, and deterministic projections after concurrent rips/edits. Yjs Awareness
- Gestures: pull generates branches, swipe masks text, pinch zooms multiverse; avoids edge conflicts per HIG. Apple HIG
- Resilience: offline edits persist; reconnect resyncs; server horizontal scale with Redis. y-indexeddb y-redis
Stretch Goals (optional)
Section titled “Stretch Goals (optional)”- Cloudflare Durable Objects room-affine transport. Cloudflare DO
- Rich-text path with Peritext-inspired spans. Ink & Switch Peritext
- Export/import: JSON tree + linearized page; replay masks. Yjs