diff --git a/docs/architecture/change-tracker.md b/docs/architecture/change-tracker.md new file mode 100644 index 0000000000..21527b521c --- /dev/null +++ b/docs/architecture/change-tracker.md @@ -0,0 +1,91 @@ +# 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