Skip to content

feat(realtime): accurate in-file presence + collaborative-caret polish - #5965

Merged
waleedlatif1 merged 11 commits into
realtime-roomsfrom
realtime-rooms-presence-polish
Jul 26, 2026
Merged

feat(realtime): accurate in-file presence + collaborative-caret polish#5965
waleedlatif1 merged 11 commits into
realtime-roomsfrom
realtime-rooms-presence-polish

Conversation

@waleedlatif1

Copy link
Copy Markdown
Collaborator

Follow-up to the tables/files collaboration work, on the realtime-rooms integration branch. Pure UX + architecture polish — no protocol/schema changes.

Files — collaborative editing

  • Carets/selection were never appearing. Root cause: React StrictMode's dev remount nulls the reused awareness's local state, which permanently no-ops setLocalStateField; the provider now reseeds an empty local state so the caret + selection publish again. (No-op in prod; self-heals on reconnect.)
  • Caret name tag now shows while the peer is active or on hover and fades when idle (Google-Docs behaviour), with an edge-aware left-flip and a fix for clipping inside markdown tables. A small colored cap — a collapsed presence tag in our own rounded-xs + notch language, not the GDocs teardrop — marks a dormant caret and hints "hover for who". One --caret-color variable drives the bar, cap, and tag.
  • "Who's in this file" avatars now derive from the file-doc room (the same awareness that drives the carets), scoped by a FileDocRoomProvider + useFileDocOthers hook — the canonical RoomProvider/useOthers pattern. The avatar url + user id ride in the awareness user payload. Avatars now mount in the file-detail header (they were only in the list header, so two people on the same file saw nothing).
  • Remote text-selection highlight confirmed wired.

Files — presence scope

  • The file list no longer shows presence (browsing a file browser isn't collaborating).
  • The workspace-files room is now live-tree-only on both client and server: all presence bookkeeping removed; the workspace-switch/leave logic tracks membership via native socket.io socket.rooms instead of the room-manager presence store. The workspace-files-changed live-tree invalidation is unchanged.

Tables — remote selection overlay

  • Name tag on the shared canvas presence tokens (--surface-1, text-xs), portaled to the body so it's never clipped by the grid/header.
  • Co-selected cells show only the local selection ("mine goes over") via cellInBounds against the existing computeNormalizedSelection (also fixes single-cell suppression).
  • Sharp edge border restored (reverted an inset-ring iteration).

Shared

  • PresenceAvatarUser.socketId is 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), full lint: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.

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).
@vercel

vercel Bot commented Jul 25, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped Jul 26, 2026 6:18pm

Request Review

@cursor

cursor Bot commented Jul 25, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Touches realtime join/leave race logic and adds a new file-doc presence protocol path; mistakes could cause ghost collaborators or missed live-tree invalidations, but changes are well-tested and scoped to collaboration UX rather than auth or persistence.

Overview
In-file collaboration now shows who’s editing via a new file-doc-presence event: the realtime server builds the roster from authenticated socket session data (not client awareness), broadcasts it on join/leave, and the UI exposes it through FileDocRoomProvider / FileDocAvatars on the open file header (removed from the file list). Join/leave generation guards are tightened so fast open→close or file switches don’t register ghost owners; cleanupFileDocForSocket only clears join generation on true disconnect (endOfLife).

Workspace-files rooms are live-tree-only: Redis room-manager presence, avatars, and related join payload fields are removed; membership uses native socket.rooms, with the same scoped leave/join-generation pattern to avoid stranded memberships after async authorize.

Editor UX: FileDocProvider reseeds cleared local awareness (StrictMode remount); caret-presence drives Google-Docs-style name labels (activity fade, edge flip, table clipping fix) and wires custom caret rendering into TipTap. use-file-doc-collaboration subscribes to the server roster and defers Y.Doc teardown across dev remounts.

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 document.body to avoid clipping.

Shared: PresenceAvatarUser.socketId is optional for user-deduped file rosters.

Reviewed by Cursor Bugbot for commit 1309adc. Configure here.

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

@greptile-apps

greptile-apps Bot commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This follow-up completes the realtime collaboration presence and room-lifecycle polish.

  • Replaces workspace-files presence bookkeeping with native Socket.IO room membership and generation-guarded joins and leaves.
  • Publishes a server-authenticated, per-session file-document roster for collaborator avatars.
  • Adds file-editor caret labels, remote selection styling, and table selection overlays.
  • Updates the shared realtime protocol and focused lifecycle, roster, caret, and selection tests.

Confidence Score: 5/5

The 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.

Important Files Changed

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
Loading

Reviews (10): Last reviewed commit: "fix(realtime): cancel an in-flight file-..." | Re-trigger Greptile

Comment thread apps/realtime/src/handlers/workspace-files.ts

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ 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.
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ 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.
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@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.
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@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).
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@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.
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@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.
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

Comment thread apps/realtime/src/handlers/workspace-files.ts
Comment thread apps/realtime/src/handlers/file-doc.ts
… 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).
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

1 issue from previous review remains unresolved.

Fix All in Cursor

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.
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

Comment thread apps/realtime/src/handlers/file-doc.ts
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.
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ 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.

@waleedlatif1
waleedlatif1 merged commit 0a36d64 into realtime-rooms Jul 26, 2026
5 checks passed
@waleedlatif1
waleedlatif1 deleted the realtime-rooms-presence-polish branch July 27, 2026 16:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant