feat(files): collaborative document editing — client provider + editor [5/N] - #5946
Conversation
…5/N] Custom Socket.IO Yjs provider (the client half of the file-doc relay merged in #5941): the Socket.IO analogue of y-websocket's WebsocketProvider, speaking the same y-protocols sync + awareness so TipTap's Collaboration/CollaborationCaret work unmodified. Binds its clientId at join, exchanges sync only after the join ack, publishes only its own awareness, re-syncs on reconnect, and detaches all listeners on destroy. 14 tests. Deps: yjs + y-protocols + lib0 + @tiptap/extension-collaboration + -collaboration-caret. Editor integration (wiring into the rich-markdown editor + seeding + CSS) follows in this branch.
Integrates the Yjs collaboration layer (Collaboration + CollaborationCaret) into the file editor, live carets + text selection in each user's color, name on hover. - use-file-doc-collaboration hook: owns the per-file Y.Doc + Awareness (created synchronously so the editor binds at mount) + the FileDocProvider (connects async over the shared socket). Gated to an editable, round-trip-safe, non-streaming workspace document with a known user — decided once at mount, since TipTap fixes the extension set at editor creation. - editor-extensions: per-instance Collaboration/CollaborationCaret when collab is on; StarterKit's built-in history disabled ONLY on that path (Yjs owns undo/redo) — the headless markdown round-trip path is untouched. Selection is a translucent tint of the caret color; the manual streaming reconcile loop is bypassed (the Y.Doc is the source of truth). - Seeding: on SEED_REQUEST + synced, setContent(markdown) + the initialContentLoaded flag in ONE Yjs transaction (the relay's exactly-once contract). - Autosave gate: the editor reports synced+seeded up to the parent, which holds the markdown-mirror save until then, so an empty/partially-synced doc can never overwrite the real file. - CSS: caret + name-on-hover label + lighter-tint selection, themed under .rich-markdown-prose. Full editor suite (442 tests) green; apps/sim typecheck clean. Live carets + concurrent-edit convergence need a two-browser verification before merge.
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
|
@cursor review |
PR SummaryHigh Risk Overview The client adds Reviewed by Cursor Bugbot for commit bad7cdd. Configure here. |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 4800157. Configure here.
Greptile SummaryAdds collaborative rich-text editing for workspace Markdown files.
Confidence Score: 5/5The PR appears safe to merge with no blocking failures remaining in the follow-up scope. No blocking failure remains.
|
| Filename | Overview |
|---|---|
| apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/rich-markdown-editor.tsx | Integrates collaborative document lifecycle, seeding, readiness gating, editor binding, and Files-surface isolation without leaving an eligible blocking follow-up issue. |
| apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/collaboration/file-doc-provider.ts | Implements the Socket.IO-backed Yjs synchronization and awareness provider with latched seed and join-error state. |
| apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/collaboration/use-file-doc-collaboration.ts | Owns stable per-file Yjs resources and connects their provider to the shared workspace socket. |
| apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/use-editable-file-content.ts | Adds the collaboration readiness condition to the existing autosave gate. |
| apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/editor-extensions.ts | Adds per-editor TipTap collaboration and caret extensions while disabling local history on collaborative instances. |
Sequence Diagram
sequenceDiagram
participant E as Collaborative Editor
participant P as FileDocProvider
participant R as Realtime Relay
participant Y as Shared Y.Doc
E->>P: Bind document and awareness
P->>R: JOIN(fileId, clientId)
R-->>P: JOIN_SUCCESS
P->>R: Yjs sync step 1
R-->>P: Yjs sync step 2
P->>Y: Apply authoritative state
alt Empty shared document
R-->>P: SEED_REQUEST
E->>Y: Import Markdown and set seed flag
end
Y-->>E: Synced and seeded
E->>E: Enable editing and autosave
E->>P: Local updates and awareness
P->>R: Relay updates
R-->>P: Remote updates and awareness
Reviews (11): Last reviewed commit: "fix(files): remove unsafe outage seed fa..." | Re-trigger Greptile
Greptile review + an independent regression audit found three real lifecycle gaps plus a session race; consolidated fix: - Seed-request race: the transient seed-request event could fire before the editor subscribes, leaving the doc empty forever. The provider now LATCHES it as `shouldSeed`, and the editor checks the flag on subscription. - Fatal join, no fallback: a non-retryable join error (access denied, client-id conflict) left the collab-latched editor empty with autosave disabled. It now DEGRADES to local editing — seeds the loaded content and ungates autosave, so the file stays editable (edits persist via the markdown mirror; only live sync is off). - Stream-after-collab: an agent stream starting on a collab-open file was skipped entirely (stale display). The reconcile loop now runs while streaming even in collab mode (guard is `collaborationEnabled && !isStreaming`). - Session race (regression audit F1): on a cold load useSession is briefly undefined, latching collaboration OFF permanently → both cold-loaders solo-save (last-write-wins). The editor now waits for the session before deciding. Also folds in the /simplify wins: collaborationEnabled is a useState-init const (the ref latch was provably always-resolved on render 1); the seed body is held by pointer so it parses once at seed time; seeding + gating + degrade are one lifecycle effect; the doc is only allocated when collaboration is enabled (F2); and the selection tint reuses the now-exported withAlpha instead of reimplementing it. apps/sim typecheck clean; editor suite (442) + provider tests green.
Comment pass (per the /you-might-not-need-a-comment skill): the four inline // blocks that document a symbol become TSDoc for consistency with their siblings — FileDocProvider join/handleJoinSuccess/handleJoinError and the editor's seedContentRef. The remaining inline comments are genuine line-specific WHY (transaction-origin, awareness-ownership, cleanup ordering, collab-vs-streaming) that can't be expressed as symbol TSDoc; no restatement/noise was found.
|
@cursor review |
The previous collab guard (`collaborationEnabled && !isStreaming`) let the stream display run but skipped the SETTLE branch once the stream ended, leaving a collab-open editor read-only after an agent stream. The guard now also allows the effect through while settling (`!wasStreamingRef.current`, true until the settle branch consumes it), so the editor is re-enabled and finalized after a stream; steady-state collab still skips the manual reconcile loop.
|
@cursor review |
…ccess Round 3 review (Greptile + Cursor): - Editable-before-seed (High): the editor was editable before the shared doc was synced+seeded, so a user could type into an empty doc and lose it to the seed. Added a local collabReady state (synced+seeded, or degraded-writable) that starts false for a collaborative document and gates isEditable + a reactive setEditable. - ACCESS_DENIED can't save: the fatal-degrade made the editor writable, but a permission denial means the content PUT would 403 too. ACCESS_DENIED now degrades to READ-ONLY (content shown, autosave gated); other recoverable fatals stay locally editable. - Draft-recovery race: local collabReady starting false means draft persistence is never enabled for a collaborative document (on top of the existing cancel path). apps/sim typecheck clean; editor suite (442) green.
|
@cursor review |
- Fix collaborative caret name-label text: white was unreadable on the light-pastel user colors; use fixed dark (#1a1a1a) against the theme-independent per-user background, and widen the caret hover target via a ::before so the name is discoverable without the label ever intercepting clicks. - Remove pre-existing dead exports from lib/workspaces/colors.ts (getPresenceColors, createUserColorMap, APP_COLORS, PresenceColorPalette, and the now-orphaned buildGradient) — zero consumers repo-wide. - Drop the stale 'degraded to writable' clause from the collabReady doc (that path was removed) and tighten/strip comments that restated adjacent TSDoc.
|
@cursor review |
…time-outage fallback Review round (Greptile P1 + 2 Cursor High): - Stream reconcile loop is now fully inert in collaborative mode. Its setContent syncs a full-document replace into the shared Y.Doc (the ySyncPlugin writes it regardless of emitUpdate:false), which would wipe peers' concurrent edits and feed the settle path a paced, incomplete prefix. Collaboration and agent-streaming are mutually exclusive: collab wins and owns the doc; editability is held read-only for the stream's duration by the reactive effect (which now owns the whole collab lifecycle, not just the non-streaming steady state). - Realtime-outage fallback: if the provider never attaches (socket down), a grace timer seeds the loaded content read-only so the user reads their file instead of a blank editor, instead of staying blank + unsavable indefinitely. A reconnect re-runs the effect onto the normal server-seeded sync path.
|
@cursor review |
…gent-streaming) Greptile P1 follow-up: disabling the stream reconcile in collab mode meant an agent streaming into a collaboratively-open file never showed its output. Root cause is that collaboration and agent-streaming are two different surfaces that should never share one editor: the Files page is the collaborative editing surface and never streams; the mothership/Chat surface streams agent output and should stay solo. They only collided because collaborationEnabled did not gate on surface. Add an explicit `collaborative` opt-in threaded FileViewer -> RichMarkdownEditor, set only by the Files page. Collaboration and streaming are now disjoint by construction, so the reconcile loop's collab guard is belt-and-suspenders and the agent surface streams normally (solo, as before collaboration existed).
|
@cursor review |
…sence Cursor High: useSocket() returns a socket object even while disconnected, so FileDocProvider is always created (non-null) and the grace timer — which only armed when provider was null — never fired on a genuine outage, leaving the editor blank + read-only. Arm the grace timer regardless of provider presence and gate the fallback on the live socket connection state (new FileDocProvider.isConnected). A connected-but-slow sync no longer seeds here (which would race the server's seeder election); seedFromLoaded still self-guards on initialContentLoaded so a completed sync makes it a no-op.
|
@cursor review |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 288960e. Configure here.
… reconnect) Greptile P1: the grace-timer fallback seeded the loaded markdown into the persistent local Y.Doc when the socket stayed disconnected past the grace period. If the room was already seeded by another collaborator, reconnecting merged those independent CRDT inserts with the populated server document, duplicating text and autosaving the merged result. There is no safe way to show content through the collaborative editor during an outage — any write into the bound Y.Doc syncs regardless of origin. So drop the fallback (and the isConnected getter added for it): a collaborative document is read-only until synced + seeded, full stop. Proper outage resilience (offline persistence / a reconnecting indicator) is a separate follow-up, not a workaround that can corrupt documents.
|
@cursor review |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit bad7cdd. Configure here.
What
The client half of collaborative document editing, on top of the merged server relay (#5941). Two people editing the same file's rich-text editor now see each other's live carets + text selection in their own color (Google-Docs style), with the name on hover.
Caret positioning and concurrent-edit convergence cannot be certified headlessly. Before merge, please open the same workspace file as two different users in two browsers and confirm:
Everything below is unit-tested + typechecks, but that last mile is yours.
Contents
use-file-doc-collaborationhook — owns the per-fileY.Doc+Awareness(created synchronously so the editor binds at mount) and theFileDocProvider(connects async over the shared socket). Enabled only for an editable, round-trip-safe, non-streaming workspace document with a known user, decided once at mount.FileDocProvider(14 tests) — the Socket.IO analogue of y-websocket's provider; binds its clientId at join, exchanges sync only after the ack, publishes only its own awareness, re-syncs on reconnect, detaches on destroy.Collaboration/CollaborationCaret; StarterKit's history disabled only on the collab path (Yjs owns undo/redo) so the headless markdown round-trip path is untouched; the manual streaming reconcile loop is bypassed when collaborating.SEED_REQUEST+synced,setContent(markdown)+ theinitialContentLoadedflag in onedoc.transact(the relay's exactly-once contract).synced && seeded, so an empty/partially-synced doc can never overwrite the real file (the data-loss vector the earlier audit flagged)..rich-markdown-prose.Infra reuse
Same socket server, Redis fan-out, and auth spine as the rest of realtime (no new services). Uses the existing content PUT for the markdown mirror (no new routes/contracts). Deps added:
yjs,y-protocols,lib0,@tiptap/extension-collaboration,@tiptap/extension-collaboration-caret.Gates
Full
apps/simtypecheck clean; the editor suite (442 tests) + provider tests green; biome +check:api-validationpass.Deferred (follow-ups)
Durable Yjs snapshot persistence (content is durable via the markdown mirror today); presence avatars in the editor header; Tables cell-selection presence.