feat(widgets): rich combo widget for remote options with previews#11310
feat(widgets): rich combo widget for remote options with previews#11310
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
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:
📝 WalkthroughWalkthroughAdds a remote-backed RichComboWidget and supporting schemas, helpers, and tests: remote fetching with retries/backoff and Cache API partitioning, schema-driven item mapping and search, audio preview controls, and configurable dropdown UI flags and locales. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant RichCombo as RichComboWidget
participant Cache as Browser Cache
participant FetchUtil as fetchRemoteRoute
participant Remote as Remote API
participant Renderer as Dropdown Renderer
User->>RichCombo: mount / open / refresh
activate RichCombo
RichCombo->>Cache: check cached items (route+config+scope)
alt cache hit
Cache-->>RichCombo: return cached items
else cache miss
RichCombo->>FetchUtil: fetch page (params, timeout, signal)
FetchUtil->>Remote: HTTP GET (Comfy base + route, auth header)
Remote-->>FetchUtil: response
FetchUtil-->>RichCombo: items (maybe wrapped)
alt paginated
loop pages
RichCombo->>FetchUtil: fetch next page (signal)
FetchUtil->>Remote: HTTP GET
Remote-->>FetchUtil: page response
FetchUtil-->>RichCombo: append items
end
end
RichCombo->>Cache: store items (if enabled)
end
RichCombo->>RichCombo: mapToDropdownItem / buildSearchText
RichCombo->>Renderer: render dropdown, controls, audio previews
deactivate RichCombo
User->>Renderer: click play audio
Renderer->>Renderer: toggle audio element (play/pause)
Renderer-->>User: audio playback state
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Poem
🚥 Pre-merge checks | ✅ 6 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (6 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 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 |
🎨 Storybook: ✅ Built — View Storybook |
🎭 Playwright: ✅ 1465 passed, 0 failed · 1 flaky📊 Browser Reports
|
📦 Bundle: 5.26 MB gzip 🔴 +5 kBDetailsSummary
Category Glance App Entry Points — 22.6 kB (baseline 22.5 kB) • 🔴 +15 BMain entry bundles and manifests
Status: 1 added / 1 removed Graph Workspace — 1.24 MB (baseline 1.24 MB) • 🔴 +15 BGraph editor runtime, canvas, workflow orchestration
Status: 1 added / 1 removed Views & Navigation — 81.8 kB (baseline 81.8 kB) • ⚪ 0 BTop-level views, pages, and routed surfaces
Status: 9 added / 9 removed / 2 unchanged Panels & Settings — 489 kB (baseline 489 kB) • ⚪ 0 BConfiguration panels, inspectors, and settings screens
Status: 10 added / 10 removed / 11 unchanged User & Accounts — 17.5 kB (baseline 17.5 kB) • ⚪ 0 BAuthentication, profile, and account management bundles
Status: 5 added / 5 removed / 2 unchanged Editors & Dialogs — 112 kB (baseline 112 kB) • ⚪ 0 BModals, dialogs, drawers, and in-app editors
Status: 4 added / 4 removed UI Components — 57.8 kB (baseline 62.9 kB) • 🟢 -5.11 kBReusable component library chunks
Status: 5 added / 6 removed / 8 unchanged Data & Services — 3.05 MB (baseline 3.05 MB) • 🔴 +1.3 kBStores, services, APIs, and repositories
Status: 13 added / 13 removed / 4 unchanged Utilities & Hooks — 369 kB (baseline 364 kB) • 🔴 +5.12 kBHelpers, composables, and utility bundles
Status: 17 added / 16 removed / 14 unchanged Vendor & Third-Party — 9.94 MB (baseline 9.94 MB) • ⚪ 0 BExternal libraries and shared vendor chunks Status: 16 unchanged Other — 8.86 MB (baseline 8.84 MB) • 🔴 +17.2 kBBundles that do not match a named category
Status: 62 added / 62 removed / 73 unchanged ⚡ Performance Report
No regressions detected. All metrics
Historical variance (last 15 runs)
Trend (last 15 commits on main)
Raw data{
"timestamp": "2026-05-04T16:29:14.177Z",
"gitSha": "62d1c65cc91fc9fe495dbc9723ff408f48726ca6",
"branch": "feat/RemoteComboOptions",
"measurements": [
{
"name": "canvas-idle",
"durationMs": 2031.25399999999,
"styleRecalcs": 8,
"styleRecalcDurationMs": 7.248999999999999,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 359.75,
"heapDeltaBytes": 22756992,
"heapUsedBytes": 71202928,
"domNodes": 16,
"jsHeapTotalBytes": 14680064,
"scriptDurationMs": 18.970999999999997,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "canvas-idle",
"durationMs": 2024.1350000000011,
"styleRecalcs": 10,
"styleRecalcDurationMs": 9.27,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 361.815,
"heapDeltaBytes": 23288688,
"heapUsedBytes": 71465464,
"domNodes": 20,
"jsHeapTotalBytes": 14680064,
"scriptDurationMs": 23.31,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333335,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "canvas-idle",
"durationMs": 2027.5010000000293,
"styleRecalcs": 11,
"styleRecalcDurationMs": 11.346,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 363.556,
"heapDeltaBytes": 23446404,
"heapUsedBytes": 72424672,
"domNodes": 22,
"jsHeapTotalBytes": 14942208,
"scriptDurationMs": 23.948,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "canvas-mouse-sweep",
"durationMs": 1921.2170000000128,
"styleRecalcs": 73,
"styleRecalcDurationMs": 38.62200000000001,
"layouts": 12,
"layoutDurationMs": 3.686,
"taskDurationMs": 895.6080000000001,
"heapDeltaBytes": -889852,
"heapUsedBytes": 47688276,
"domNodes": -263,
"jsHeapTotalBytes": 15331328,
"scriptDurationMs": 138.142,
"eventListeners": -133,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333335,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "canvas-mouse-sweep",
"durationMs": 1821.066999999971,
"styleRecalcs": 72,
"styleRecalcDurationMs": 36.163000000000004,
"layouts": 12,
"layoutDurationMs": 3.4869999999999997,
"taskDurationMs": 799.6959999999999,
"heapDeltaBytes": 5204984,
"heapUsedBytes": 53892008,
"domNodes": -265,
"jsHeapTotalBytes": 16642048,
"scriptDurationMs": 129.62300000000002,
"eventListeners": -131,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66999999999998,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "canvas-mouse-sweep",
"durationMs": 1834.6050000000105,
"styleRecalcs": 72,
"styleRecalcDurationMs": 39.720000000000006,
"layouts": 12,
"layoutDurationMs": 3.6420000000000003,
"taskDurationMs": 803.16,
"heapDeltaBytes": 3075804,
"heapUsedBytes": 51401024,
"domNodes": -265,
"jsHeapTotalBytes": 15331328,
"scriptDurationMs": 131.27700000000002,
"eventListeners": -133,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "canvas-zoom-sweep",
"durationMs": 1718.0630000000292,
"styleRecalcs": 30,
"styleRecalcDurationMs": 17.444000000000003,
"layouts": 6,
"layoutDurationMs": 0.5890000000000001,
"taskDurationMs": 291.21799999999996,
"heapDeltaBytes": 101816,
"heapUsedBytes": 48504528,
"domNodes": 76,
"jsHeapTotalBytes": 14417920,
"scriptDurationMs": 18.302000000000003,
"eventListeners": 19,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "canvas-zoom-sweep",
"durationMs": 1733.981999999969,
"styleRecalcs": 32,
"styleRecalcDurationMs": 18.535000000000004,
"layouts": 6,
"layoutDurationMs": 0.6449999999999999,
"taskDurationMs": 299.635,
"heapDeltaBytes": 210812,
"heapUsedBytes": 48767140,
"domNodes": 76,
"jsHeapTotalBytes": 15466496,
"scriptDurationMs": 20.304000000000002,
"eventListeners": 19,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "canvas-zoom-sweep",
"durationMs": 1717.2759999999698,
"styleRecalcs": 30,
"styleRecalcDurationMs": 16.928,
"layouts": 6,
"layoutDurationMs": 0.7030000000000001,
"taskDurationMs": 292.189,
"heapDeltaBytes": 181404,
"heapUsedBytes": 48565152,
"domNodes": 75,
"jsHeapTotalBytes": 14680064,
"scriptDurationMs": 19.823999999999998,
"eventListeners": 19,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "dom-widget-clipping",
"durationMs": 598.1560000000172,
"styleRecalcs": 10,
"styleRecalcDurationMs": 7.536000000000001,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 354.3310000000001,
"heapDeltaBytes": 8425468,
"heapUsedBytes": 57167764,
"domNodes": 16,
"jsHeapTotalBytes": 16252928,
"scriptDurationMs": 60.295,
"eventListeners": 2,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "dom-widget-clipping",
"durationMs": 519.9399999999628,
"styleRecalcs": 11,
"styleRecalcDurationMs": 7.3500000000000005,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 320.098,
"heapDeltaBytes": 9336480,
"heapUsedBytes": 57724088,
"domNodes": 17,
"jsHeapTotalBytes": 15466496,
"scriptDurationMs": 55.821999999999996,
"eventListeners": 0,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.799999999999727
},
{
"name": "dom-widget-clipping",
"durationMs": 571.1459999999988,
"styleRecalcs": 11,
"styleRecalcDurationMs": 8.501999999999999,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 346.5590000000001,
"heapDeltaBytes": 8850064,
"heapUsedBytes": 57213316,
"domNodes": 18,
"jsHeapTotalBytes": 15466496,
"scriptDurationMs": 59.620000000000005,
"eventListeners": 0,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.669999999999998,
"p95FrameDurationMs": 16.700000000000273
},
{
"name": "large-graph-idle",
"durationMs": 2058.3750000000123,
"styleRecalcs": 9,
"styleRecalcDurationMs": 8.911999999999999,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 542.0210000000001,
"heapDeltaBytes": 7566804,
"heapUsedBytes": 66230976,
"domNodes": -262,
"jsHeapTotalBytes": -233472,
"scriptDurationMs": 97.29899999999999,
"eventListeners": -129,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66999999999998,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "large-graph-idle",
"durationMs": 2040.8210000000508,
"styleRecalcs": 8,
"styleRecalcDurationMs": 8.132,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 540.1819999999999,
"heapDeltaBytes": 11801564,
"heapUsedBytes": 70149436,
"domNodes": -265,
"jsHeapTotalBytes": -233472,
"scriptDurationMs": 96.299,
"eventListeners": -129,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "large-graph-idle",
"durationMs": 2020.7810000000563,
"styleRecalcs": 8,
"styleRecalcDurationMs": 7.945999999999998,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 512.806,
"heapDeltaBytes": 2905404,
"heapUsedBytes": 62386412,
"domNodes": -262,
"jsHeapTotalBytes": 4542464,
"scriptDurationMs": 87.43599999999999,
"eventListeners": -131,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "large-graph-pan",
"durationMs": 2135.5649999999855,
"styleRecalcs": 68,
"styleRecalcDurationMs": 18.435,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 1116.9220000000003,
"heapDeltaBytes": -5709112,
"heapUsedBytes": 53872684,
"domNodes": -266,
"jsHeapTotalBytes": 5271552,
"scriptDurationMs": 409.38599999999997,
"eventListeners": -127,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "large-graph-pan",
"durationMs": 2134.1559999999618,
"styleRecalcs": 67,
"styleRecalcDurationMs": 16.345,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 1090.4389999999999,
"heapDeltaBytes": 4636040,
"heapUsedBytes": 64209844,
"domNodes": -268,
"jsHeapTotalBytes": 1806336,
"scriptDurationMs": 395.59999999999997,
"eventListeners": -127,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "large-graph-pan",
"durationMs": 2109.8709999999983,
"styleRecalcs": 68,
"styleRecalcDurationMs": 17.432999999999996,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 1090.172,
"heapDeltaBytes": 22872280,
"heapUsedBytes": 82500280,
"domNodes": -266,
"jsHeapTotalBytes": 6000640,
"scriptDurationMs": 393.063,
"eventListeners": -127,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66999999999998,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "large-graph-zoom",
"durationMs": 3164.492999999993,
"styleRecalcs": 65,
"styleRecalcDurationMs": 17.965,
"layouts": 60,
"layoutDurationMs": 7.414000000000001,
"taskDurationMs": 1353.7310000000002,
"heapDeltaBytes": 9039548,
"heapUsedBytes": 69935012,
"domNodes": -271,
"jsHeapTotalBytes": 6057984,
"scriptDurationMs": 495.569,
"eventListeners": -127,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333335,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "large-graph-zoom",
"durationMs": 3131.330000000048,
"styleRecalcs": 65,
"styleRecalcDurationMs": 18.439,
"layouts": 60,
"layoutDurationMs": 7.199,
"taskDurationMs": 1313.795,
"heapDeltaBytes": -7469304,
"heapUsedBytes": 53424800,
"domNodes": -270,
"jsHeapTotalBytes": 5271552,
"scriptDurationMs": 491.80400000000003,
"eventListeners": -157,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "large-graph-zoom",
"durationMs": 3131.1549999999215,
"styleRecalcs": 65,
"styleRecalcDurationMs": 18.064,
"layouts": 60,
"layoutDurationMs": 7.412999999999999,
"taskDurationMs": 1316.098,
"heapDeltaBytes": 14756144,
"heapUsedBytes": 75519616,
"domNodes": -268,
"jsHeapTotalBytes": 290816,
"scriptDurationMs": 484.605,
"eventListeners": -125,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333335,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "minimap-idle",
"durationMs": 2020.7530000000133,
"styleRecalcs": 10,
"styleRecalcDurationMs": 10.651999999999997,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 550.8689999999999,
"heapDeltaBytes": 8727460,
"heapUsedBytes": 68730016,
"domNodes": -259,
"jsHeapTotalBytes": 3231744,
"scriptDurationMs": 97.25,
"eventListeners": -131,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "minimap-idle",
"durationMs": 2036.670000000072,
"styleRecalcs": 9,
"styleRecalcDurationMs": 8.991,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 545.529,
"heapDeltaBytes": -9436080,
"heapUsedBytes": 52120060,
"domNodes": -263,
"jsHeapTotalBytes": 4018176,
"scriptDurationMs": 97.937,
"eventListeners": -131,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "minimap-idle",
"durationMs": 2007.3840000000018,
"styleRecalcs": 10,
"styleRecalcDurationMs": 10.473000000000003,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 542.2249999999999,
"heapDeltaBytes": 4605952,
"heapUsedBytes": 65153016,
"domNodes": -264,
"jsHeapTotalBytes": 290816,
"scriptDurationMs": 95.515,
"eventListeners": -129,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333335,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "subgraph-dom-widget-clipping",
"durationMs": 574.292000000014,
"styleRecalcs": 46,
"styleRecalcDurationMs": 10.481,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 351.35499999999996,
"heapDeltaBytes": 9444688,
"heapUsedBytes": 58032300,
"domNodes": 18,
"jsHeapTotalBytes": 15466496,
"scriptDurationMs": 118.47799999999998,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "subgraph-dom-widget-clipping",
"durationMs": 544.9339999998983,
"styleRecalcs": 45,
"styleRecalcDurationMs": 8.802999999999999,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 344.74899999999997,
"heapDeltaBytes": 9213432,
"heapUsedBytes": 57510552,
"domNodes": 15,
"jsHeapTotalBytes": 15990784,
"scriptDurationMs": 116.242,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "subgraph-dom-widget-clipping",
"durationMs": 561.3390000000891,
"styleRecalcs": 45,
"styleRecalcDurationMs": 9.910000000000002,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 352.196,
"heapDeltaBytes": 9510024,
"heapUsedBytes": 59051836,
"domNodes": 16,
"jsHeapTotalBytes": 15728640,
"scriptDurationMs": 122.09899999999999,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.663333333333338,
"p95FrameDurationMs": 16.700000000000273
},
{
"name": "subgraph-idle",
"durationMs": 1999.7020000000134,
"styleRecalcs": 9,
"styleRecalcDurationMs": 8.068999999999999,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 338.255,
"heapDeltaBytes": 22516304,
"heapUsedBytes": 71293776,
"domNodes": 17,
"jsHeapTotalBytes": 14680064,
"scriptDurationMs": 14.773999999999996,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "subgraph-idle",
"durationMs": 1989.2569999999523,
"styleRecalcs": 9,
"styleRecalcDurationMs": 8.569,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 333.789,
"heapDeltaBytes": 23198700,
"heapUsedBytes": 71842248,
"domNodes": 18,
"jsHeapTotalBytes": 14942208,
"scriptDurationMs": 14.537999999999995,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "subgraph-idle",
"durationMs": 2000.3779999999551,
"styleRecalcs": 9,
"styleRecalcDurationMs": 8.808,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 326.428,
"heapDeltaBytes": 22524936,
"heapUsedBytes": 71338556,
"domNodes": 17,
"jsHeapTotalBytes": 14417920,
"scriptDurationMs": 14.07,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "subgraph-mouse-sweep",
"durationMs": 1692.7510000000439,
"styleRecalcs": 75,
"styleRecalcDurationMs": 36.55700000000001,
"layouts": 16,
"layoutDurationMs": 4.837,
"taskDurationMs": 664.183,
"heapDeltaBytes": 14847240,
"heapUsedBytes": 64351316,
"domNodes": 61,
"jsHeapTotalBytes": 15204352,
"scriptDurationMs": 92.222,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "subgraph-mouse-sweep",
"durationMs": 1687.8909999999223,
"styleRecalcs": 75,
"styleRecalcDurationMs": 36.26,
"layouts": 16,
"layoutDurationMs": 4.621,
"taskDurationMs": 650.3149999999999,
"heapDeltaBytes": 14383840,
"heapUsedBytes": 62997800,
"domNodes": 61,
"jsHeapTotalBytes": 15466496,
"scriptDurationMs": 92.399,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.670000000000012,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "subgraph-mouse-sweep",
"durationMs": 1690.1079999998956,
"styleRecalcs": 75,
"styleRecalcDurationMs": 36.426,
"layouts": 16,
"layoutDurationMs": 4.555,
"taskDurationMs": 714.39,
"heapDeltaBytes": -1702940,
"heapUsedBytes": 46929688,
"domNodes": -262,
"jsHeapTotalBytes": 14807040,
"scriptDurationMs": 94.80300000000001,
"eventListeners": -133,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "viewport-pan-sweep",
"durationMs": 8195.746999999983,
"styleRecalcs": 250,
"styleRecalcDurationMs": 51.633,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 3690.786,
"heapDeltaBytes": 19767980,
"heapUsedBytes": 79292484,
"domNodes": -262,
"jsHeapTotalBytes": 6291456,
"scriptDurationMs": 1236.068,
"eventListeners": -113,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "viewport-pan-sweep",
"durationMs": 8127.2840000000315,
"styleRecalcs": 249,
"styleRecalcDurationMs": 50.96200000000001,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 3665.6120000000005,
"heapDeltaBytes": 9213524,
"heapUsedBytes": 67623760,
"domNodes": -263,
"jsHeapTotalBytes": 6787072,
"scriptDurationMs": 1237.3159999999998,
"eventListeners": -113,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "viewport-pan-sweep",
"durationMs": 8171.502999999916,
"styleRecalcs": 249,
"styleRecalcDurationMs": 50.526,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 3632.9439999999995,
"heapDeltaBytes": 9518932,
"heapUsedBytes": 68041668,
"domNodes": -263,
"jsHeapTotalBytes": 7311360,
"scriptDurationMs": 1249.7019999999998,
"eventListeners": -113,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.80000000000109
},
{
"name": "vue-large-graph-idle",
"durationMs": 12316.667999999992,
"styleRecalcs": 0,
"styleRecalcDurationMs": 0,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 12275.838,
"heapDeltaBytes": -25283756,
"heapUsedBytes": 172113828,
"domNodes": -8331,
"jsHeapTotalBytes": 25489408,
"scriptDurationMs": 628.14,
"eventListeners": -16464,
"totalBlockingTimeMs": 0,
"frameDurationMs": 17.219999999999953,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "vue-large-graph-idle",
"durationMs": 12027.682000000026,
"styleRecalcs": 0,
"styleRecalcDurationMs": 0,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 12016.596,
"heapDeltaBytes": -25481940,
"heapUsedBytes": 172487076,
"domNodes": -8331,
"jsHeapTotalBytes": 26275840,
"scriptDurationMs": 586.253,
"eventListeners": -16464,
"totalBlockingTimeMs": 0,
"frameDurationMs": 17.780000000000047,
"p95FrameDurationMs": 16.80000000000291
},
{
"name": "vue-large-graph-idle",
"durationMs": 12361.976000000028,
"styleRecalcs": 0,
"styleRecalcDurationMs": 0,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 12325.838000000002,
"heapDeltaBytes": -25507556,
"heapUsedBytes": 171835644,
"domNodes": -8331,
"jsHeapTotalBytes": 23654400,
"scriptDurationMs": 615.917,
"eventListeners": -16464,
"totalBlockingTimeMs": 0,
"frameDurationMs": 17.219999999999953,
"p95FrameDurationMs": 16.80000000000291
},
{
"name": "vue-large-graph-pan",
"durationMs": 14297.843,
"styleRecalcs": 65,
"styleRecalcDurationMs": 16.139999999999986,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 14271.356999999998,
"heapDeltaBytes": -18784700,
"heapUsedBytes": 190706268,
"domNodes": -8331,
"jsHeapTotalBytes": 23568384,
"scriptDurationMs": 893.347,
"eventListeners": -16460,
"totalBlockingTimeMs": 0,
"frameDurationMs": 17.223333333333237,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "vue-large-graph-pan",
"durationMs": 14159.182999999985,
"styleRecalcs": 67,
"styleRecalcDurationMs": 16.345,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 14135.386,
"heapDeltaBytes": -51344960,
"heapUsedBytes": 153773392,
"domNodes": -8331,
"jsHeapTotalBytes": -3608576,
"scriptDurationMs": 857.65,
"eventListeners": -16490,
"totalBlockingTimeMs": 0,
"frameDurationMs": 17.219999999999953,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "vue-large-graph-pan",
"durationMs": 14141.977999999996,
"styleRecalcs": 65,
"styleRecalcDurationMs": 16.315999999999995,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 14108.292000000003,
"heapDeltaBytes": -25427364,
"heapUsedBytes": 176913768,
"domNodes": -8331,
"jsHeapTotalBytes": 23568384,
"scriptDurationMs": 898.5340000000001,
"eventListeners": -16462,
"totalBlockingTimeMs": 0,
"frameDurationMs": 17.223333333333358,
"p95FrameDurationMs": 16.80000000000291
},
{
"name": "workflow-execution",
"durationMs": 467.88500000002387,
"styleRecalcs": 22,
"styleRecalcDurationMs": 25.822,
"layouts": 5,
"layoutDurationMs": 1.7329999999999999,
"taskDurationMs": 123.46100000000001,
"heapDeltaBytes": 5312608,
"heapUsedBytes": 55219972,
"domNodes": 192,
"jsHeapTotalBytes": 0,
"scriptDurationMs": 20.582000000000004,
"eventListeners": 69,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.663333333333338,
"p95FrameDurationMs": 16.700000000000273
},
{
"name": "workflow-execution",
"durationMs": 138.14500000000862,
"styleRecalcs": 11,
"styleRecalcDurationMs": 21.185000000000002,
"layouts": 4,
"layoutDurationMs": 1.52,
"taskDurationMs": 100.67499999999998,
"heapDeltaBytes": 3488108,
"heapUsedBytes": 53614464,
"domNodes": 144,
"jsHeapTotalBytes": 0,
"scriptDurationMs": 24.65,
"eventListeners": 37,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "workflow-execution",
"durationMs": 448.7410000000409,
"styleRecalcs": 17,
"styleRecalcDurationMs": 21.197999999999997,
"layouts": 5,
"layoutDurationMs": 1.2009999999999998,
"taskDurationMs": 112.69100000000002,
"heapDeltaBytes": 5247128,
"heapUsedBytes": 62159624,
"domNodes": 157,
"jsHeapTotalBytes": 3145728,
"scriptDurationMs": 23.108000000000004,
"eventListeners": 69,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000273
}
]
} |
Codecov Report❌ Patch coverage is
@@ Coverage Diff @@
## main #11310 +/- ##
===========================================
- Coverage 71.73% 56.16% -15.58%
===========================================
Files 1492 1387 -105
Lines 83893 71082 -12811
Branches 22192 19814 -2378
===========================================
- Hits 60181 39922 -20259
- Misses 22856 30633 +7777
+ Partials 856 527 -329
Flags with carried forward coverage won't be shown. Click here to find out more.
... and 985 files with indirect coverage changes 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
src/renderer/extensions/vueNodes/widgets/components/form/dropdown/FormDropdownMenuItem.vue (1)
3-10: Prefer$tin template whentis only used there.Since
tis only used in the template (for aria-label translations), you can use the built-in$tin the template instead of importinguseI18nand destructuringtin the script.♻️ Suggested simplification
<script setup lang="ts"> -import { computed, inject, ref } from 'vue' -import { useI18n } from 'vue-i18n' +import { computed, inject, ref } from 'vue' import { cn } from '@/utils/tailwindUtil' import { AssetKindKey } from './types' import type { LayoutMode } from './types' -const { t } = useI18n() -Then in template:
:aria-label=" isPlayingAudio - ? t('widgets.remoteCombo.pauseAudioPreview') - : t('widgets.remoteCombo.playAudioPreview') + ? $t('widgets.remoteCombo.pauseAudioPreview') + : $t('widgets.remoteCombo.playAudioPreview') "Based on learnings: "In Vue single-file components where the i18n t function is only used within the template, prefer using the built-in $t in the template instead of importing useI18n and destructuring t in the script."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/renderer/extensions/vueNodes/widgets/components/form/dropdown/FormDropdownMenuItem.vue` around lines 3 - 10, The component imports useI18n and destructures t at module scope (const { t } = useI18n()) even though t is only used in the template; remove the import and the const { t } = useI18n() declaration and update the template to use the built-in $t directly (keep existing aria-label usages but call $t('...') in the template) so you no longer reference useI18n or t in the script section.src/renderer/extensions/vueNodes/widgets/components/RichComboWidget.vue (1)
32-43: Consider documenting the synthetic URL scheme.The cache key uses a synthetic URL (
https://cache.comfy.invalid/) that isn't a real endpoint. While functional, a brief inline note explaining this is a deliberate cache-key-only URL would help future maintainers understand the pattern.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/renderer/extensions/vueNodes/widgets/components/RichComboWidget.vue` around lines 32 - 43, Add a short inline comment inside the buildCacheKey function explaining that the returned URL (https://cache.comfy.invalid/) is a synthetic, non-routable URL used solely to construct a deterministic cache key rather than to call a real endpoint; place the comment immediately above the return statement in buildCacheKey so future maintainers understand the deliberate URL choice and its role in cache key generation.
🤖 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/schemas/nodeDefSchema.ts`:
- Around line 37-47: zRemoteComboConfig can currently accept an absolute route
while use_comfy_api is true, which leads to malformed downstream URLs; add a
cross-field validation on zRemoteComboConfig (using .superRefine or .refine)
that checks if use_comfy_api === true and the route is an absolute URL (e.g.,
starts with "http://" or "https://"), then add an error via ctx.addIssue
explaining the invalid combination; update the validation on the route check
accordingly so the superRefine can reliably detect absolute URLs and reject the
pair (refer to zRemoteComboConfig, route, and use_comfy_api).
---
Nitpick comments:
In
`@src/renderer/extensions/vueNodes/widgets/components/form/dropdown/FormDropdownMenuItem.vue`:
- Around line 3-10: The component imports useI18n and destructures t at module
scope (const { t } = useI18n()) even though t is only used in the template;
remove the import and the const { t } = useI18n() declaration and update the
template to use the built-in $t directly (keep existing aria-label usages but
call $t('...') in the template) so you no longer reference useI18n or t in the
script section.
In `@src/renderer/extensions/vueNodes/widgets/components/RichComboWidget.vue`:
- Around line 32-43: Add a short inline comment inside the buildCacheKey
function explaining that the returned URL (https://cache.comfy.invalid/) is a
synthetic, non-routable URL used solely to construct a deterministic cache key
rather than to call a real endpoint; place the comment immediately above the
return statement in buildCacheKey so future maintainers understand the
deliberate URL choice and its role in cache key generation.
🪄 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: b725cc33-3f60-45df-948d-8ffc3f86338e
📒 Files selected for processing (12)
src/locales/en/main.jsonsrc/renderer/extensions/vueNodes/widgets/components/RichComboWidget.vuesrc/renderer/extensions/vueNodes/widgets/components/WidgetSelect.vuesrc/renderer/extensions/vueNodes/widgets/components/form/dropdown/FormDropdown.vuesrc/renderer/extensions/vueNodes/widgets/components/form/dropdown/FormDropdownMenu.vuesrc/renderer/extensions/vueNodes/widgets/components/form/dropdown/FormDropdownMenuActions.vuesrc/renderer/extensions/vueNodes/widgets/components/form/dropdown/FormDropdownMenuItem.vuesrc/renderer/extensions/vueNodes/widgets/components/form/dropdown/types.tssrc/renderer/extensions/vueNodes/widgets/utils/fetchRemoteRoute.tssrc/renderer/extensions/vueNodes/widgets/utils/itemSchemaUtils.test.tssrc/renderer/extensions/vueNodes/widgets/utils/itemSchemaUtils.tssrc/schemas/nodeDefSchema.ts
0e30e28 to
48e5a6f
Compare
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (2)
src/schemas/nodeDefSchema.validation.test.ts (1)
76-83: Tighten helper typing and use a function declarationLine 76 uses
object+unknown, which weakens compile-time checks in a schema validation test. Use a typed function declaration withsatisfies ComfyNodeDeffor better type safety and consistency with coding guidelines.Suggested refactor
- const buildNodeDef = (remoteCombo: object): unknown => ({ - ...EXAMPLE_NODE_DEF, - input: { - required: { - voice: ['COMBO', { remote_combo: remoteCombo }] - } - } - }) + function buildNodeDef( + remoteCombo: Record<string, unknown> + ): ComfyNodeDef { + return { + ...EXAMPLE_NODE_DEF, + input: { + required: { + voice: ['COMBO', { remote_combo: remoteCombo }] + } + } + } satisfies ComfyNodeDef + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/schemas/nodeDefSchema.validation.test.ts` around lines 76 - 83, Replace the loose arrow helper buildNodeDef with a typed function declaration that enforces ComfyNodeDef: change buildNodeDef to a function named buildNodeDef(remoteCombo: unknown | SpecificType) that returns a value that satisfies ComfyNodeDef (use the TypeScript "satisfies ComfyNodeDef" pattern on the returned object) and tighten the parameter type instead of using plain object and unknown; keep the same shape that spreads EXAMPLE_NODE_DEF and sets input.required.voice to ['COMBO', { remote_combo: remoteCombo }] so tests remain unchanged but gain compile-time type safety.src/renderer/extensions/vueNodes/widgets/utils/fetchRemoteRoute.ts (1)
12-15: The schema validation already prevents absolute URLs withuse_comfy_api=true.The
zRemoteComboConfig.superRefine()explicitly rejectsuse_comfy_api=truepaired with absolute routes, requiring relative paths when using the Comfy API. The original concern about malformed URLs is already guarded by the schema validation.That said, using
new URL(route, getComfyApiBaseUrl()).toString()would be a reasonable defensive improvement for robustness and clarity, handling edge cases like base URL trailing slashes uniformly:Optional hardening
function resolveRoute(route: string, useComfyApi?: boolean): string { if (useComfyApi) { - return getComfyApiBaseUrl() + route + return new URL(route, getComfyApiBaseUrl()).toString() } return route }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/renderer/extensions/vueNodes/widgets/utils/fetchRemoteRoute.ts` around lines 12 - 15, The schema already prevents absolute routes when useComfyApi is true, but to harden resolveRoute implement URL resolution using the base API URL: in function resolveRoute(route: string, useComfyApi?: boolean) call new URL(route, getComfyApiBaseUrl()).toString() when useComfyApi is true so trailing slashes and concatenation edge-cases are handled consistently; otherwise return the original route unchanged.
🤖 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/renderer/extensions/vueNodes/widgets/components/RichComboWidget.vue`:
- Around line 250-296: TerminalSuccess is only used to decide whether to set
cache and to mark success, but the code currently only sets error.value when
rawItems.value.length === 0 which hides failures that occur after some pages
loaded; update the end-of-fetch logic in the async fetch loop (where
terminalSuccess, rawItems, setCache, loading, loadingMore, and error are
handled) to set an error state whenever terminalSuccess is false (even if
rawItems has items) — e.g., set error.value =
t('widgets.remoteCombo.loadFailedPartial') or reuse the existing message to
indicate a partial load, ensure loading.value and loadingMore.value are still
cleared and that caching only happens when terminalSuccess is true, and keep the
existing abort checks (controller.signal.aborted) intact so partial results are
shown but the UI is visibly in an error state.
- Around line 157-173: Replace raw payload/error logs in RichComboWidget.vue: do
not log res.data or the full err object; instead log only safe metadata (e.g.,
config.route, config.response_key, HTTP status/code and maybe
t('widgets.remoteCombo.loadFailed') context). Update the block that checks
fetchedItems (remove received: res.data from console.error) and the catch block
(replace console.error('RichComboWidget: fetch error', err) with a console.error
that includes only config.route, config.response_key, and the HTTP status or a
sanitized error message). Apply the same change to the other occurrences
mentioned (around where setCache, rawItems, applyAutoSelect are used) so no auth
headers or response payloads are ever logged.
- Around line 32-42: buildCacheKey currently only uses useAuthStore().userId to
scope comfy-api cache entries, which misses other auth contexts (workspace
headers or API keys) so cached options can bleed between auths; update
buildCacheKey to include a non-secret auth-scope identifier from the auth store
(e.g., useAuthStore().authScope or a new getter like
useAuthStore().getAuthScope()) when config.use_comfy_api is true, and append
that value to the URLSearchParams (do not include secrets or full headers);
alternatively, if adding a scope identifier isn't available, add logic to
clear/expire comfy-api cache entries on auth context changes by detecting
changes via useAuthStore().getAuthHeader() and invalidating prior keys.
---
Nitpick comments:
In `@src/renderer/extensions/vueNodes/widgets/utils/fetchRemoteRoute.ts`:
- Around line 12-15: The schema already prevents absolute routes when
useComfyApi is true, but to harden resolveRoute implement URL resolution using
the base API URL: in function resolveRoute(route: string, useComfyApi?: boolean)
call new URL(route, getComfyApiBaseUrl()).toString() when useComfyApi is true so
trailing slashes and concatenation edge-cases are handled consistently;
otherwise return the original route unchanged.
In `@src/schemas/nodeDefSchema.validation.test.ts`:
- Around line 76-83: Replace the loose arrow helper buildNodeDef with a typed
function declaration that enforces ComfyNodeDef: change buildNodeDef to a
function named buildNodeDef(remoteCombo: unknown | SpecificType) that returns a
value that satisfies ComfyNodeDef (use the TypeScript "satisfies ComfyNodeDef"
pattern on the returned object) and tighten the parameter type instead of using
plain object and unknown; keep the same shape that spreads EXAMPLE_NODE_DEF and
sets input.required.voice to ['COMBO', { remote_combo: remoteCombo }] so tests
remain unchanged but gain compile-time type safety.
🪄 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: 03eed29a-9e0c-4c36-9ccc-a0635de13baf
📒 Files selected for processing (13)
src/locales/en/main.jsonsrc/renderer/extensions/vueNodes/widgets/components/RichComboWidget.vuesrc/renderer/extensions/vueNodes/widgets/components/WidgetSelect.vuesrc/renderer/extensions/vueNodes/widgets/components/form/dropdown/FormDropdown.vuesrc/renderer/extensions/vueNodes/widgets/components/form/dropdown/FormDropdownMenu.vuesrc/renderer/extensions/vueNodes/widgets/components/form/dropdown/FormDropdownMenuActions.vuesrc/renderer/extensions/vueNodes/widgets/components/form/dropdown/FormDropdownMenuItem.vuesrc/renderer/extensions/vueNodes/widgets/components/form/dropdown/types.tssrc/renderer/extensions/vueNodes/widgets/utils/fetchRemoteRoute.tssrc/renderer/extensions/vueNodes/widgets/utils/itemSchemaUtils.test.tssrc/renderer/extensions/vueNodes/widgets/utils/itemSchemaUtils.tssrc/schemas/nodeDefSchema.tssrc/schemas/nodeDefSchema.validation.test.ts
✅ Files skipped from review due to trivial changes (1)
- src/renderer/extensions/vueNodes/widgets/utils/itemSchemaUtils.test.ts
🚧 Files skipped from review as they are similar to previous changes (6)
- src/renderer/extensions/vueNodes/widgets/components/form/dropdown/types.ts
- src/locales/en/main.json
- src/renderer/extensions/vueNodes/widgets/components/form/dropdown/FormDropdownMenu.vue
- src/renderer/extensions/vueNodes/widgets/components/form/dropdown/FormDropdownMenuActions.vue
- src/schemas/nodeDefSchema.ts
- src/renderer/extensions/vueNodes/widgets/utils/itemSchemaUtils.ts
| if (!hasMore || pageItems.length === 0) { | ||
| terminalSuccess = true | ||
| break | ||
| } | ||
| page++ | ||
| } catch (err: unknown) { | ||
| if (controller.signal.aborted) return | ||
|
|
||
| if (!isRetriableError(err)) { | ||
| console.error( | ||
| `RichComboWidget: non-retriable error on page ${page}`, | ||
| err | ||
| ) | ||
| break | ||
| } | ||
| consecutiveErrors++ | ||
| if (consecutiveErrors >= maxRetries) { | ||
| console.error( | ||
| `RichComboWidget: giving up after ${maxRetries} consecutive errors on page ${page}`, | ||
| err | ||
| ) | ||
| break | ||
| } | ||
| // Retry same page after backoff | ||
| const delay = getBackoff(consecutiveErrors) | ||
| await new Promise((resolve) => setTimeout(resolve, delay)) | ||
| } | ||
| } | ||
|
|
||
| if (controller.signal.aborted) return | ||
|
|
||
| // Cache the accumulated result before releasing loading state; an abort | ||
| // during setCache then skips the state reset instead of flickering it. | ||
| // Only cache on terminal success — caching partial results would poison the | ||
| // next mount with an incomplete list and never re-fetch the missing pages. | ||
| if (terminalSuccess && rawItems.value.length > 0) { | ||
| await setCache(config, rawItems.value) | ||
| } | ||
|
|
||
| if (controller.signal.aborted) return | ||
|
|
||
| loading.value = false | ||
| loadingMore.value = false | ||
|
|
||
| if (!terminalSuccess && rawItems.value.length === 0) { | ||
| error.value = t('widgets.remoteCombo.loadFailed') | ||
| } |
There was a problem hiding this comment.
Don't silently leave the user on a truncated paginated result.
If a later page fails after earlier pages loaded, terminalSuccess stays false but Line 294 only sets error when rawItems is empty. The dropdown then looks complete even though some pages never arrived. Keep the partial items if you want, but still surface a failure state so users know the list is incomplete and should be refreshed.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/renderer/extensions/vueNodes/widgets/components/RichComboWidget.vue`
around lines 250 - 296, TerminalSuccess is only used to decide whether to set
cache and to mark success, but the code currently only sets error.value when
rawItems.value.length === 0 which hides failures that occur after some pages
loaded; update the end-of-fetch logic in the async fetch loop (where
terminalSuccess, rawItems, setCache, loading, loadingMore, and error are
handled) to set an error state whenever terminalSuccess is false (even if
rawItems has items) — e.g., set error.value =
t('widgets.remoteCombo.loadFailedPartial') or reuse the existing message to
indicate a partial load, ensure loading.value and loadingMore.value are still
cleared and that caching only happens when terminalSuccess is true, and keep the
existing abort checks (controller.signal.aborted) intact so partial results are
shown but the UI is visibly in an error state.
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (1)
src/renderer/extensions/vueNodes/widgets/components/RichComboWidget.vue (1)
297-312:⚠️ Potential issue | 🟠 MajorSurface paginated failures even when earlier pages loaded.
Lines 310-312 only set
errorwhenrawItemsis empty. If a later page fails after earlier pages succeeded, the dropdown looks complete even though the result set is truncated.Suggested fix
- if (!terminalSuccess && rawItems.value.length === 0) { + if (!terminalSuccess) { error.value = t('widgets.remoteCombo.loadFailed') }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/renderer/extensions/vueNodes/widgets/components/RichComboWidget.vue` around lines 297 - 312, The code only sets error.value when no items were loaded, so partial results hide paginated failures; update the post-fetch logic in the block using terminalSuccess, rawItems, setCache, controller.signal.aborted, loading, loadingMore and error so that after resetting loading/loadingMore (and after the controller.signal.aborted check) you set error.value = t('widgets.remoteCombo.loadFailed') whenever terminalSuccess is false (regardless of rawItems.length) — this surfaces later-page failures while still preserving the existing behavior of only calling setCache(config, rawItems.value) on terminalSuccess.
🤖 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/renderer/extensions/vueNodes/widgets/components/RichComboWidget.vue`:
- Around line 414-419: handleRefresh currently calls clearCache(config) then
immediately starts fetchItems(true), which can race with the cache delete; make
handleRefresh await the cache eviction to ensure clearCache(config) completes
before starting the replacement fetch. Specifically, change handleRefresh to be
async (or return the clearCache promise) and await clearCache(config) (using
await clearCache(config)) before calling fetchItems(true); keep the
abortController?.abort() and error.value = null behavior intact and only await
when config is non-null to avoid changing semantics when there's no config.
- Around line 402-435: The watch block that clears selectedSet then only re-adds
when an item is found discards a stale modelValue id; instead when modelValue
(modelValue.value) exists but items.value.find(...) returns undefined, keep or
re-add that id into selectedSet so the UI still shows the stored id; also update
the placeholder computed to, before falling back to the generic upload
placeholder, check if modelValue.value exists and no matching item is present
and return that stale id (or a localized “stale id” string containing
modelValue.value) so the user can see which remote id is stored; refer to the
watch([...], ...) block, selectedSet, modelValue, items, and placeholder
computed to implement these changes.
---
Duplicate comments:
In `@src/renderer/extensions/vueNodes/widgets/components/RichComboWidget.vue`:
- Around line 297-312: The code only sets error.value when no items were loaded,
so partial results hide paginated failures; update the post-fetch logic in the
block using terminalSuccess, rawItems, setCache, controller.signal.aborted,
loading, loadingMore and error so that after resetting loading/loadingMore (and
after the controller.signal.aborted check) you set error.value =
t('widgets.remoteCombo.loadFailed') whenever terminalSuccess is false
(regardless of rawItems.length) — this surfaces later-page failures while still
preserving the existing behavior of only calling setCache(config,
rawItems.value) on terminalSuccess.
🪄 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: 94903f56-5aa5-4fef-a47e-e0ad12a0c718
📒 Files selected for processing (4)
src/renderer/extensions/vueNodes/widgets/components/RichComboWidget.vuesrc/renderer/extensions/vueNodes/widgets/utils/fetchRemoteRoute.test.tssrc/renderer/extensions/vueNodes/widgets/utils/richComboHelpers.test.tssrc/renderer/extensions/vueNodes/widgets/utils/richComboHelpers.ts
| watch( | ||
| [modelValue, items], | ||
| ([val]) => { | ||
| selectedSet.value.clear() | ||
| if (val) { | ||
| const item = items.value.find((i) => i.id === val) | ||
| if (item) selectedSet.value.add(item.id) | ||
| } | ||
| }, | ||
| { immediate: true } | ||
| ) | ||
|
|
||
| function handleRefresh() { | ||
| abortController?.abort() | ||
| error.value = null | ||
| const config = remoteConfig.value | ||
| if (config) void clearCache(config) | ||
| void fetchItems(true) | ||
| } | ||
|
|
||
| function handleSelection(selected: Set<string>) { | ||
| modelValue.value = selected.values().next().value | ||
| } | ||
|
|
||
| const placeholder = computed(() => { | ||
| if (loading.value) return t('widgets.remoteCombo.loading') | ||
| if (error.value) return error.value | ||
| if (loadingMore.value) { | ||
| return t('widgets.remoteCombo.itemsLoaded', { | ||
| count: items.value.length | ||
| }) | ||
| } | ||
| return t('widgets.uploadSelect.placeholder') | ||
| }) |
There was a problem hiding this comment.
Preserve the stale remote id in the placeholder.
When modelValue contains an unknown id, Line 405 clears the visible selection, and Lines 434-435 fall back to the generic placeholder. That breaks the PR’s stale-id contract: the saved value survives, but the user can no longer see which stale id is currently stored.
Suggested fix
+const staleSelectionLabel = computed(() => {
+ const value = modelValue.value
+ if (!value) return null
+ return items.value.some((item) => item.id === value) ? null : value
+})
+
const placeholder = computed(() => {
if (loading.value) return t('widgets.remoteCombo.loading')
if (error.value) return error.value
if (loadingMore.value) {
return t('widgets.remoteCombo.itemsLoaded', {
count: items.value.length
})
}
+ if (staleSelectionLabel.value) return staleSelectionLabel.value
return t('widgets.uploadSelect.placeholder')
})🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/renderer/extensions/vueNodes/widgets/components/RichComboWidget.vue`
around lines 402 - 435, The watch block that clears selectedSet then only
re-adds when an item is found discards a stale modelValue id; instead when
modelValue (modelValue.value) exists but items.value.find(...) returns
undefined, keep or re-add that id into selectedSet so the UI still shows the
stored id; also update the placeholder computed to, before falling back to the
generic upload placeholder, check if modelValue.value exists and no matching
item is present and return that stale id (or a localized “stale id” string
containing modelValue.value) so the user can see which remote id is stored;
refer to the watch([...], ...) block, selectedSet, modelValue, items, and
placeholder computed to implement these changes.
There was a problem hiding this comment.
🧹 Nitpick comments (2)
src/renderer/extensions/vueNodes/widgets/components/RichComboWidget.test.ts (2)
49-50: Assert translated messages (ort(...)) instead of raw i18n keys.With empty
messages(Line 49), these assertions only prove key echoing, not that translations exist. Prefer loadingen/main.jsonor asserting withi18n.global.t(...).Proposed fix
+import en from '@/locales/en/main.json' - -const i18n = createI18n({ legacy: false, locale: 'en', messages: { en: {} } }) +const i18n = createI18n({ legacy: false, locale: 'en', messages: { en } }) ... - expect(screen.getByTestId('placeholder').textContent).toBe( - 'widgets.remoteCombo.loadFailed' - ) + expect(screen.getByTestId('placeholder').textContent).toBe( + i18n.global.t('widgets.remoteCombo.loadFailed') + ) ... - expect(screen.getByTestId('placeholder').textContent).toBe( - 'widgets.uploadSelect.placeholder' - ) + expect(screen.getByTestId('placeholder').textContent).toBe( + i18n.global.t('widgets.uploadSelect.placeholder') + )As per coding guidelines: "Use
vue-i18nfor ALL user-facing strings, configured insrc/locales/en/main.json."Also applies to: 194-196, 210-212, 286-288
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/renderer/extensions/vueNodes/widgets/components/RichComboWidget.test.ts` around lines 49 - 50, The test is currently initializing createI18n with an empty messages object, so assertions only echo keys; load the real English messages and assert translated strings (or use i18n.global.t(...)) instead. Replace the createI18n call that sets messages: {} by importing the English bundle (e.g. the exported object from src/locales/en/main.json) and pass it into createI18n, then update assertions that compare to raw keys to instead call i18n.global.t('your.key') or compare against the actual translated string from the imported messages; apply the same change for the other failing assertions referenced (around the other line groups).
68-84: Test stub is intentionally minimal; consider whether accessible query migration is necessary here.The suggestion to use
getByRole/text-based queries is technically valid—buttons can be queried by name, and span text bygetByText. However, this is a minimal test stub designed to verify RichComboWidget's prop passing and event emission, not FormDropdown's rendering. The stub'sdata-testidattributes keep the focus on widget logic without coupling to accessibility structure. The tests already use accessible queries where they matter (e.g.,getByTextfor item visibility at lines 183–184,getByLabelTextat line 248). If deeper FormDropdown interaction testing is added, migrating this stub to semantic queries would be more justified.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/renderer/extensions/vueNodes/widgets/components/RichComboWidget.test.ts` around lines 68 - 84, The test stub in RichComboWidget.test.ts intentionally uses data-testid selectors for the FormDropdown stub (data-testid="dropdown", "placeholder", "items-count", "item-{id}", "deselect") to keep focus on RichComboWidget's prop-passing and event emission rather than accessibility rendering; add a brief inline comment in the test next to the stub explaining this intent and why migration to getByRole/getByText isn't done here (but note that accessible queries should be used if deeper FormDropdown interaction tests are added), so reviewers understand the deliberate choice without changing the test behavior.
🤖 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/renderer/extensions/vueNodes/widgets/components/RichComboWidget.test.ts`:
- Around line 49-50: The test is currently initializing createI18n with an empty
messages object, so assertions only echo keys; load the real English messages
and assert translated strings (or use i18n.global.t(...)) instead. Replace the
createI18n call that sets messages: {} by importing the English bundle (e.g. the
exported object from src/locales/en/main.json) and pass it into createI18n, then
update assertions that compare to raw keys to instead call
i18n.global.t('your.key') or compare against the actual translated string from
the imported messages; apply the same change for the other failing assertions
referenced (around the other line groups).
- Around line 68-84: The test stub in RichComboWidget.test.ts intentionally uses
data-testid selectors for the FormDropdown stub (data-testid="dropdown",
"placeholder", "items-count", "item-{id}", "deselect") to keep focus on
RichComboWidget's prop-passing and event emission rather than accessibility
rendering; add a brief inline comment in the test next to the stub explaining
this intent and why migration to getByRole/getByText isn't done here (but note
that accessible queries should be used if deeper FormDropdown interaction tests
are added), so reviewers understand the deliberate choice without changing the
test behavior.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 58f90ae3-f24c-4ff4-b6e7-b0cdc20a4457
📒 Files selected for processing (2)
src/renderer/extensions/vueNodes/widgets/components/RichComboWidget.test.tssrc/renderer/extensions/vueNodes/widgets/components/RichComboWidget.vue
🚧 Files skipped from review as they are similar to previous changes (1)
- src/renderer/extensions/vueNodes/widgets/components/RichComboWidget.vue
9e3bb8a to
99ead1c
Compare
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (1)
src/renderer/extensions/vueNodes/widgets/utils/itemSchemaUtils.test.ts (1)
96-183: Add a regression test for missing configured optional fields.
mapToDropdownItemcurrently yields''(notundefined) whendescription_fieldorpreview_url_fieldis configured but missing in data. A focused test would lock this edge behavior and prevent regressions.Suggested test addition
describe('mapToDropdownItem', () => { + it('uses empty strings when configured optional fields are missing', () => { + const item = mapToDropdownItem( + { id: 'v1', label: 'Roger' }, + { + value_field: 'id', + label_field: 'label', + description_field: 'desc', + preview_url_field: 'sample', + preview_type: 'audio' + } + ) + + expect(item.description).toBe('') + expect(item.preview_url).toBe('') + })🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/renderer/extensions/vueNodes/widgets/utils/itemSchemaUtils.test.ts` around lines 96 - 183, Add a regression test to ensure mapToDropdownItem returns undefined (not empty string) for optional configured fields when they are missing in the source data: create a new test in itemSchemaUtils.test.ts that calls mapToDropdownItem with description_field and preview_url_field set in the config but omitted from the item object, then assert item.description === undefined and item.preview_url === undefined (reference the mapToDropdownItem function in your test). This locks the edge-case behavior and prevents future regressions.
🤖 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/renderer/extensions/vueNodes/widgets/components/form/dropdown/FormDropdownMenuItem.vue`:
- Around line 43-49: The empty catch in toggleAudioPreview swallows play errors;
update toggleAudioPreview (which uses audioRef) to handle rejections by logging
the error (e.g., console.error or the component logger) and surfacing a
user-friendly notification or emitting an event (e.g.,
this.$emit('audioPreviewError', error) or calling the existing
toast/notification helper) so failures are visible and actionable instead of
being silently ignored.
In `@src/renderer/extensions/vueNodes/widgets/utils/richComboHelpers.test.ts`:
- Around line 69-82: The test suite for getBackoff is missing an assertion for
attempt 0 which can allow an off-by-one regression; update the
describe('getBackoff') tests (the block containing the it('grows exponentially
from 1s')) to include expect(getBackoff(0)).toBe(1000) and adjust the test
description if desired so the baseline is explicit, ensuring the exponential
checks for getBackoff(1..4) and the cap tests for higher attempts remain
unchanged.
- Around line 27-67: Tests for buildCacheKey currently assert route,
responseKey, and auth-scope but miss asserting partitioning by page_size and
use_comfy_api; add test cases that call buildCacheKey (and parseKey) with
different page_size values and with use_comfy_api true vs false (using
baseConfig as the base), then assert the resulting keys are distinct (Set size
=== expected) and that parseKey(...).get('page_size') and
parseKey(...).get('use_comfy_api') reflect the values used so future changes
don’t merge those cache buckets accidentally.
---
Nitpick comments:
In `@src/renderer/extensions/vueNodes/widgets/utils/itemSchemaUtils.test.ts`:
- Around line 96-183: Add a regression test to ensure mapToDropdownItem returns
undefined (not empty string) for optional configured fields when they are
missing in the source data: create a new test in itemSchemaUtils.test.ts that
calls mapToDropdownItem with description_field and preview_url_field set in the
config but omitted from the item object, then assert item.description ===
undefined and item.preview_url === undefined (reference the mapToDropdownItem
function in your test). This locks the edge-case behavior and prevents future
regressions.
🪄 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: 5520af06-eb4a-4fb1-b4b1-ec1a31687746
📒 Files selected for processing (17)
src/locales/en/main.jsonsrc/renderer/extensions/vueNodes/widgets/components/RichComboWidget.test.tssrc/renderer/extensions/vueNodes/widgets/components/RichComboWidget.vuesrc/renderer/extensions/vueNodes/widgets/components/WidgetSelect.vuesrc/renderer/extensions/vueNodes/widgets/components/form/dropdown/FormDropdown.vuesrc/renderer/extensions/vueNodes/widgets/components/form/dropdown/FormDropdownMenu.vuesrc/renderer/extensions/vueNodes/widgets/components/form/dropdown/FormDropdownMenuActions.vuesrc/renderer/extensions/vueNodes/widgets/components/form/dropdown/FormDropdownMenuItem.vuesrc/renderer/extensions/vueNodes/widgets/components/form/dropdown/types.tssrc/renderer/extensions/vueNodes/widgets/utils/fetchRemoteRoute.test.tssrc/renderer/extensions/vueNodes/widgets/utils/fetchRemoteRoute.tssrc/renderer/extensions/vueNodes/widgets/utils/itemSchemaUtils.test.tssrc/renderer/extensions/vueNodes/widgets/utils/itemSchemaUtils.tssrc/renderer/extensions/vueNodes/widgets/utils/richComboHelpers.test.tssrc/renderer/extensions/vueNodes/widgets/utils/richComboHelpers.tssrc/schemas/nodeDefSchema.tssrc/schemas/nodeDefSchema.validation.test.ts
✅ Files skipped from review due to trivial changes (4)
- src/renderer/extensions/vueNodes/widgets/components/form/dropdown/types.ts
- src/locales/en/main.json
- src/renderer/extensions/vueNodes/widgets/utils/itemSchemaUtils.ts
- src/renderer/extensions/vueNodes/widgets/utils/richComboHelpers.ts
🚧 Files skipped from review as they are similar to previous changes (7)
- src/renderer/extensions/vueNodes/widgets/components/form/dropdown/FormDropdown.vue
- src/schemas/nodeDefSchema.validation.test.ts
- src/renderer/extensions/vueNodes/widgets/components/form/dropdown/FormDropdownMenuActions.vue
- src/schemas/nodeDefSchema.ts
- src/renderer/extensions/vueNodes/widgets/components/RichComboWidget.test.ts
- src/renderer/extensions/vueNodes/widgets/utils/fetchRemoteRoute.ts
- src/renderer/extensions/vueNodes/widgets/components/RichComboWidget.vue
| function toggleAudioPreview(event: Event) { | ||
| event.stopPropagation() | ||
| const audio = audioRef.value | ||
| if (!audio) return | ||
| if (audio.paused) { | ||
| void audio.play().catch(() => {}) | ||
| } else { |
There was a problem hiding this comment.
Handle audio play failures instead of swallowing them.
The empty catch in audio.play().catch(() => {}) suppresses failures with no propagation path.
Proposed fix
const emit = defineEmits<{
click: [index: number]
mediaLoad: [event: Event]
+ mediaError: [error: Error]
}>()
@@
function toggleAudioPreview(event: Event) {
event.stopPropagation()
const audio = audioRef.value
if (!audio) return
if (audio.paused) {
- void audio.play().catch(() => {})
+ void audio.play().catch((error: unknown) => {
+ isPlayingAudio.value = false
+ emit(
+ 'mediaError',
+ error instanceof Error
+ ? error
+ : new Error('Audio preview failed to play')
+ )
+ })
} else {
audio.pause()
}
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@src/renderer/extensions/vueNodes/widgets/components/form/dropdown/FormDropdownMenuItem.vue`
around lines 43 - 49, The empty catch in toggleAudioPreview swallows play
errors; update toggleAudioPreview (which uses audioRef) to handle rejections by
logging the error (e.g., console.error or the component logger) and surfacing a
user-friendly notification or emitting an event (e.g.,
this.$emit('audioPreviewError', error) or calling the existing
toast/notification helper) so failures are visible and actionable instead of
being silently ignored.
| describe('buildCacheKey', () => { | ||
| it('encodes the route and response_key', () => { | ||
| const params = parseKey( | ||
| buildCacheKey( | ||
| { | ||
| ...baseConfig, | ||
| route: '/voices', | ||
| response_key: 'data.items' | ||
| }, | ||
| 'fb:user-a' | ||
| ) | ||
| ) | ||
| expect(params.get('route')).toBe('/voices') | ||
| expect(params.get('responseKey')).toBe('data.items') | ||
| }) | ||
|
|
||
| it('partitions by authScope', () => { | ||
| const a = buildCacheKey(baseConfig, 'ws:team-a') | ||
| const b = buildCacheKey(baseConfig, 'ws:team-b') | ||
| expect(a).not.toBe(b) | ||
| expect(parseKey(a).get('u')).toBe('ws:team-a') | ||
| expect(parseKey(b).get('u')).toBe('ws:team-b') | ||
| }) | ||
|
|
||
| it('treats workspace, firebase, and api-key scopes as distinct buckets', () => { | ||
| const ws = buildCacheKey(baseConfig, 'ws:abc') | ||
| const fb = buildCacheKey(baseConfig, 'fb:abc') | ||
| const apikey = buildCacheKey(baseConfig, 'apikey') | ||
| expect(new Set([ws, fb, apikey]).size).toBe(3) | ||
| }) | ||
|
|
||
| it('falls back to "anon" when authScope is missing', () => { | ||
| expect(parseKey(buildCacheKey(baseConfig, null)).get('u')).toBe('anon') | ||
| expect(parseKey(buildCacheKey(baseConfig, undefined)).get('u')).toBe('anon') | ||
| }) | ||
|
|
||
| it('treats missing optional fields as empty so the key stays stable', () => { | ||
| const params = parseKey(buildCacheKey(baseConfig, 'fb:user-a')) | ||
| expect(params.get('responseKey')).toBe('') | ||
| }) | ||
| }) |
There was a problem hiding this comment.
Add cache-key regression tests for all contract dimensions.
Current tests lock route, responseKey, and auth-scope behavior, but they don’t assert the page_size / use_comfy_api cache partition contract called out in the PR. Please add explicit cases so future changes can’t silently merge distinct cache buckets.
✅ Suggested test additions
describe('buildCacheKey', () => {
+ it('partitions by page_size to avoid cache collisions', () => {
+ const p50 = buildCacheKey({ ...baseConfig, page_size: 50 }, 'fb:user-a')
+ const p100 = buildCacheKey({ ...baseConfig, page_size: 100 }, 'fb:user-a')
+ expect(p50).not.toBe(p100)
+ })
+
+ it('does not user-scope when use_comfy_api is disabled', () => {
+ const cfg = { ...baseConfig, use_comfy_api: false }
+ const a = buildCacheKey(cfg, 'fb:user-a')
+ const b = buildCacheKey(cfg, 'fb:user-b')
+ expect(a).toBe(b)
+ })
+
it('encodes the route and response_key', () => {As per coding guidelines: "Write tests for all changes, especially bug fixes to catch future regressions".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/renderer/extensions/vueNodes/widgets/utils/richComboHelpers.test.ts`
around lines 27 - 67, Tests for buildCacheKey currently assert route,
responseKey, and auth-scope but miss asserting partitioning by page_size and
use_comfy_api; add test cases that call buildCacheKey (and parseKey) with
different page_size values and with use_comfy_api true vs false (using
baseConfig as the base), then assert the resulting keys are distinct (Set size
=== expected) and that parseKey(...).get('page_size') and
parseKey(...).get('use_comfy_api') reflect the values used so future changes
don’t merge those cache buckets accidentally.
99ead1c to
8f3ff03
Compare
Adds a Vue-native renderer for combo inputs that declare `remote_combo=` (RemoteComboOptions on the backend). Wired through WidgetSelect; runs in parallel to the existing useRemoteWidget composable, which continues to handle plain `remote=` combos. The widget fetches a single items array from a relative `/proxy/...` route — the frontend always prepends the comfy-api base URL and injects auth headers (no opt-out flag while the feature is partner-node-only). Items are mapped via the per-node `item_schema`, with image/video/audio previews, search across multiple fields, optional auto-select first/last, and a refresh button. Caching: browser Cache API with TTL from `refresh`, partitioned by full auth scope (workspace / firebase uid / api-key / anon). Refresh button sequences cache delete before refetch to avoid the fast-response race. Logging: auth headers and response bodies are redacted from error logs. Also adds an audio preview branch to FormDropdownMenuItem — used by the new widget when `preview_type='audio'`. Tests cover: single-shot fetch, error classification, retry exhaustion, refresh, deselect, stale-id preservation, cache-key partitioning, route resolution, item-schema mapping, and Zod relative-route validation.
8f3ff03 to
97c2a0d
Compare
Summary
Adds
RichComboWidget— a Vue-native renderer for combo inputs that declareremote_combo=(RemoteComboOptions on the backend), with previews, search, pagination, and persistent caching. Runs in parallel to the existinguseRemoteWidgetcomposable, which continues to handle plainremote=combos.Changes
RichComboWidget.vue— fetches items from a remote route (optionally paginated as?page=N&page_size=Mreturning{items, has_more}), maps each item via a configurableitem_schema, and renders them inFormDropdownwith image/video/audio previews, search, auto-select, and a refresh button. Cache-API-backed persistent cache (survives page reloads), retry with exponential backoff, abort on unmount/refresh.WidgetSelect.vue— picksRichComboWidgetwhen the spec hasremote_combo, otherwise falls through to the existing dropdown / control-widget paths.nodeDefSchema.ts:zRemoteComboConfig(route, item_schema, refresh_button, auto_select, refresh, response_key, timeout, max_retries, use_comfy_api, page_size) andzRemoteItemSchema(value/label/description/preview fields, preview_type, search_fields).itemSchemaUtils.ts(dot-path traversal, label template substitution, item mapping, search-text indexing) andfetchRemoteRoute.ts(axios GET with optional comfy-api base URL + auth header injection).FormDropdownMenuItem.vue— used by the new widget whenpreview_type='audio', and picked up by the legacyLoadAudiodropdown which previously rendered audio URLs as broken<img>.FormDropdown.vue/FormDropdownMenu.vue/FormDropdownMenuActions.vue: optionalshowSort/showLayoutSwitcherprops (defaulttrue), and an optionaldescriptionrow inFormDropdownItemrendered in list / list-small layouts.widgets.remoteCombo.*.Review Focus
RichComboWidget.vuewatch at the bottom of<script setup>;handleSelectionmirrorsuseWidgetSelectActions.updateSelectedItemsso user-initiated deselection clears the model toundefined.)LoadAudiodropdown. The audio preview branch inFormDropdownMenuItem.vuefires wheneverassetKind='audio' && previewUrl.useWidgetSelectItemsalready populatespreview_urlfor audio items andWidgetSelectDropdownalready providesassetKind='audio', so the legacy dropdown picks this up automatically — net positive (previously broken<img>), but worth a deliberate look.buildCacheKeykeys onroute + use_comfy_api + response_key + page_size, and additionally onuserIdonly whenuse_comfy_api=true. Non-comfy-api routes intentionally share cache across users on the same machine; comfy-api routes are partitioned per user.{items: [...], has_more: bool}; the frontend issuespage=0,1,…untilhas_more=falseor emptyitems. Partial caches are explicitly not written — terminal failure refetches from page 0 on next mount. Documented atnodeDefSchema.tsand in the backendRemoteComboOptionsdocstring.useRemoteWidget(in-memoryMap, 4 s timeout, 512 ms backoff cap) handles plainremote=.RichComboWidget(Cache API, 30 s timeout, 16 s backoff cap) handlesremote_combo=. Intentional — different reliability profiles for different use cases.Core PR
Comfy-Org/ComfyUI#13432
Screenshots (if applicable)
Screenshots
┆Issue is synchronized with this Notion page by Unito