The ChangeTracker class (src/scripts/changeTracker.ts) manages undo/redo
history by comparing serialized graph snapshots.
captureCanvasState() is the core method. It:
- Serializes the current graph via
app.rootGraph.serialize() - Deep-compares the result against the last known
activeState - If different, pushes
activeStateontoundoQueueand replaces it
It is not reactive. Changes to the graph (widget values, node positions,
links, etc.) are only captured when captureCanvasState() is explicitly triggered.
INVARIANT: captureCanvasState() asserts that it is called on the active
workflow's tracker. Calling it on an inactive tracker logs a warning and
returns early, preventing cross-workflow data corruption.
These are set up once in ChangeTracker.init():
| Trigger | Event / Hook | What It Catches |
|---|---|---|
| Keyboard (non-modifier, non-repeat) | window keydown |
Shortcuts, typing in canvas |
| Modifier key release | window keyup |
Releasing Ctrl/Shift/Alt/Meta |
| Mouse click | window mouseup |
General clicks on native DOM |
| Canvas mouse up | LGraphCanvas.processMouseUp override |
LiteGraph canvas interactions |
| Number/string dialog | LGraphCanvas.prompt override |
Dialog popups for editing widgets |
| Context menu close | LiteGraph.ContextMenu.close override |
COMBO widget menus in LiteGraph |
| Active input element | bindInput (change/input/blur on focused element) |
Native HTML input edits |
| Prompt queued | api promptQueued event |
Dynamic widget changes on queue |
| Graph cleared | api graphCleared event |
Full graph clear |
| Transaction end | litegraph:canvas after-change event |
Batched operations via beforeChange/afterChange |
The automatic triggers above are designed around LiteGraph's native DOM rendering. They do not cover:
- Vue-rendered widgets — Vue handles events internally without triggering
native DOM events that the tracker listens to (e.g.,
mouseupon a Vue dropdown doesn't bubble the same way as a native LiteGraph widget click) - Programmatic graph mutations — Any code that modifies the graph outside of user interaction (e.g., applying a template, pasting nodes, aligning)
- Async operations — File uploads, API calls that change widget values after the initial user gesture
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
// After mutating the graph:
useWorkflowStore().activeWorkflow?.changeTracker?.captureCanvasState()These locations call captureCanvasState() directly:
WidgetSelectDropdown.vue— After dropdown selection and file uploadColorPickerButton.vue— After changing node colorsNodeSearchBoxPopover.vue— After adding a node from searchbuilderViewOptions.ts— After setting default viewuseSelectionOperations.ts— After align, copy, paste, duplicate, groupuseSelectedNodeActions.ts— After pin, bypass, collapseuseGroupMenuOptions.ts— After group operationsuseSubgraphOperations.ts— After subgraph enter/exituseCanvasRefresh.ts— After canvas refreshuseCoreCommands.ts— After metadata/subgraph commandsappModeStore.ts— After app mode transitions
workflowService.ts calls captureCanvasState() indirectly via
deactivate() and prepareForSave() (see Lifecycle Methods below).
Deprecated:
checkState()is an alias forcaptureCanvasState()kept for extension compatibility. Extension authors should migrate tocaptureCanvasState(). See the@deprecatedJSDoc on the method.
| Method | Caller | Purpose |
|---|---|---|
captureCanvasState() |
Event handlers, UI interactions | Snapshots canvas into activeState, pushes undo. Asserts active tracker. |
deactivate() |
beforeLoadNewGraph only |
captureCanvasState() (skipped during undo/redo) + store(). Freezes state for tab switch. Must be called while this workflow is still active. |
prepareForSave() |
Save paths only | Active: calls captureCanvasState(). Inactive: no-op (state was frozen by deactivate()). |
store() |
Internal to deactivate() |
Saves viewport scale/offset, node outputs, subgraph navigation. |
restore() |
afterLoadNewGraph |
Restores viewport, outputs, subgraph navigation. |
reset() |
afterLoadNewGraph, save |
Resets initial state (marks workflow as "clean"). |
For operations that make multiple changes that should be a single undo entry:
changeTracker.beforeChange()
// ... multiple graph mutations ...
changeTracker.afterChange() // calls captureCanvasState() when nesting count hits 0The litegraph:canvas custom event also supports this with before-change /
after-change sub-types.
captureCanvasState()asserts it is called on the active workflow's tracker; inactive trackers get an early return (and a warning log)captureCanvasState()is a no-op duringloadGraphData(guarded byisLoadingGraph) to prevent cross-workflow corruptioncaptureCanvasState()is a no-op during undo/redo (guarded by_restoringState) to prevent undo history corruptioncaptureCanvasState()is a no-op whenchangeCount > 0(inside a transaction)undoQueueis capped at 50 entries (MAX_HISTORY)graphEqualignores node order andds(pan/zoom) when comparing