Skip to Content
API Referencetrack-changes

@stesura/track-changes

Suggesting mode: edits become tracked insertions, deletions and format changes that can be accepted or rejected, plus document compare. Framework-free; the toolbar and menus are in @stesura/track-changes-react. See the Track Changes guide.

Setup

Mount trackChangesPlugin() and install trackChangesTransactionModifier; see Setup.

trackChangesPlugin

The track-changes plugin. Owns the enabled/visible state, syncs it both ways with the userPreferences.trackChanges store, and reflects visibility as data-show-track-change on the editor DOM. Also repairs stale tracking state after edits (see appendTransaction).

Edits are rewritten into track changes by trackChangesTransactionModifier, installed separately; paste/drop sanitization is core’s trackChangesStripPastePlugin (SEC-42).

Parameters:
NameTypeDefault
__0TrackChangesPluginOptions
Returns:
Plugin<TrackChangesState>

TrackChangesPluginOptions

Options for trackChangesPlugin.

NameTypeDefault
detached{ enabled: boolean; show: boolean; }

A fixed starting state, never synced with the userPreferences store: for a state that isn’t the user’s editor, such as compare’s diff, whose tracking must not flip every live editor’s.

trackChangesTransactionModifier

transactionModifier that rewrites document-changing transactions: with tracking on into suggestions, with tracking off by stripping suggestion marks and attrs from inserted content (see handleStepOff), so a paste can’t bring in another session’s pending changes.

Collab and history transactions pass through (already tracked at their origin), as do skipTrackChanges and sub-editor ones.

Parameters:
NameTypeDefault
stateEditorState
trTransaction
userDataTrackChangesUser
forceTrackChangesboolean

Set in "review" mode: track regardless of the user’s toggle, and drop the edit if the rewrite fails (fail closed).

Returns:
Transaction

isTrackChangesEnabled

Whether edits should be recorded as track changes. Reads plugin state, not the global preference store: document compare mounts its own plugin with tracking on and leaves the preference alone.

Parameters:
NameTypeDefault
stateEditorState
Returns:
boolean

Commands

toggleTrackChanges

Flips whether edits are tracked. Turning tracking on also turns visibility on, since recording suggestions nobody can see is never the intent; turning it off leaves visibility alone. false when isTrackChangesLocked.

Parameters:
NameTypeDefault
showboolean

When given, sets visibility instead.

Returns:
Command

enableTrackChanges

Turns change tracking on, and visibility with it (see toggleTrackChanges).

Parameters:
NameTypeDefault
showboolean

When given, sets visibility instead of turning it on.

Returns:
Command

disableTrackChanges

Turns change tracking off; existing suggestions stay pending. Returns false when nothing would change, or when isTrackChangesLocked.

Parameters:
NameTypeDefault
showboolean

When given, also sets visibility. Omit to leave it as is.

Returns:
Command

toggleTrackChangesVisibility

Flips whether suggestions are displayed; tracking is untouched. Hidden, the document reads as if every suggestion were accepted.

Parameters:
NameTypeDefault
stateEditorState
dispatch(tr: Transaction) => void
viewEditorView
Returns:
boolean

selectTrackChanges

Select every node carrying the suggestion selectionId, via inline marks or block-level attrs (a deleted row or pending attr change has no marked text).

Parameters:
NameTypeDefault
selectionIdstring
Returns:
Command

selectNextTrackChanges

Select the next suggestion after the selection, resolving nothing. The fallback for “Accept/Reject and move to next” off a suggestion (TC-74). Uses TextSelection.between: a deleteClosure range ends on a block boundary (TC-80).

Parameters:
NameTypeDefault
stateEditorState
dispatch(tr: Transaction) => void
viewEditorView
Returns:
boolean

resolveTrackChanges

Resolve the suggestion under the cursor, or every suggestion the selection touches.

Empty selection: the track mark at the cursor picks the walk. The ancestor blocks’ lifecycle range is used when there is no mark, or when the mark agrees with it on author and (for a lifecycle mark) category; otherwise the mark’s own contiguous run. Either way the resolve is scoped to that author, edit burst (see ResolveTrackChangesFilter, TC-46) and lifecycle category (TC-36); modifications ride along (TC-48). With neither, the innermost ancestor’s pending attr/type change (TC-90).

Non-empty selection (including a NodeSelection): no filter, with each range widened to cover every suggestion it touches in full (TC-45).

Returns false when nothing in scope resolves; a dry run returns whether the document has any pending change.

Parameters:
NameTypeDefault
action"approve" | "reject"
moveToNextboolean
Returns:
Command

resolveAllTrackChanges

Approve or reject every track change in the document. false when there are none.

Parameters:
NameTypeDefault
actionAction
Returns:
Command

resolveAllAndStopTrackChanges

Resolve everything and turn tracking off, so a failed resolve leaves tracking on (TC-33): off on the first chunk, and an abandoned resolve drops every chunk. Undo restores the changes but not tracking, as in Word (TC-73). false when there is nothing to resolve, tracking is already off, or isTrackChangesLocked.

Parameters:
NameTypeDefault
actionAction
Returns:
Command

Resolving programmatically

resolveTrackChangesInRange

Approve or reject, into tr, every suggestion intersecting [from, to] that passes filter.

Three surfaces: inline marks (insertion / deletion / modification), the block lifecycle attr trackChanges (insertion, deletion, or deleteClosure = only the closing ¶ is deleted), and the block trackChangesModification attr, which can coexist with the lifecycle.

approve insertion → keep content, strip mark / attr reject insertion → delete content; a block whose content isn’t wholly inserted (a tracked Enter, a split’s first part) joins forward instead approve deletion → delete content / block reject deletion → keep content, restore the attr’s previous or strip approve deleteClosure → join forward into the next block reject deleteClosure → strip the attr approve modification → strip the record reject modification → restore the recorded previous value, then strip

The walk reads state.doc and writes tr: pos / node.nodeSize are state.doc coordinates, mapped* are tr.doc. Whole-block deletes and forward joins are deferred past the walk and stored as state.doc positions, mapped through tr.mapping exactly once when applied.

Parameters:
NameTypeDefault
stateEditorState
fromnumber
tonumber
trTransaction
action"approve" | "reject"
filterResolveTrackChangesFilter
Returns:
void

ResolveTrackChangesFilter

Scopes which marks / block attrs resolveTrackChangesInRange resolves. Criteria combine with AND; no filter resolves everything in the range.

  • userId → only items by this author.
  • requestedById → only items an agent made for this user (null: made for no one).
  • dateMs → only items in the edit burst around this timestamp (below).
  • trackChangesId → only items stamped with this id.
  • category → only lifecycle items of this kind (deleteClosure counts as deletion), so one Accept inside an insertion leaves the deletions around it alone (TC-36). Modifications are not gated by it: a format change typed into an insertion resolves with it (TC-48).

Burst window (TC-46): one user action can span several id/date groups (the engine only reuses a date for adjacent edits within 5s), so the window is chained rather than a fixed ±delta. Starting at [dateMs, dateMs], it extends over the sorted in-scope timestamps while each next one is within burstMs (default 60000) of the current edge. Sorted order, not doc order, keeps the result independent of where the caret sat.

NameTypeDefault
userIdstring | null
requestedByIdstring | null
dateMsnumber
burstMsnumber
trackChangesIdstring
category"deletion" | "insertion"

Reading state

getTrackChangesAtCursor

Report which track-change kinds are present at the selection, across every surface selectionSurfaces returns. Several kinds can coexist.

Parameters:
NameTypeDefault
stateEditorState
Returns:
TrackChangesAtCursor

TrackChangesAtCursor

The track-change kinds present at the selection. Each flag merges inline marks with block-level attrs on the selected node and its ancestors.

NameTypeDefault
insertionboolean

Inline insertion mark OR block-level trackChanges.type === "insertion".

deletionboolean

Inline deletion mark OR block-level trackChanges.type === "deletion".

deleteClosureboolean

Block-level trackChanges.type === "deleteClosure". No inline equivalent.

modificationboolean

Inline modification mark OR block-level trackChangesModification.

modificationsModificationDescription[]

Every pending format change at the cursor, inline and block-level.

attributionTrackChangesAttribution | null

Author and date of the closest change (inline marks before block attrs), so keyboard users get what the hover tooltip shows. null when none.

TrackChangesAttribution

Author and timestamp of a pending change. Either field may be null.

NameTypeDefault
userNamestring | null
datestring | null
requestedByNamestring | null

The user an agent made the change for: “AI Assistant for Michael”.

describeModificationsAtPos

Describe every pending format change at pos: modification marks on the adjacent inline node plus trackChangesModification on every ancestor. Position-based counterpart of getTrackChangesAtCursor, for hover UIs.

Parameters:
NameTypeDefault
stateEditorState
posnumber
blocksOnlyboolean

Skip the inline node: for block hovers, where it’s just the first child (TC-84).

Returns:
ModificationDescription[]

ModificationDescription

One pending format change, from an inline modification mark or a block’s trackChangesModification attr. label / previous / next are English display defaults; kind + key are stable ids for i18n.

NameTypeDefault
kind"nodeType" | "addMark" | "removeMark" | "attr"
keystring

The mark name ("strong", "textStyle.color"), node attr name, or "nodeType".

labelstring

Display label, e.g. “Bold”, “Alignment”, “Block type”.

previousstring | null

Display value before the change. null means “at the default”, which a UI must render distinctly from a set value (TC-30). undefined means not representable (an object attr) or not applicable (a one-sided mark delta).

nextstring | null

Display value after the change. See ModificationDescription.previous.

idstring | null
userIdstring | null
userNamestring | null
datestring | null

anyTrackChangesInDoc

Whether the document holds any pending track change; the resolver commands’ dry-run answer. O(1) from the plugin’s hasPending flag, falling back to docHasTrackChanges without the real plugin (test stubs, headless).

Parameters:
NameTypeDefault
stateEditorState
Returns:
boolean

resolvedView

The document as accept all ("final") or reject all ("original") would leave it, without dispatching: what an agent reads as the clean text. The resolve runs in a scratch state holding only a fresh numbering plugin, so counters renumber as they would, while the live editor’s plugins (pagination, collab, guards) neither see it nor can refuse it. Memoized per document.

Parameters:
NameTypeDefault
state{ doc: Node; }
view"final" | "original"
Returns:
ResolvedView

ResolvedView

A document with every tracked change resolved one way, and the mapping into it.

NameTypeDefault
docNode
mappingMapping

Listing changes

describeTrackedChanges lists the pending changes, filtered and paged, with per-author, per-kind and per-clause counts.

describeTrackedChanges

The pending tracked changes of a state’s body, filtered and paged, in document order: for a changes list in your own UI. Works on a compare state from createDiffState too; the AI’s list_changes and compare_details tools read it. Headers, footers and note bodies are not read.

Parameters:
NameTypeDefault
stateEditorState
optionsDescribeTrackedChangesOptions
Returns:
DescribeTrackedChangesResult

DescribeTrackedChangesOptions

Options for describeTrackedChanges.

NameTypeDefault
filterTrackedChangesFilter
pageTrackedChangesPage
contextWordsnumber

Default 8.

DescribeTrackedChangesResult

One page of changes, with totals and counts over every change matching the filter.

NameTypeDefault
changesTrackedChange[]

This page, in document order.

totalnumber

Changes matching the filter, all pages.

counts{ byAuthor: { id: string | null; name: string | null; requestedBy?: { id: string; name?: string; }; count: number; }[]; byKind: Record<TrackedChangeKind, number>; byClause: { ...; }[]; }

Over every change matching the filter, not only this page.

nextRefstring | null

The next page’s fromRef, or null on the last page.

missingReftrue

page.fromRef matched no change and no block: the page is empty.

TrackedChange

One pending tracked change, as describeTrackedChanges lists it.

NameTypeDefault
refstring

Unique within one result and stable while the document doesn’t change around it: the block id plus the change’s index in the block (p_402#2). The fromRef cursor.

idstring | null

The mark or attr id. Not unique: an agent’s tool call stamps one id on every change it makes. A replacement carries its deletion’s.

kindTrackedChangeKind
scopeTrackedChangeScope
author{ id: string | null; name: string | null; }
datestring | null
requestedBy{ id: string; name?: string; }

The user an agent worked for, as on comments’ CommentAuthor.requestedBy.

blockIdstring | null

The top-level body block holding the change, as read_document addresses it.

clausestring | null

The block’s clause label (8.2(a)), else the nearest numbered block’s before it: a deleted or unnumbered block reads as part of the clause it sits in. null before the first one.

beforestring

The text the change removes; "" for an insertion or a format change.

afterstring

The text the change adds; "" for a deletion. A format change’s affected text.

context{ before: string; after: string; }

Up to contextWords words each side, in the final view (every other change accepted), cut at the textblock’s edges; … marks a cut.

modificationsModificationDescription[]

For formatting and attribute: what changed.

TrackedChangeKind

type TrackedChangeKind = "inserted" | "deleted" | "replaced" | "formatting" | "attribute";

replaced is a deletion next to an insertion by the same author; formatting an inline format change; attribute a block’s attr or type change.

TrackedChangeScope

type TrackedChangeScope = "inline" | "block" | "paragraphMark";

What the change covers: inline text, a whole block, or only a block’s paragraph mark.

TrackedChangesFilter

Which changes describeTrackedChanges returns. Criteria combine with AND.

NameTypeDefault
authorstring

Author id or name, case-insensitive; also matches an agent’s requester (“Michael” finds his agent’s).

requestedByIdstring | null

Only changes an agent made for this user; null, only changes made for no one.

clausestring

A clause and its sub-clauses by label tokens: "8" matches 8, 8.2, 8.2(a), not 80.

kindTrackedChangeKind | TrackedChangeKind[]

TrackedChangesPage

A page of describeTrackedChanges results: a fromRef cursor and a size.

NameTypeDefault
fromRefstring

Start at this change’s ref, inclusive; a block id starts at that block’s first change.

limitnumber

Default 50.

Error reporting

Reports carry no document content; see Error reporting.

setTrackChangesErrorHandler

Register the reporter for track-changes failures (global; the last call wins, null clears it). By failure.phase:

  • "rewrite": the change was not tracked. Live edits still land untracked, so treat it as an error. Forced callers (compare, AI suggestions) get a throw; review mode drops the edit.
  • "resolve": an accept/reject threw; the command returned false and the document was not changed.
Parameters:
NameTypeDefault
handler(failure: TrackChangesFailure) => void
Returns:
void

TrackChangesFailure

What setTrackChangesErrorHandler’s handler receives: a failed rewrite or a failed accept/reject.

NameTypeDefault
phase"resolve" | "rewrite"
errorRedactedError

TrackChangesRewriteFailure

What a failed rewrite reports.

NameTypeDefault
phase"rewrite"
errorRedactedError
trackingboolean

Whether the rewrite was suggesting changes (on) or only stripping them (off).

stepCountnumber
stepsStepSummary[]

TrackChangesResolveFailure

What a failed accept/reject reports. The document was not changed.

NameTypeDefault
phase"resolve"
errorRedactedError
action"approve" | "reject"

RedactedError

A thrown value reduced to its name, and its message and stack with every quoted substring replaced by "…": PM’s content errors quote the document (TC-83).

NameTypeDefault
namestring
messagestring
stackstring

StepSummary

The redacted view of one step: positions and sizes only, never content.

NameTypeDefault
stepTypestring
fromnumber
tonumber
posnumber
sliceSizenumber

Size of the step’s slice; its content is omitted.

attrNamestring

Attr name only; the value can be anything.

markTypestring

Mark type name only; mark attrs carry hrefs and user names.

stepCountnumber

A batch’s inner step count.

stepsStepSummary[]

A batch’s first inner steps, summarized: a bulk edit holds thousands.

Document compare

createDiffState

Build an EditorState showing startJson → endJson as pending suggestions: the recreateTransform diff, applied with tracking forced on. Derived data (counters, _ attrs, numbering caches) isn’t diffed (CMP-4): both docs are stripped of it and plugins derive it again. Nodes pair by content, and ids aren’t diffed: accept gives endJson with A’s ids on the nodes that pair (CMP-7, CMP-9).

Parameters:
NameTypeDefault
schemaSchema<any, any>
startJsonJSONContent
endJsonJSONContent
pluginsPlugin<any>[]

Editor plugins for the diff state. A track-changes plugin detached from the preference store is appended (the real one would flip tracking in every live editor); don’t pass trackChangesPlugin(). Pass the editor’s full set: without the numbering plugin the diff shows no list counters.

optionsCreateDiffStateOptions

wordDiffs diffs changed text by word (within [-90-]{+120+} days) instead of by character, the default. Blocks pair the same either way; only the inline changes inside a paired block differ.

Returns:
EditorState

CreateDiffStateOptions

Options of createDiffState.

NameTypeDefault
wordDiffsboolean

readJsonFile

Read a user-picked .json file into a JSONContent doc for createDiffState. Rejects files over MAX_COMPARE_FILE_BYTES before reading, and unparseable ones with “Invalid JSON file”.

Parameters:
NameTypeDefault
fileFile
Returns:
Promise<JSONContent>

Lower-level building blocks

transformToTrackChangesTransaction

Rebuild originalTransaction step by step into a new transaction that records its changes instead of applying them: deletions become deletion marks, insertions get insertion marks, attr changes go on trackChangesModification. With tracking off, steps apply verbatim with tracking state stripped from inserted content.

Returns the original unchanged when the schema has no track-changes marks, the transaction changes nothing, or tracking is off and nothing it inserts carries tracking state (carriesTrackingState).

Parameters:
NameTypeDefault
originalTransactionTransaction
stateEditorState
userDataTrackChangesUser
forceTrackChangesboolean

Track regardless of the plugin’s enabled state (document compare, AI suggestions, review mode).

trackChangesIdstring

Id for every mark/attr this transform creates, unless a merge reuses an existing one. Pass one to resolve this edit later (e.g. AI suggestions).

Returns:
Transaction

Testing

From @stesura/track-changes/testing.

createTrackChangesTestState

Build an EditorState with the real trackChangesPlugin, for command and plugin specs. Sets the global userPreferences.trackChanges store first; the value persists across the test process.

Parameters:
NameTypeDefault
__0{ doc: TaggedNode; enabled?: boolean; show?: boolean; selection?: number; }
Returns:
EditorState

trackChangesStubPlugin

Minimal trackChangesKey plugin for engine unit tests; unlike the real plugin it touches no global store. Its state is static (meta is ignored). Use createTrackChangesTestState for the real plugin’s behaviour.

Parameters:
NameTypeDefault
__0{ enabled?: boolean; show?: boolean; markActivityMap?: Map<string, number>; }
Returns:
Plugin<{ enabled: boolean; show: boolean; markActivityMap: Map<string, number>; }>

normalizeRevisionIds

Return a copy of doc with placeholder id / date on every track mark and trackChanges / trackChangesModification attr, so docs compare with eq() despite the engine minting fresh ids and timestamps.

Parameters:
NameTypeDefault
docNode
Returns:
Node

trackChangesTestSchema

The core schema plus difficulty, block_wrapper and verse.

NameTypeDefault
spec{ nodes: OrderedMap<NodeSpec>; marks: OrderedMap<MarkSpec>; topNode?: string; }

The spec  on which the schema is based, with the added guarantee that its nodes and marks properties are OrderedMap instances (not raw objects).

nodes{ readonly [x: string]: NodeType; } & { readonly [key: string]: NodeType; }

An object mapping the schema’s node names to node type objects.

marks{ readonly [x: string]: MarkType; } & { readonly [key: string]: MarkType; }

A map from mark names to mark type objects.

linebreakReplacementNodeType | null

The linebreak replacement  node defined in this schema, if any.

topNodeTypeNodeType

The type of the default top node  for this schema.

cached{ [key: string]: any; }

An object for storing whatever values modules may want to compute and cache per schema. (If you want to store something in it, try to use property names unlikely to clash.)

node(type: string | NodeType, attrs?: Attrs | null | undefined, content?: Node | readonly Node[] | Fragment | undefined, marks?: readonly Mark[] | undefined) => Node

Create a node in this schema. The type may be a string or a NodeType instance. Attributes will be extended with defaults, content may be a Fragment, null, a Node, or an array of nodes.

text(text: string, marks?: readonly Mark[] | null | undefined) => Node

Create a text node in the schema. Empty text nodes are not allowed.

mark(type: string | MarkType, attrs?: Attrs | null | undefined) => Mark

Create a mark with the given type and attributes.

nodeFromJSON(json: any) => Node

Deserialize a node from its JSON representation. This method is bound.

markFromJSON(json: any) => Mark

Deserialize a mark from its JSON representation. This method is bound.

tb

Typed builders bound to trackChangesTestSchema.

NameTypeDefault
docNodeBuilder
pNodeBuilder
headingNodeBuilder
sNodeBuilder
shNodeBuilder
scNodeBuilder
sfNodeBuilder
hfcNodeBuilder
fNodeBuilder
wrapperNodeBuilder
verseNodeBuilder

Custom textblock carrying the block track-changes attrs — see verse.

tableNodeBuilder
trNodeBuilder
tdNodeBuilder
brNodeBuilder

hard_break: the cheapest inline atom.

imgNodeBuilder
seq_fieldNodeBuilder

A caption number (Word’s SEQ field), an inline atom.

phNodeBuilder

placeholder: an inline atom with text content (Word’s placeholder text).

strongMarkBuilder
deletionMarkBuilder
insertionMarkBuilder
modificationMarkBuilder
difficultyMarkBuilder
schemaSchema<any, any>

tsdoc

A valid doc wrapping content in the minimal section structure. Header and footer hold an empty paragraph, since header_footer_content requires block+.

Parameters:
NameTypeDefault
contentTaggedNode[]
Returns:
TaggedNode
Last updated on