Skip to content

Collaboration: tracked-change comment cards don't render from a loaded Y.Doc (redline shows, cards missing on reload and for the author) #3603

@NicoGrapsas

Description

@NicoGrapsas

What happened?

Collaborative tracked-change suggestions render the redline but not the comment cards

Summary

In collaborative mode (Yjs + Hocuspocus), tracked-change "suggestion" cards (the side cards with author/accept/reject) fail to render in several situations even though the redline marks render
correctly
:

  1. After a page reload, the redline is present but the comment cards are gone — on all clients.
  2. When a user makes a tracked change, the card shows up on the remote peer but not on the author who made the edit.
  3. When the shared document is populated from the Y.Doc rather than by a live local edit (page reload, late join, or programmatically seeding the shared doc with seedEditorStateToYDoc), the redline shows
    but the cards are missing and the change cannot be accepted/rejected.

The common thread: the tracked-change marks live in the editor fragment (ydoc.getXmlFragment("supereditor")) and are synced/persisted correctly, but the comment cards are only materialized from live
tracked-changes-changed transactions
and are never rebuilt from marks that already exist in a loaded document.

Environment

  • @harbour-enterprises/superdoc 1.38.0
  • SuperDoc configured with:
    new SuperDoc({
      comments: { visible: true },
      trackChanges: { visible: true },
      modules: { collaboration: { provider, ydoc } }, // Hocuspocus provider + Y.Doc
      // ...
    })
  • Two browser clients connected to the same Hocuspocus room.

Steps to reproduce

Repro A — reload drops the cards

  1. Open a document collaboratively with at least one tracked change (suggestion) present.
  2. Confirm the redline and the side comment card render.
  3. Reload the page.
  4. ❌ The redline renders, but the comment card is gone.

Repro B — author doesn't see their own suggestion

  1. Two clients (A and B) in the same room, suggesting mode.
  2. A makes a tracked change by editing the body.
  3. ❌ B sees the comment card; A (the author) never does.
  4. Reload either client → card gone on both (see Repro A).

Expected behavior

Tracked-change comment cards should render for every tracked change present in the document, regardless of whether the change arrived via a live transaction, a remote collaboration update, a page reload, or
a programmatic seed of the shared Y.Doc — and the author should see their own change's card.

Root cause

Two issues in the comments/collaboration layer (symbol names as they appear in the distributed build):

1. Cards are only built from live transactions, never rebuilt from a loaded document.

syncTrackedChangeComments({ superdoc, editor }) (which enumerates getTrackChanges(editor.state) and calls createCommentForTrackChanges(...)) is the routine that builds the cards. In collaborative mode
it is only invoked from the live editor.on("tracked-changes-changed", ...) handler. When a document is loaded from the Y.Doc (reload / late join / seedEditorStateToYDoc), the tracked-change marks are
applied as the editor's initial state, not as a tracked-changes-changed event, so syncTrackedChangeComments never runs and no cards are created. The non-collaborative import path does bootstrap
them (processLoadedDocxCommentsbootstrapImportedTrackedChangeComments), but that path is gated by editor.options.shouldLoadComments || replacedFile and doesn't run for content that arrives through
the synced Y.Doc fragment.

2. The author's own edits are skipped.

The tracked-changes-changed handler short-circuits on the author's own body edits:

editor.on?.("tracked-changes-changed", ({ editor: sourceEditor, source }) => {
  if (source === "body-edit") return;   // <-- author's own edit never builds a card
  if (!shouldRenderCommentsInViewing.value) { /* ... */ return; }
  syncTrackedChangeComments({ superdoc: proxy.$superdoc, editor: sourceEditor ?? editor });
});

The remote peer receives the same change as a non-body-edit collaboration update and does build the card, which is why it shows on the peer but not on the author.

Suggested fix

Two changes, both reusing the existing syncTrackedChangeComments so no new comment logic is introduced. Deriving cards locally (broadcastChanges: false) keeps them a pure function of the synced marks
and avoids writing duplicates into the shared comments array.

1. Rebuild tracked-change cards when a collaborative editor is set up / synced, so loaded/seeded documents get their cards:

// after the collaborative editor is ready
if (superdoc?.isCollaborative) {
  const rebuild = () => {
    try {
      syncTrackedChangeComments({ superdoc, editor, broadcastChanges: false });
    } catch {}
  };
  // run once content is applied; a small retry covers the sync race
  setTimeout(rebuild, 0);
  setTimeout(rebuild, 300);
}

2. Don't drop the author's own body-edit changes — schedule a debounced local rebuild instead of returning:

editor.on?.("tracked-changes-changed", ({ editor: sourceEditor, source }) => {
  if (source === "body-edit") {
    if (shouldRenderCommentsInViewing.value) {
      clearTimeout(bodyEditTimer);
      bodyEditTimer = setTimeout(
        () => syncTrackedChangeComments({ superdoc, editor: sourceEditor ?? editor, broadcastChanges: false }),
        400
      );
    }
    return;
  }
  // ...existing path unchanged...
});

We applied both changes as a local patch against 1.38.0 and they resolve all three symptoms: cards now persist across reload, the author sees their own suggestion, and seeded/generated content renders cards
that can be accepted/rejected.

Possible alternative

It would also help to expose a public API to rebuild tracked-change comments for the current document (e.g. a superdoc.refreshTrackedChangeComments() or having the collaboration sync path run it on
provider "synced"), so integrators don't have to patch the bundle for loaded/seeded documents.

Steps to reproduce

No response

SuperDoc version

No response

Browser

None

Additional context

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions