fix: filter WS messages by workflow tab to prevent cross-tab state leaking#10689
fix: filter WS messages by workflow tab to prevent cross-tab state leaking#10689christian-byrne wants to merge 11 commits intomainfrom
Conversation
🎭 Playwright: ✅ 1183 passed, 0 failed · 2 flaky📊 Browser Reports
|
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughWebSocket-driven execution and preview handlers are now scoped to the active workflow via an exported Changes
Sequence Diagram(s)sequenceDiagram
participant API as "Server (WS API)"
participant App as "ComfyApp (socket handlers)"
participant Exec as "executionStore"
participant WF as "workflowStore"
API->>App: send execution event (prompt_id / jobId)
App->>Exec: isJobForActiveWorkflow(prompt_id)?
Exec->>WF: read activeWorkflow?.path
alt job matches active workflow
Exec-->>App: true
App->>Exec: update activeJob, node outputs, nodeProgressStates, previews
Exec->>Exec: update nodeProgressStatesByJob (cache)
else job not for active workflow
Exec-->>App: false
App->>Exec: update nodeProgressStatesByJob (cache only, no UI mutations)
end
note right of Exec: switching active workflow triggers\nrehydration: populate nodeProgressStates from most recent cached job or clear
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Poem
Caution Pre-merge checks failedPlease resolve all errors before merging. Addressing warnings is optional.
❌ Failed checks (1 error, 1 warning)
✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
🎨 Storybook: ✅ Built — View Storybook |
📦 Bundle: 5.15 MB gzip 🔴 +860 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.22 MB (baseline 1.22 MB) • ⚪ 0 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 — 482 kB (baseline 482 kB) • ⚪ 0 BConfiguration panels, inspectors, and settings screens
Status: 10 added / 10 removed / 11 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 — 110 kB (baseline 110 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 — 3.02 MB (baseline 3.01 MB) • 🔴 +3.23 kBStores, services, APIs, and repositories
Status: 13 added / 13 removed / 4 unchanged Utilities & Hooks — 345 kB (baseline 345 kB) • ⚪ 0 BHelpers, composables, and utility bundles
Status: 13 added / 13 removed / 14 unchanged Vendor & Third-Party — 9.83 MB (baseline 9.83 MB) • ⚪ 0 BExternal libraries and shared vendor chunks Status: 16 unchanged Other — 8.57 MB (baseline 8.57 MB) • 🟢 -59 BBundles that do not match a named category
Status: 56 added / 56 removed / 79 unchanged ⚡ Performance Report
All metrics
Historical variance (last 15 runs)
Trend (last 15 commits on main)
Raw data{
"timestamp": "2026-04-19T02:44:00.725Z",
"gitSha": "7fb2cfdff5b0212afcd6a0f0f68e851e925b219b",
"branch": "prompt-tab-graph-state-sync-fix",
"measurements": [
{
"name": "canvas-idle",
"durationMs": 2017.601999999897,
"styleRecalcs": 6,
"styleRecalcDurationMs": 5.277999999999998,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 344.526,
"heapDeltaBytes": 19968820,
"heapUsedBytes": 64492572,
"domNodes": 12,
"jsHeapTotalBytes": 23592960,
"scriptDurationMs": 17.859000000000005,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "canvas-idle",
"durationMs": 2021.221999999966,
"styleRecalcs": 10,
"styleRecalcDurationMs": 10.112000000000002,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 365.907,
"heapDeltaBytes": 21011192,
"heapUsedBytes": 64551888,
"domNodes": 20,
"jsHeapTotalBytes": 23068672,
"scriptDurationMs": 23.315,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "canvas-idle",
"durationMs": 2022.9170000000067,
"styleRecalcs": 8,
"styleRecalcDurationMs": 7.265000000000001,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 353.87199999999996,
"heapDeltaBytes": -5137780,
"heapUsedBytes": 45428628,
"domNodes": 16,
"jsHeapTotalBytes": 24641536,
"scriptDurationMs": 17.729000000000003,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "canvas-mouse-sweep",
"durationMs": 1963.9319999999998,
"styleRecalcs": 78,
"styleRecalcDurationMs": 53.318000000000005,
"layouts": 12,
"layoutDurationMs": 4.195000000000001,
"taskDurationMs": 867.5189999999999,
"heapDeltaBytes": 15618344,
"heapUsedBytes": 59522628,
"domNodes": 63,
"jsHeapTotalBytes": 23855104,
"scriptDurationMs": 143.94,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "canvas-mouse-sweep",
"durationMs": 1999.9990000000025,
"styleRecalcs": 82,
"styleRecalcDurationMs": 41.415,
"layouts": 12,
"layoutDurationMs": 3.4799999999999995,
"taskDurationMs": 932.1780000000001,
"heapDeltaBytes": 15586140,
"heapUsedBytes": 59548564,
"domNodes": 62,
"jsHeapTotalBytes": 23330816,
"scriptDurationMs": 128.056,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "canvas-mouse-sweep",
"durationMs": 2022.5500000000238,
"styleRecalcs": 84,
"styleRecalcDurationMs": 44.979000000000006,
"layouts": 12,
"layoutDurationMs": 3.401,
"taskDurationMs": 978.4499999999999,
"heapDeltaBytes": 15825060,
"heapUsedBytes": 58915516,
"domNodes": 67,
"jsHeapTotalBytes": 23068672,
"scriptDurationMs": 145.543,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "canvas-zoom-sweep",
"durationMs": 1729.0500000000293,
"styleRecalcs": 30,
"styleRecalcDurationMs": 19.125,
"layouts": 6,
"layoutDurationMs": 0.6479999999999999,
"taskDurationMs": 328.96000000000004,
"heapDeltaBytes": 24929724,
"heapUsedBytes": 67905536,
"domNodes": 77,
"jsHeapTotalBytes": 20971520,
"scriptDurationMs": 29.002,
"eventListeners": 19,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "canvas-zoom-sweep",
"durationMs": 1757.325000000037,
"styleRecalcs": 30,
"styleRecalcDurationMs": 19.097999999999995,
"layouts": 6,
"layoutDurationMs": 0.719,
"taskDurationMs": 351.812,
"heapDeltaBytes": 24901520,
"heapUsedBytes": 68590996,
"domNodes": 77,
"jsHeapTotalBytes": 22020096,
"scriptDurationMs": 31.418999999999997,
"eventListeners": 19,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "canvas-zoom-sweep",
"durationMs": 1723.0079999999361,
"styleRecalcs": 33,
"styleRecalcDurationMs": 17.9,
"layouts": 6,
"layoutDurationMs": 0.6890000000000001,
"taskDurationMs": 306.542,
"heapDeltaBytes": 24657212,
"heapUsedBytes": 68213868,
"domNodes": 79,
"jsHeapTotalBytes": 21233664,
"scriptDurationMs": 26.492999999999995,
"eventListeners": 19,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "dom-widget-clipping",
"durationMs": 589.6910000000162,
"styleRecalcs": 13,
"styleRecalcDurationMs": 10.596,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 371.333,
"heapDeltaBytes": 6977824,
"heapUsedBytes": 50664244,
"domNodes": 21,
"jsHeapTotalBytes": 12058624,
"scriptDurationMs": 69.54599999999999,
"eventListeners": 2,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "dom-widget-clipping",
"durationMs": 536.8409999999812,
"styleRecalcs": 10,
"styleRecalcDurationMs": 7.443999999999999,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 323.19999999999993,
"heapDeltaBytes": 6607184,
"heapUsedBytes": 50604460,
"domNodes": 16,
"jsHeapTotalBytes": 11796480,
"scriptDurationMs": 58.334,
"eventListeners": 2,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "dom-widget-clipping",
"durationMs": 564.4179999999324,
"styleRecalcs": 11,
"styleRecalcDurationMs": 8.361999999999998,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 319.608,
"heapDeltaBytes": 6644796,
"heapUsedBytes": 50685196,
"domNodes": 18,
"jsHeapTotalBytes": 12582912,
"scriptDurationMs": 56.138,
"eventListeners": 2,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.663333333333338,
"p95FrameDurationMs": 16.700000000000273
},
{
"name": "large-graph-idle",
"durationMs": 2069.1210000001092,
"styleRecalcs": 10,
"styleRecalcDurationMs": 12.045,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 560.61,
"heapDeltaBytes": 5115108,
"heapUsedBytes": 58876460,
"domNodes": -259,
"jsHeapTotalBytes": 15740928,
"scriptDurationMs": 107.75399999999999,
"eventListeners": -127,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "large-graph-idle",
"durationMs": 2062.059999999974,
"styleRecalcs": 9,
"styleRecalcDurationMs": 10.016,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 582.754,
"heapDeltaBytes": 4373020,
"heapUsedBytes": 57930724,
"domNodes": -259,
"jsHeapTotalBytes": 16158720,
"scriptDurationMs": 105.641,
"eventListeners": -127,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "large-graph-idle",
"durationMs": 2035.6559999999035,
"styleRecalcs": 10,
"styleRecalcDurationMs": 10.468000000000002,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 542.463,
"heapDeltaBytes": 4309088,
"heapUsedBytes": 57658984,
"domNodes": -258,
"jsHeapTotalBytes": 16683008,
"scriptDurationMs": 101.34500000000001,
"eventListeners": -127,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "large-graph-pan",
"durationMs": 2098.8820000000032,
"styleRecalcs": 68,
"styleRecalcDurationMs": 17.952999999999996,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 1042.245,
"heapDeltaBytes": 9768648,
"heapUsedBytes": 75080864,
"domNodes": -262,
"jsHeapTotalBytes": 15167488,
"scriptDurationMs": 381.19199999999995,
"eventListeners": -129,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "large-graph-pan",
"durationMs": 2117.7559999999858,
"styleRecalcs": 68,
"styleRecalcDurationMs": 17.676,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 1069.8539999999998,
"heapDeltaBytes": 20028376,
"heapUsedBytes": 75797032,
"domNodes": -262,
"jsHeapTotalBytes": 17149952,
"scriptDurationMs": 385.405,
"eventListeners": -127,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "large-graph-pan",
"durationMs": 2115.2009999999564,
"styleRecalcs": 67,
"styleRecalcDurationMs": 16.677999999999997,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 1075.227,
"heapDeltaBytes": 16627108,
"heapUsedBytes": 71751528,
"domNodes": -263,
"jsHeapTotalBytes": 17674240,
"scriptDurationMs": 388.81399999999996,
"eventListeners": -127,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "large-graph-zoom",
"durationMs": 3132.8119999999444,
"styleRecalcs": 64,
"styleRecalcDurationMs": 17.046000000000003,
"layouts": 60,
"layoutDurationMs": 7.35,
"taskDurationMs": 1299.144,
"heapDeltaBytes": 7186000,
"heapUsedBytes": 64426992,
"domNodes": -266,
"jsHeapTotalBytes": 16683008,
"scriptDurationMs": 480.72799999999995,
"eventListeners": -123,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "large-graph-zoom",
"durationMs": 3169.777999999951,
"styleRecalcs": 66,
"styleRecalcDurationMs": 20.208999999999996,
"layouts": 60,
"layoutDurationMs": 7.495,
"taskDurationMs": 1294.47,
"heapDeltaBytes": 7182668,
"heapUsedBytes": 65001076,
"domNodes": -265,
"jsHeapTotalBytes": 17731584,
"scriptDurationMs": 485.477,
"eventListeners": -123,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "large-graph-zoom",
"durationMs": 3135.116000000039,
"styleRecalcs": 64,
"styleRecalcDurationMs": 17.968,
"layouts": 60,
"layoutDurationMs": 7.6979999999999995,
"taskDurationMs": 1342.102,
"heapDeltaBytes": 7012920,
"heapUsedBytes": 66054352,
"domNodes": -269,
"jsHeapTotalBytes": 16478208,
"scriptDurationMs": 486.818,
"eventListeners": -125,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "minimap-idle",
"durationMs": 2046.3799999999992,
"styleRecalcs": 8,
"styleRecalcDurationMs": 9.047000000000002,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 546.953,
"heapDeltaBytes": 3699824,
"heapUsedBytes": 60227160,
"domNodes": -264,
"jsHeapTotalBytes": 16683008,
"scriptDurationMs": 93.935,
"eventListeners": -127,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333335,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "minimap-idle",
"durationMs": 2020.1620000000275,
"styleRecalcs": 8,
"styleRecalcDurationMs": 8.625,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 514.294,
"heapDeltaBytes": 2793812,
"heapUsedBytes": 59759424,
"domNodes": -264,
"jsHeapTotalBytes": 15896576,
"scriptDurationMs": 90.75599999999999,
"eventListeners": -127,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "minimap-idle",
"durationMs": 2038.772999999992,
"styleRecalcs": 7,
"styleRecalcDurationMs": 7.681999999999998,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 577.973,
"heapDeltaBytes": 3059568,
"heapUsedBytes": 59872316,
"domNodes": -264,
"jsHeapTotalBytes": 15896576,
"scriptDurationMs": 100.47399999999999,
"eventListeners": -127,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "subgraph-dom-widget-clipping",
"durationMs": 573.2480000000351,
"styleRecalcs": 47,
"styleRecalcDurationMs": 14.141999999999998,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 358.263,
"heapDeltaBytes": 6712660,
"heapUsedBytes": 50591524,
"domNodes": 20,
"jsHeapTotalBytes": 13107200,
"scriptDurationMs": 121.52199999999999,
"eventListeners": 8,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.663333333333338,
"p95FrameDurationMs": 16.700000000000273
},
{
"name": "subgraph-dom-widget-clipping",
"durationMs": 538.2370000000947,
"styleRecalcs": 46,
"styleRecalcDurationMs": 10.768,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 352.677,
"heapDeltaBytes": 6678244,
"heapUsedBytes": 50615224,
"domNodes": 18,
"jsHeapTotalBytes": 13107200,
"scriptDurationMs": 119.216,
"eventListeners": 8,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000273
},
{
"name": "subgraph-dom-widget-clipping",
"durationMs": 537.3549999999341,
"styleRecalcs": 46,
"styleRecalcDurationMs": 10.350000000000001,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 345.14500000000004,
"heapDeltaBytes": 6482328,
"heapUsedBytes": 49535168,
"domNodes": 18,
"jsHeapTotalBytes": 12582912,
"scriptDurationMs": 117.93399999999998,
"eventListeners": 8,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.799999999999727
},
{
"name": "subgraph-idle",
"durationMs": 2022.0919999999296,
"styleRecalcs": 11,
"styleRecalcDurationMs": 10.484,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 405.33099999999996,
"heapDeltaBytes": 20127536,
"heapUsedBytes": 64017372,
"domNodes": 21,
"jsHeapTotalBytes": 22544384,
"scriptDurationMs": 23.717999999999996,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "subgraph-idle",
"durationMs": 1999.145999999996,
"styleRecalcs": 7,
"styleRecalcDurationMs": 5.258000000000003,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 333.919,
"heapDeltaBytes": 19932484,
"heapUsedBytes": 63172680,
"domNodes": 12,
"jsHeapTotalBytes": 22282240,
"scriptDurationMs": 14.178999999999997,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "subgraph-idle",
"durationMs": 2020.0869999999895,
"styleRecalcs": 10,
"styleRecalcDurationMs": 12.024000000000001,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 341.003,
"heapDeltaBytes": 20142116,
"heapUsedBytes": 64066540,
"domNodes": 20,
"jsHeapTotalBytes": 22544384,
"scriptDurationMs": 18.445,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "subgraph-mouse-sweep",
"durationMs": 2007.8629999999293,
"styleRecalcs": 87,
"styleRecalcDurationMs": 46.65800000000001,
"layouts": 16,
"layoutDurationMs": 4.43,
"taskDurationMs": 922.1189999999999,
"heapDeltaBytes": 11794724,
"heapUsedBytes": 55730616,
"domNodes": 72,
"jsHeapTotalBytes": 22544384,
"scriptDurationMs": 102.95700000000001,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "subgraph-mouse-sweep",
"durationMs": 1967.1660000000202,
"styleRecalcs": 84,
"styleRecalcDurationMs": 48.06100000000001,
"layouts": 16,
"layoutDurationMs": 4.398999999999999,
"taskDurationMs": 898.606,
"heapDeltaBytes": 12207040,
"heapUsedBytes": 56051552,
"domNodes": 73,
"jsHeapTotalBytes": 23068672,
"scriptDurationMs": 98.04899999999999,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "subgraph-mouse-sweep",
"durationMs": 1988.4690000000091,
"styleRecalcs": 88,
"styleRecalcDurationMs": 47.59,
"layouts": 16,
"layoutDurationMs": 4.413,
"taskDurationMs": 933.4480000000001,
"heapDeltaBytes": 11429244,
"heapUsedBytes": 55409532,
"domNodes": 73,
"jsHeapTotalBytes": 23330816,
"scriptDurationMs": 106.846,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "viewport-pan-sweep",
"durationMs": 8165.397999999982,
"styleRecalcs": 250,
"styleRecalcDurationMs": 51.982000000000006,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 3670.3900000000003,
"heapDeltaBytes": 10094304,
"heapUsedBytes": 65101864,
"domNodes": -256,
"jsHeapTotalBytes": 17440768,
"scriptDurationMs": 1235.6529999999998,
"eventListeners": -123,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "viewport-pan-sweep",
"durationMs": 8159.941000000003,
"styleRecalcs": 250,
"styleRecalcDurationMs": 52.232,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 3920.689,
"heapDeltaBytes": 27413944,
"heapUsedBytes": 81063304,
"domNodes": -259,
"jsHeapTotalBytes": 23703552,
"scriptDurationMs": 1388.236,
"eventListeners": -111,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "viewport-pan-sweep",
"durationMs": 8172.494000000029,
"styleRecalcs": 252,
"styleRecalcDurationMs": 54.565,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 3714.7520000000004,
"heapDeltaBytes": 1496748,
"heapUsedBytes": 56618892,
"domNodes": -257,
"jsHeapTotalBytes": 17702912,
"scriptDurationMs": 1262.8999999999999,
"eventListeners": -113,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "vue-large-graph-idle",
"durationMs": 12434.883000000013,
"styleRecalcs": 0,
"styleRecalcDurationMs": 0,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 12419.384999999998,
"heapDeltaBytes": -46439740,
"heapUsedBytes": 170083448,
"domNodes": -9848,
"jsHeapTotalBytes": 26537984,
"scriptDurationMs": 672.7700000000001,
"eventListeners": -23961,
"totalBlockingTimeMs": 0,
"frameDurationMs": 17.219999999999953,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "vue-large-graph-idle",
"durationMs": 12414.038000000004,
"styleRecalcs": 0,
"styleRecalcDurationMs": 0,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 12383.36,
"heapDeltaBytes": -29586052,
"heapUsedBytes": 188853268,
"domNodes": -9850,
"jsHeapTotalBytes": 23392256,
"scriptDurationMs": 606.862,
"eventListeners": -23957,
"totalBlockingTimeMs": 0,
"frameDurationMs": 17.779999999999927,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "vue-large-graph-idle",
"durationMs": 12383.950000000026,
"styleRecalcs": 0,
"styleRecalcDurationMs": 0,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 12370.842,
"heapDeltaBytes": -49186768,
"heapUsedBytes": 168307020,
"domNodes": -9850,
"jsHeapTotalBytes": 26013696,
"scriptDurationMs": 621.103,
"eventListeners": -23959,
"totalBlockingTimeMs": 0,
"frameDurationMs": 17.223333333333358,
"p95FrameDurationMs": 16.80000000000291
},
{
"name": "vue-large-graph-pan",
"durationMs": 14530.074000000013,
"styleRecalcs": 69,
"styleRecalcDurationMs": 16.60600000000001,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 14481.099,
"heapDeltaBytes": -53456580,
"heapUsedBytes": 162881628,
"domNodes": -9849,
"jsHeapTotalBytes": -15142912,
"scriptDurationMs": 906.1719999999999,
"eventListeners": -23953,
"totalBlockingTimeMs": 0,
"frameDurationMs": 17.776666666666642,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "vue-large-graph-pan",
"durationMs": 14631.280999999944,
"styleRecalcs": 67,
"styleRecalcDurationMs": 16.695999999999987,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 14588.412,
"heapDeltaBytes": -41255416,
"heapUsedBytes": 163063312,
"domNodes": -9846,
"jsHeapTotalBytes": -10878976,
"scriptDurationMs": 889.664,
"eventListeners": -23955,
"totalBlockingTimeMs": 0,
"frameDurationMs": 17.776666666666642,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "vue-large-graph-pan",
"durationMs": 14579.788000000008,
"styleRecalcs": 67,
"styleRecalcDurationMs": 16.693000000000012,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 14559.348,
"heapDeltaBytes": -53185512,
"heapUsedBytes": 162691556,
"domNodes": -9850,
"jsHeapTotalBytes": -15228928,
"scriptDurationMs": 888.038,
"eventListeners": -23953,
"totalBlockingTimeMs": 0,
"frameDurationMs": 17.776666666666642,
"p95FrameDurationMs": 16.80000000000291
},
{
"name": "workflow-execution",
"durationMs": 468.845999999985,
"styleRecalcs": 16,
"styleRecalcDurationMs": 22.814,
"layouts": 5,
"layoutDurationMs": 1.3470000000000002,
"taskDurationMs": 126.07400000000001,
"heapDeltaBytes": 5316336,
"heapUsedBytes": 56956308,
"domNodes": 167,
"jsHeapTotalBytes": 524288,
"scriptDurationMs": 28.078000000000007,
"eventListeners": 71,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "workflow-execution",
"durationMs": 456.7010000000664,
"styleRecalcs": 15,
"styleRecalcDurationMs": 22.028,
"layouts": 5,
"layoutDurationMs": 1.509,
"taskDurationMs": 122.81400000000004,
"heapDeltaBytes": 4964000,
"heapUsedBytes": 49913932,
"domNodes": 154,
"jsHeapTotalBytes": 0,
"scriptDurationMs": 24.267000000000003,
"eventListeners": 71,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.669999999999998,
"p95FrameDurationMs": 16.799999999999727
},
{
"name": "workflow-execution",
"durationMs": 462.74399999992966,
"styleRecalcs": 18,
"styleRecalcDurationMs": 25.089999999999996,
"layouts": 5,
"layoutDurationMs": 1.3050000000000002,
"taskDurationMs": 131.395,
"heapDeltaBytes": 5152732,
"heapUsedBytes": 49810112,
"domNodes": 156,
"jsHeapTotalBytes": 262144,
"scriptDurationMs": 30.319,
"eventListeners": 71,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.799999999999727
}
]
} |
eae437c to
9d30ac8
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
src/stores/executionStore.ts (2)
262-267:⚠️ Potential issue | 🟠 MajorKeep queued-job completion bookkeeping outside the tab filter.
execution_cachedandexecutedonly updatequeuedJobs[prompt_id].nodes, but the new early returns now drop that bookkeeping whenever another workflow tab is active. If the user switches back mid-run,nodesExecutedandexecutionProgressstay permanently stale.Suggested fix
function handleExecutionCached(e: CustomEvent<ExecutionCachedWsMessage>) { - if (!isJobForActiveWorkflow(e.detail.prompt_id)) return - if (!activeJob.value) return + const job = queuedJobs.value[e.detail.prompt_id] + if (!job) return for (const n of e.detail.nodes) { - activeJob.value.nodes[n] = true + job.nodes[n] = true } } function handleExecuted(e: CustomEvent<ExecutedWsMessage>) { - if (!isJobForActiveWorkflow(e.detail.prompt_id)) return - if (!activeJob.value) return - activeJob.value.nodes[e.detail.node] = true + const job = queuedJobs.value[e.detail.prompt_id] + if (!job) return + job.nodes[e.detail.node] = true }Also applies to: 278-282
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/stores/executionStore.ts` around lines 262 - 267, The code in handleExecutionCached currently early-returns based on isJobForActiveWorkflow/activeJob and therefore skips updating the global queuedJobs bookkeeping; update queuedJobs[prompt_id].nodes unconditionally (before any isJobForActiveWorkflow or activeJob checks) so queuedJobs[prompt_id].nodes is always mutated for each e.detail.nodes, then keep the existing tab-filtered behavior only for updating activeJob.value (i.e., call isJobForActiveWorkflow and update activeJob.value.nodes afterward). Apply the same change to the corresponding executed handler (the block referenced at lines 278-282) so nodesExecuted/executionProgress are kept current regardless of the active tab.
337-362:⚠️ Potential issue | 🟠 MajorMove the active-workflow check before preview revocation.
progress_statestill callsrevokePreviewsByExecutionId(nodeId)before the newisJobForActiveWorkflow(jobId)guard. That means a job from another workflow can still clear previews in the current tab when execution IDs overlap.Suggested fix
function handleProgressState(e: CustomEvent<ProgressStateWsMessage>) { const { nodes, prompt_id: jobId } = e.detail + const isActiveWorkflowJob = isJobForActiveWorkflow(jobId) // Revoke previews for nodes that are starting to execute - const previousForJob = nodeProgressStatesByJob.value[jobId] || {} - for (const nodeId in nodes) { - const nodeState = nodes[nodeId] - if (nodeState.state === 'running' && !previousForJob[nodeId]) { - // This node just started executing, revoke its previews - // Note that we're doing the *actual* node id instead of the display node id - // here intentionally. That way, we don't clear the preview every time a new node - // within an expanded graph starts executing. - const { revokePreviewsByExecutionId } = useNodeOutputStore() - revokePreviewsByExecutionId(nodeId) - } - } + if (isActiveWorkflowJob) { + const previousForJob = nodeProgressStatesByJob.value[jobId] || {} + for (const nodeId in nodes) { + const nodeState = nodes[nodeId] + if (nodeState.state === 'running' && !previousForJob[nodeId]) { + const { revokePreviewsByExecutionId } = useNodeOutputStore() + revokePreviewsByExecutionId(nodeId) + } + } + } // Update the per-job progress map (always, regardless of active tab) nodeProgressStatesByJob.value = { ...nodeProgressStatesByJob.value, [jobId]: nodes } evictOldProgressJobs() // Only update the "current view" progress if this job belongs to the active workflow tab - if (!isJobForActiveWorkflow(jobId)) return + if (!isActiveWorkflowJob) return🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/stores/executionStore.ts` around lines 337 - 362, Move the active-workflow guard so previews are only revoked for the active tab: inside handleProgressState, keep the update to nodeProgressStatesByJob and the evictOldProgressJobs call (so per-job progress is always recorded), then check isJobForActiveWorkflow(jobId) and return early if false; only after that perform the loop that calls revokePreviewsByExecutionId(nodeId) (from useNodeOutputStore()) and any "current view" updates. This ensures revokePreviewsByExecutionId, the preview-revoking loop, and current-view progress updates run only for jobs that belong to the active workflow while still preserving nodeProgressStatesByJob and evictOldProgressJobs behavior.src/scripts/app.ts (1)
724-737:⚠️ Potential issue | 🟠 MajorDon’t discard inactive-tab outputs and previews outright.
These early returns stop leakage into the current canvas, but they also bypass the only code here that persists executed outputs and preview URLs. If a run finishes while another workflow tab is active, switching back to the originating tab leaves its outputs/previews stale or missing because nothing stored those payloads for that workflow. Please route them into workflow/job-scoped state and only gate the current-canvas side effects (
node.onExecuted, current preview rendering, etc.).Also applies to: 768-786
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/scripts/app.ts` around lines 724 - 737, The handler registered on api.addEventListener('executed') currently bails out early using useExecutionStore().isJobForActiveWorkflow(detail.prompt_id) which prevents persisting outputs/previews for inactive workflow tabs; change the flow so setNodeOutputsByExecutionId(executionId, detail.output, {...}) (and any preview URL storage) is always called to persist run results into workflow/job-scoped state, then only gate UI/canvas side-effects (calls to getNodeByExecutionId(this.rootGraph, executionId) and node.onExecuted(detail.output), preview rendering, etc.) behind the isJobForActiveWorkflow check so inactive tabs still receive stored outputs when the user returns. Ensure the same change is applied to the similar block around lines 768-786.
🤖 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/stores/executionStore.ts`:
- Around line 626-647: The progress_text handler (handleProgressText) currently
relies on activeJobId and thus can apply updates from a job belonging to another
workflow; update handleProgressText to call isJobForActiveWorkflow(prompt_id) at
the start and return/ignore the message if it returns false, ensuring the
handler uses the incoming prompt_id rather than activeJobId to gate updates;
also apply the same isJobForActiveWorkflow(prompt_id) check to the related
progress_text handling site referenced nearby so all progress_text processing is
skipped for jobs not belonging to the active workflow.
---
Outside diff comments:
In `@src/scripts/app.ts`:
- Around line 724-737: The handler registered on
api.addEventListener('executed') currently bails out early using
useExecutionStore().isJobForActiveWorkflow(detail.prompt_id) which prevents
persisting outputs/previews for inactive workflow tabs; change the flow so
setNodeOutputsByExecutionId(executionId, detail.output, {...}) (and any preview
URL storage) is always called to persist run results into workflow/job-scoped
state, then only gate UI/canvas side-effects (calls to
getNodeByExecutionId(this.rootGraph, executionId) and
node.onExecuted(detail.output), preview rendering, etc.) behind the
isJobForActiveWorkflow check so inactive tabs still receive stored outputs when
the user returns. Ensure the same change is applied to the similar block around
lines 768-786.
In `@src/stores/executionStore.ts`:
- Around line 262-267: The code in handleExecutionCached currently early-returns
based on isJobForActiveWorkflow/activeJob and therefore skips updating the
global queuedJobs bookkeeping; update queuedJobs[prompt_id].nodes
unconditionally (before any isJobForActiveWorkflow or activeJob checks) so
queuedJobs[prompt_id].nodes is always mutated for each e.detail.nodes, then keep
the existing tab-filtered behavior only for updating activeJob.value (i.e., call
isJobForActiveWorkflow and update activeJob.value.nodes afterward). Apply the
same change to the corresponding executed handler (the block referenced at lines
278-282) so nodesExecuted/executionProgress are kept current regardless of the
active tab.
- Around line 337-362: Move the active-workflow guard so previews are only
revoked for the active tab: inside handleProgressState, keep the update to
nodeProgressStatesByJob and the evictOldProgressJobs call (so per-job progress
is always recorded), then check isJobForActiveWorkflow(jobId) and return early
if false; only after that perform the loop that calls
revokePreviewsByExecutionId(nodeId) (from useNodeOutputStore()) and any "current
view" updates. This ensures revokePreviewsByExecutionId, the preview-revoking
loop, and current-view progress updates run only for jobs that belong to the
active workflow while still preserving nodeProgressStatesByJob and
evictOldProgressJobs behavior.
🪄 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: 1ead92af-bc9b-4a6b-895c-98f3ef4c6eb2
📒 Files selected for processing (4)
browser_tests/tests/tabProgressIsolation.spec.tssrc/scripts/app.tssrc/stores/executionStore.test.tssrc/stores/executionStore.ts
…aking Add isJobForActiveWorkflow() to executionStore that checks whether an incoming WS message's prompt_id belongs to the currently active workflow tab. Gate all visual-state handlers (executed, execution_cached, progress, progress_state, b_preview_with_metadata) so outputs, previews, and progress indicators only apply to the workflow the user is viewing. The fix leverages the existing jobIdToSessionWorkflowPath map (populated at queue time by storeJob()) and workflowStore.activeWorkflow.path to determine whether a job belongs to the active tab. Lifecycle events (execution_start, execution_success, execution_error, execution_interrupted) are NOT filtered — they manage global state (activeJobId, queue status) that needs to update regardless of which tab is active. Graceful degradation: returns true (permissive) when prompt_id is missing, job is unknown, or no active workflow — so legacy messages and jobs from before this session continue to work. Closes #3377 Closes #2660 Closes #1293 Closes #3856 Closes #10688
9d30ac8 to
4f505dc
Compare
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@browser_tests/tests/tabProgressIsolation.spec.ts`:
- Around line 30-41: The test's page.evaluate callback silently returns when the
test bridge is missing (__pinia or __executionStore), causing false positives;
update the code so it fails fast by asserting the bridge and throwing if
missing: inside the evaluate callback (used in comfyPage.page.evaluate) check
for (window as any).__pinia and (window as any).__executionStore and throw a
descriptive error if absent, or better add a helper method on comfyPage (e.g.,
comfyPage.assertBridgeInstalled) that performs this check and call it before
running ensureSessionWorkflowPath; reference the functions/objects __pinia,
__executionStore, ensureSessionWorkflowPath and comfyPage to locate where to add
the assertion/throw.
In `@src/stores/executionStore.ts`:
- Around line 354-363: The current-view progress map nodeProgressStates is only
updated when a matching progress_state arrives, so switching workflow tabs can
display stale progress; add logic to sync or clear nodeProgressStates whenever
the active workflow changes: on workflowStore.activeWorkflow?.path change, look
up the latest entry in nodeProgressStatesByJob (using the same job-to-workflow
matching logic as isJobForActiveWorkflow) and set nodeProgressStates to that
job's nodes, or clear nodeProgressStates if no matching job exists; implement
this by adding a watcher/reactive effect tied to
workflowStore.activeWorkflow?.path that calls the existing
evictOldProgressJobs/isJobForActiveWorkflow helpers to find and hydrate/clear
nodeProgressStates.
- Around line 263-264: handleProgressText and related progress_text handlers
still gate on the global activeJobId/activeJob instead of using the
workflow-scoped guard; update handleProgressText to call
isJobForActiveWorkflow(e.detail.prompt_id) (or include that check alongside the
existing activeJob checks) before applying progress_text so previews from
background jobs can't appear in the wrong workflow when node IDs overlap, and
make the same change in the other progress handlers referenced (the other
handlers that currently check activeJobId/activeJob) to use
isJobForActiveWorkflow for consistent workflow-scoped filtering.
- Around line 638-646: isJobForActiveWorkflow currently returns true when
jobIdToSessionWorkflowPath lacks a mapping, allowing unknown jobs to bypass the
tab filter; change its logic so a missing mapping is treated as not active
(return false) instead of true. Additionally, create a pre-response association
for new prompts by setting jobIdToSessionWorkflowPath.value.set(promptId,
workflowStore.activeWorkflow?.path) before awaiting the API call that creates
the job (the place currently invoking storeJob after await
api.queuePrompt(...)), and ensure you remove or update that temporary mapping on
API error or when the true mapping arrives (handleExecutionStart/storeJob) so
the race no longer updates the wrong active workflow.
🪄 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: 7be627f4-fa18-4e0f-94ea-82c0c5c6ff7f
📒 Files selected for processing (4)
browser_tests/tests/tabProgressIsolation.spec.tssrc/scripts/app.tssrc/stores/executionStore.test.tssrc/stores/executionStore.ts
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (3)
src/stores/executionStore.ts (3)
626-647:⚠️ Potential issue | 🟠 Major
progress_textstill bypasses the new workflow-path guard.Line 529 still compares
prompt_idwithactiveJobIdinstead of usingisJobForActiveWorkflow(). After a tab switch mid-run, text previews can still render on same-ID nodes in the current workflow even though the other visual-state handlers are filtered.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/stores/executionStore.ts` around lines 626 - 647, The progress_text handler currently compares prompt_id to activeJobId and bypasses the workflow-path guard; update the handler to use the existing isJobForActiveWorkflow(prompt_id) helper instead of comparing to activeJobId so progress_text messages are filtered by workflow path like other visual-state handlers (locate the progress_text message handler and replace the prompt_id === activeJobId check with a call to isJobForActiveWorkflow(prompt_id)).
354-374:⚠️ Potential issue | 🟠 MajorSwitching tabs still leaves the previous workflow's progress snapshot live.
Lines 363 and 379 stop future writes from the wrong job, but they never clear or rehydrate
nodeProgressStates/_executingNodeProgresswhenworkflowStore.activeWorkflow?.pathchanges. After switching from workflow A to workflow B, the UI can keep showing A's last progress until B emits its own update.Also applies to: 377-379
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/stores/executionStore.ts` around lines 354 - 374, The current logic updates nodeProgressStatesByJob and only prevents future writes for non-active jobs via isJobForActiveWorkflow, but never clears or rehydrates nodeProgressStates/_executingNodeProgress when the active workflow tab changes; add logic so that when workflowStore.activeWorkflow?.path (or the active job id) changes you set nodeProgressStates to the saved snapshot for that new active job from nodeProgressStatesByJob (or to an empty object if none), and reset _executingNodeProgress to undefined if there is no executing node for the new job; implement this by watching the active workflow/job change (or adding a small rehydrate helper invoked when isJobForActiveWorkflow would flip) and call the existing evictOldProgressJobs as needed to keep state consistent.
638-646:⚠️ Potential issue | 🟠 MajorUnmapped jobs still leak through during the WebSocket-before-HTTP window.
Lines 254-259 already note that
execution_startcan beatstoreJob(), but Lines 642-643 returntruewhenjobIdToSessionWorkflowPathhas no entry. In that window, the firstprogress/executed/progress_statemessages for a newly queued job can still update whichever workflow is open. Pre-seed the mapping before awaitingqueuePrompt(), or stop treating an unmappedprompt_idas active onceexecution_starthas been seen.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/stores/executionStore.ts` around lines 638 - 646, The bug is that isJobForActiveWorkflow treats an unmapped promptId as active, allowing messages to leak during the WebSocket-before-HTTP window; fix by pre-seeding jobIdToSessionWorkflowPath for the promptId before awaiting queuePrompt() (or any async handoff) so the mapping exists when execution_start/progress events arrive: in the code that calls queuePrompt()/storeJob(), add jobIdToSessionWorkflowPath.value.set(promptId, workflowStore.activeWorkflow?.path) immediately when scheduling the job (and only remove/update it on finalization), or alternatively change isJobForActiveWorkflow to return false for unmapped promptIds once an execution_start marker for that prompt has been observed (use the existing execution_start tracking state) so unmapped ids are not treated as active. Ensure you reference jobIdToSessionWorkflowPath, isJobForActiveWorkflow, queuePrompt()/storeJob(), and workflowStore.activeWorkflow when making the change.
🤖 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/stores/executionStore.ts`:
- Around line 354-362: The revokePreviewsByExecutionId call is happening before
you check isJobForActiveWorkflow(jobId), causing previews for the current tab to
be revoked by background jobs; move the revokePreviewsByExecutionId(executionId)
call so it runs only after the isJobForActiveWorkflow(jobId) guard (i.e., inside
the same block that updates the "current view" progress) and keep the per-job
state update and evictOldProgressJobs() unchanged (update
nodeProgressStatesByJob, call evictOldProgressJobs(), then if
isJobForActiveWorkflow(jobId) { revokePreviewsByExecutionId(...); /* update
current view state as before */ }).
---
Duplicate comments:
In `@src/stores/executionStore.ts`:
- Around line 626-647: The progress_text handler currently compares prompt_id to
activeJobId and bypasses the workflow-path guard; update the handler to use the
existing isJobForActiveWorkflow(prompt_id) helper instead of comparing to
activeJobId so progress_text messages are filtered by workflow path like other
visual-state handlers (locate the progress_text message handler and replace the
prompt_id === activeJobId check with a call to
isJobForActiveWorkflow(prompt_id)).
- Around line 354-374: The current logic updates nodeProgressStatesByJob and
only prevents future writes for non-active jobs via isJobForActiveWorkflow, but
never clears or rehydrates nodeProgressStates/_executingNodeProgress when the
active workflow tab changes; add logic so that when
workflowStore.activeWorkflow?.path (or the active job id) changes you set
nodeProgressStates to the saved snapshot for that new active job from
nodeProgressStatesByJob (or to an empty object if none), and reset
_executingNodeProgress to undefined if there is no executing node for the new
job; implement this by watching the active workflow/job change (or adding a
small rehydrate helper invoked when isJobForActiveWorkflow would flip) and call
the existing evictOldProgressJobs as needed to keep state consistent.
- Around line 638-646: The bug is that isJobForActiveWorkflow treats an unmapped
promptId as active, allowing messages to leak during the WebSocket-before-HTTP
window; fix by pre-seeding jobIdToSessionWorkflowPath for the promptId before
awaiting queuePrompt() (or any async handoff) so the mapping exists when
execution_start/progress events arrive: in the code that calls
queuePrompt()/storeJob(), add jobIdToSessionWorkflowPath.value.set(promptId,
workflowStore.activeWorkflow?.path) immediately when scheduling the job (and
only remove/update it on finalization), or alternatively change
isJobForActiveWorkflow to return false for unmapped promptIds once an
execution_start marker for that prompt has been observed (use the existing
execution_start tracking state) so unmapped ids are not treated as active.
Ensure you reference jobIdToSessionWorkflowPath, isJobForActiveWorkflow,
queuePrompt()/storeJob(), and workflowStore.activeWorkflow when making the
change.
🪄 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: ce9605d2-f254-432a-ab12-3daca164f31a
📒 Files selected for processing (3)
src/scripts/app.tssrc/stores/executionStore.test.tssrc/stores/executionStore.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- src/scripts/app.ts
Moves revokePreviewsByExecutionId behind the tab filter so background jobs with overlapping node IDs cannot clear previews in the currently viewed workflow. Addresses review feedback: #10689 (comment)
Adds a watcher on workflowStore.activeWorkflow?.path that syncs the "current view" nodeProgressStates from nodeProgressStatesByJob when the user switches tabs, preventing stale progress from the previous workflow from being displayed. Addresses review feedback: #10689 (comment)
Adds tests for: - nodeProgressStates rehydration from nodeProgressStatesByJob on tab switch - nodeProgressStates clearing when switching to workflow with no jobs - Makes mockActiveWorkflow a Vue ref so watchers fire in tests Covers review feedback: #10689 (comment) #10689 (comment)
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/stores/executionStore.test.ts`:
- Around line 1003-1017: The test pins nodeProgressStates to workflow-A after
switching mockActiveWorkflow to workflow B; fix by making the assertion
consistent with the intended behavior: either (A) do not switch the active tab —
keep mockActiveWorkflow.value pointing to '/workflow-a' before firing
fireEvent('progress_state', ...) so
expect(store.nodeProgressStates['node-1']?.value).toBe(5) remains valid, or (B)
if you want to test the switch, await the rehydration/flush (e.g., await
nextTick()/flushPromises) after setting mockActiveWorkflow.value = { path:
'/workflow-b' }, then fireEvent('progress_state', { prompt_id: 'job-a', nodes:
nodesA2 }) and assert that nodeProgressStatesByJob (or
store.nodeProgressStatesByJob['job-a']) updated but store.nodeProgressStates for
'node-1' did not change (or is cleared) — update the expectation to reference
nodeProgressStatesByJob when verifying progress-by-job and nodeProgressStates
when verifying the active-workflow view.
🪄 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: d5f37985-f05f-4651-a939-23c9b10cf884
📒 Files selected for processing (2)
src/stores/executionStore.test.tssrc/stores/executionStore.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- src/stores/executionStore.ts
christian-byrne
left a comment
There was a problem hiding this comment.
There is issue that if we are maintaining a mapping from job id -> workflow name, then we need a way to persist that in session-specific memory so that it survives across reload / tab suspension / etc. Otherwise the mapping won't exist and we wont know what workflow the message should be applied to. If we default to showing when we don't know, then cross-window/browsertab will still leak. If we try to re-build the mapping from the /jobs response that is shared across all clients, it becomes a bit of a perf bottleneck and also still has error cases when the job is not returned yet in the /jobs response but messages are already being sent.
…inter Extracts a reusable SessionTabMap utility that persists a Map<string, string> to sessionStorage scoped per browser tab (keyed by window.name clientId). Handles clientId rotation on WS reconnect via prefix-scan migration. Replaces the in-memory-only jobIdToSessionWorkflowPath with a persisted version so the job→workflow mapping survives page reloads, preventing cross-tab progress leaking after refresh. Addresses review feedback: #10689 (comment)
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 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/stores/executionStore.ts`:
- Around line 633-650: When switching active workflow in the watcher (the
function watching workflowStore.activeWorkflow?.path) you must also clear or
update _executingNodeProgress to avoid showing stale progress from the previous
job; locate the same watcher that currently sets nodeProgressStates.value using
nodeProgressStatesByJob.value and jobIdToSessionWorkflowPath.value and ensure
_executingNodeProgress is either set to the matching job's executing progress
object or reset to an empty/default state whenever nodeProgressStates.value is
reset (including the early return when !newPath and the fallback that sets
nodeProgressStates.value = {}).
In `@src/utils/sessionTabMap.ts`:
- Around line 10-13: Guard the maxEntries parameter in createSessionTabMap to
prevent negative values causing an infinite eviction loop: validate or normalize
maxEntries (e.g., coerce to an integer and clamp to a minimum of 0 or throw for
negative inputs) before entering the eviction logic, and update the eviction
condition to ensure it only attempts deletions while map.size > maxEntries and
map.size > 0 (so deleting undefined cannot loop forever); reference
createSessionTabMap, the maxEntries parameter, the eviction loop that deletes
the oldest key (using map.keys().next().value), and the SessionTabMap return to
locate and fix the code.
- Around line 61-73: The migrate function currently removes the legacy
sessionStorage key before safely parsing and persisting its contents; change
migrate so it first reads and JSON.parse()s the value into a Map (inside a
try/catch), only call sessionStorage.removeItem(existingKey) after parsing
succeeds and after persist(prefix, migrated) completes, and if parsing throws
just continue (do not remove the legacy key); keep the function signature and
return behavior (return migrated Map on success, continue loop on failure) and
reference the symbols migrate, existingKey, persist, and sessionStorage when
making the change.
🪄 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: dc186fc4-5303-4446-907b-0205bb5cf130
📒 Files selected for processing (3)
src/stores/executionStore.tssrc/utils/sessionTabMap.test.tssrc/utils/sessionTabMap.ts
There was a problem hiding this comment.
🧹 Nitpick comments (1)
src/utils/sessionTabMap.test.ts (1)
151-165: Minor: test title is slightly imprecise.The title says "on each set call" but the no-op test (lines 28-35) demonstrates that identical key/value pairs do not produce a new reference. Consider a more precise title.
✏️ Suggested title refinement
describe('reactivity', () => { - it('produces a new Map reference on each set call', () => { + it('produces a new Map reference on each effective set', () => { const { map, set } = createSessionTabMap(PREFIX)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/utils/sessionTabMap.test.ts` around lines 151 - 165, Test title is imprecise; update the description string of the test in the describe('reactivity') block for createSessionTabMap so it reflects that a new Map reference is produced only when the set call actually changes data (e.g., "produces a new Map reference when set changes data" or "produces a new Map reference on mutating set calls"), and keep references to map.value and set('key','value') unchanged so the test still asserts that identical key/value no-ops do not produce a new reference.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@src/utils/sessionTabMap.test.ts`:
- Around line 151-165: Test title is imprecise; update the description string of
the test in the describe('reactivity') block for createSessionTabMap so it
reflects that a new Map reference is produced only when the set call actually
changes data (e.g., "produces a new Map reference when set changes data" or
"produces a new Map reference on mutating set calls"), and keep references to
map.value and set('key','value') unchanged so the test still asserts that
identical key/value no-ops do not produce a new reference.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 0d15d993-0138-4009-9146-1817e8e89d51
📒 Files selected for processing (1)
src/utils/sessionTabMap.test.ts
Addresses review feedback: #10689 (comment)
Addresses review feedback: #10689 (comment)
Addresses review feedback: #10689 (comment)
Codecov Report❌ Patch coverage is
@@ Coverage Diff @@
## main #10689 +/- ##
===========================================
- Coverage 64.80% 45.56% -19.24%
===========================================
Files 1459 1352 -107
Lines 84898 69593 -15305
Branches 23628 19345 -4283
===========================================
- Hits 55015 31709 -23306
- Misses 28902 37286 +8384
+ Partials 981 598 -383
Flags with carried forward coverage won't be shown. Click here to find out more.
... and 1029 files with indirect coverage changes 🚀 New features to boost your workflow:
|
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Superseded by the simpler Backend
Frontend consumer
Closing in favor of routing by |
Closes the remaining cross-tab leak surface that #10689 also targeted: - handleExecutionCached / handleExecuted (executionStore): write per-job via prompt_id instead of via activeJob, so a job from another workflow tab can no longer mark canvas nodes on the active job by mistake. - 'executed' listener (app.ts): always persist outputs into the job-scoped node-output store; gate node.onExecuted (canvas-visible side effect) behind messageMatchesActiveWorkflow so node IDs from another workflow cannot fire onExecuted on the wrong canvas node. - 'b_preview_with_metadata' listener (app.ts): always store the preview URL in per-job state so the originating tab can show it on return; gate revokePreviewsByExecutionId / setNodePreviewsByExecutionId behind messageMatchesActiveWorkflow so previews don't render on the wrong canvas. api.ts: forward optional workflow_id from binary preview metadata. - executionStore: expose messageMatchesActiveWorkflow on the store surface for app.ts.
Closes the remaining cross-tab leak surface that #10689 also targeted: - handleExecutionCached / handleExecuted (executionStore): write per-job via prompt_id instead of via activeJob, so a job from another workflow tab can no longer mark canvas nodes on the active job by mistake. - 'executed' listener (app.ts): always persist outputs into the job-scoped node-output store; gate node.onExecuted (canvas-visible side effect) behind messageMatchesActiveWorkflow so node IDs from another workflow cannot fire onExecuted on the wrong canvas node. - 'b_preview_with_metadata' listener (app.ts): always store the preview URL in per-job state so the originating tab can show it on return; gate revokePreviewsByExecutionId / setNodePreviewsByExecutionId behind messageMatchesActiveWorkflow so previews don't render on the wrong canvas. api.ts: forward optional workflow_id from binary preview metadata. - executionStore: expose messageMatchesActiveWorkflow on the store surface for app.ts.
Summary
When multiple workflow tabs are open and executing simultaneously, WebSocket messages (node outputs, previews, progress indicators) from one workflow leak into the other tab, causing incorrect visual state — wrong node highlights, mismatched previews, and corrupted progress bars.
This PR adds an
isJobForActiveWorkflow(promptId)helper toexecutionStorethat checks whether a job'sprompt_idmaps to the currently active workflow tab (via the existingjobIdToSessionWorkflowPathmap). Visual-state WS handlers are gated behind this check so they only apply to the workflow the user is looking at.Filtered handlers (visual state only)
handleExecuted— node output displayhandleExecutionCached— cached node markershandleProgress— per-node step progresshandleProgressState— only thenodeProgressStates"current view" map (the per-jobnodeProgressStatesByJobmap is always updated)handleProgressText— already had filteringexecutedlistener inapp.ts— node output store updatesb_preview_with_metadatalistener inapp.ts— preview image displayNOT filtered (lifecycle events — manage global state)
execution_start— setsactiveJobId, manages queueexecution_success/execution_error/execution_interrupted— clear active job, update queue statusGraceful degradation
isJobForActiveWorkflowreturnstrue(permissive) when:promptIdis null/undefined (legacy messages without prompt_id)promptIdis not in the session map (jobs from before this session or another browser tab)This ensures backward compatibility — legacy messages and pre-session jobs are never incorrectly suppressed.
Issues
Closes #3377
Closes #2660
Closes #1293
Closes #3856
Closes #10688
Testing
executionStore.test.ts(38 existing + 16 new)isJobForActiveWorkflow— null/undefined/unknown/no-active-workflow/match/mismatchbrowser_tests/tests/tabProgressIsolation.spec.ts┆Issue is synchronized with this Notion page by Unito