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>
92 lines
4.7 KiB
Markdown
92 lines
4.7 KiB
Markdown
# 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:
|
|
|
|
1. Serializes the current graph via `app.rootGraph.serialize()`
|
|
2. Deep-compares the result against the last known `activeState`
|
|
3. If different, pushes `activeState` onto `undoQueue` and 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., `mouseup` on 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
|
|
|
|
```typescript
|
|
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 upload
|
|
- `ColorPickerButton.vue` — After changing node colors
|
|
- `NodeSearchBoxPopover.vue` — After adding a node from search
|
|
- `useAppSetDefaultView.ts` — After setting default view
|
|
- `useSelectionOperations.ts` — After align, copy, paste, duplicate, group
|
|
- `useSelectedNodeActions.ts` — After pin, bypass, collapse
|
|
- `useGroupMenuOptions.ts` — After group operations
|
|
- `useSubgraphOperations.ts` — After subgraph enter/exit
|
|
- `useCanvasRefresh.ts` — After canvas refresh
|
|
- `useCoreCommands.ts` — After metadata/subgraph commands
|
|
- `workflowService.ts` — After workflow service operations
|
|
|
|
## Transaction Guards
|
|
|
|
For operations that make multiple changes that should be a single undo entry:
|
|
|
|
```typescript
|
|
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 during `loadGraphData` (guarded by
|
|
`isLoadingGraph`) to prevent cross-workflow corruption
|
|
- `checkState()` is a no-op when `changeCount > 0` (inside a transaction)
|
|
- `undoQueue` is capped at 50 entries (`MAX_HISTORY`)
|
|
- `graphEqual` ignores node order and `ds` (pan/zoom) when comparing
|