[backport core/1.49] feat(first-run-tour): read tour roles off an unpinned graph - #14664
Conversation
> **Stack — 6 of 6.** Rebuilt from scratch after the review on #13725, split into short PRs. > Based on #13290 (coachmark engine); each PR stacks on the one before it. > > 1. #14140 — engine: canvas targets for the coachmark tour > 2. #14141 — Getting Started screen + entry gating > 3. #14142 — role pins + live drift guard > 4. #14143 — the tour itself + e2e walk > 5. #14144 — post-tour nudge > 6. #14145 — unpinned workflows (share / template links) ← this PR ## Summary Pins cover the eight templates the grid can show. This covers everything else — a shared workflow, or a template URL nobody curated — by reading the roles off the graph itself. Simon asked that any-workflow generality land as its own stacked PR with adversarial tests rather than riding along untested; this is that PR. ## Changes - `heuristicRoles` finds the source, prompt and sink on an unpinned graph. Pins still win where they exist; anything else falls through to here, so the heuristics are the default path rather than dead code. - Prompt selection is ranked, not matched: labels are normalised for case and separators, wiring to a `positive` input outranks a label saying so, and `negative` / `system` disqualify. A tie returns nothing rather than guessing. - `?share=` and `?template=` links offer the tour once their workflow is actually on canvas. ## Review focus The tests are adversarial by design — negative-before-positive ports, a node carrying both boxes, disagreeing sinks, bypassed nodes, ties, a 50-node graph — all built on real `LGraph` instances rather than copied templates. That was the condition for bringing heuristics back at all. --------- Co-authored-by: pythongosssss <125205205+pythongosssss@users.noreply.github.com> Co-authored-by: GitHub Action <action@github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: AustinMroz <austin@comfy.org> Co-authored-by: Maanil Verma <maanil@comfy.org> Co-authored-by: bymyself <cbyrne@comfy.org>
📄 Knowledge reviewDosu skipped reviewing this PR because your organization has used its |
🎭 Playwright: ✅ 1786 passed, 0 failed · 2 flaky📊 Browser Reports
🎨 Storybook: ✅ Built — View Storybook📦 Bundle Size
⚡ Performance Report
Show regressions
All metrics
Historical variance (last 15 runs)
Trend (last 15 commits on main)
Raw data{
"timestamp": "2026-08-04T05:12:52.185Z",
"gitSha": "185d5b7fde434f5309c493337d679bbd286409ef",
"branch": "backport-14145-to-core-1.49",
"measurements": [
{
"name": "canvas-idle",
"durationMs": 2019.7450000000003,
"styleRecalcs": 10,
"styleRecalcDurationMs": 9.22,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 470.39099999999996,
"heapDeltaBytes": 5347368,
"heapUsedBytes": 73619596,
"domNodes": 20,
"jsHeapTotalBytes": 19779584,
"scriptDurationMs": 15.903,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "canvas-idle",
"durationMs": 2057.47800000006,
"styleRecalcs": 11,
"styleRecalcDurationMs": 9.410000000000002,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 485.20699999999994,
"heapDeltaBytes": 5207872,
"heapUsedBytes": 73297616,
"domNodes": 22,
"jsHeapTotalBytes": 19779584,
"scriptDurationMs": 17.406000000000002,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "canvas-mouse-sweep",
"durationMs": 2081.591000000003,
"styleRecalcs": 76,
"styleRecalcDurationMs": 44.047000000000004,
"layouts": 12,
"layoutDurationMs": 3.913,
"taskDurationMs": 1072.6200000000001,
"heapDeltaBytes": -17784080,
"heapUsedBytes": 50381200,
"domNodes": -281,
"jsHeapTotalBytes": 20955136,
"scriptDurationMs": 140.25400000000002,
"eventListeners": -151,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "canvas-mouse-sweep",
"durationMs": 1834.3569999999545,
"styleRecalcs": 75,
"styleRecalcDurationMs": 35.29900000000001,
"layouts": 12,
"layoutDurationMs": 3.666,
"taskDurationMs": 888.5079999999999,
"heapDeltaBytes": -21109884,
"heapUsedBytes": 47121980,
"domNodes": -282,
"jsHeapTotalBytes": 20430848,
"scriptDurationMs": 125.33,
"eventListeners": -153,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "canvas-zoom-sweep",
"durationMs": 1731.3739999999598,
"styleRecalcs": 32,
"styleRecalcDurationMs": 18.629,
"layouts": 6,
"layoutDurationMs": 0.662,
"taskDurationMs": 382.656,
"heapDeltaBytes": 8059532,
"heapUsedBytes": 76403524,
"domNodes": 78,
"jsHeapTotalBytes": 19779584,
"scriptDurationMs": 19.318,
"eventListeners": 19,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "canvas-zoom-sweep",
"durationMs": 1755.5739999999105,
"styleRecalcs": 32,
"styleRecalcDurationMs": 18.201999999999998,
"layouts": 6,
"layoutDurationMs": 0.6910000000000001,
"taskDurationMs": 378.71700000000004,
"heapDeltaBytes": 7878968,
"heapUsedBytes": 75934368,
"domNodes": 78,
"jsHeapTotalBytes": 20828160,
"scriptDurationMs": 19.041000000000004,
"eventListeners": 19,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "dom-widget-clipping",
"durationMs": 620.7300000000373,
"styleRecalcs": 11,
"styleRecalcDurationMs": 7.953,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 402.70500000000004,
"heapDeltaBytes": -12055392,
"heapUsedBytes": 56276104,
"domNodes": 18,
"jsHeapTotalBytes": 21352448,
"scriptDurationMs": 59.065999999999995,
"eventListeners": 2,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "dom-widget-clipping",
"durationMs": 600.1400000000103,
"styleRecalcs": 11,
"styleRecalcDurationMs": 7.4860000000000015,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 385.874,
"heapDeltaBytes": -12286844,
"heapUsedBytes": 55976944,
"domNodes": 18,
"jsHeapTotalBytes": 21090304,
"scriptDurationMs": 61.137,
"eventListeners": 2,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "large-graph-idle",
"durationMs": 2023.181999999963,
"styleRecalcs": 9,
"styleRecalcDurationMs": 7.5729999999999995,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 659.168,
"heapDeltaBytes": 5039548,
"heapUsedBytes": 64184832,
"domNodes": -281,
"jsHeapTotalBytes": 4775936,
"scriptDurationMs": 102.04199999999999,
"eventListeners": -179,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.670000000000012,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "large-graph-idle",
"durationMs": 2041.0570000000234,
"styleRecalcs": 9,
"styleRecalcDurationMs": 8.948,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 678.083,
"heapDeltaBytes": 7712864,
"heapUsedBytes": 66720080,
"domNodes": -282,
"jsHeapTotalBytes": 4513792,
"scriptDurationMs": 106.524,
"eventListeners": -179,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "large-graph-pan",
"durationMs": 2144.3160000000034,
"styleRecalcs": 68,
"styleRecalcDurationMs": 13.519999999999998,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 1272.125,
"heapDeltaBytes": 5668036,
"heapUsedBytes": 66031336,
"domNodes": -281,
"jsHeapTotalBytes": 5525504,
"scriptDurationMs": 426.194,
"eventListeners": -147,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "large-graph-pan",
"durationMs": 2262.047999999936,
"styleRecalcs": 70,
"styleRecalcDurationMs": 14.48,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 1298.67,
"heapDeltaBytes": -4954504,
"heapUsedBytes": 55265004,
"domNodes": -283,
"jsHeapTotalBytes": 4251648,
"scriptDurationMs": 439.50600000000003,
"eventListeners": -181,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "large-graph-zoom",
"durationMs": 3212.742999999989,
"styleRecalcs": 64,
"styleRecalcDurationMs": 13.219999999999999,
"layouts": 60,
"layoutDurationMs": 7.3069999999999995,
"taskDurationMs": 1538.224,
"heapDeltaBytes": -745848,
"heapUsedBytes": 61355280,
"domNodes": -289,
"jsHeapTotalBytes": 8183808,
"scriptDurationMs": 553.981,
"eventListeners": -183,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "large-graph-zoom",
"durationMs": 3246.010999999953,
"styleRecalcs": 63,
"styleRecalcDurationMs": 12.795999999999998,
"layouts": 60,
"layoutDurationMs": 7.263,
"taskDurationMs": 1530.0810000000001,
"heapDeltaBytes": 509592,
"heapUsedBytes": 62698652,
"domNodes": -292,
"jsHeapTotalBytes": 6610944,
"scriptDurationMs": 539.0319999999999,
"eventListeners": -183,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "minimap-idle",
"durationMs": 2027.0790000000147,
"styleRecalcs": 9,
"styleRecalcDurationMs": 8.046000000000001,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 660.051,
"heapDeltaBytes": 8872036,
"heapUsedBytes": 69591452,
"domNodes": -282,
"jsHeapTotalBytes": 3989504,
"scriptDurationMs": 101.132,
"eventListeners": -149,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "minimap-idle",
"durationMs": 2026.8459999999777,
"styleRecalcs": 7,
"styleRecalcDurationMs": 7.721000000000002,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 680.5880000000001,
"heapDeltaBytes": 8457832,
"heapUsedBytes": 69580384,
"domNodes": -284,
"jsHeapTotalBytes": 3727360,
"scriptDurationMs": 107.20200000000001,
"eventListeners": -147,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "subgraph-dom-widget-clipping",
"durationMs": 603.1689999999799,
"styleRecalcs": 47,
"styleRecalcDurationMs": 10.781,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 403.0440000000001,
"heapDeltaBytes": -10995196,
"heapUsedBytes": 57314300,
"domNodes": 20,
"jsHeapTotalBytes": 21614592,
"scriptDurationMs": 120.42800000000001,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "subgraph-dom-widget-clipping",
"durationMs": 601.20100000006,
"styleRecalcs": 47,
"styleRecalcDurationMs": 10.783000000000001,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 399.966,
"heapDeltaBytes": -11188736,
"heapUsedBytes": 57199272,
"domNodes": 20,
"jsHeapTotalBytes": 21090304,
"scriptDurationMs": 117.54100000000001,
"eventListeners": 8,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "subgraph-idle",
"durationMs": 2009.352999999976,
"styleRecalcs": 10,
"styleRecalcDurationMs": 8.831,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 452.335,
"heapDeltaBytes": 4894424,
"heapUsedBytes": 73161104,
"domNodes": 20,
"jsHeapTotalBytes": 20303872,
"scriptDurationMs": 13.636000000000001,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "subgraph-idle",
"durationMs": 2018.4470000000374,
"styleRecalcs": 9,
"styleRecalcDurationMs": 7.15,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 453.62800000000004,
"heapDeltaBytes": 4944436,
"heapUsedBytes": 73141692,
"domNodes": 18,
"jsHeapTotalBytes": 20303872,
"scriptDurationMs": 13.139999999999999,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "subgraph-mouse-sweep",
"durationMs": 1697.9149999999663,
"styleRecalcs": 76,
"styleRecalcDurationMs": 36.089999999999996,
"layouts": 16,
"layoutDurationMs": 4.606,
"taskDurationMs": 789.9860000000001,
"heapDeltaBytes": -4506048,
"heapUsedBytes": 63901388,
"domNodes": 61,
"jsHeapTotalBytes": 20828160,
"scriptDurationMs": 92.143,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66999999999998,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "subgraph-mouse-sweep",
"durationMs": 1751.751000000013,
"styleRecalcs": 76,
"styleRecalcDurationMs": 39.461999999999996,
"layouts": 16,
"layoutDurationMs": 4.8149999999999995,
"taskDurationMs": 800.6560000000001,
"heapDeltaBytes": -4405884,
"heapUsedBytes": 63834752,
"domNodes": 63,
"jsHeapTotalBytes": 20828160,
"scriptDurationMs": 93.892,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "subgraph-transition-enter",
"durationMs": 1383.9100000000144,
"styleRecalcs": 19,
"styleRecalcDurationMs": 30.307000000000002,
"layouts": 15,
"layoutDurationMs": 12.368,
"taskDurationMs": 903.9970000000002,
"heapDeltaBytes": 6963188,
"heapUsedBytes": 98973136,
"domNodes": 13673,
"jsHeapTotalBytes": 14680064,
"scriptDurationMs": 32.969,
"eventListeners": 2375,
"totalBlockingTimeMs": 130,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "viewport-pan-sweep",
"durationMs": 8384.736999999972,
"styleRecalcs": 249,
"styleRecalcDurationMs": 35.535,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 4487.947,
"heapDeltaBytes": 11348072,
"heapUsedBytes": 70444316,
"domNodes": -281,
"jsHeapTotalBytes": 6279168,
"scriptDurationMs": 1386.377,
"eventListeners": -161,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333338,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "viewport-pan-sweep",
"durationMs": 8341.07199999994,
"styleRecalcs": 249,
"styleRecalcDurationMs": 35.507,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 4496.206000000001,
"heapDeltaBytes": 13145076,
"heapUsedBytes": 72109988,
"domNodes": -285,
"jsHeapTotalBytes": 6279168,
"scriptDurationMs": 1396.8559999999998,
"eventListeners": -131,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "vue-large-graph-idle",
"durationMs": 17032.96700000004,
"styleRecalcs": 0,
"styleRecalcDurationMs": 0,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 16999.395999999997,
"heapDeltaBytes": -61149000,
"heapUsedBytes": 165335528,
"domNodes": -8312,
"jsHeapTotalBytes": -3088384,
"scriptDurationMs": 583.24,
"eventListeners": -16387,
"totalBlockingTimeMs": 0,
"frameDurationMs": 17.77333333333336,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "vue-large-graph-idle",
"durationMs": 17103.926,
"styleRecalcs": 0,
"styleRecalcDurationMs": 0,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 17076.143000000004,
"heapDeltaBytes": -43768920,
"heapUsedBytes": 167588136,
"domNodes": -8312,
"jsHeapTotalBytes": -10166272,
"scriptDurationMs": 573.292,
"eventListeners": -16385,
"totalBlockingTimeMs": 0,
"frameDurationMs": 18.333333333333332,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "vue-large-graph-pan",
"durationMs": 20768.12799999999,
"styleRecalcs": 141,
"styleRecalcDurationMs": 16.244999999999955,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 20703.456000000002,
"heapDeltaBytes": -38315456,
"heapUsedBytes": 170093388,
"domNodes": -8312,
"jsHeapTotalBytes": -10760192,
"scriptDurationMs": 902.4239999999999,
"eventListeners": -16385,
"totalBlockingTimeMs": 162,
"frameDurationMs": 17.779999999999927,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "vue-large-graph-pan",
"durationMs": 20788.701999999943,
"styleRecalcs": 142,
"styleRecalcDurationMs": 16.898000000000025,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 20742.058,
"heapDeltaBytes": -30229320,
"heapUsedBytes": 178877756,
"domNodes": -8312,
"jsHeapTotalBytes": -9973760,
"scriptDurationMs": 878.9149999999998,
"eventListeners": -16383,
"totalBlockingTimeMs": 170,
"frameDurationMs": 17.776666666666763,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "workflow-execution",
"durationMs": 483.20799999999053,
"styleRecalcs": 15,
"styleRecalcDurationMs": 24.926999999999996,
"layouts": 3,
"layoutDurationMs": 1.2,
"taskDurationMs": 140.737,
"heapDeltaBytes": -15554508,
"heapUsedBytes": 51822600,
"domNodes": 134,
"jsHeapTotalBytes": 7720960,
"scriptDurationMs": 10.792000000000002,
"eventListeners": 99,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333335,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "workflow-execution",
"durationMs": 459.49499999994714,
"styleRecalcs": 12,
"styleRecalcDurationMs": 19.482,
"layouts": 3,
"layoutDurationMs": 0.714,
"taskDurationMs": 117.675,
"heapDeltaBytes": -15883148,
"heapUsedBytes": 51450880,
"domNodes": 123,
"jsHeapTotalBytes": 7196672,
"scriptDurationMs": 8.921999999999999,
"eventListeners": 97,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333335,
"p95FrameDurationMs": 16.700000000000728
}
]
} |
Codecov Report❌ Patch coverage is
@@ Coverage Diff @@
## core/1.49 #14664 +/- ##
=============================================
+ Coverage 78.58% 78.59% +0.01%
=============================================
Files 1788 1789 +1
Lines 104332 104420 +88
Branches 31984 32881 +897
=============================================
+ Hits 81994 82074 +80
- Misses 21908 21916 +8
Partials 430 430
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
christian-byrne
left a comment
There was a problem hiding this comment.
Approving — final backport of the first-run tour stack (#14145), reviewed and approved on main. CI green: 49 success.
This completes the feature on 1.49. It carries heuristicRoles.ts and its test, which are the last files the release branches were missing.
Its earlier auto-backport attempts failed twice, both times because the PRs beneath it were not yet on the branch — the bot cherry-picks each PR of a stack independently. Once #14144's backport landed, this one was produced cleanly on the first try. That pattern held at all four levels, so it is a property of the workflow rather than anything about this code.
Two things worth passing to whoever owns PR Backport:
- The manual
workflow_dispatchis broken — it fails validation with "PR not found or inaccessible" because itsgh pr viewcalls lack repo context. Already tracked in open PR #13699. - The working re-trigger is the
labeledevent — remove and re-addneeds-backporton the merged PR.
Findings on this PR remain tracked as #14623 and #14625, neither blocking, and both now have fixes in flight.
Backport of #14145 to
core/1.49Automatically created by backport workflow.