refactor: harden ChangeTracker lifecycle with self-defending API#10816
refactor: harden ChangeTracker lifecycle with self-defending API#10816
Conversation
… assertion Add self-defending invariant to captureCanvasState (formerly checkState): only the active workflow's tracker may read from the canvas. Calling it on an inactive tracker logs an error in DEV and returns early, preventing silent cross-workflow data corruption. Mechanical rename across all 26 production/test files and docs.
- deactivate(): unified freeze step combining captureCanvasState() + store(), called when a workflow goes inactive - prepareForSave(): save-path API that flushes canvas state only for the active workflow (no-op for inactive, whose state was frozen by deactivate)
- beforeLoadNewGraph: replace store() with deactivate() so activeState is guaranteed fresh before tab switch - saveWorkflow/saveWorkflowAs: replace isActive guards + captureCanvasState with prepareForSave() — no caller-side active checks needed
- Guard captureCanvasState against _restoringState to prevent undo history corruption when deactivate() is called during undo/redo - Add deprecated checkState() wrapper for extension backward compat - Use optional chaining for deactivate() call in beforeLoadNewGraph - Add deactivate/prepareForSave to mock utility - Document lifecycle methods and preconditions in JSDoc and arch docs
Cover captureCanvasState guards (no graph, changeCount, isLoadingGraph, _restoringState, inactive tracker), state capture with undo queue management, deactivate (active + inactive paths), prepareForSave (active + inactive paths), and deprecated checkState wrapper.
🎨 Storybook: ✅ Built — View Storybook |
🎭 Playwright: ✅ 931 passed, 0 failed · 7 flaky📊 Browser Reports
|
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
✅ Files skipped from review due to trivial changes (1)
📝 WalkthroughWalkthroughRenames the change-tracker snapshot API from Changes
Sequence Diagram(s)sequenceDiagram
participant UI as UI/Component
participant Store as workflowStore
participant CT as ChangeTracker
participant App as app/rootGraph
participant Stores as output/subgraph stores
UI->>Store: mutate canvas / mark dirty
Store->>CT: activeWorkflow.changeTracker.captureCanvasState()
CT->>CT: run guards (graph present, changeCount==0, not restoring, isActive)
alt guards pass
CT->>App: serialize rootGraph
App-->>CT: serialized state
CT->>CT: compare with activeState
alt state changed
CT->>CT: push previous to undoQueue
CT->>CT: clear redoQueue
CT->>CT: update activeState & updateModified()
end
else guards fail
CT-->>Store: no-op (DEV-log)
end
Note right of CT: deactivate()/prepareForSave() call capture + store snapshots, then trigger Stores snapshot/export
CT->>Stores: snapshot outputs / export subgraph (on deactivate)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
📦 Bundle: 5.11 MB gzip 🔴 +197 BDetailsSummary
Category Glance App Entry Points — 22.3 kB (baseline 22.3 kB) • ⚪ 0 BMain entry bundles and manifests
Status: 1 added / 1 removed Graph Workspace — 1.2 MB (baseline 1.2 MB) • 🔴 +41 BGraph editor runtime, canvas, workflow orchestration
Status: 1 added / 1 removed Views & Navigation — 76.6 kB (baseline 76.6 kB) • ⚪ 0 BTop-level views, pages, and routed surfaces
Status: 9 added / 9 removed / 2 unchanged Panels & Settings — 484 kB (baseline 484 kB) • ⚪ 0 BConfiguration panels, inspectors, and settings screens
Status: 10 added / 10 removed / 12 unchanged User & Accounts — 17.1 kB (baseline 17.1 kB) • ⚪ 0 BAuthentication, profile, and account management bundles
Status: 5 added / 5 removed / 2 unchanged Editors & Dialogs — 109 kB (baseline 109 kB) • ⚪ 0 BModals, dialogs, drawers, and in-app editors
Status: 2 added / 2 removed UI Components — 60.3 kB (baseline 60.3 kB) • ⚪ 0 BReusable component library chunks
Status: 5 added / 5 removed / 8 unchanged Data & Services — 2.97 MB (baseline 2.97 MB) • 🔴 +1.45 kBStores, services, APIs, and repositories
Status: 13 added / 13 removed / 4 unchanged Utilities & Hooks — 338 kB (baseline 338 kB) • ⚪ 0 BHelpers, composables, and utility bundles
Status: 13 added / 13 removed / 13 unchanged Vendor & Third-Party — 9.8 MB (baseline 9.8 MB) • ⚪ 0 BExternal libraries and shared vendor chunks Status: 16 unchanged Other — 8.44 MB (baseline 8.44 MB) • 🔴 +16 BBundles that do not match a named category
Status: 55 added / 55 removed / 79 unchanged ⚡ Performance Report
All metrics
Historical variance (last 15 runs)
Trend (last 15 commits on main)
Raw data{
"timestamp": "2026-04-03T05:22:32.136Z",
"gitSha": "2cdda4ab9385f9f546563c9d2254f70c8faa7281",
"branch": "refactor/changetracker-lifecycle-hardening",
"measurements": [
{
"name": "canvas-idle",
"durationMs": 2013.8279999999895,
"styleRecalcs": 8,
"styleRecalcDurationMs": 9.187000000000001,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 375.48100000000005,
"heapDeltaBytes": -5107496,
"heapUsedBytes": 44269556,
"domNodes": 16,
"jsHeapTotalBytes": 24641536,
"scriptDurationMs": 16.832,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "canvas-idle",
"durationMs": 2023.1390000000147,
"styleRecalcs": 10,
"styleRecalcDurationMs": 9.321,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 413.73,
"heapDeltaBytes": 21051624,
"heapUsedBytes": 65192544,
"domNodes": 20,
"jsHeapTotalBytes": 22806528,
"scriptDurationMs": 29.239999999999995,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "canvas-idle",
"durationMs": 2021.255999999994,
"styleRecalcs": 9,
"styleRecalcDurationMs": 8.419999999999998,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 430.246,
"heapDeltaBytes": 20056916,
"heapUsedBytes": 63205888,
"domNodes": 18,
"jsHeapTotalBytes": 23068672,
"scriptDurationMs": 23.810000000000002,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "canvas-mouse-sweep",
"durationMs": 2060.5860000000007,
"styleRecalcs": 85,
"styleRecalcDurationMs": 45.245,
"layouts": 12,
"layoutDurationMs": 3.3869999999999996,
"taskDurationMs": 939.3979999999999,
"heapDeltaBytes": 16256108,
"heapUsedBytes": 59155640,
"domNodes": 67,
"jsHeapTotalBytes": 22806528,
"scriptDurationMs": 130.898,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "canvas-mouse-sweep",
"durationMs": 1999.4689999999764,
"styleRecalcs": 81,
"styleRecalcDurationMs": 45.371,
"layouts": 12,
"layoutDurationMs": 3.5020000000000002,
"taskDurationMs": 1048.146,
"heapDeltaBytes": 16136608,
"heapUsedBytes": 59163900,
"domNodes": 65,
"jsHeapTotalBytes": 23330816,
"scriptDurationMs": 141.46,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "canvas-mouse-sweep",
"durationMs": 2032.8980000000456,
"styleRecalcs": 84,
"styleRecalcDurationMs": 43.839,
"layouts": 12,
"layoutDurationMs": 3.5829999999999993,
"taskDurationMs": 1018.7539999999999,
"heapDeltaBytes": 16224340,
"heapUsedBytes": 59086276,
"domNodes": 65,
"jsHeapTotalBytes": 23068672,
"scriptDurationMs": 145.063,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "canvas-zoom-sweep",
"durationMs": 1752.049999999997,
"styleRecalcs": 32,
"styleRecalcDurationMs": 17.827,
"layouts": 6,
"layoutDurationMs": 0.6679999999999999,
"taskDurationMs": 295.91999999999996,
"heapDeltaBytes": 24701180,
"heapUsedBytes": 67202584,
"domNodes": 79,
"jsHeapTotalBytes": 20447232,
"scriptDurationMs": 22.989999999999995,
"eventListeners": 19,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "canvas-zoom-sweep",
"durationMs": 1748.0720000000929,
"styleRecalcs": 32,
"styleRecalcDurationMs": 20.412,
"layouts": 6,
"layoutDurationMs": 0.7020000000000002,
"taskDurationMs": 366.09899999999993,
"heapDeltaBytes": 24511608,
"heapUsedBytes": 67508524,
"domNodes": 81,
"jsHeapTotalBytes": 20185088,
"scriptDurationMs": 30.11,
"eventListeners": 19,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "canvas-zoom-sweep",
"durationMs": 1729.0239999999812,
"styleRecalcs": 33,
"styleRecalcDurationMs": 19.791999999999998,
"layouts": 6,
"layoutDurationMs": 0.58,
"taskDurationMs": 371.21,
"heapDeltaBytes": 24719364,
"heapUsedBytes": 67254380,
"domNodes": 81,
"jsHeapTotalBytes": 20447232,
"scriptDurationMs": 33.292,
"eventListeners": 19,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "dom-widget-clipping",
"durationMs": 588.1939999999872,
"styleRecalcs": 13,
"styleRecalcDurationMs": 10.626000000000001,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 356.69700000000006,
"heapDeltaBytes": 6477780,
"heapUsedBytes": 49311548,
"domNodes": 22,
"jsHeapTotalBytes": 13107200,
"scriptDurationMs": 69.56099999999999,
"eventListeners": 2,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.799999999999727
},
{
"name": "dom-widget-clipping",
"durationMs": 557.4390000000449,
"styleRecalcs": 13,
"styleRecalcDurationMs": 9.312,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 357.024,
"heapDeltaBytes": 6513372,
"heapUsedBytes": 50781640,
"domNodes": 22,
"jsHeapTotalBytes": 13631488,
"scriptDurationMs": 68.048,
"eventListeners": 2,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.669999999999998,
"p95FrameDurationMs": 16.799999999999727
},
{
"name": "dom-widget-clipping",
"durationMs": 582.2220000000016,
"styleRecalcs": 12,
"styleRecalcDurationMs": 9.746,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 374.035,
"heapDeltaBytes": 6526540,
"heapUsedBytes": 49345276,
"domNodes": 20,
"jsHeapTotalBytes": 13107200,
"scriptDurationMs": 68.679,
"eventListeners": 2,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.663333333333338,
"p95FrameDurationMs": 16.700000000000273
},
{
"name": "large-graph-idle",
"durationMs": 2025.2729999999701,
"styleRecalcs": 10,
"styleRecalcDurationMs": 9.366000000000001,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 522.596,
"heapDeltaBytes": 3988512,
"heapUsedBytes": 54924284,
"domNodes": -257,
"jsHeapTotalBytes": 15921152,
"scriptDurationMs": 95.18499999999999,
"eventListeners": -125,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.670000000000012,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "large-graph-idle",
"durationMs": 2043.3249999999816,
"styleRecalcs": 11,
"styleRecalcDurationMs": 11.7,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 674.7850000000001,
"heapDeltaBytes": 5095320,
"heapUsedBytes": 55952708,
"domNodes": -258,
"jsHeapTotalBytes": 15921152,
"scriptDurationMs": 116.72099999999999,
"eventListeners": -127,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "large-graph-idle",
"durationMs": 2025.166000000013,
"styleRecalcs": 10,
"styleRecalcDurationMs": 8.674999999999999,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 664.8739999999999,
"heapDeltaBytes": 4273192,
"heapUsedBytes": 55379768,
"domNodes": -259,
"jsHeapTotalBytes": 15659008,
"scriptDurationMs": 115.98800000000001,
"eventListeners": -127,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "large-graph-pan",
"durationMs": 2146.4520000000107,
"styleRecalcs": 69,
"styleRecalcDurationMs": 15.801000000000002,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 1074.4399999999998,
"heapDeltaBytes": 20000908,
"heapUsedBytes": 72233540,
"domNodes": -260,
"jsHeapTotalBytes": 18223104,
"scriptDurationMs": 412.89500000000004,
"eventListeners": -127,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333335,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "large-graph-pan",
"durationMs": 2111.9300000000294,
"styleRecalcs": 68,
"styleRecalcDurationMs": 15.201999999999996,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 1176.29,
"heapDeltaBytes": 15317752,
"heapUsedBytes": 68549516,
"domNodes": -261,
"jsHeapTotalBytes": 19271680,
"scriptDurationMs": 424.543,
"eventListeners": -127,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "large-graph-pan",
"durationMs": 2153.015000000096,
"styleRecalcs": 69,
"styleRecalcDurationMs": 16.304999999999996,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 1175.471,
"heapDeltaBytes": 17472420,
"heapUsedBytes": 71055592,
"domNodes": -262,
"jsHeapTotalBytes": 18223104,
"scriptDurationMs": 413.17699999999996,
"eventListeners": -127,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "large-graph-zoom",
"durationMs": 3163.558999999964,
"styleRecalcs": 66,
"styleRecalcDurationMs": 15.831999999999999,
"layouts": 60,
"layoutDurationMs": 7.158000000000001,
"taskDurationMs": 1308.665,
"heapDeltaBytes": 7690788,
"heapUsedBytes": 62460168,
"domNodes": -264,
"jsHeapTotalBytes": 16969728,
"scriptDurationMs": 494.59100000000007,
"eventListeners": -125,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "large-graph-zoom",
"durationMs": 3166.3049999999657,
"styleRecalcs": 66,
"styleRecalcDurationMs": 17.956,
"layouts": 60,
"layoutDurationMs": 7.747000000000001,
"taskDurationMs": 1421.98,
"heapDeltaBytes": 7588912,
"heapUsedBytes": 62336388,
"domNodes": -265,
"jsHeapTotalBytes": 17756160,
"scriptDurationMs": 512.6469999999999,
"eventListeners": -123,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333335,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "large-graph-zoom",
"durationMs": 3177.4480000000267,
"styleRecalcs": 65,
"styleRecalcDurationMs": 16.131000000000004,
"layouts": 60,
"layoutDurationMs": 7.652,
"taskDurationMs": 1436.106,
"heapDeltaBytes": 569336,
"heapUsedBytes": 55801676,
"domNodes": -266,
"jsHeapTotalBytes": 18280448,
"scriptDurationMs": 514.729,
"eventListeners": -123,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "minimap-idle",
"durationMs": 2012.0130000000245,
"styleRecalcs": 10,
"styleRecalcDurationMs": 9.926,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 517.9860000000001,
"heapDeltaBytes": 3803316,
"heapUsedBytes": 56487344,
"domNodes": -260,
"jsHeapTotalBytes": 16445440,
"scriptDurationMs": 92.30499999999999,
"eventListeners": -127,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "minimap-idle",
"durationMs": 2029.137999999989,
"styleRecalcs": 5,
"styleRecalcDurationMs": 5.5329999999999995,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 657.246,
"heapDeltaBytes": 1951152,
"heapUsedBytes": 56848340,
"domNodes": -267,
"jsHeapTotalBytes": 15921152,
"scriptDurationMs": 110.91699999999999,
"eventListeners": -127,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "minimap-idle",
"durationMs": 2048.674999999889,
"styleRecalcs": 11,
"styleRecalcDurationMs": 10.274999999999999,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 671.195,
"heapDeltaBytes": 5148560,
"heapUsedBytes": 57811120,
"domNodes": -256,
"jsHeapTotalBytes": 16445440,
"scriptDurationMs": 119.222,
"eventListeners": -127,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "subgraph-dom-widget-clipping",
"durationMs": 546.970999999985,
"styleRecalcs": 48,
"styleRecalcDurationMs": 11.712000000000002,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 350.332,
"heapDeltaBytes": 6642572,
"heapUsedBytes": 49706112,
"domNodes": 22,
"jsHeapTotalBytes": 13893632,
"scriptDurationMs": 120.485,
"eventListeners": 8,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000273
},
{
"name": "subgraph-dom-widget-clipping",
"durationMs": 561.2449999999853,
"styleRecalcs": 48,
"styleRecalcDurationMs": 12.893,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 371.023,
"heapDeltaBytes": 6514608,
"heapUsedBytes": 49240368,
"domNodes": 22,
"jsHeapTotalBytes": 13369344,
"scriptDurationMs": 121.34899999999999,
"eventListeners": 8,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000273
},
{
"name": "subgraph-dom-widget-clipping",
"durationMs": 604.684999999904,
"styleRecalcs": 48,
"styleRecalcDurationMs": 11.947000000000001,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 390.72499999999997,
"heapDeltaBytes": 6522760,
"heapUsedBytes": 49590576,
"domNodes": 22,
"jsHeapTotalBytes": 13893632,
"scriptDurationMs": 139.764,
"eventListeners": 8,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.799999999999727
},
{
"name": "subgraph-idle",
"durationMs": 2028.347999999994,
"styleRecalcs": 12,
"styleRecalcDurationMs": 10.999,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 340.96999999999997,
"heapDeltaBytes": 20241288,
"heapUsedBytes": 63342468,
"domNodes": 24,
"jsHeapTotalBytes": 22544384,
"scriptDurationMs": 18.298000000000002,
"eventListeners": 10,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "subgraph-idle",
"durationMs": 2015.2500000000373,
"styleRecalcs": 11,
"styleRecalcDurationMs": 10.528999999999998,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 428.44199999999995,
"heapDeltaBytes": 19938884,
"heapUsedBytes": 62744876,
"domNodes": 22,
"jsHeapTotalBytes": 23068672,
"scriptDurationMs": 25.738000000000003,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "subgraph-idle",
"durationMs": 1992.8519999999708,
"styleRecalcs": 11,
"styleRecalcDurationMs": 11.314000000000002,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 452.383,
"heapDeltaBytes": 19902316,
"heapUsedBytes": 63053280,
"domNodes": 22,
"jsHeapTotalBytes": 23068672,
"scriptDurationMs": 25.836000000000006,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "subgraph-mouse-sweep",
"durationMs": 1724.2200000000025,
"styleRecalcs": 76,
"styleRecalcDurationMs": 38.258,
"layouts": 16,
"layoutDurationMs": 4.548,
"taskDurationMs": 679.795,
"heapDeltaBytes": 2636524,
"heapUsedBytes": 54034960,
"domNodes": 63,
"jsHeapTotalBytes": 25690112,
"scriptDurationMs": 100.965,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "subgraph-mouse-sweep",
"durationMs": 2041.3220000000365,
"styleRecalcs": 87,
"styleRecalcDurationMs": 48.300999999999995,
"layouts": 16,
"layoutDurationMs": 4.311,
"taskDurationMs": 987.74,
"heapDeltaBytes": 11919248,
"heapUsedBytes": 55017088,
"domNodes": 72,
"jsHeapTotalBytes": 22806528,
"scriptDurationMs": 110.61000000000001,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "subgraph-mouse-sweep",
"durationMs": 1984.1159999999718,
"styleRecalcs": 85,
"styleRecalcDurationMs": 48.367,
"layouts": 16,
"layoutDurationMs": 4.391,
"taskDurationMs": 938.2850000000001,
"heapDeltaBytes": 11854740,
"heapUsedBytes": 54765012,
"domNodes": 74,
"jsHeapTotalBytes": 22544384,
"scriptDurationMs": 109.16999999999999,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "viewport-pan-sweep",
"durationMs": 8153.112999999962,
"styleRecalcs": 252,
"styleRecalcDurationMs": 42.854000000000006,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 3619.65,
"heapDeltaBytes": 18448828,
"heapUsedBytes": 68944432,
"domNodes": -253,
"jsHeapTotalBytes": 20582400,
"scriptDurationMs": 1226.4219999999998,
"eventListeners": -105,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "viewport-pan-sweep",
"durationMs": 8250.720999999998,
"styleRecalcs": 251,
"styleRecalcDurationMs": 45.406,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 4119.263,
"heapDeltaBytes": 2491888,
"heapUsedBytes": 53182564,
"domNodes": -255,
"jsHeapTotalBytes": 19591168,
"scriptDurationMs": 1381.5819999999999,
"eventListeners": -107,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "viewport-pan-sweep",
"durationMs": 8222.616000000016,
"styleRecalcs": 252,
"styleRecalcDurationMs": 45.233000000000004,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 4001.8949999999995,
"heapDeltaBytes": 25766656,
"heapUsedBytes": 76333208,
"domNodes": -256,
"jsHeapTotalBytes": 20582400,
"scriptDurationMs": 1301.763,
"eventListeners": -111,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "vue-large-graph-idle",
"durationMs": 11759.192999999981,
"styleRecalcs": 0,
"styleRecalcDurationMs": 0,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 11737.689999999999,
"heapDeltaBytes": -50073940,
"heapUsedBytes": 161087860,
"domNodes": -8331,
"jsHeapTotalBytes": 19984384,
"scriptDurationMs": 599.453,
"eventListeners": -16467,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333326,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "vue-large-graph-idle",
"durationMs": 12711.825999999974,
"styleRecalcs": 0,
"styleRecalcDurationMs": 0,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 12696.391000000001,
"heapDeltaBytes": -45841200,
"heapUsedBytes": 157504340,
"domNodes": -8333,
"jsHeapTotalBytes": 17100800,
"scriptDurationMs": 609.448,
"eventListeners": -16468,
"totalBlockingTimeMs": 0,
"frameDurationMs": 17.77333333333336,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "vue-large-graph-idle",
"durationMs": 12541.823000000022,
"styleRecalcs": 0,
"styleRecalcDurationMs": 0,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 12522.945,
"heapDeltaBytes": -30268208,
"heapUsedBytes": 165958992,
"domNodes": -8331,
"jsHeapTotalBytes": 27848704,
"scriptDurationMs": 641.0089999999999,
"eventListeners": -16466,
"totalBlockingTimeMs": 0,
"frameDurationMs": 17.219999999999953,
"p95FrameDurationMs": 16.80000000000291
},
{
"name": "vue-large-graph-pan",
"durationMs": 14091.229,
"styleRecalcs": 64,
"styleRecalcDurationMs": 12.600000000000028,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 14055.667,
"heapDeltaBytes": -29908584,
"heapUsedBytes": 164355660,
"domNodes": -8331,
"jsHeapTotalBytes": 25227264,
"scriptDurationMs": 833.6170000000001,
"eventListeners": -16460,
"totalBlockingTimeMs": 0,
"frameDurationMs": 17.219999999999953,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "vue-large-graph-pan",
"durationMs": 14760.340999999926,
"styleRecalcs": 68,
"styleRecalcDurationMs": 14.77700000000004,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 14735.603000000001,
"heapDeltaBytes": -49319304,
"heapUsedBytes": 148317252,
"domNodes": -8331,
"jsHeapTotalBytes": -5181440,
"scriptDurationMs": 866.361,
"eventListeners": -16486,
"totalBlockingTimeMs": 0,
"frameDurationMs": 17.219999999999953,
"p95FrameDurationMs": 16.80000000000291
},
{
"name": "vue-large-graph-pan",
"durationMs": 14652.584000000046,
"styleRecalcs": 68,
"styleRecalcDurationMs": 14.783999999999963,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 14625.259999999998,
"heapDeltaBytes": -61797728,
"heapUsedBytes": 149216460,
"domNodes": -8333,
"jsHeapTotalBytes": -462848,
"scriptDurationMs": 923.769,
"eventListeners": -16486,
"totalBlockingTimeMs": 0,
"frameDurationMs": 17.223333333333237,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "workflow-execution",
"durationMs": 445.3560000000607,
"styleRecalcs": 21,
"styleRecalcDurationMs": 30.841,
"layouts": 6,
"layoutDurationMs": 1.8359999999999999,
"taskDurationMs": 146.146,
"heapDeltaBytes": 4771780,
"heapUsedBytes": 50035340,
"domNodes": 170,
"jsHeapTotalBytes": 262144,
"scriptDurationMs": 35.001000000000005,
"eventListeners": 71,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.669999999999998,
"p95FrameDurationMs": 16.700000000000273
},
{
"name": "workflow-execution",
"durationMs": 453.58599999997296,
"styleRecalcs": 18,
"styleRecalcDurationMs": 23.671,
"layouts": 5,
"layoutDurationMs": 1.374,
"taskDurationMs": 128.97099999999998,
"heapDeltaBytes": 4461364,
"heapUsedBytes": 54924040,
"domNodes": 158,
"jsHeapTotalBytes": 262144,
"scriptDurationMs": 29.602000000000004,
"eventListeners": 71,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000273
},
{
"name": "workflow-execution",
"durationMs": 464.32900000002064,
"styleRecalcs": 17,
"styleRecalcDurationMs": 22.223999999999997,
"layouts": 5,
"layoutDurationMs": 1.362,
"taskDurationMs": 124.783,
"heapDeltaBytes": 4467384,
"heapUsedBytes": 49107960,
"domNodes": 157,
"jsHeapTotalBytes": 0,
"scriptDurationMs": 30.230000000000004,
"eventListeners": 71,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.799999999999727
}
]
} |
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
src/scripts/changeTracker.ts (1)
83-92:⚠️ Potential issue | 🟠 MajorReturn early before
store()whendeactivate()is called on an inactive tracker.
captureCanvasState()is self-defending now, butstore()still reads global canvas/output/navigation state. If the active pointer has already moved, this freezes the new workflow's viewport/output state into the old tracker, andrestore()will later bring back the wrong UI state.Proposed fix
deactivate() { + const isActive = useWorkflowStore().activeWorkflow?.changeTracker === this + if (!isActive) { + if (import.meta.env.DEV) { + logger.error('deactivate() called on inactive tracker for:', this.workflow.path) + } + return + } + this.captureCanvasState() this.store() }Also applies to: 103-105
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/scripts/changeTracker.ts` around lines 83 - 92, The store() method reads global canvas/output/navigation state even after the tracker has been deactivated; add a defensive early return at the top of store() that checks the tracker's active/deactivated flag (e.g., this.active or this.deactivated) and returns immediately if the tracker is not active so it doesn't capture the new workflow's UI state; apply the same guard to the other similar block mentioned (lines around 103-105) so deactivate() prevents storing stale state and restore() will not reapply incorrect UI state.docs/architecture/change-tracker.md (1)
60-75:⚠️ Potential issue | 🟡 MinorClarify that
workflowService.tsis no longer a direct caller.This section now says the listed files call
captureCanvasState()explicitly, butworkflowService.tsgoes throughdeactivate()/prepareForSave(). Either drop that entry here or reword the heading to include indirect lifecycle helpers.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/architecture/change-tracker.md` around lines 60 - 75, The list incorrectly claims workflowService.ts explicitly calls captureCanvasState(); instead clarify that workflowService.ts triggers capture via lifecycle helpers (deactivate() / prepareForSave()) or remove it from the explicit-call list. Edit the heading or the list entry so it either reads "Existing Manual or Indirect Call Sites" or delete the workflowService.ts bullet and add a parenthetical note referencing deactivate() and prepareForSave() to indicate indirect invocation of captureCanvasState().
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/platform/workflow/core/services/workflowService.ts`:
- Around line 141-143: The isSelfOverwrite branch is re-entering the save
lifecycle by calling workflow.changeTracker?.prepareForSave() and await
saveWorkflow(workflow) which duplicates work and emits conflicting telemetry;
remove the await saveWorkflow(workflow) call from the isSelfOverwrite branch
(and also remove the prepareForSave() call here so prepareForSave is only
executed once inside saveWorkflow) so the outer is_new: true telemetry path
performs the actual prepare/save and emits a single save event.
In `@src/utils/__tests__/litegraphTestUtils.ts`:
- Around line 259-261: The shared mock used by createMockChangeTracker() omits
the deprecated checkState() alias; re-add checkState to the mock (e.g. as a
vi.fn() or as an alias to captureCanvasState) so tests that call
tracker.checkState() continue to work and the mock stays compatible with the
real tracker API; update the mock object that currently defines
captureCanvasState, deactivate, and prepareForSave to also include checkState
(or forward to captureCanvasState).
---
Outside diff comments:
In `@docs/architecture/change-tracker.md`:
- Around line 60-75: The list incorrectly claims workflowService.ts explicitly
calls captureCanvasState(); instead clarify that workflowService.ts triggers
capture via lifecycle helpers (deactivate() / prepareForSave()) or remove it
from the explicit-call list. Edit the heading or the list entry so it either
reads "Existing Manual or Indirect Call Sites" or delete the workflowService.ts
bullet and add a parenthetical note referencing deactivate() and
prepareForSave() to indicate indirect invocation of captureCanvasState().
In `@src/scripts/changeTracker.ts`:
- Around line 83-92: The store() method reads global canvas/output/navigation
state even after the tracker has been deactivated; add a defensive early return
at the top of store() that checks the tracker's active/deactivated flag (e.g.,
this.active or this.deactivated) and returns immediately if the tracker is not
active so it doesn't capture the new workflow's UI state; apply the same guard
to the other similar block mentioned (lines around 103-105) so deactivate()
prevents storing stale state and restore() will not reapply incorrect UI state.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 9c6546ba-5b87-4c48-b1b0-0dce771d20d9
📒 Files selected for processing (27)
browser_tests/tests/actionbar.spec.tsbrowser_tests/tests/browserTabTitle.spec.tsbrowser_tests/tests/changeTrackerLoadGuard.spec.tsbrowser_tests/tests/subgraph/subgraphNested.spec.tsbrowser_tests/tests/workflowPersistence.spec.tsdocs/architecture/change-tracker.mdsrc/components/builder/BuilderFooterToolbar.test.tssrc/components/builder/builderViewOptions.test.tssrc/components/builder/builderViewOptions.tssrc/components/graph/selectionToolbox/ColorPickerButton.test.tssrc/components/graph/selectionToolbox/ColorPickerButton.vuesrc/components/searchbox/NodeSearchBoxPopover.vuesrc/composables/graph/useCanvasRefresh.tssrc/composables/graph/useGroupMenuOptions.tssrc/composables/graph/useSelectedNodeActions.tssrc/composables/graph/useSelectionOperations.tssrc/composables/graph/useSubgraphOperations.tssrc/composables/useCoreCommands.test.tssrc/composables/useCoreCommands.tssrc/platform/workflow/core/services/workflowService.tssrc/renderer/extensions/vueNodes/widgets/components/WidgetSelectDropdown.test.tssrc/renderer/extensions/vueNodes/widgets/components/WidgetSelectDropdown.vuesrc/scripts/changeTracker.test.tssrc/scripts/changeTracker.tssrc/stores/appModeStore.test.tssrc/stores/appModeStore.tssrc/utils/__tests__/litegraphTestUtils.ts
| if (isSelfOverwrite) { | ||
| if (workflowStore.isActive(workflow)) workflow.changeTracker?.checkState() | ||
| workflow.changeTracker?.prepareForSave() | ||
| await saveWorkflow(workflow) |
There was a problem hiding this comment.
Avoid re-entering saveWorkflow() from the self-overwrite branch.
This branch already ends in the outer is_new: true telemetry path, but saveWorkflow() also runs prepareForSave() and emits the normal is_new: false save event. A same-path “Save As” therefore double-enters the save lifecycle and records two conflicting save events.
♻️ Proposed fix
if (isSelfOverwrite) {
workflow.changeTracker?.prepareForSave()
- await saveWorkflow(workflow)
+ await workflowStore.saveWorkflow(workflow)
} else {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/platform/workflow/core/services/workflowService.ts` around lines 141 -
143, The isSelfOverwrite branch is re-entering the save lifecycle by calling
workflow.changeTracker?.prepareForSave() and await saveWorkflow(workflow) which
duplicates work and emits conflicting telemetry; remove the await
saveWorkflow(workflow) call from the isSelfOverwrite branch (and also remove the
prepareForSave() call here so prepareForSave is only executed once inside
saveWorkflow) so the outer is_new: true telemetry path performs the actual
prepare/save and emits a single save event.
There was a problem hiding this comment.
This is pre-existing behavior -- the self-overwrite branch called await saveWorkflow(workflow) before this PR as well. This refactor only changed checkState() to prepareForSave() at the call site; the saveWorkflow() delegation and telemetry flow were not introduced here. prepareForSave() is idempotent (either a no-op or re-captures the same canvas state), so the double call is harmless. Fixing the double-enter / double-telemetry is a valid improvement but belongs in a separate PR.
There was a problem hiding this comment.
@jaeone94, understood — the await saveWorkflow(workflow) delegation in the self-overwrite branch is indeed pre-existing, and this PR's only change there is the checkState() → prepareForSave() substitution. Since prepareForSave() is idempotent, the double-call is harmless.
The double-enter / double-telemetry concern is still valid as a standalone improvement. Would you like me to open a follow-up issue to track it?
🧠 Learnings used
Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 9345
File: src/platform/workflow/management/stores/workflowStore.ts:488-491
Timestamp: 2026-03-03T01:32:58.349Z
Learning: In `src/platform/workflow/management/stores/workflowStore.ts`, `openWorkflowPaths` maintains unique workflow paths by design. Operations like `openWorkflowsInBackground` use `_.union` to enforce uniqueness. When updating paths (e.g., during rename), replacing the first occurrence with `indexOf`/`splice` is sufficient because duplicates should not exist.
Learnt from: christian-byrne
Repo: Comfy-Org/ComfyUI_frontend PR: 8951
File: src/platform/workflow/management/stores/workflowStore.ts:301-301
Timestamp: 2026-03-05T23:21:47.113Z
Learning: In src/platform/workflow/management/stores/workflowStore.ts, `createTemporary()` and `createNewTemporary()` intentionally inject a UUID `id` into workflow data via `ensureWorkflowId()` to give workflows stable identity for sharing. This is a known behavioral change introduced in PR `#8951`. Extensions relying on serialized content matching their input may be silently affected.
Learnt from: christian-byrne
Repo: Comfy-Org/ComfyUI_frontend PR: 10689
File: src/stores/executionStore.ts:637-645
Timestamp: 2026-03-29T00:40:33.555Z
Learning: In `src/stores/executionStore.ts` (Comfy-Org/ComfyUI_frontend PR `#10689`), `isJobForActiveWorkflow()` intentionally returns `true` (permissive) when `jobIdToSessionWorkflowPath` does not contain a mapping for the given `promptId`. This is deliberate graceful degradation: returning `false` for unknown jobs would break legitimate pre-session jobs and legacy WS messages that lack a `prompt_id`. The narrow race window where a WS message arrives before the HTTP response from `queuePrompt` self-resolves once `storeJob` populates the map. Do not suggest changing the unknown-job fallback to `false` or adding a pre-response association to close this race — the added complexity has no real benefit given the narrow window.
Learnt from: jaeone94
Repo: Comfy-Org/ComfyUI_frontend PR: 9921
File: src/platform/workflow/core/services/workflowService.ts:540-541
Timestamp: 2026-03-14T17:10:20.194Z
Learning: In `src/platform/workflow/core/services/workflowService.ts`, `showPendingWarnings()` intentionally only handles `missingNodeTypes` from `wf.pendingWarnings`. `missingModels` is no longer set in `pendingWarnings` because the legacy missing-models dialog has been removed. Missing models are now surfaced directly via `surfaceMissingModels()` inside `runMissingModelPipeline` in `src/scripts/app.ts` for both Cloud and OSS environments. Do not suggest re-adding `missingModels` handling to `showPendingWarnings()`.
Learnt from: xmarre
Repo: Comfy-Org/ComfyUI_frontend PR: 10367
File: src/platform/workflow/persistence/stores/workflowDraftStoreV2.ts:274-274
Timestamp: 2026-03-21T16:32:23.142Z
Learning: In `src/platform/workflow/persistence/stores/workflowDraftStoreV2.ts`, `updatedAt` in `getDraft()` intentionally returns `payload.updatedAt` (content freshness timestamp), NOT the index/MRU recency timestamp. These are two separate concepts: `payload.updatedAt` tracks when draft content last changed and is used by `ComfyWorkflow.load()` for remote-vs-draft staleness comparisons; MRU recency lives in the index order and is refreshed by `markDraftUsed()` without mutating the stored payload. Do NOT suggest collapsing these into a single timestamp — doing so would make a restored/accessed draft appear newer than the remote workflow even if the content hadn't changed.
Learnt from: christian-byrne
Repo: Comfy-Org/ComfyUI_frontend PR: 10547
File: browser_tests/tests/workflowPersistence.spec.ts:459-471
Timestamp: 2026-03-27T05:12:16.400Z
Learning: In `browser_tests/tests/workflowPersistence.spec.ts` (Comfy-Org/ComfyUI_frontend PR `#10547`), the G10 test for PR `#8715` (transient blob:/api-view URL serialization) intentionally validates only the static/default workflow export — not an executed/transient widget state. Putting an image widget into its executed state requires actual model inference (image output generation), which is not available in the current E2E test infrastructure. The test still covers the regression if the `widget.serialize` disable path breaks. Do not flag the absence of transient-state setup in this test; full integration coverage is tracked separately.
Learnt from: xmarre
Repo: Comfy-Org/ComfyUI_frontend PR: 10367
File: browser_tests/tests/workflowPersistence.spec.ts:68-77
Timestamp: 2026-03-21T16:57:48.590Z
Learning: In `browser_tests/tests/workflowPersistence.spec.ts`, the `waitForTabStatePersistence` helper intentionally does NOT validate `activeIndex` on the persisted `OpenPaths` session storage entry. The test immediately overwrites `openPointer.activeIndex = 1` after the wait resolves, so the previously persisted `activeIndex` value is irrelevant. The only invariant being guarded is that `openPointer.paths.length >= expectedMinPaths` (i.e., all workflow paths have been flushed into storage before the test proceeds). Do not suggest adding `activeIndex` validation to this wait predicate in future reviews.
Learnt from: Myestery
Repo: Comfy-Org/ComfyUI_frontend PR: 7422
File: .github/workflows/pr-update-playwright-expectations.yaml:131-135
Timestamp: 2025-12-12T23:02:37.473Z
Learning: In the `.github/workflows/pr-update-playwright-expectations.yaml` workflow in the Comfy-Org/ComfyUI_frontend repository, the snapshot update process is intentionally scoped to only add and update snapshot images. Deletions of snapshot files are handled explicitly outside this workflow and should not be suggested as part of this automation.
Learnt from: christian-byrne
Repo: Comfy-Org/ComfyUI_frontend PR: 9901
File: src/platform/workspace/composables/useCreateWorkspaceUrlLoader.ts:48-64
Timestamp: 2026-03-17T20:38:03.421Z
Learning: In `src/platform/workspace/composables/useCreateWorkspaceUrlLoader.ts`, the URL trigger (`?create_workspace=1`) and preserved query are cleaned up **before** `dialogService.showCreateWorkspaceDialog()` is awaited. This is intentional: it prevents an infinite reload loop if the lazy-loaded dialog component has a persistent failure. The console.error on failure is acceptable for now; do not flag this ordering as a bug. A follow-up issue exists to add a user-visible toast/retry without restoring the URL.
Learnt from: jaeone94
Repo: Comfy-Org/ComfyUI_frontend PR: 10309
File: src/platform/missingMedia/composables/useMissingMediaInteractions.ts:56-71
Timestamp: 2026-03-25T15:00:58.823Z
Learning: In `src/platform/missingMedia/composables/useMissingMediaInteractions.ts` (Comfy-Org/ComfyUI_frontend), the `applyValueToNodes` function intentionally omits `node.onWidgetChanged()` and `graph._version++` after setting the combo widget value. This mirrors the identical pattern in `src/platform/missingModel/composables/useMissingModelInteractions.ts` (lines 184-186). Undo/redo has been verified working in both pipelines. Do not flag the absence of `onWidgetChanged` or `_version++` as a bug in either pipeline; any change to add them must be applied consistently to both pipelines together in a dedicated follow-up PR.
Learnt from: jaeone94
Repo: Comfy-Org/ComfyUI_frontend PR: 10309
File: src/platform/missingMedia/composables/useMissingMediaInteractions.ts:153-195
Timestamp: 2026-03-25T15:31:50.781Z
Learning: In `src/platform/missingMedia/composables/useMissingMediaInteractions.ts` (Comfy-Org/ComfyUI_frontend PR `#10309`), `cancelSelection()` does not abort in-flight `api.fetchApi()` uploads. A follow-up issue (`#10511`) exists to add a per-upload `AbortController` map (keyed by `name`), pass its signal into `api.fetchApi`, guard post-await mutations with `signal.aborted` checks, and have `cancelSelection()` call `abort()` before clearing state. The same fix must be applied to the `missingModel` pipeline counterpart composable in the same PR to keep both pipelines consistent. Do not re-raise this as a blocking issue in PR `#10309`; reference issue `#10511` instead.
Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 7415
File: browser_tests/tests/mobileBaseline.spec.ts:17-22
Timestamp: 2025-12-13T05:54:35.779Z
Learning: In browser_tests tests for the Comfy-Org/ComfyUI_frontend repository, the `comfyPage.loadWorkflow()` method already handles all necessary synchronization and waiting. No additional `await comfyPage.nextFrame()` call is needed before taking screenshots after loading a workflow.
Learnt from: christian-byrne
Repo: Comfy-Org/ComfyUI_frontend PR: 10140
File: apps/website/package.json:1-79
Timestamp: 2026-03-17T07:28:27.677Z
Learning: In Comfy-Org/ComfyUI_frontend, PRs managed via Graphite stacked sets (using `gt split`) may show browser_tests/ files in the diff as artifacts of the split process. These files contain no actual changes relative to origin/main and should not be flagged as unrelated scope changes. Verify against the PR title/description stacking context before raising scope concerns.
Learnt from: Yourz
Repo: Comfy-Org/ComfyUI_frontend PR: 9965
File: src/platform/cloud/onboarding/CloudSubscriptionRedirectView.vue:77-79
Timestamp: 2026-03-15T15:24:20.611Z
Learning: Comfy-Org/ComfyUI_frontend maintainers prefer PRs to stay narrowly scoped to the stated fix. Cross-file refactors (e.g., adding concurrency guards in composables like useBillingContext) should be proposed as separate follow-up issues rather than requested within a targeted bug-fix PR.
Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 0
File: :0-0
Timestamp: 2026-03-20T19:58:37.779Z
Learning: In Comfy-Org/ComfyUI_frontend PR `#10348`, the PR titled "fix: clean up knip config, upgrade to v6, remove unused exports" is a dependency upgrade (knip v5 → v6) + config cleanup, not a bug fix. The appropriate conventional commit type is `chore:` or `build:`, not `fix:`.
Learnt from: benceruleanlu
Repo: Comfy-Org/ComfyUI_frontend PR: 0
File: :0-0
Timestamp: 2026-03-27T05:26:01.429Z
Learning: In Comfy-Org/ComfyUI_frontend PR `#10526`, the async scope-disposal race in `src/platform/cloud/notification/components/DesktopCloudNotificationController.vue` (desktop/mac-only cloud promo flow) is intentionally covered only at the unit level via `DesktopCloudNotificationController.test.ts`, using a deferred `settingStore.load()` promise and fake timers to deterministically verify that the dialog is not scheduled or shown after the owning scope is disposed. A Playwright E2E test is not appropriate here because: (1) the non-deterministic ordering cannot be reliably reproduced in Playwright, (2) the feature requires a real macOS Electron environment not available in CI, and (3) a timing-driven pseudo-E2E would be flaky and weaker than the unit test. If broader coverage is needed later, the right approach is a separate desktop promo smoke test, not a race-specific E2E regression. Do not request a Playwright/E2E test for this specific race condition scenario.
Learnt from: CR
Repo: Comfy-Org/ComfyUI_frontend PR: 0
File: coderabbit-custom-pre-merge-checks-unique-id-file-non-traceable-F7F2B60C-1728-4C9A-8889-4F2235E186CA.txt:0-0
Timestamp: 2026-03-24T17:16:12.386Z
Learning: For bug-fix PRs, ensure end-to-end regression coverage by either: (1) changing at least one file under `browser_tests/`, or (2) providing a concrete, non-placeholder explanation in the PR description of why an end-to-end regression test was not added. Use only PR metadata available in the review context (PR title, commit subjects, files changed relative to base, PR description). Do not rely on shell commands or inspect files outside the PR.
Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 7169
File: src/platform/remote/comfyui/jobs/jobTypes.ts:1-107
Timestamp: 2025-12-09T03:39:54.501Z
Learning: In the ComfyUI_frontend project, Zod is on v3.x. Do not suggest Zod v4 standalone validators (z.uuid, z.ulid, z.cuid2, z.nanoid) until an upgrade to Zod 4 is performed. When reviewing TypeScript files (e.g., src/platform/remote/comfyui/jobs/jobTypes.ts) validate against Zod 3 capabilities and avoid introducing v4-specific features; flag any proposal to upgrade or incorporate v4-only validators and propose staying with compatible 3.x patterns.
Learnt from: christian-byrne
Repo: Comfy-Org/ComfyUI_frontend PR: 7358
File: src/components/dialog/content/signin/SignUpForm.vue:45-54
Timestamp: 2025-12-11T12:25:15.470Z
Learning: This repository uses CI automation to format code (pnpm format). Do not include manual formatting suggestions in code reviews for Comfy-Org/ComfyUI_frontend. If formatting issues are detected, rely on the CI formatter or re-run pnpm format. Focus reviews on correctness, readability, performance, accessibility, and maintainability rather than style formatting.
Learnt from: christian-byrne
Repo: Comfy-Org/ComfyUI_frontend PR: 7416
File: src/stores/imagePreviewStore.ts:5-7
Timestamp: 2025-12-13T11:03:11.264Z
Learning: In the ComfyUI_frontend repository, lint rules require keeping 'import type' statements separate from non-type imports, even if importing from the same module. Do not suggest consolidating them into a single import statement. Ensure type imports remain on their own line (import type { ... } from 'module') and regular imports stay on separate lines.
Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 7537
File: src/components/ui/button/Button.stories.ts:45-55
Timestamp: 2025-12-17T00:40:09.635Z
Learning: Prefer pure function declarations over function expressions (e.g., use function foo() { ... } instead of const foo = () => { ... }) for pure functions in the repository. Function declarations are more functional-leaning, offer better hoisting clarity, and can improve readability and tooling consistency. Apply this guideline across TypeScript files in Comfy-Org/ComfyUI_frontend, including story and UI component code, except where a function expression is semantically required (e.g., callbacks, higher-order functions with closures).
Learnt from: kaili-yang
Repo: Comfy-Org/ComfyUI_frontend PR: 7805
File: src/composables/useCoreCommands.ts:439-439
Timestamp: 2025-12-30T22:22:33.836Z
Learning: When accessing reactive properties from Pinia stores in TypeScript files, avoid using .value on direct property access (e.g., useStore().isOverlayExpanded). Pinia auto-wraps refs when accessed directly, returning the primitive value. The .value accessor is only needed when destructuring store properties or when using storeToRefs().
Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 7906
File: src/components/sidebar/tabs/AssetsSidebarTab.vue:545-552
Timestamp: 2026-01-12T17:39:27.738Z
Learning: In Vue/TypeScript files (src/**/*.{ts,tsx,vue}), prefer if/else statements over ternary operators when performing side effects or actions (e.g., mutating state, calling methods with side effects). Ternaries should be reserved for computing and returning values.
Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 8248
File: src/platform/assets/schemas/assetSchema.ts:109-118
Timestamp: 2026-01-22T23:44:59.888Z
Learning: Do not export declarations (e.g., schemas, constants, types) unless they are actually used elsewhere in the codebase. Keep the public API surface minimal by exporting only items with concrete usage. This reduces coupling and keeps imports explicit. In src/platform/assets/schemas/assetSchema.ts, ensure only necessary exports are exposed; remove unused exports or switch to non-exported/internal where appropriate.
Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 8722
File: src/composables/maskeditor/useBrushDrawing.ts:5-5
Timestamp: 2026-02-08T03:19:12.009Z
Learning: Treat unresolved import warnings caused by pnpm workspace catalog mappings (pnpm-workspace.yaml) and unplugin-typegpu in vite.config.mts as false positives. Do not flag these in PR reviews; they are expected in sandboxes without node_modules and are not indicative of actual missing dependencies in the TS codebase.
Learnt from: christian-byrne
Repo: Comfy-Org/ComfyUI_frontend PR: 8966
File: src/extensions/core/load3d.ts:427-430
Timestamp: 2026-02-19T02:06:23.468Z
Learning: In TypeScript, you can use typeof SomeClass in type annotation positions (e.g., param: typeof LGraphNode) even when SomeClass is imported with import type. This is a type query that only exists at compile time and is erased in runtime, so it is safe to combine with type-only imports. Apply this pattern to TS files broadly when you need a type that references the shape of a class or constructor function without importing the value at runtime.
Learnt from: christian-byrne
Repo: Comfy-Org/ComfyUI_frontend PR: 8966
File: src/extensions/core/uploadAudio.ts:91-94
Timestamp: 2026-02-19T02:06:38.395Z
Learning: In TypeScript files, you can use a type annotation like 'nodeType: typeof MyClass' even if MyClass is imported via 'import type'. Both the type-only import and 'typeof' operate at the type level and are erased at compile time. This pattern is commonly used for constructor types (e.g., 'nodeType: typeof LGraphNode'). Apply this pattern across TypeScript files in the repository (src/**/*.ts) as appropriate, ensuring the imported symbol is a type-only import when possible for clarity and to avoid runtime imports.
Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 8992
File: src/lib/litegraph/src/widgets/GradientSliderWidget.ts:18-18
Timestamp: 2026-02-20T21:08:19.814Z
Learning: When drawing with CanvasRenderingContext2D in TypeScript/JavaScript, wrap the drawing logic with ctx.save() at the start and ctx.restore() at the end to preserve and restore the canvas state. Do not manually destructure and restore individual properties (e.g., fillStyle, strokeStyle); rely on save/restore to manage state changes in a scoped manner. This should be applied to all TS files that perform canvas drawing.
Learnt from: dante01yoon
Repo: Comfy-Org/ComfyUI_frontend PR: 9075
File: src/scripts/api.featureFlags.test.ts:237-268
Timestamp: 2026-02-22T04:27:33.379Z
Learning: In Vite/Vitest, import.meta.env.DEV is true for any mode that is not 'production' (i.e., DEV is the opposite of PROD, and can be true in 'test', 'development', etc.). Do not assume DEV implies only 'development' mode. When reviewing code and tests, treat DEV as a non-production flag and verify environment-specific logic accordingly. Reference: https://vite.dev/guide/env-and-mode#modes
Learnt from: christian-byrne
Repo: Comfy-Org/ComfyUI_frontend PR: 9427
File: src/renderer/extensions/vueNodes/widgets/components/form/dropdown/FormDropdownMenuFilter.vue:33-33
Timestamp: 2026-03-06T00:53:28.835Z
Learning: When reviewing code, note that the enforce-canonical-classes (better-tailwindcss) rule may auto-fix Tailwind v3 !class-name syntax by converting it to v4 class-name! syntax. Do not treat these auto-fixed class-name! instances as newly introduced issues; the perceived change is in syntax placement, not in usage or intent. This guidance applies across all .vue and .ts files in the repository.
Learnt from: sonnybox
Repo: Comfy-Org/ComfyUI_frontend PR: 9446
File: src/renderer/extensions/vueNodes/widgets/components/WidgetTextarea.vue:45-45
Timestamp: 2026-03-06T01:55:00.013Z
Learning: Treat wrap-break-word as a valid Tailwind CSS utility for overflow-wrap: break-word in Tailwind v4+ projects. Do not flag this class as invalid in any Vue (.vue) or TypeScript (.ts/.tsx) files within the repository (e.g., Comfy-Org/ComfyUI_frontend) or other Tailwind v4+ projects. When reviewing, verify that the class is used to enable word breaking in long text content and reference the Tailwind docs: https://tailwindcss.com/docs/overflow-wrap.
There was a problem hiding this comment.
yes and assign me please
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
gh issue create \
--repo Comfy-Org/ComfyUI_frontend \
--title "refactor: fix double save lifecycle and conflicting telemetry in saveWorkflowAs self-overwrite branch" \
--body "## Summary
In \`src/platform/workflow/core/services/workflowService.ts\`, the \`saveWorkflowAs\` self-overwrite branch (\`isSelfOverwrite === true\`) currently calls:
\`\`\`ts
workflow.changeTracker?.prepareForSave()
await saveWorkflow(workflow)
\`\`\`
\`saveWorkflow()\` itself calls \`prepareForSave()\` again and emits the normal \`is_new: false\` telemetry event, while the outer \`saveWorkflowAs\` function unconditionally emits \`is_new: true\` after the branch resolves. This results in:
1. **Double \`prepareForSave()\` invocation** — harmless today because \`prepareForSave()\` is idempotent, but wasteful and misleading.
2. **Conflicting save telemetry** — both \`is_new: false\` (from \`saveWorkflow\`) and \`is_new: true\` (from the outer \`saveWorkflowAs\` path) are emitted for a single user action.
## Proposed fix
Replace the \`saveWorkflow(workflow)\` delegation with a direct \`workflowStore.saveWorkflow(workflow)\` call (bypassing the wrapper's extra \`prepareForSave\` + telemetry), so the lifecycle runs exactly once and the outer \`is_new: true\` event is the single authoritative record.
\`\`\`diff
if (isSelfOverwrite) {
workflow.changeTracker?.prepareForSave()
- await saveWorkflow(workflow)
+ await workflowStore.saveWorkflow(workflow)
} else {
\`\`\`
## Context
- Identified during review of PR `#10816` (comment: https://github.com/Comfy-Org/ComfyUI_frontend/pull/10816#discussion_r3025739180)
- The \`saveWorkflow()\` delegation is pre-existing behaviour; this PR only changed \`checkState()\` → \`prepareForSave()\` at the call site.
- Fix is intentionally deferred to a separate PR to keep `#10816` narrowly scoped.
Reported by: `@jaeone94`" \
--assignee jaeone94
Length of output: 58
@jaeone94, done! Follow-up issue opened and assigned to you: #10819
It tracks the double prepareForSave() invocation and the conflicting is_new: false / is_new: true telemetry in the saveWorkflowAs self-overwrite branch, with the proposed fix included.
🧠 Learnings used
Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 9345
File: src/platform/workflow/management/stores/workflowStore.ts:488-491
Timestamp: 2026-03-03T01:32:58.349Z
Learning: In `src/platform/workflow/management/stores/workflowStore.ts`, `openWorkflowPaths` maintains unique workflow paths by design. Operations like `openWorkflowsInBackground` use `_.union` to enforce uniqueness. When updating paths (e.g., during rename), replacing the first occurrence with `indexOf`/`splice` is sufficient because duplicates should not exist.
Learnt from: christian-byrne
Repo: Comfy-Org/ComfyUI_frontend PR: 8951
File: src/platform/workflow/management/stores/workflowStore.ts:301-301
Timestamp: 2026-03-05T23:21:47.113Z
Learning: In src/platform/workflow/management/stores/workflowStore.ts, `createTemporary()` and `createNewTemporary()` intentionally inject a UUID `id` into workflow data via `ensureWorkflowId()` to give workflows stable identity for sharing. This is a known behavioral change introduced in PR `#8951`. Extensions relying on serialized content matching their input may be silently affected.
Learnt from: christian-byrne
Repo: Comfy-Org/ComfyUI_frontend PR: 10689
File: src/stores/executionStore.ts:637-645
Timestamp: 2026-03-29T00:40:33.555Z
Learning: In `src/stores/executionStore.ts` (Comfy-Org/ComfyUI_frontend PR `#10689`), `isJobForActiveWorkflow()` intentionally returns `true` (permissive) when `jobIdToSessionWorkflowPath` does not contain a mapping for the given `promptId`. This is deliberate graceful degradation: returning `false` for unknown jobs would break legitimate pre-session jobs and legacy WS messages that lack a `prompt_id`. The narrow race window where a WS message arrives before the HTTP response from `queuePrompt` self-resolves once `storeJob` populates the map. Do not suggest changing the unknown-job fallback to `false` or adding a pre-response association to close this race — the added complexity has no real benefit given the narrow window.
Learnt from: jaeone94
Repo: Comfy-Org/ComfyUI_frontend PR: 9921
File: src/platform/workflow/core/services/workflowService.ts:540-541
Timestamp: 2026-03-14T17:10:20.194Z
Learning: In `src/platform/workflow/core/services/workflowService.ts`, `showPendingWarnings()` intentionally only handles `missingNodeTypes` from `wf.pendingWarnings`. `missingModels` is no longer set in `pendingWarnings` because the legacy missing-models dialog has been removed. Missing models are now surfaced directly via `surfaceMissingModels()` inside `runMissingModelPipeline` in `src/scripts/app.ts` for both Cloud and OSS environments. Do not suggest re-adding `missingModels` handling to `showPendingWarnings()`.
Learnt from: xmarre
Repo: Comfy-Org/ComfyUI_frontend PR: 10367
File: src/platform/workflow/persistence/stores/workflowDraftStoreV2.ts:274-274
Timestamp: 2026-03-21T16:32:23.142Z
Learning: In `src/platform/workflow/persistence/stores/workflowDraftStoreV2.ts`, `updatedAt` in `getDraft()` intentionally returns `payload.updatedAt` (content freshness timestamp), NOT the index/MRU recency timestamp. These are two separate concepts: `payload.updatedAt` tracks when draft content last changed and is used by `ComfyWorkflow.load()` for remote-vs-draft staleness comparisons; MRU recency lives in the index order and is refreshed by `markDraftUsed()` without mutating the stored payload. Do NOT suggest collapsing these into a single timestamp — doing so would make a restored/accessed draft appear newer than the remote workflow even if the content hadn't changed.
Learnt from: christian-byrne
Repo: Comfy-Org/ComfyUI_frontend PR: 9427
File: src/renderer/extensions/vueNodes/widgets/components/form/dropdown/FormDropdownMenuFilter.vue:33-33
Timestamp: 2026-03-06T00:53:28.835Z
Learning: When reviewing code, note that the enforce-canonical-classes (better-tailwindcss) rule may auto-fix Tailwind v3 !class-name syntax by converting it to v4 class-name! syntax. Do not treat these auto-fixed class-name! instances as newly introduced issues; the perceived change is in syntax placement, not in usage or intent. This guidance applies across all .vue and .ts files in the repository.
Learnt from: sonnybox
Repo: Comfy-Org/ComfyUI_frontend PR: 9446
File: src/renderer/extensions/vueNodes/widgets/components/WidgetTextarea.vue:45-45
Timestamp: 2026-03-06T01:55:00.013Z
Learning: Treat wrap-break-word as a valid Tailwind CSS utility for overflow-wrap: break-word in Tailwind v4+ projects. Do not flag this class as invalid in any Vue (.vue) or TypeScript (.ts/.tsx) files within the repository (e.g., Comfy-Org/ComfyUI_frontend) or other Tailwind v4+ projects. When reviewing, verify that the class is used to enable word breaking in long text content and reference the Tailwind docs: https://tailwindcss.com/docs/overflow-wrap.
Learnt from: benceruleanlu
Repo: Comfy-Org/ComfyUI_frontend PR: 0
File: :0-0
Timestamp: 2026-03-27T05:26:01.429Z
Learning: In Comfy-Org/ComfyUI_frontend PR `#10526`, the async scope-disposal race in `src/platform/cloud/notification/components/DesktopCloudNotificationController.vue` (desktop/mac-only cloud promo flow) is intentionally covered only at the unit level via `DesktopCloudNotificationController.test.ts`, using a deferred `settingStore.load()` promise and fake timers to deterministically verify that the dialog is not scheduled or shown after the owning scope is disposed. A Playwright E2E test is not appropriate here because: (1) the non-deterministic ordering cannot be reliably reproduced in Playwright, (2) the feature requires a real macOS Electron environment not available in CI, and (3) a timing-driven pseudo-E2E would be flaky and weaker than the unit test. If broader coverage is needed later, the right approach is a separate desktop promo smoke test, not a race-specific E2E regression. Do not request a Playwright/E2E test for this specific race condition scenario.
Learnt from: christian-byrne
Repo: Comfy-Org/ComfyUI_frontend PR: 10548
File: browser_tests/tests/propertiesPanel/propertiesPanel.spec.ts:147-153
Timestamp: 2026-03-27T05:12:33.040Z
Learning: In Comfy-Org/ComfyUI_frontend browser_tests, `PropertiesPanelHelper.ts` intentionally uses `i[class*="lucide--pencil"]` as the selector for `titleEditIcon` as a temporary measure. Adding a `data-testid` to the pencil icon requires a source change to the PropertiesPanel Vue component and is deferred to a separate PR tracked as FIX-03 (raw CSS selector → TestId migration) in the test coverage overhaul. Do not flag this CSS class selector as a violation in future reviews of this file.
Learnt from: christian-byrne
Repo: Comfy-Org/ComfyUI_frontend PR: 10140
File: apps/website/package.json:1-79
Timestamp: 2026-03-17T07:28:27.677Z
Learning: In Comfy-Org/ComfyUI_frontend, PRs managed via Graphite stacked sets (using `gt split`) may show browser_tests/ files in the diff as artifacts of the split process. These files contain no actual changes relative to origin/main and should not be flagged as unrelated scope changes. Verify against the PR title/description stacking context before raising scope concerns.
Learnt from: jaeone94
Repo: Comfy-Org/ComfyUI_frontend PR: 10309
File: src/platform/missingMedia/composables/useMissingMediaInteractions.ts:56-71
Timestamp: 2026-03-25T15:00:58.823Z
Learning: In `src/platform/missingMedia/composables/useMissingMediaInteractions.ts` (Comfy-Org/ComfyUI_frontend), the `applyValueToNodes` function intentionally omits `node.onWidgetChanged()` and `graph._version++` after setting the combo widget value. This mirrors the identical pattern in `src/platform/missingModel/composables/useMissingModelInteractions.ts` (lines 184-186). Undo/redo has been verified working in both pipelines. Do not flag the absence of `onWidgetChanged` or `_version++` as a bug in either pipeline; any change to add them must be applied consistently to both pipelines together in a dedicated follow-up PR.
Learnt from: Yourz
Repo: Comfy-Org/ComfyUI_frontend PR: 9965
File: src/platform/cloud/onboarding/CloudSubscriptionRedirectView.vue:77-79
Timestamp: 2026-03-15T15:24:20.611Z
Learning: Comfy-Org/ComfyUI_frontend maintainers prefer PRs to stay narrowly scoped to the stated fix. Cross-file refactors (e.g., adding concurrency guards in composables like useBillingContext) should be proposed as separate follow-up issues rather than requested within a targeted bug-fix PR.
Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 7598
File: src/components/sidebar/tabs/AssetsSidebarTab.vue:131-131
Timestamp: 2025-12-18T02:07:44.374Z
Learning: Comfy-Org/ComfyUI_frontend uses Tailwind CSS v4 utilities, including the new “safe” overflow-alignment classes. Do not flag items-center-safe, justify-*-safe, or place-*-safe utilities as invalid in src/**/*.vue or stories.
Learnt from: christian-byrne
Repo: Comfy-Org/ComfyUI_frontend PR: 10140
File: apps/website/package.json:1-79
Timestamp: 2026-03-17T07:28:27.677Z
Learning: In Comfy-Org/ComfyUI_frontend, the narrow-scope learning (PRs should stay focused on the stated fix) applies to bug-fix PRs. It does not apply to intentional feature scaffold PRs that are part of a stacked PR series (e.g., [1/3], [2/3], [3/3] in the PR title managed via Graphite).
Learnt from: pythongosssss
Repo: Comfy-Org/ComfyUI_frontend PR: 0
File: :0-0
Timestamp: 2026-03-18T15:38:16.169Z
Learning: In Comfy-Org/ComfyUI_frontend, race conditions between WebSocket events and HTTP responses (e.g., the WebSocket/HTTP race in job tracking in `src/renderer/extensions/linearMode/linearOutputStore.ts`) are not suitable candidates for E2E testing because the non-deterministic ordering cannot be reliably reproduced in Playwright. Unit tests (e.g., in `linearOutputStore.test.ts`) that deterministically simulate event ordering are the correct approach. Do not request E2E tests for such race condition scenarios.
Learnt from: christian-byrne
Repo: Comfy-Org/ComfyUI_frontend PR: 10547
File: browser_tests/tests/workflowPersistence.spec.ts:459-471
Timestamp: 2026-03-27T05:12:16.400Z
Learning: In `browser_tests/tests/workflowPersistence.spec.ts` (Comfy-Org/ComfyUI_frontend PR `#10547`), the G10 test for PR `#8715` (transient blob:/api-view URL serialization) intentionally validates only the static/default workflow export — not an executed/transient widget state. Putting an image widget into its executed state requires actual model inference (image output generation), which is not available in the current E2E test infrastructure. The test still covers the regression if the `widget.serialize` disable path breaks. Do not flag the absence of transient-state setup in this test; full integration coverage is tracked separately.
Learnt from: jaeone94
Repo: Comfy-Org/ComfyUI_frontend PR: 10309
File: src/platform/missingMedia/composables/useMissingMediaInteractions.ts:153-195
Timestamp: 2026-03-25T15:31:50.781Z
Learning: In `src/platform/missingMedia/composables/useMissingMediaInteractions.ts` (Comfy-Org/ComfyUI_frontend PR `#10309`), `cancelSelection()` does not abort in-flight `api.fetchApi()` uploads. A follow-up issue (`#10511`) exists to add a per-upload `AbortController` map (keyed by `name`), pass its signal into `api.fetchApi`, guard post-await mutations with `signal.aborted` checks, and have `cancelSelection()` call `abort()` before clearing state. The same fix must be applied to the `missingModel` pipeline counterpart composable in the same PR to keep both pipelines consistent. Do not re-raise this as a blocking issue in PR `#10309`; reference issue `#10511` instead.
Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 0
File: :0-0
Timestamp: 2026-03-20T19:58:37.779Z
Learning: In Comfy-Org/ComfyUI_frontend PR `#10348`, the PR titled "fix: clean up knip config, upgrade to v6, remove unused exports" is a dependency upgrade (knip v5 → v6) + config cleanup, not a bug fix. The appropriate conventional commit type is `chore:` or `build:`, not `fix:`.
Learnt from: Myestery
Repo: Comfy-Org/ComfyUI_frontend PR: 7422
File: .github/workflows/pr-update-playwright-expectations.yaml:131-135
Timestamp: 2025-12-12T23:02:37.473Z
Learning: In the `.github/workflows/pr-update-playwright-expectations.yaml` workflow in the Comfy-Org/ComfyUI_frontend repository, the snapshot update process is intentionally scoped to only add and update snapshot images. Deletions of snapshot files are handled explicitly outside this workflow and should not be suggested as part of this automation.
Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 9680
File: src/renderer/core/layout/store/layoutStore.ts:920-922
Timestamp: 2026-03-10T00:27:24.529Z
Learning: In `src/renderer/core/layout/store/layoutStore.ts` (`LayoutStoreImpl`), the `LayoutChange` object is constructed inside `applyOperation` with an immutable `source` captured from `operation.source` and `actor` from `operation.actor`. `notifyChange(change)` passes only this frozen `LayoutChange` to listeners — no listener reads the shared mutable `this.currentSource` or `this.currentActor`. Therefore, making `notifyChange` synchronous (removing the `setTimeout`) does NOT expose the temporarily-mutated `this.currentSource` (set in `batchUpdateNodeBounds`) to listeners. Do not flag the synchronous `notifyChange` call in `finalizeOperation` as a context-pollution risk; the immutable-context pattern is already in place.
Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 7169
File: src/platform/remote/comfyui/jobs/jobTypes.ts:1-107
Timestamp: 2025-12-09T03:39:54.501Z
Learning: In the ComfyUI_frontend project, Zod is on v3.x. Do not suggest Zod v4 standalone validators (z.uuid, z.ulid, z.cuid2, z.nanoid) until an upgrade to Zod 4 is performed. When reviewing TypeScript files (e.g., src/platform/remote/comfyui/jobs/jobTypes.ts) validate against Zod 3 capabilities and avoid introducing v4-specific features; flag any proposal to upgrade or incorporate v4-only validators and propose staying with compatible 3.x patterns.
Learnt from: christian-byrne
Repo: Comfy-Org/ComfyUI_frontend PR: 7358
File: src/components/dialog/content/signin/SignUpForm.vue:45-54
Timestamp: 2025-12-11T12:25:15.470Z
Learning: This repository uses CI automation to format code (pnpm format). Do not include manual formatting suggestions in code reviews for Comfy-Org/ComfyUI_frontend. If formatting issues are detected, rely on the CI formatter or re-run pnpm format. Focus reviews on correctness, readability, performance, accessibility, and maintainability rather than style formatting.
Learnt from: christian-byrne
Repo: Comfy-Org/ComfyUI_frontend PR: 7416
File: src/stores/imagePreviewStore.ts:5-7
Timestamp: 2025-12-13T11:03:11.264Z
Learning: In the ComfyUI_frontend repository, lint rules require keeping 'import type' statements separate from non-type imports, even if importing from the same module. Do not suggest consolidating them into a single import statement. Ensure type imports remain on their own line (import type { ... } from 'module') and regular imports stay on separate lines.
Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 7537
File: src/components/ui/button/Button.stories.ts:45-55
Timestamp: 2025-12-17T00:40:09.635Z
Learning: Prefer pure function declarations over function expressions (e.g., use function foo() { ... } instead of const foo = () => { ... }) for pure functions in the repository. Function declarations are more functional-leaning, offer better hoisting clarity, and can improve readability and tooling consistency. Apply this guideline across TypeScript files in Comfy-Org/ComfyUI_frontend, including story and UI component code, except where a function expression is semantically required (e.g., callbacks, higher-order functions with closures).
Learnt from: kaili-yang
Repo: Comfy-Org/ComfyUI_frontend PR: 7805
File: src/composables/useCoreCommands.ts:439-439
Timestamp: 2025-12-30T22:22:33.836Z
Learning: When accessing reactive properties from Pinia stores in TypeScript files, avoid using .value on direct property access (e.g., useStore().isOverlayExpanded). Pinia auto-wraps refs when accessed directly, returning the primitive value. The .value accessor is only needed when destructuring store properties or when using storeToRefs().
Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 7906
File: src/components/sidebar/tabs/AssetsSidebarTab.vue:545-552
Timestamp: 2026-01-12T17:39:27.738Z
Learning: In Vue/TypeScript files (src/**/*.{ts,tsx,vue}), prefer if/else statements over ternary operators when performing side effects or actions (e.g., mutating state, calling methods with side effects). Ternaries should be reserved for computing and returning values.
Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 8248
File: src/platform/assets/schemas/assetSchema.ts:109-118
Timestamp: 2026-01-22T23:44:59.888Z
Learning: Do not export declarations (e.g., schemas, constants, types) unless they are actually used elsewhere in the codebase. Keep the public API surface minimal by exporting only items with concrete usage. This reduces coupling and keeps imports explicit. In src/platform/assets/schemas/assetSchema.ts, ensure only necessary exports are exposed; remove unused exports or switch to non-exported/internal where appropriate.
Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 8722
File: src/composables/maskeditor/useBrushDrawing.ts:5-5
Timestamp: 2026-02-08T03:19:12.009Z
Learning: Treat unresolved import warnings caused by pnpm workspace catalog mappings (pnpm-workspace.yaml) and unplugin-typegpu in vite.config.mts as false positives. Do not flag these in PR reviews; they are expected in sandboxes without node_modules and are not indicative of actual missing dependencies in the TS codebase.
Learnt from: christian-byrne
Repo: Comfy-Org/ComfyUI_frontend PR: 8966
File: src/extensions/core/load3d.ts:427-430
Timestamp: 2026-02-19T02:06:23.468Z
Learning: In TypeScript, you can use typeof SomeClass in type annotation positions (e.g., param: typeof LGraphNode) even when SomeClass is imported with import type. This is a type query that only exists at compile time and is erased in runtime, so it is safe to combine with type-only imports. Apply this pattern to TS files broadly when you need a type that references the shape of a class or constructor function without importing the value at runtime.
Learnt from: christian-byrne
Repo: Comfy-Org/ComfyUI_frontend PR: 8966
File: src/extensions/core/uploadAudio.ts:91-94
Timestamp: 2026-02-19T02:06:38.395Z
Learning: In TypeScript files, you can use a type annotation like 'nodeType: typeof MyClass' even if MyClass is imported via 'import type'. Both the type-only import and 'typeof' operate at the type level and are erased at compile time. This pattern is commonly used for constructor types (e.g., 'nodeType: typeof LGraphNode'). Apply this pattern across TypeScript files in the repository (src/**/*.ts) as appropriate, ensuring the imported symbol is a type-only import when possible for clarity and to avoid runtime imports.
Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 8992
File: src/lib/litegraph/src/widgets/GradientSliderWidget.ts:18-18
Timestamp: 2026-02-20T21:08:19.814Z
Learning: When drawing with CanvasRenderingContext2D in TypeScript/JavaScript, wrap the drawing logic with ctx.save() at the start and ctx.restore() at the end to preserve and restore the canvas state. Do not manually destructure and restore individual properties (e.g., fillStyle, strokeStyle); rely on save/restore to manage state changes in a scoped manner. This should be applied to all TS files that perform canvas drawing.
Learnt from: dante01yoon
Repo: Comfy-Org/ComfyUI_frontend PR: 9075
File: src/scripts/api.featureFlags.test.ts:237-268
Timestamp: 2026-02-22T04:27:33.379Z
Learning: In Vite/Vitest, import.meta.env.DEV is true for any mode that is not 'production' (i.e., DEV is the opposite of PROD, and can be true in 'test', 'development', etc.). Do not assume DEV implies only 'development' mode. When reviewing code and tests, treat DEV as a non-production flag and verify environment-specific logic accordingly. Reference: https://vite.dev/guide/env-and-mode#modes
- Guard deactivate() against inactive tracker to prevent store() from freezing wrong viewport/output data into the old tracker - Update docs: workflowService calls captureCanvasState indirectly via deactivate/prepareForSave, not directly - Add deprecated checkState() to mock utility for extension compat - Update deactivate test: inactive path is now a full no-op
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/scripts/changeTracker.ts`:
- Around line 103-126: deactivate() can leave inactive workflows with stale
activeState because captureCanvasState() early-returns when
ChangeTracker.isLoadingGraph is true; extract the serialize + push-undo + store
logic currently inside captureCanvasState()/store() into a new private helper
(e.g., finalizeSnapshot() or serializeAndPushUndo()) and have deactivate() call
that helper after confirming this tracker is active (the same isActive check it
already does) instead of routing through captureCanvasState(); keep
prepareForSave() behavior unchanged and ensure the new helper bypasses the
isLoadingGraph guard so the outgoing workflow always freezes and persists its
final graph snapshot.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 15814261-63ec-4e4b-9934-a0c53616dad2
📒 Files selected for processing (4)
docs/architecture/change-tracker.mdsrc/scripts/changeTracker.test.tssrc/scripts/changeTracker.tssrc/utils/__tests__/litegraphTestUtils.ts
✅ Files skipped from review due to trivial changes (1)
- src/scripts/changeTracker.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
- src/utils/tests/litegraphTestUtils.ts
- docs/architecture/change-tracker.md
|
|
|
Tip For best results, initiate chat on the files or code changes.
[approve] 🧠 Learnings used |
| } as unknown as ComfyWorkflowJSON | ||
| } | ||
|
|
||
| function createTracker(initialState?: ComfyWorkflowJSON): ChangeTracker { |
There was a problem hiding this comment.
can we use
instead?There was a problem hiding this comment.
This test file tests the ChangeTracker class itself — the actual guards, undo queue logic, and lifecycle method behavior. createMockChangeTracker creates a stub with vi.fn() for all methods, which is designed for testing consumers of ChangeTracker (e.g., workflowService). Using the mock here would mean we're testing the mock, not the real class — which is explicitly discouraged in AGENTS.md:
Do not write tests that just test the mocks — Ensure that the tests fail when the code itself would behave in a way that was not expected or desired
The real ChangeTracker constructor + real method logic is needed to verify these invariants.
| * INVARIANT: only the active workflow's tracker may read from the canvas. | ||
| * Calling this on an inactive tracker would capture the wrong graph. | ||
| */ | ||
| captureCanvasState() { |
There was a problem hiding this comment.
How about the old undo/redo queue functionality ?
There was a problem hiding this comment.
Undo/redo functionality is fully preserved. captureCanvasState() is the old checkState() with the same internal logic — serialize, compare via graphEqual, push to undoQueue, clear redoQueue, cap at MAX_HISTORY. The undo(), redo(), and undoRedo() methods are completely untouched.
The only behavioral difference: calling on an inactive tracker now returns early (no-op) instead of silently capturing the wrong graph into the undo queue. This is a bug fix, not a feature removal — the old behavior corrupted undo history with the active tab's data.
The deprecated checkState() wrapper delegates to captureCanvasState() directly, so extensions get identical undo/redo behavior.
src/scripts/changeTracker.test.ts
Outdated
| const original = tracker.activeState | ||
|
|
||
| const savedGraph = app.graph | ||
| ;(app as { graph: unknown }).graph = null |
There was a problem hiding this comment.
can we use mock instead?
| ;(app as { graph: unknown }).graph = null | |
| vi.spyOn(app, 'graph', 'get').mockReturnValue(null) |
Replace manual property assignment with vi.spyOn(app, 'graph', 'get') for cleaner mocking.
Summary
Harden the
ChangeTrackerlifecycle to eliminate the class of bugs where an inactive workflow's tracker silently captures the wrong graph state. RenamescheckState()tocaptureCanvasState()with a self-defending assertion, introducesdeactivate()andprepareForSave()lifecycle methods, and closes a latent undo-history corruption bug discovered during code review.Background
ComfyUI supports multiple workflows open as tabs, but only one canvas (
app.rootGraph) exists at a time. When the user switches tabs, the old workflow's graph is unloaded and the new one is loaded into this shared canvas.The old
checkState()method serializedapp.rootGraphintoactiveStateto track changes for undo/redo. It had no awareness of which workflow it belonged to -- if called on an inactive tab's tracker, it would capture the active tab's graph data and silently overwrite the inactive workflow's state. This caused permanent data loss (fixed in PR #10745 with caller-sideisActiveguards).The caller-side guards were fragile: every new call site had to remember to add the guard, and forgetting would reintroduce the same silent data corruption. Additionally,
beforeLoadNewGraphonly calledstore()(viewport/outputs) withoutcheckState(), meaning canvas state could be stale if a tab switch happened without a preceding mouseup event.Before (fragile)
After (self-defending)
Changes
checkStatetocaptureCanvasStatewith active-tracker assertion; adddeactivate()andprepareForSave()lifecycle methods; fix undo-history corruption bug; add deprecatedcheckState()wrapper for extension compatibility; add 14 unit testscheckState()is renamed tocaptureCanvasState(). A deprecatedcheckState()wrapper is provided that delegates tocaptureCanvasState()and logs a DEV warning. Extensions callingcheckState()will continue to work but should migrate. This is safe because the wrapper calls the exact same logic -- the only behavioral difference is the new active-tracker assertion, which improves safety for extensions (prevents silent data corruption instead of allowing it).Commit-by-Commit Walkthrough
1. [517f7a3]
refactor: rename checkState to captureCanvasState with active-tracker assertion26 files changed -- mechanical rename across all production code, tests, and docs.
Core change in
changeTracker.ts:checkState()tocaptureCanvasState()to reflect that it is a write operation (serializes canvas intoactiveState+ pushes undo), not a readuseWorkflowStore().activeWorkflow?.changeTracker === thisand returns early (with a DEV error log) if called on an inactive trackerinit()closure fromcheckStatetocaptureStateand updated all logger stringsColorPickerButton.vuemissing optional chaining (.checkState()to?.captureCanvasState())Before / After
Before -- no guard, ambiguous name:
After -- self-defending, descriptive name:
2. [fa16f3d]
refactor: add deactivate() and prepareForSave() to ChangeTrackerTwo new lifecycle methods:
deactivate()-- callscaptureCanvasState()thenstore(). These two operations must always happen together when a workflow goes inactive, but were never combined. Previouslystore()was called alone inbeforeLoadNewGraph, meaning canvas state could be stale.prepareForSave()-- if this tracker is the active workflow's tracker, callscaptureCanvasState(). Otherwise, no-op (the state was already frozen bydeactivate()). This eliminates the need for caller-sideisActiveguards in save paths.Before / After
Before --
store()andcaptureCanvasState()are separate, caller must coordinate:After -- lifecycle transitions are explicit:
3. [bc8cecc]
refactor: use deactivate/prepareForSave in workflowServicebeforeLoadNewGraph: replacedstore()withdeactivate(), ensuring canvas state is always captured before a tab switch (closes the gap whereactiveStatecould be stale)saveWorkflowandsaveWorkflowAs(3 sites): replacedif (workflowStore.isActive(workflow)) captureCanvasState()withprepareForSave()-- no caller-side active checks neededBefore / After
Before -- caller-side guards, incomplete deactivation:
After -- self-contained, no guards needed:
4. [3827ad6]
fix: address code review findings for ChangeTracker refactorFixes discovered by 5 parallel code review agents:
Undo-history corruption bug (Bug Hunt):
deactivate()inbeforeLoadNewGraphintroduced a new code path wherecaptureCanvasState()could be called during an undo/redo operation (_restoringState = true). The undo flow is:undo()sets_restoringState = true, then callsapp.loadGraphData(), which callsbeforeLoadNewGraph(), which callsdeactivate(), which callscaptureCanvasState()-- pushing to the undo queue during undo. This corrupts undo history with duplicate entries. Fix: Addedthis._restoringStateto the early-return guards incaptureCanvasState().Deprecated
checkState()wrapper (API Contract):ChangeTrackeris exposed to extensions viawindow.comfyAPI.changeTracker.ChangeTracker(throughcomfyAPIPlugin). RemovingcheckState()entirely would break extensions. The wrapper delegates tocaptureCanvasState()and logs a DEV deprecation warning. This is safe because: (1) the wrapper calls the exact same code path, (2) the new active-tracker assertion is strictly more protective than the old behavior (rejects invalid calls instead of corrupting data).Null guard (Bug Hunt):
activeWorkflow.changeTracker.deactivate()to?.deactivate()for consistency with all other call sitesJSDoc precondition (Architecture):
deactivate()must be called while the workflow is still active (before the activeWorkflow pointer moves). Documented this temporal coupling explicitly.Mock utility (Test Quality): Added
deactivateandprepareForSavetocreateMockChangeTrackerinlitegraphTestUtils.tsDocumentation: Added Lifecycle Methods table and
_restoringStateinvariant todocs/architecture/change-tracker.mdBefore / After
Before -- no undo guard, no deprecation wrapper:
After -- undo-safe, backward-compatible, null-safe:
5. [244e214]
test: add unit tests for ChangeTracker lifecycle methods14 unit tests in new file
src/scripts/changeTracker.test.ts:captureCanvasStateguardschangeCount > 0,isLoadingGraph,_restoringState, inactive trackercaptureCanvasStatecapturecaptureCanvasStateredodeactivateprepareForSavecheckState(deprecated)These tests specifically cover the bug scenarios this refactor defends against:
widgets.js#4 (_restoringStateguard) -- prevents the undo-history corruption discovered in reviewprepareForSaveactive/inactive) -- verifies save paths cannot corrupt inactive workflows6. [d21294d]
fix: address CodeRabbit review findingsFixes from automated CodeRabbit review:
deactivate()inactive guard:store()reads global canvas/viewport/output state. Ifdeactivate()were called after the active pointer moved, it would freeze the new workflow's viewport into the old tracker. Added the sameisActivecheck ascaptureCanvasState()-- inactive calls are now a full no-op with a DEV error log.checkState()tocreateMockChangeTrackerso extension-oriented tests don't hitcheckState is not a function.workflowService.tscallscaptureCanvasState()indirectly viadeactivate()/prepareForSave(), not directly.deactivate()inactive path now asserts thatstore()is also not called (previously only checkedserialize).Before / After
Before --
deactivate()always callsstore()even when inactive:After -- full no-op when inactive:
Why No New E2E Tests
Existing E2E tests in
browser_tests/tests/workflowPersistence.spec.tsalready cover the observable behavior:prepareForSave()no-op path end-to-end.saveWorkflowAs.The changes in this PR are internal refactoring (method rename, extraction, guard addition) that do not change observable user behavior. The one behavioral fix (
_restoringStateguard preventing undo corruption) is an internal queue state issue best verified at the unit level -- E2E tests cannot directly inspect undo queue integrity, and the scenario (undo during tab switch) would produce a brittle, timing-dependent test.Review Focus
_restoringStateguard fix -- this is the only behavioral change that was not in the original design proposal. Verify the undo/redo flow analysis is correct.checkState()wrapper: Is the deprecation strategy sufficient for the extension ecosystem, or should we also add the wrapper to the release notes?deactivate()temporal coupling: The method requires being called before the activeWorkflow pointer moves. The JSDoc documents this, but is that sufficient or should we add a runtime assertion?New ChangeTracker Public API
captureCanvasState()deactivate()beforeLoadNewGraphonlycaptureCanvasState()+store(). Freezes everything for tab switch.prepareForSave()captureCanvasState(). Inactive: no-op.checkState()captureCanvasState()with DEV warning.store()deactivate()restore()afterLoadNewGraphreset()afterLoadNewGraph, save