A document editor can show a perfectly reasonable AI suggestion that is already wrong to apply.
Imagine a paragraph in a shared creative brief:
The father leaves the room.
A writer asks an agent to rewrite it. While the suggestion is being prepared, a collaborator changes the brief:
The father stays and helps his daughter.
The agent returns a polished version of the first sentence. Someone clicks Apply. The problem is no longer the quality of the generated sentence. It is whether the proposal still refers to the document people are editing now.
A shared editing system can merge updates without understanding that a pending rewrite expresses an outdated decision. That requires a separate check in the proposal workflow.
Keep the proposal separate from the edit
The implementation examined here is in Breatic's public repository. Its document-edit tool returns a proposal card rather than immediately rewriting the shared document.
A proposal carries the target Space, a block ID, the block's original plain text, the proposed content, and its own proposal ID. Comments, rewrites and inserts use the same proposal shape, with different requirements for their targets.
The original text is important. An ID tells the application which block the model saw. It does not tell the application whether that block still says the same thing.
For a rewrite, the relevant check is conceptually this:
const block = findBlockById(currentDocument, proposal.blockId);
if (block === null) return { outcome: "blockGone" };
if (plainText(block) !== proposal.original) {
return { outcome: "changed" };
}
This is an explanatory sketch, not a standalone API example. In the implementation, the check reads the editor's current ProseMirror document. It compares the block's plain text, not a full structural hash of every attribute and mark.
If the collaborator has rewritten the paragraph, the proposal is refused instead of silently replacing the new wording. The user can ask for another proposal against the current text.
A real example-project capture. The scene is illustrative; the rewrite has not been applied in this image.
Check again after asynchronous preparation
Putting the check in the click handler is not enough if the handler then awaits work.
The proposed Markdown has to become editor blocks. Preparing those blocks can also wait for media configuration. During that interval, another edit can arrive, the target can disappear, or the current editor can become read-only.
The landing function therefore checks the current state twice: before preparation and again after it finishes, immediately before the synchronous block write. A simplified outline is:
const initial = inspectCurrentState();
if (initial !== null) return initial;
const prepared = await prepareNewContent(proposal.body);
const ready = inspectCurrentState();
if (ready !== null) return ready;
writePreparedBlocks(prepared);
inspectCurrentState stands for more than the text comparison. It checks whether the request is still available, whether this proposal has already landed, the current write admission, whether the editor is editable, and the current target.
Notice what is absent from the second check: a target position saved before the await. The implementation looks up the block again by ID. A collaborator may have moved it, including into a table cell, without changing its words. The new content must be normalized for its current location.
Applying once needs a durable trace
A second click should not insert the same proposal again. A local applied = true flag does not explain what happened after a reload.
For inserted or rewritten content, the implementation derives root block IDs from the proposal ID and each output block's index. A later render can look for that trace in the shared document. Comments instead carry proposal metadata on their thread and must still have a range in the document to count as landed.
The trace describes the document's current state. If the write is undone and its output disappears, the proposal no longer counts as landed. If it is redone, the trace is there again.
The write is also bracketed as one undo step. Accepting a multi-block suggestion should not require a user to undo its fragments one by one, or undo text they typed just before accepting it.
Test the gap rather than the happy path
The useful regression case deliberately pauses asynchronous preparation. While it is paused, the test changes the target, removes it, or makes the editor read-only. It then releases preparation and checks the result.
The public test suite asserts that these cases return changed, blockGone, or readOnly, leave the current document intact, and do not add an undo item. Other cases move the target into or out of a table cell during preparation, and verify that output is normalized for the new location.
The tests also cover repeated application, undo and redo, and reading the resulting document through a second Yjs document. These are assertions present in the repository; this article does not report a new benchmark or a fresh run of the full suite.
What this check does not solve
The plain-text comparison is a deliberately narrow guard. It does not detect every formatting-only change or establish that the model's interpretation of the brief is correct. A final browser-side check is also not a distributed lock against every remote update that might arrive later.
Its job is more specific: refuse a proposal when the current local document no longer contains the text it was proposed against, and avoid using state captured before an asynchronous pause.
The practical place to review a collaborative AI editing feature is the moment between “the user accepted this suggestion” and “the shared document changed.” That interval has its own state, permissions and failure cases, even when the suggestion itself looks finished.
Implementation references
- Proposal schema and original-text contract
- Proposal generation without immediate document writes
- Commit-time checks and undo boundary
- Regression tests for changes during preparation
Disclosure: This article was prepared with AI assistance, using the linked public implementation and tests as its technical sources.

Top comments (0)