fix(cli): headless bridge syncs browser-authored comment metadata from Y.Array (SD-3214)#3402
Draft
tupizz wants to merge 3 commits into
Draft
fix(cli): headless bridge syncs browser-authored comment metadata from Y.Array (SD-3214)#3402tupizz wants to merge 3 commits into
tupizz wants to merge 3 commits into
Conversation
…m Y.Array (SD-3214)
The headless SDK's `editor.doc.comments.list()` was returning partial data
for browser-authored comments: `id`, `target`, `anchoredText`, `status`
populated correctly from the PM anchor mark (synced via y-prosemirror), but
`text`, `creatorName`, `creatorEmail`, `createdTime` came back empty.
Cause: browser SuperDoc clients write comment data over two channels —
the PM XmlFragment carries anchor marks, and `ydoc.getArray('comments')`
carries metadata. The CLI's `headless-comment-bridge.ts` only had the
write half of this dance. It never fed Y.Array entries into the editor's
`CommentEntityStore`, which is what `comments.list()` reads from.
Fix:
- New shared helper in super-editor: `syncCommentEntitiesFromCollaboration`
maps Y.Array entry shape to `CommentEntityRecord`, upserts via the existing
`upsertCommentEntity`, filters out tracked-change entries (separate domain),
and supports an optional `previouslySynced` set to prune remote deletions
via `removeCommentEntityTree`.
- Bridge gains `attachEditor(editor)` called after `Editor.open()` resolves.
Seeds the store from the current Y.Array snapshot, then installs an
observer that re-syncs on remote events while skipping own-origin echoes
using the same `transaction.origin.user` shape the browser writers use.
- `document.ts` wires `attachEditor` once.
CLI engine-agnostic boundary preserved: the bridge forwards the editor
handle to the shared helper but never touches `editor.state` / `editor.storage`
directly. The helper lives in super-editor so the next consumer (mobile
SDK, edge worker) doesn't re-derive the mapping.
Coverage:
- 13 new unit tests in `comment-entity-store.test.ts` cover upsert paths,
alias handling (`text` vs `commentText`), tracked-change filtering,
importedId fallback, merge-without-clobber, resolution metadata, and
the four deletion scenarios (prune previously-synced, leave locally-
authored alone, cascade thread replies, no-op when no removals).
- 8 integration tests in `sd-3214-browser-comment-metadata.regression.test.ts`
cover: pre-open Y.Array entry, post-open observe, remote update,
Option A (two Editors on shared Y.Doc), Option A origin-filter
(no self-echo), Option A remote delete, Option B (two Y.Docs with
Yjs update relay), Option B late-join.
Out of scope (separate gap, documented in the test file): the CLI's
write-side delete path doesn't currently propagate to Y.Array because
the `removeComment` editor command sets `tr.setMeta` but never emits
`commentsUpdate({ type: 'deleted' })`. The bridge's existing 'deleted'
handler is therefore unused for CLI-initiated deletes. This affects
the customer's "agent resolves comments" flow and is worth its own
ticket. The bridge's READ-side prune (this PR) is correct: it would
fire if any client — including a future CLI write path or a real
browser — removes the Y.Array entry.
Full super-editor suite (13115) and CLI suite (1246) green.
The customer's "agent resolves comments" flow needs CLI-initiated
`comments.delete` and `comments.patch({ status: 'resolved' })` calls
to reach other browser collaborators via Y.Array. Pre-fix, none of
those write paths propagated:
- The engine commands (`resolveComment`, `reopenComment`, `removeComment`)
set `tr.setMeta(CommentsPluginKey, { event: ... })` but never call
`editor.emit('commentsUpdate', ...)`.
- The headless bridge's existing `case 'deleted'`/`case 'update'`
handlers therefore never fire for CLI-initiated mutations.
- The browser's own delete path (`commentsStore.deleteComment`) sidesteps
this by manually calling `superdoc.emit('comments-update', ...)` +
`syncCommentsToClients(...)`, but `ui.comments.delete` (which routes
through `editor.doc.comments.delete`) hits the same gap — so the
browser's Document-API surface was also silently broken for collab.
Fix: emit `commentsUpdate` from `resolveCommentHandler`,
`reopenCommentHandler`, and `removeCommentHandler` in `comments-wrappers.ts`
after a successful mutation. Symmetric with how `addCommentHandler` and
`editCommentHandler` already rely on engine-level emits.
Effects:
- CLI bridge's `onCommentsUpdate('deleted' | 'update' | 'resolved')`
handlers fire and call `deleteYComment` / `updateYComment` correctly.
- Browser's `onEditorCommentsUpdate` DELETED/UPDATE branches in
`SuperDoc.vue` start receiving events for the `ui.comments.*` surface,
refreshing the Vue list to match the entity store.
- Browser's `commentsStore.deleteComment` path (calls `editor.commands.removeComment`
directly, bypassing the wrapper) is unaffected — no double-fire.
Adds 2 integration tests covering the customer's flow: agent deletes a
comment and Y.Array reflects it; agent resolves a comment and `isDone`
+ `resolvedTime` reach Y.Array.
Full super-editor suite (13117) and CLI suite (1248) green.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes SD-3214. Two commits in this PR:
editor.doc.comments.list()now surfaces full metadata (text,creatorName,creatorEmail,createdTime) for browser-authored comments. Pre-fix it only had anchor-derived fields.comments.delete/comments.patch({status:'resolved'})/comments.patch({status:'active'})now propagate to other collaborators via Y.Array. Without this, the customer's "agent resolves/responds to comments" workflow would have read correctly but written into a black hole.Customer use case (Viven Puthenpurayil, via Lighthouse): an agent reads comments from the document, addresses them, then resolves/responds. Both halves of the workflow work end-to-end after this PR.
Root causes
Read-side gap (commit 1)
Browser and CLI share most of the core (
Editor, comment mark,CommentEntityStore, Document API adapters). The per-environment orchestration was asymmetric:The browser had both halves of the Y.Array dance. The CLI bridge had only the write half — so nothing pushed Y.Array entries into the editor's
CommentEntityStore, which is whatcomments.list()reads from.Write-side gap (commit 2)
The engine commands
resolveComment,reopenComment, andremoveCommentsettr.setMeta(CommentsPluginKey, { event: ... })but never calleditor.emit('commentsUpdate', ...). So:case 'deleted' | 'update' | 'resolved'handlers (which calldeleteYComment/updateYComment) were unreachable from CLI-initiated mutations.ui.comments.*surface (which routes througheditor.doc.comments.*) was silently broken for collab — fine for local Vue list, but Y.Array never updated.commentsStore.deleteCommentpath stays unaffected because it callseditor.commands.removeCommentdirectly and manually handles Y.Array sync viasyncCommentsToClients.Fix
Commit 1: read-side
packages/super-editor: new shared helpersyncCommentEntitiesFromCollaboration(editor, entries, { previouslySynced? })incomment-entity-store.ts. Maps Y.Array entry shape toCommentEntityRecord, upserts viaupsertCommentEntity, filters outtrackedChange: trueentries (different domain), and prunes remote deletions when given the prior sync set (cascades thread replies viaremoveCommentEntityTree).apps/cli: bridge gainsattachEditor(editor)called afterEditor.open(). Seeds the store from the current Y.Array, then installs an observer that re-syncs on remote events while skipping own-origin echoes using thetransaction.origin.usershape browser writers use. TrackspreviousSyncedIdsacross observer fires for prune correctness.Commit 2: write-side
packages/super-editor:resolveCommentHandler,reopenCommentHandler, andremoveCommentHandlerincomments-wrappers.tsnow emitcommentsUpdateafter a successful mutation. Symmetric with howaddCommentHandlerandeditCommentHandleralready rely on engine-level emits.CLI engine-agnostic boundary preserved: the bridge forwards an opaque editor handle to the helper but never touches
editor.state/editor.storagedirectly. The helper lives insuper-editorso the next consumer (mobile SDK, edge worker) doesn't re-derive the mapping.Audit notes (why the write-side fix went in the wrapper, not the engine command)
Initial scoping put the write-side gap as a follow-up because changing engine commands risks double-fire in the browser. The audit found:
ui.comments.delete→editor.doc.comments.delete→ wrapper handler → engine command. No emit anywhere. Vue list silently goes stale; Y.Array unchanged.commentsStore.deleteComment(sidebar trash click, etc.) → engine command directly. Manualsuperdoc.emit+syncCommentsToClients. Bypasses the wrapper entirely.Emitting from the wrapper (this PR) fixes path A for both browser and CLI without affecting path B. Engine commands stay untouched — no double-fire risk.
How to reproduce the original bugs
Read-side (pre-fix, fails; post-fix, passes)
Write-side (pre-fix, fails; post-fix, passes)
Test plan
Unit (13 new cases in
comment-entity-store.test.ts)textalias forcommentText, importedId fallback, missing id skip.Integration (10 cases in
sd-3214-browser-comment-metadata.regression.test.ts)comments.delete→ Y.Array reflects removal.comments.patch({status:'resolved'})→ Y.Array getsisDone: true+resolvedTime.Suites
pnpm --filter @superdoc-dev/cli test— 1248 pass, 0 fail, 8269 expect(), 52 files.pnpm --filter super-editor test— 1035 files / 13117 pass / 13 skip.headless-comment-bridge.test.ts(27 cases) untouched and green.comments-wrappers.test.tsgreen — no double-emit regressions.Acceptance criteria (from ticket)
doc.comments.list()returns full metadata for browser-authored comments.Files changed
packages/super-editor/.../helpers/comment-entity-store.tssyncCommentEntitiesFromCollaborationhelper.packages/super-editor/.../plan-engine/comments-wrappers.tscommentsUpdateafter successful mutation.packages/super-editor/src/editors/v1/index.jsapps/cli/src/lib/headless-comment-bridge.tsattachEditor, Y.Array observer, prune-aware re-sync.apps/cli/src/lib/document.tscommentBridge.attachEditor(editor)afterEditor.open.apps/cli/src/__tests__/lib/_collab-password-worker.tsapps/cli/src/lib/__tests__/sd-3214-browser-comment-metadata.regression.test.tspackages/super-editor/.../helpers/comment-entity-store.test.tsRelated