Skip to content

fix(ai): operations on collaborative documents - #2952

Merged
nperez0111 merged 1 commit into
mainfrom
fix/ai-collab-position-tracking
Aug 10, 2026
Merged

fix(ai): operations on collaborative documents#2952
nperez0111 merged 1 commit into
mainfrom
fix/ai-collab-position-tracking

Conversation

@nperez0111

@nperez0111 nperez0111 commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes #2946

Running any AI request on a selection in a Yjs-collaborative document fails with Position not found, cannot track positions.

Rationale

AIExtension.invokeAI forks the Y.Doc before a request starts, so AI changes don't reach other collaborators until they're accepted. Forking swaps the ySync plugin, which reconfigures the ProseMirror state — and ProseMirror carries over the state of plugins that share a key instead of re-initializing them.

The new plugin's binding still ends up on the forked fragment (it's set from the plugin's view, via a transaction), but type and doc keep pointing at the fragment the editor was bound to before. The plugin state ends up split across two Y.Docs:

ySyncPluginKey.getState(state)
// { type: originalFragment, doc: originalDoc, binding: newBinding(forkedFragment) }

RelativePositionMappingExtension.mapPosition then resolved with the stale doc and the new binding.type. y-prosemirror decodes the position against doc, notices the resulting type isn't part of documentType, and returns null — every single lookup, not just garbage-collected ones.

createUpdateBlockTool calls trackPosition for updateSelection.from/to to follow the selection across the LLM round-trip, so it threw on the first chunk of the response. The LLM call itself succeeded (chat.status === "ready"), which is why the AI menu showed a generic error despite a perfectly valid tool call.

Changes

  • ForkYDocExtension: re-point the ySync plugin state's type/doc at the newly bound fragment, on both fork() and merge().
  • RelativePositionMappingExtension: resolve relative positions against the doc that owns the currently bound type, rather than the plugin state's doc.

Impact

Fixing the plugin state at the source also covers other readers of ySyncPluginKey.getState(...).type / .doc while forked, including y-prosemirror's own sync plugin, which wraps local ProseMirror changes in a transaction on pluginState.doc.

The mapping change is a no-op when not forked: binding.type.doc and pluginState.doc are the same Y.Doc.

Positions tracked before a fork keep resolving after it — forking copies the Y.Doc, so item IDs are preserved on both sides — which the second test below covers.

Testing

  • ForkYDoc.test.ts: tracking a position while forked, and tracking one across fork → edit → merge. Both fail on main with Position not found, cannot track positions.
  • collabUpdate.test.ts: runs the update stream tool end-to-end against a forked collaborative editor, parameterized over local/collaborative so the two are held to the same result. Runs fully offline — the tool call is fed straight into StreamToolExecutor, no LLM.

packages/core 716 passed / 9 skipped, packages/xl-ai 204 passed / 260 skipped (skips are the API-key-gated model suites, unchanged).

Checklist

  • Code follows the project's coding standards.
  • Unit tests covering the new feature have been added.
  • All existing tests pass.
  • The documentation has been updated to reflect the new feature

Summary by CodeRabbit

  • Bug Fixes

    • Improved collaborative editing when documents are forked and merged, preserving text positions and selections.
    • Prevented stale document or fragment references after switching between forked and merged content.
    • Improved position tracking for updates made within collaborative documents.
    • Fixed AI HTML updates in both local and collaborative editors, including scenarios with and without an active selection.
  • Tests

    • Added coverage for fork/merge position tracking and AI update workflows.

@vercel

vercel Bot commented Aug 10, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
blocknote Ready Ready Preview Aug 10, 2026 10:02am
blocknote-website Ready Ready Preview Aug 10, 2026 10:02am

Request Review

@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The PR rebinds ProseMirror Yjs synchronization state during document fork and merge operations. Position mapping now uses the bound Y.Doc. New tests cover tracked positions and AI updates in local and collaborative editors.

Changes

Yjs position synchronization

Layer / File(s) Summary
Fork and merge synchronization rebinding
packages/core/src/yjs/extensions/ForkYDoc.ts, packages/core/src/yjs/extensions/RelativePositionMapping.ts
Fork and merge operations rebind ySync to the active fragment and Y.Doc. Relative positions resolve through the bound Yjs type document.
Position and AI update regression coverage
packages/core/src/yjs/extensions/ForkYDoc.test.ts, packages/xl-ai/src/api/formats/html-blocks/collabUpdate.test.ts, packages/xl-ai/package.json
Tests verify tracked positions across fork and merge operations and AI updates in local and collaborative editors. The AI package adds yjs as a development dependency.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Editor
  participant ForkYDoc
  participant ySync
  participant RelativePositionMapping
  Editor->>ForkYDoc: Fork collaborative document
  ForkYDoc->>ySync: Bind forked fragment and Y.Doc
  RelativePositionMapping->>RelativePositionMapping: Resolve position from bound Y.Doc
  Editor->>ForkYDoc: Merge forked changes
  ForkYDoc->>ySync: Restore original fragment and Y.Doc
Loading

Possibly related PRs

Poem

A rabbit watched the forked docs divide,
Then tracked each position on the ride.
The sync state followed, neat and true,
While AI updates hopped through too.
Merge restored the path with care—
No stale Y.Doc hiding there.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly describes the primary fix for AI operations on collaborative documents.
Description check ✅ Passed The description explains the issue, rationale, changes, impact, and testing, with only optional template sections omitted.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/ai-collab-position-tracking

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/xl-ai/src/api/formats/html-blocks/collabUpdate.test.ts`:
- Around line 55-59: Update the returned fork function around the yForkDoc
extension lookup to fail explicitly when ForkYDocExtension is unavailable
instead of returning undefined through optional chaining. Preserve the existing
extension.fork() behavior when the extension is present, and ensure
collaborative tests cannot silently skip the forked Y.Doc path.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: fe468dca-4bf7-4407-839d-708a56da3637

📥 Commits

Reviewing files that changed from the base of the PR and between 4998d23 and edbeefb.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (5)
  • packages/core/src/yjs/extensions/ForkYDoc.test.ts
  • packages/core/src/yjs/extensions/ForkYDoc.ts
  • packages/core/src/yjs/extensions/RelativePositionMapping.ts
  • packages/xl-ai/package.json
  • packages/xl-ai/src/api/formats/html-blocks/collabUpdate.test.ts

Comment on lines +55 to +59
return {
editor,
fork: () =>
editor.getExtension<typeof ForkYDocExtension>("yForkDoc")?.fork(),
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Fail when the fork extension is unavailable.

Optional chaining makes fork() a no-op when yForkDoc is missing. The collaborative tests can then pass without testing the forked Y.Doc path. Throw if the extension is unavailable.

Proposed fix
   return {
     editor,
-    fork: () =>
-      editor.getExtension<typeof ForkYDocExtension>("yForkDoc")?.fork(),
+    fork: () => {
+      const forkYDoc = editor.getExtension<typeof ForkYDocExtension>("yForkDoc");
+      if (!forkYDoc) {
+        throw new Error("yForkDoc extension is not available");
+      }
+      forkYDoc.fork();
+    },
   };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return {
editor,
fork: () =>
editor.getExtension<typeof ForkYDocExtension>("yForkDoc")?.fork(),
};
return {
editor,
fork: () => {
const forkYDoc = editor.getExtension<typeof ForkYDocExtension>("yForkDoc");
if (!forkYDoc) {
throw new Error("yForkDoc extension is not available");
}
forkYDoc.fork();
},
};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/xl-ai/src/api/formats/html-blocks/collabUpdate.test.ts` around lines
55 - 59, Update the returned fork function around the yForkDoc extension lookup
to fail explicitly when ForkYDocExtension is unavailable instead of returning
undefined through optional chaining. Preserve the existing extension.fork()
behavior when the extension is present, and ensure collaborative tests cannot
silently skip the forked Y.Doc path.

@pkg-pr-new

pkg-pr-new Bot commented Aug 10, 2026

Copy link
Copy Markdown

Open in StackBlitz

@blocknote/ariakit

npm i https://pkg.pr.new/@blocknote/ariakit@2952

@blocknote/code-block

npm i https://pkg.pr.new/@blocknote/code-block@2952

@blocknote/core

npm i https://pkg.pr.new/@blocknote/core@2952

@blocknote/mantine

npm i https://pkg.pr.new/@blocknote/mantine@2952

@blocknote/react

npm i https://pkg.pr.new/@blocknote/react@2952

@blocknote/server-util

npm i https://pkg.pr.new/@blocknote/server-util@2952

@blocknote/shadcn

npm i https://pkg.pr.new/@blocknote/shadcn@2952

@blocknote/xl-ai

npm i https://pkg.pr.new/@blocknote/xl-ai@2952

@blocknote/xl-docx-exporter

npm i https://pkg.pr.new/@blocknote/xl-docx-exporter@2952

@blocknote/xl-email-exporter

npm i https://pkg.pr.new/@blocknote/xl-email-exporter@2952

@blocknote/xl-multi-column

npm i https://pkg.pr.new/@blocknote/xl-multi-column@2952

@blocknote/xl-odt-exporter

npm i https://pkg.pr.new/@blocknote/xl-odt-exporter@2952

@blocknote/xl-pdf-exporter

npm i https://pkg.pr.new/@blocknote/xl-pdf-exporter@2952

commit: e867078

@nperez0111 nperez0111 changed the title fix: AI operations on collaborative documents fix(ai): operations on collaborative documents Aug 10, 2026
`AIExtension.invokeAI` forks the Y.Doc before a request starts, so AI
changes don't reach other collaborators until they're accepted. Forking
swaps the `ySync` plugin, which reconfigures the ProseMirror state — and
ProseMirror carries over the state of plugins that share a key rather
than re-initializing them.

The new plugin's `binding` still ends up on the forked fragment (it's
set from the plugin's view, via a transaction), but `type` and `doc`
keep pointing at the fragment the editor was bound to before. That left
the plugin state split across two Y.Docs.

`RelativePositionMappingExtension` resolved tracked positions with the
stale `doc` and the new `binding.type`, so the decoded type was never
part of the bound fragment and every lookup returned `null`. The
`update` tool tracks the selection across the LLM round-trip, so any AI
request on a selection in a collaborative document failed with
"Position not found, cannot track positions".

Re-point the plugin state's `type`/`doc` at the newly bound fragment on
both fork and merge, and resolve relative positions against the doc that
owns the bound type.
@github-actions

github-actions Bot commented Aug 10, 2026

Copy link
Copy Markdown
PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-08-10 10:25 UTC

@nperez0111
nperez0111 merged commit 824abce into main Aug 10, 2026
19 checks passed
@nperez0111
nperez0111 deleted the fix/ai-collab-position-tracking branch August 10, 2026 10:25
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.

AI "update" tool intermittently throws when running against a Yjs-collaborative document

1 participant