feat(realtime): accurate in-file presence + collaborative-caret polish - #5965
Conversation
Follow-up to the tables/files collaboration work (realtime-rooms).
Files — collaborative editing:
- Fix carets/selection never appearing: React StrictMode remounts null the reused
awareness's local state, permanently no-op'ing setLocalStateField; the provider now
reseeds an empty local state so the caret + selection publish again.
- Caret name tag: shown while the peer is active or on hover, fades when idle
(Google-Docs behaviour), with an edge-aware left-flip and a table-cell clip fix. A
small colored cap (a collapsed presence tag in our rounded-xs + notch language) marks
a dormant caret and hints "hover for who". One --caret-color var drives the bar, cap,
and tag.
- "Who's in this file" avatars now derive from the file-doc room (the caret awareness),
scoped by a FileDocRoomProvider + useFileDocOthers hook (the RoomProvider/useOthers
pattern); the avatar url + user id ride in the awareness payload. Avatars mount in the
file-detail header (were only in the list header, so co-editors saw nothing).
Files — the list no longer shows presence (browsing isn't collaborating); the
workspace-files room is live-tree-only on both client and server (dropped all presence
bookkeeping; membership tracked via native socket.io rooms).
Tables — remote selection overlay: name tag on the shared canvas presence tokens,
portaled so it's never clipped; co-selected cells show only the local selection
("mine goes over") via cellInBounds against the normalized selection; sharp edge border
restored.
Shared — PresenceAvatarUser.socketId is optional (file-doc entries are deduped per user).
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
PR SummaryMedium Risk Overview Workspace-files rooms are live-tree-only: Redis room-manager presence, avatars, and related join payload fields are removed; membership uses native Editor UX: Disconnect handling skips room-manager presence cleanup for workspace-files and file-doc room types. Tables: remote selection overlays hide when the local selection fully covers a peer’s range, use shared presence styling, and portal name tags to Shared: Reviewed by Cursor Bugbot for commit 1309adc. Configure here. |
|
@cursor review |
Greptile SummaryThis follow-up completes the realtime collaboration presence and room-lifecycle polish.
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains in the previously reported workspace-room race handling, file-document leave cancellation, or authenticated roster identity flow.
|
| Filename | Overview |
|---|---|
| apps/realtime/src/handlers/workspace-files.ts | Replaces Redis-backed browser presence with native room membership and correctly guards asynchronous joins against workspace switches and scoped leaves. |
| apps/realtime/src/handlers/file-doc.ts | Adds a server-authenticated collaborator roster and preserves monotonic join generations across file switches while cancelling same-file pending joins. |
| apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/collaboration/use-file-doc-collaboration.ts | Consumes the trusted file-document roster, excludes the local socket, and deduplicates displayed collaborators by authenticated user ID. |
| apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/collaboration/caret-presence.ts | Adds awareness-driven caret-label activation, fading, and edge-aware placement. |
| apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/remote-selection-overlay.tsx | Adds portaled remote-selection identity labels and updated collaborative selection presentation. |
| packages/realtime-protocol/src/file-doc.ts | Extends the shared file-document protocol with the trusted per-session presence roster contract. |
Sequence Diagram
sequenceDiagram
participant UI as File editor
participant RT as Realtime service
participant Auth as Workspace authorization
participant Peers as File-doc peers
UI->>RT: JOIN(fileId, clientId)
RT->>Auth: authorize write access
Auth-->>RT: allowed
RT->>RT: Record authenticated session owner
RT-->>UI: JOIN_SUCCESS
RT-->>Peers: Trusted presence roster
UI->>RT: Awareness updates
RT-->>Peers: Caret and selection updates
UI->>RT: LEAVE(fileId)
RT->>RT: Remove owner and awareness
RT-->>Peers: Refreshed presence roster
Reviews (10): Last reviewed commit: "fix(realtime): cancel an in-flight file-..." | Re-trigger Greptile
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 2c4dbbe. Configure here.
… the file roster Addresses the two P1 review findings on the file presence work. - Stale-join race (workspace-files.ts): a fast workspace A→B switch could let A's async authorize finish after B and, via native socket.rooms, leave B and rejoin A — stranding the browser on the wrong room and missing B's `workspace-files-changed` invalidations. Added a per-socket monotonic join generation checked after authorize (mirrors the tables and file-doc guards) + a test. - Peer-metadata impersonation (file presence roster): the roster derived identity from the client-set Yjs awareness `user` field, so an authorized collaborator could publish another user's id/name/avatar (and dedup-by-claimed-id could suppress the real entry). The roster is now SERVER-AUTHENTICATED: the file-doc relay carries each owner's session identity (userName + resolveAvatarUrl) and broadcasts a trusted `file-doc-presence` roster on join/leave; the client renders that instead of awareness. The awareness `user` payload is back to caret-only fields (name/color/clientId), and the client avatar-url plumbing is removed. Added a roster test.
|
@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 a2283bb. Configure here.
Follow-up hardening from a 4-axis parallel audit (architecture / correctness+security /
React / cleanliness) + an 8-pass /cleanup. No blockers were found; these are the real
minor items across the whole PR.
- connection.ts: gate the disconnect union on presence-bearing room types (workflow/table)
so the now-native workspace-files room — and the pre-existing file-doc room — no longer
emit a dead `presence-update` no client listens to.
- FileDocRoomProvider: split into separate others/setter contexts so a roster change
re-renders only the header avatar stack, never the editor that merely publishes it.
- use-file-doc-collaboration: recreate the Y.Doc/Awareness if a StrictMode dev remount
already destroyed the reused instance (isDestroyed guard).
- remote-selection-overlay: the pointer hit-test skips a locally-covered box, so a
co-selected cell no longer pops a floating name over no visible selection.
- Dedup the `#000000` caret fallback into DEFAULT_CARET_COLOR; align the caret name tag to
text-xs (11px) for exact parity with the canvas/tables presence tags.
- Fix stale comments left by the roster refactor ("awareness roster" → server-authenticated),
correct a caret-CSS cross-reference, and document why file-doc uses its own roster instead
of room-manager presence.
|
@cursor review |
…vered-selection name tag - use-file-doc-collaboration: recreate the Y.Doc/Awareness inside the provider effect (not just render) so a StrictMode remount never binds the provider to a destroyed doc — render runs before the remount setup, so a render-only guard can't cover it. - remote-selection-overlay: re-derive the hovered box in render excluding locally-covered selections, so the floating name tag drops when the local selection grows to cover it without another pointer move. - rich-markdown-editor.css: seat the dormant caret cap flush at left:0 (caret content box) instead of hanging off the left edge.
|
@cursor review |
A StrictMode dev remount fired the destroy effect and killed the Y.Doc the editor had already frozen into its extension set, so a joining peer's synced content landed in a doc the editor no longer rendered — the peer saw a blank document. Create the doc/awareness once and never destroy-and-recreate them (the provider owns them by reference only and never destroys them either), so the provider always rebinds the exact instances the editor holds. GC reclaims them on real unmount.
… flush Presence: the roster deduped collaborators by userId on the server AND the client excluded self by userId, so opening a file in two tabs of the same account removed every one of your sessions and showed no avatar — even though the canvas presence (avatars.tsx) counts other sessions by socketId. Broadcast one roster entry per session (socketId included), have the client exclude only its OWN socket and dedupe the rest per user for the avatar stack. A second tab now registers as present, matching the canvas model. Caret: seat the dormant cap's square bottom-left corner flush on the pole's top-left (left:-1px backs out the caret's 1px left border; top overlaps a hair so no gap).
|
@cursor review |
… doc Keeping the Y.Doc/Awareness alive across a StrictMode remount fixed the blank-join bug but leaked the Awareness setInterval (stale-peer expiry) for every file opened, since nothing destroyed it. Defer teardown to a setTimeout on cleanup and cancel it on the synchronous remount setup: a StrictMode fake unmount cancels the scheduled destroy (instances stay alive for the frozen editor), while a real unmount has no remount so the destroy runs and clears the timer + doc.
|
@cursor review |
Turning the caret ::before into the small dormant cap dropped the full-height transparent hit-slop that made 'hover for who' easy to trigger, leaving only the ~2px bar + 8x5px cap as the hover target. Add a dedicated transparent ::after hit-slop spanning the caret's full height (and over the cap) so the name label is comfortably hoverable again — nothing visible changes.
|
@cursor review |
… leaves A join awaiting authorization when the Files view unmounts would complete afterwards and strand the socket in a room it has left. Track the intended workspace and advance joinGeneration on a leave that targets it (or an unscoped leave), so the stale join aborts. Guarded on the intended workspace so a deferred leave for a DIFFERENT workspace can't abort the join the client has since switched to (the file-doc race from #5941).
|
@cursor review |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
1 issue from previous review remains unresolved.
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 84bd92d. Configure here.
…tches cleanupFileDocForSocket deleted the socket's joinGeneration entry, but it runs on a document switch and leave — not just disconnect. Resetting the monotonic counter let a later join reuse a low number that a still in-flight earlier join also held, so the stale join passed the guard and rebound the socket to the wrong document (its editor would sync into another file's Y.Doc). Drop the generation ONLY on true disconnect (new endOfLife arg, passed from the disconnect handler); a switch relies on a newer join bumping the counter, a disconnect on the socket.disconnected guard. +2 tests.
|
@cursor review |
The LEAVE handler returned early when the socket wasn't registered yet and never advanced the join generation, so a join still awaiting authorization could complete after the client left, register as an owner, and broadcast a ghost collaborator until disconnect. Track the intended file (currentFileId) and advance the generation on a leave that targets it (or an unscoped leave), mirroring the workspace-files fix. Guarded on the intent so a deferred leave for a DIFFERENT file can't abort the join the client switched to (the #5941 regression). +1 test.
|
@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 1309adc. Configure here.

Follow-up to the tables/files collaboration work, on the
realtime-roomsintegration branch. Pure UX + architecture polish — no protocol/schema changes.Files — collaborative editing
setLocalStateField; the provider now reseeds an empty local state so the caret + selection publish again. (No-op in prod; self-heals on reconnect.)rounded-xs+ notch language, not the GDocs teardrop — marks a dormant caret and hints "hover for who". One--caret-colorvariable drives the bar, cap, and tag.FileDocRoomProvider+useFileDocOthershook — the canonicalRoomProvider/useOtherspattern. The avatar url + user id ride in the awarenessuserpayload. Avatars now mount in the file-detail header (they were only in the list header, so two people on the same file saw nothing).Files — presence scope
socket.roomsinstead of the room-manager presence store. Theworkspace-files-changedlive-tree invalidation is unchanged.Tables — remote selection overlay
--surface-1,text-xs), portaled to the body so it's never clipped by the grid/header.cellInBoundsagainst the existingcomputeNormalizedSelection(also fixes single-cell suppression).Shared
PresenceAvatarUser.socketIdis now optional (file-doc roster entries are deduped per user, not per socket).Quality
Ran
/simplify+/cleanup(12 review agents) over the diff and applied the findings; every change is behind biome,tsc --noEmit(both apps), fulllint:check, and the realtime + collab test suites (39 + 19 tests). Not yet exercised in a live 2-browser session — that's the recommended verification before this rides to staging.