Revise Developer Platform menu

Tracked changes

Suggestions your users can accept or reject, from people and agents alike.

These docs are for version 2.3.x. See the latest version.

Modes#

A document in suggesting mode records every change as a tracked suggestion instead of applying it. This covers typing, toolbar commands, and agent tool calls. An agent can draft in this mode, and the user reviews each change.

TypeScript
editor.view.setDocumentMode("suggesting");

Set the starting mode per document, or editor-wide:

TSX
<ReviseEditor
  defaultDocumentMode="suggesting"
  initialDocuments={[{ id: "a", source: file, documentMode: "editing" }]}
/>

defaultDocumentMode sets the starting mode only. Changing the prop later does not affect a document that is already open. Use view.setDocumentMode() for that.

The user can switch modes. To lock a participant into suggesting, use a role instead. role="suggester" pins the mode, refuses direct edits, and hides accept and reject everywhere.

Reviewing#

The review controller is the host API for tracked changes. It is not an agent tool: it exposes interactive state and moves the caret and viewport.

TypeScript
const state = editor.review.getState();
// { open, targetCount, openSuggestionIds, currentSuggestionIds, ... }

editor.review.open();
editor.review.next();
editor.review.acceptCurrent();
editor.review.rejectCurrent();
editor.review.acceptAll();

Subscribe to render your own review UI:

TypeScript
useEffect(() => editor.review.subscribe(setReviewState), [editor]);

Targeted operations#

To act on specific suggestions, such as everything one agent run produced, pass their IDs:

TypeScript
const ids = editor.review.getOpenSuggestionIds();

editor.review.navigateToSuggestion(ids[0]);
editor.review.previewSuggestions(ids, "accept"); // preview only, no changes
editor.review.acceptSuggestions(ids); // { resolved, missing, unresolved }

previewCurrent() and previewAll() do the same for the current stop and the whole document. Pass null to clear a preview.

Display#

TypeScript
editor.review.setShowRemovals(false);
editor.review.setSuggestionViewMode("final"); // "all-markup" | "final" | "original"

"final" shows the document with every suggestion accepted. "original" shows it with none accepted. Neither changes the document, so either works as a read-only "clean copy" toggle.

Direct mode#

To apply an agent edit without tracking it, such as a formatting sweep or a find-and-replace the user asked for, pass directMode per call:

TypeScript
await editor.tools.execute(
  "style_blocks",
  { selectors: "*", attrs: [{ name: "fontFamily", value: "Georgia" }] },
  { directMode: true },
);

In a delegated agent run, every tool call in the loop inherits the mode. One flag covers the whole task.

Direct mode skips tracked changes. Use it only for changes the user already approved in your UI.

Attribution#

Each suggestion records its author, and agent suggestions are marked as such. review.getState().visibleAgentSuggestionIds lists only the agent's suggestions, and nextAgentSuggestion() steps through only those. Use them to review the agent's changes without the user's own edits.

Changes made through tools.execute() and agent runs are agent changes, signed "Revise Agent". To sign them as your product's assistant, name it:

TSX
<ReviseEditor agent={{ name: "Acme Assistant" }} />

People's own edits use currentUser.

Export#

exportDocx() writes tracked changes as Word revision marks. A reviewer who opens the file in Word sees the same suggestions, and accepting them there gives the same result as accepting them in Revise.

Suggestions use the identity you pass as currentUser. Without it, human suggestions export with the reviewer name "Anonymous" and agent suggestions as "Revise Agent". The name is recorded at edit time, not at export, so pass currentUser before anyone edits.

Word round trip#

DOCX import and export preserve revision marks. From a Word file, through the editor, and back:

Word markupRound trip
w:ins / w:del, including nested insert-inside-deletePreserved, as layered suggestions
Run formatting changes (w:rPrChange)Preserved, with the prior properties, so rejecting reverts
Paragraph property changes (w:pPrChange)Preserved, with the prior properties
Paragraph-mark (pilcrow) revisionsPreserved, as pending splits and merges
Tracked deletion of a section breakPreserved, both directions
Table rows, cells, and table propertiesPreserved, as one resolvable change per table
Author, date, and Word revision idsPreserved, both directions
Comments, threading, resolved state, anchors inside tracked runsPreserved

Tracked row insertions and deletions (w:trPr → w:ins / w:del), cell revisions (w:cellIns, w:cellDel, w:cellMerge), and table property changes (w:tblPrChange) import as resolvable suggestions. They export as the same markup, with authors intact.

Linked moves (w:moveFrom / w:moveTo pairs) import as one move suggestion. Accepting either half keeps the text at its destination. Rejecting either half restores the original location. Export writes the paired move markup back. An orphaned or mismatched half imports as an ordinary insertion or deletion, so unrelated content is never resolved together.

Custom review panel#

listChanges() returns every pending change with the metadata a panel needs. Use it to render the list yourself instead of using the built-in ribbon:

TypeScript
for (const change of editor.review.listChanges()) {
  // { id, kind, author, authorType, createdAt, blockIds,
  //   insertedText, deletedText, description, agentModel,
  //   moveSourceBlockIds, moveDestinationBlockIds }
}

editor.review.getChange(id); // one row, or null once it is resolved
FieldValue
kind"insert", "delete", or "format" (as in Word), or "move" for a linked move pair
authorA person's name, "Revise Agent", an external agent's name, or "Anonymous" when no identity was supplied. Imported Word redlines keep their original reviewer.
blockIdsThe blocks the change touches. navigateToSuggestion() scrolls to these.
moveSourceBlockIds, moveDestinationBlockIdsWhere moved text left and where it landed. deletedText and insertedText hold the text at each end.

A replacement made in one operation, such as the agent's replace tool, is one change with both deletedText and insertedText set. Its kind is the one it was recorded with, usually "delete", so check for both texts to show it as a replacement. One accept or reject settles both halves. A move is also one change for both halves, and one accept or reject settles both locations.

Wire a row to the controller with the ID:

TSX
const verb =
  change.kind === "move"
    ? "moved"
    : change.kind === "format"
      ? "reformatted"
      : change.deletedText && change.insertedText
        ? "replaced"
        : change.kind === "delete"
          ? "removed"
          : "added";

<li onClick={() => editor.review.navigateToSuggestion(change.id)}>
  <b>{change.author}</b> {verb}{" "}
  <q>{change.deletedText ?? change.insertedText}</q>
  <button onClick={() => editor.review.acceptSuggestions([change.id])}>
    Accept
  </button>
</li>

listChanges() is not a subscription. It computes the list from the document on each call, in one pass. Call it when you render, for example after review.subscribe() reports that the counts changed.