@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).
| Name | Type | Default |
|---|---|---|
__0 | TrackChangesPluginOptions |
TrackChangesPluginOptions
Options for trackChangesPlugin.
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.
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.
| Name | Type | Default |
|---|---|---|
state | EditorState |
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.
| Name | Type | Default |
|---|---|---|
show | booleanWhen given, sets visibility instead. |
enableTrackChanges
Turns change tracking on, and visibility with it (see toggleTrackChanges).
| Name | Type | Default |
|---|---|---|
show | booleanWhen given, sets visibility instead of turning it on. |
disableTrackChanges
Turns change tracking off; existing suggestions stay pending. Returns false
when nothing would change, or when isTrackChangesLocked.
| Name | Type | Default |
|---|---|---|
show | booleanWhen given, also sets visibility. Omit to leave it as is. |
toggleTrackChangesVisibility
Flips whether suggestions are displayed; tracking is untouched. Hidden, the document reads as if every suggestion were accepted.
| Name | Type | Default |
|---|---|---|
state | EditorState | |
dispatch | (tr: Transaction) => void | |
view | EditorView |
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).
| Name | Type | Default |
|---|---|---|
selectionId | string |
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).
| Name | Type | Default |
|---|---|---|
state | EditorState | |
dispatch | (tr: Transaction) => void | |
view | EditorView |
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.
| Name | Type | Default |
|---|---|---|
action | "approve" | "reject" | |
moveToNext | boolean |
resolveAllTrackChanges
Approve or reject every track change in the document. false when there are none.
| Name | Type | Default |
|---|---|---|
action | Action |
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.
| Name | Type | Default |
|---|---|---|
action | Action |
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.
| Name | Type | Default |
|---|---|---|
state | EditorState | |
from | number | |
to | number | |
tr | Transaction | |
action | "approve" | "reject" | |
filter | ResolveTrackChangesFilter |
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 (deleteClosurecounts asdeletion), 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.
| Name | Type | Default |
|---|---|---|
userId | string | null | |
requestedById | string | null | |
dateMs | number | |
burstMs | number | |
trackChangesId | string | |
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.
| Name | Type | Default |
|---|---|---|
state | EditorState |
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.
TrackChangesAttribution
Author and timestamp of a pending change. Either field may be null.
| Name | Type | Default |
|---|---|---|
userName | string | null | |
date | string | null | |
requestedByName | string | nullThe 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.
| Name | Type | Default |
|---|---|---|
state | EditorState | |
pos | number | |
blocksOnly | booleanSkip the inline node: for block hovers, where it’s just the first child (TC-84). |
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.
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).
| Name | Type | Default |
|---|---|---|
state | EditorState |
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.
| Name | Type | Default |
|---|---|---|
state | { doc: Node; } | |
view | "final" | "original" |
ResolvedView
A document with every tracked change resolved one way, and the mapping into it.
| Name | Type | Default |
|---|---|---|
doc | Node | |
mapping | Mapping |
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.
| Name | Type | Default |
|---|---|---|
state | EditorState | |
options | DescribeTrackedChangesOptions |
DescribeTrackedChangesOptions
Options for describeTrackedChanges.
| Name | Type | Default |
|---|---|---|
filter | TrackedChangesFilter | |
page | TrackedChangesPage | |
contextWords | numberDefault 8. |
DescribeTrackedChangesResult
One page of changes, with totals and counts over every change matching the filter.
TrackedChange
One pending tracked change, as describeTrackedChanges lists it.
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.
TrackedChangesPage
A page of describeTrackedChanges results: a fromRef cursor and a size.
| Name | Type | Default |
|---|---|---|
fromRef | stringStart at this change’s | |
limit | numberDefault 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 returnedfalseand the document was not changed.
| Name | Type | Default |
|---|---|---|
handler | (failure: TrackChangesFailure) => void |
TrackChangesFailure
What setTrackChangesErrorHandler’s handler receives: a failed rewrite or a
failed accept/reject.
| Name | Type | Default |
|---|---|---|
phase | "resolve" | "rewrite" | |
error | RedactedError |
TrackChangesRewriteFailure
What a failed rewrite reports.
| Name | Type | Default |
|---|---|---|
phase | "rewrite" | |
error | RedactedError | |
tracking | booleanWhether the rewrite was suggesting changes (on) or only stripping them (off). | |
stepCount | number | |
steps | StepSummary[] |
TrackChangesResolveFailure
What a failed accept/reject reports. The document was not changed.
| Name | Type | Default |
|---|---|---|
phase | "resolve" | |
error | RedactedError | |
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).
| Name | Type | Default |
|---|---|---|
name | string | |
message | string | |
stack | string |
StepSummary
The redacted view of one step: positions and sizes only, never content.
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).
CreateDiffStateOptions
Options of createDiffState.
| Name | Type | Default |
|---|---|---|
wordDiffs | boolean |
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”.
| Name | Type | Default |
|---|---|---|
file | File |
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).
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.
| Name | Type | Default |
|---|---|---|
__0 | { doc: TaggedNode; enabled?: boolean; show?: boolean; selection?: number; } |
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.
| Name | Type | Default |
|---|---|---|
__0 | { 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.
| Name | Type | Default |
|---|---|---|
doc | Node |
trackChangesTestSchema
The core schema plus difficulty, block_wrapper and verse.
| Name | Type | Default |
|---|---|---|
spec | { nodes: OrderedMap<NodeSpec>; marks: OrderedMap<MarkSpec>; topNode?: string; }The spec on which the schema is based,
with the added guarantee that its | |
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. | |
linebreakReplacement | NodeType | nullThe linebreak replacement node defined in this schema, if any. | |
topNodeType | NodeTypeThe 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) => NodeCreate a node in this schema. The | |
text | (text: string, marks?: readonly Mark[] | null | undefined) => NodeCreate a text node in the schema. Empty text nodes are not allowed. | |
mark | (type: string | MarkType, attrs?: Attrs | null | undefined) => MarkCreate a mark with the given type and attributes. | |
nodeFromJSON | (json: any) => NodeDeserialize a node from its JSON representation. This method is bound. | |
markFromJSON | (json: any) => MarkDeserialize a mark from its JSON representation. This method is bound. |
tb
Typed builders bound to trackChangesTestSchema.
tsdoc
A valid doc wrapping content in the minimal section structure. Header and
footer hold an empty paragraph, since header_footer_content requires block+.
| Name | Type | Default |
|---|---|---|
content | TaggedNode[] |