mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-13 17:10:06 +00:00
Documents the ChangeTracker undo/redo system: how checkState() works, all automatic triggers, when manual calls are needed, transaction guards, and key invariants. Companion to #9623 which fixed missing checkState() calls in Vue widgets. ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-9767-docs-add-change-tracker-architecture-documentation-3216d73d365081268c27c54e0d5824e6) by [Unito](https://www.unito.io) --------- Co-authored-by: GitHub Action <action@github.com>
4.7 KiB
4.7 KiB
Change Tracker (Undo/Redo System)
The ChangeTracker class (src/scripts/changeTracker.ts) manages undo/redo
history by comparing serialized graph snapshots.
How It Works
checkState() 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 checkState() is explicitly triggered.
Automatic Triggers
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 |
When You Must Call checkState() Manually
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
Pattern for Manual Calls
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
// After mutating the graph:
useWorkflowStore().activeWorkflow?.changeTracker?.checkState()
Existing Manual Call Sites
These locations already call checkState() explicitly:
WidgetSelectDropdown.vue— After dropdown selection and file uploadColorPickerButton.vue— After changing node colorsNodeSearchBoxPopover.vue— After adding a node from searchuseAppSetDefaultView.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 commandsworkflowService.ts— After workflow service operations
Transaction Guards
For operations that make multiple changes that should be a single undo entry:
changeTracker.beforeChange()
// ... multiple graph mutations ...
changeTracker.afterChange() // calls checkState() when nesting count hits 0
The litegraph:canvas custom event also supports this with before-change /
after-change sub-types.
Key Invariants
checkState()is a no-op duringloadGraphData(guarded byisLoadingGraph) to prevent cross-workflow corruptioncheckState()is a no-op whenchangeCount > 0(inside a transaction)undoQueueis capped at 50 entries (MAX_HISTORY)graphEqualignores node order andds(pan/zoom) when comparing