feat: add gated Hugging Face model access hints#13742
Conversation
🎭 Playwright: ✅ 1729 passed, 0 failed · 3 flaky📊 Browser Reports
🎨 Storybook: ✅ Built — View Storybook📦 Bundle: 8.08 MB gzip 🔴 +1.12 kBDetailsSummary
Category Glance App Entry Points — 3.64 kB (baseline 3.64 kB) • ⚪ 0 BMain entry bundles and manifests
Status: 1 added / 1 removed Graph Workspace — 1.25 MB (baseline 1.25 MB) • 🔴 +3.8 kBGraph editor runtime, canvas, workflow orchestration
Status: 1 added / 1 removed / 1 unchanged Views & Navigation — 112 kB (baseline 112 kB) • ⚪ 0 BTop-level views, pages, and routed surfaces
Status: 12 added / 12 removed / 4 unchanged Panels & Settings — 551 kB (baseline 551 kB) • ⚪ 0 BConfiguration panels, inspectors, and settings screens
Status: 11 added / 11 removed / 15 unchanged User & Accounts — 29.1 kB (baseline 29.1 kB) • ⚪ 0 BAuthentication, profile, and account management bundles
Status: 7 added / 7 removed / 3 unchanged Editors & Dialogs — 121 kB (baseline 121 kB) • ⚪ 0 BModals, dialogs, drawers, and in-app editors
Status: 5 added / 5 removed / 1 unchanged UI Components — 64.7 kB (baseline 64.7 kB) • ⚪ 0 BReusable component library chunks
Status: 6 added / 6 removed / 8 unchanged Data & Services — 3.37 MB (baseline 3.37 MB) • 🔴 +304 BStores, services, APIs, and repositories
Status: 14 added / 14 removed / 3 unchanged Utilities & Hooks — 357 kB (baseline 357 kB) • ⚪ 0 BHelpers, composables, and utility bundles
Status: 18 added / 18 removed / 17 unchanged Vendor & Third-Party — 15.7 MB (baseline 15.7 MB) • ⚪ 0 BExternal libraries and shared vendor chunks Status: 16 unchanged Other — 12.4 MB (baseline 12.4 MB) • 🔴 +1.06 kBBundles that do not match a named category
Status: 75 added / 75 removed / 199 unchanged ⚡ Performance Report
Show regressions
All metrics
Historical variance (last 15 runs)
Trend (last 15 commits on main)
Raw data{
"timestamp": "2026-07-24T05:40:45.092Z",
"gitSha": "3435fc826558fd4b1084590da75b0c3706d850e1",
"branch": "jaeone/fe-1173-gated-hf-download-hint-action",
"measurements": [
{
"name": "canvas-idle",
"durationMs": 2057.6620000000503,
"styleRecalcs": 11,
"styleRecalcDurationMs": 11.248999999999999,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 573.302,
"heapDeltaBytes": 3551412,
"heapUsedBytes": 71072128,
"domNodes": 22,
"jsHeapTotalBytes": 21110784,
"scriptDurationMs": 21.984,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "canvas-idle",
"durationMs": 2024.6480000000702,
"styleRecalcs": 9,
"styleRecalcDurationMs": 6.928000000000002,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 502.7579999999999,
"heapDeltaBytes": 3517548,
"heapUsedBytes": 70987856,
"domNodes": 18,
"jsHeapTotalBytes": 20848640,
"scriptDurationMs": 18.443,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "canvas-mouse-sweep",
"durationMs": 1818.3340000000499,
"styleRecalcs": 74,
"styleRecalcDurationMs": 37.273,
"layouts": 12,
"layoutDurationMs": 3.5669999999999997,
"taskDurationMs": 890.2270000000001,
"heapDeltaBytes": -19005904,
"heapUsedBytes": 48612812,
"domNodes": -268,
"jsHeapTotalBytes": 20975616,
"scriptDurationMs": 127.05,
"eventListeners": -133,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "canvas-mouse-sweep",
"durationMs": 1887.5419999999394,
"styleRecalcs": 74,
"styleRecalcDurationMs": 38.751,
"layouts": 12,
"layoutDurationMs": 3.5120000000000005,
"taskDurationMs": 888.5880000000001,
"heapDeltaBytes": -15243956,
"heapUsedBytes": 52086148,
"domNodes": -269,
"jsHeapTotalBytes": 20975616,
"scriptDurationMs": 128.775,
"eventListeners": -133,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "canvas-zoom-sweep",
"durationMs": 1737.2440000000324,
"styleRecalcs": 32,
"styleRecalcDurationMs": 17.411999999999995,
"layouts": 6,
"layoutDurationMs": 0.591,
"taskDurationMs": 372.822,
"heapDeltaBytes": 7039428,
"heapUsedBytes": 74730232,
"domNodes": 76,
"jsHeapTotalBytes": 20848640,
"scriptDurationMs": 17.496000000000002,
"eventListeners": 19,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333335,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "canvas-zoom-sweep",
"durationMs": 1706.770000000006,
"styleRecalcs": 30,
"styleRecalcDurationMs": 15.793000000000001,
"layouts": 6,
"layoutDurationMs": 0.5370000000000001,
"taskDurationMs": 380.525,
"heapDeltaBytes": 7285240,
"heapUsedBytes": 74582548,
"domNodes": 76,
"jsHeapTotalBytes": 20586496,
"scriptDurationMs": 18.298999999999996,
"eventListeners": 19,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "dom-widget-clipping",
"durationMs": 570.8050000000071,
"styleRecalcs": 10,
"styleRecalcDurationMs": 6.995999999999999,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 379.37600000000003,
"heapDeltaBytes": -11924292,
"heapUsedBytes": 55689312,
"domNodes": 16,
"jsHeapTotalBytes": 21110784,
"scriptDurationMs": 59.447,
"eventListeners": 2,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "dom-widget-clipping",
"durationMs": 575.5249999999705,
"styleRecalcs": 12,
"styleRecalcDurationMs": 8.103000000000003,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 377.65999999999997,
"heapDeltaBytes": -12286696,
"heapUsedBytes": 55241908,
"domNodes": 20,
"jsHeapTotalBytes": 21897216,
"scriptDurationMs": 60.451,
"eventListeners": 0,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "large-graph-idle",
"durationMs": 2020.2330000000188,
"styleRecalcs": 8,
"styleRecalcDurationMs": 7.861,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 666.8009999999999,
"heapDeltaBytes": 6302164,
"heapUsedBytes": 65765288,
"domNodes": -269,
"jsHeapTotalBytes": 5316608,
"scriptDurationMs": 108.99599999999998,
"eventListeners": -129,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "large-graph-idle",
"durationMs": 2013.311999999928,
"styleRecalcs": 8,
"styleRecalcDurationMs": 7.108,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 667.78,
"heapDeltaBytes": 5848996,
"heapUsedBytes": 64506432,
"domNodes": -268,
"jsHeapTotalBytes": 4792320,
"scriptDurationMs": 103.42800000000003,
"eventListeners": -129,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "large-graph-pan",
"durationMs": 2153.626000000031,
"styleRecalcs": 68,
"styleRecalcDurationMs": 13.784999999999995,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 1310.3229999999999,
"heapDeltaBytes": 1832328,
"heapUsedBytes": 61601936,
"domNodes": -271,
"jsHeapTotalBytes": 5521408,
"scriptDurationMs": 435.791,
"eventListeners": -127,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "large-graph-pan",
"durationMs": 2188.9660000000504,
"styleRecalcs": 69,
"styleRecalcDurationMs": 13.852999999999998,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 1273.1650000000002,
"heapDeltaBytes": 5487848,
"heapUsedBytes": 65053420,
"domNodes": -271,
"jsHeapTotalBytes": 4997120,
"scriptDurationMs": 439.683,
"eventListeners": -129,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "large-graph-zoom",
"durationMs": 3349.2500000000405,
"styleRecalcs": 64,
"styleRecalcDurationMs": 13.889999999999995,
"layouts": 60,
"layoutDurationMs": 7.236000000000001,
"taskDurationMs": 1604.368,
"heapDeltaBytes": 5928724,
"heapUsedBytes": 67880932,
"domNodes": -275,
"jsHeapTotalBytes": 8462336,
"scriptDurationMs": 579.45,
"eventListeners": -133,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "large-graph-zoom",
"durationMs": 3232.161000000019,
"styleRecalcs": 66,
"styleRecalcDurationMs": 16.282000000000004,
"layouts": 60,
"layoutDurationMs": 7.475,
"taskDurationMs": 1534.918,
"heapDeltaBytes": -791236,
"heapUsedBytes": 62917548,
"domNodes": -271,
"jsHeapTotalBytes": 7675904,
"scriptDurationMs": 532.1859999999999,
"eventListeners": -133,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "minimap-idle",
"durationMs": 2022.8040000000078,
"styleRecalcs": 8,
"styleRecalcDurationMs": 7.0600000000000005,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 647.609,
"heapDeltaBytes": 6319696,
"heapUsedBytes": 66337208,
"domNodes": -269,
"jsHeapTotalBytes": 4530176,
"scriptDurationMs": 101.093,
"eventListeners": -129,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "minimap-idle",
"durationMs": 2033.0860000000257,
"styleRecalcs": 8,
"styleRecalcDurationMs": 7.515999999999998,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 659.53,
"heapDeltaBytes": 6741568,
"heapUsedBytes": 69016168,
"domNodes": -269,
"jsHeapTotalBytes": 5054464,
"scriptDurationMs": 98.94600000000001,
"eventListeners": -127,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "subgraph-dom-widget-clipping",
"durationMs": 564.6199999999908,
"styleRecalcs": 47,
"styleRecalcDurationMs": 10.524,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 398.84899999999993,
"heapDeltaBytes": -11758236,
"heapUsedBytes": 55622484,
"domNodes": 20,
"jsHeapTotalBytes": 22421504,
"scriptDurationMs": 119.38300000000001,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333335,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "subgraph-dom-widget-clipping",
"durationMs": 603.5000000000537,
"styleRecalcs": 47,
"styleRecalcDurationMs": 9.718,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 408.319,
"heapDeltaBytes": -11788420,
"heapUsedBytes": 56027620,
"domNodes": 20,
"jsHeapTotalBytes": 21897216,
"scriptDurationMs": 117.015,
"eventListeners": 8,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "subgraph-idle",
"durationMs": 2047.9119999999966,
"styleRecalcs": 10,
"styleRecalcDurationMs": 9.059999999999999,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 524.378,
"heapDeltaBytes": -20604856,
"heapUsedBytes": 46901124,
"domNodes": -265,
"jsHeapTotalBytes": 20189184,
"scriptDurationMs": 16.7,
"eventListeners": -133,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.670000000000012,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "subgraph-idle",
"durationMs": 2007.3859999999968,
"styleRecalcs": 10,
"styleRecalcDurationMs": 8.396,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 452.674,
"heapDeltaBytes": 3493084,
"heapUsedBytes": 71191436,
"domNodes": 20,
"jsHeapTotalBytes": 20848640,
"scriptDurationMs": 12.855000000000002,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "subgraph-mouse-sweep",
"durationMs": 1694.735000000037,
"styleRecalcs": 76,
"styleRecalcDurationMs": 37.068999999999996,
"layouts": 16,
"layoutDurationMs": 4.38,
"taskDurationMs": 751.331,
"heapDeltaBytes": -5515248,
"heapUsedBytes": 61809876,
"domNodes": 64,
"jsHeapTotalBytes": 21110784,
"scriptDurationMs": 90.054,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "subgraph-mouse-sweep",
"durationMs": 1741.1429999999655,
"styleRecalcs": 77,
"styleRecalcDurationMs": 36.619,
"layouts": 16,
"layoutDurationMs": 4.327,
"taskDurationMs": 813.286,
"heapDeltaBytes": -20622536,
"heapUsedBytes": 46890876,
"domNodes": -265,
"jsHeapTotalBytes": 20713472,
"scriptDurationMs": 92.913,
"eventListeners": -133,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "subgraph-transition-enter",
"durationMs": 1400.4429999999957,
"styleRecalcs": 18,
"styleRecalcDurationMs": 31.166999999999994,
"layouts": 14,
"layoutDurationMs": 14.606000000000002,
"taskDurationMs": 933.3790000000001,
"heapDeltaBytes": 2076524,
"heapUsedBytes": 77350704,
"domNodes": 13673,
"jsHeapTotalBytes": 14155776,
"scriptDurationMs": 33.749,
"eventListeners": 2371,
"totalBlockingTimeMs": 153,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "viewport-pan-sweep",
"durationMs": 8282.869999999946,
"styleRecalcs": 250,
"styleRecalcDurationMs": 36.678000000000004,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 4466.706999999999,
"heapDeltaBytes": 1287996,
"heapUsedBytes": 59791992,
"domNodes": -266,
"jsHeapTotalBytes": 5840896,
"scriptDurationMs": 1397.865,
"eventListeners": -113,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "viewport-pan-sweep",
"durationMs": 8226.3650000001,
"styleRecalcs": 250,
"styleRecalcDurationMs": 36.367999999999995,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 4451.107,
"heapDeltaBytes": 10613240,
"heapUsedBytes": 69491128,
"domNodes": -266,
"jsHeapTotalBytes": 7589888,
"scriptDurationMs": 1365.6589999999999,
"eventListeners": -113,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "vue-large-graph-idle",
"durationMs": 16726.572999999975,
"styleRecalcs": 0,
"styleRecalcDurationMs": 0,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 16689.594,
"heapDeltaBytes": -58708580,
"heapUsedBytes": 166406600,
"domNodes": -8311,
"jsHeapTotalBytes": -12783616,
"scriptDurationMs": 584.004,
"eventListeners": -16382,
"totalBlockingTimeMs": 0,
"frameDurationMs": 17.77333333333336,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "vue-large-graph-idle",
"durationMs": 16836.236999999983,
"styleRecalcs": 0,
"styleRecalcDurationMs": 0,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 16816.385000000002,
"heapDeltaBytes": -55075880,
"heapUsedBytes": 166499608,
"domNodes": -8311,
"jsHeapTotalBytes": -13045760,
"scriptDurationMs": 583.6119999999999,
"eventListeners": -16382,
"totalBlockingTimeMs": 0,
"frameDurationMs": 18.333333333333332,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "vue-large-graph-pan",
"durationMs": 20621.865000000013,
"styleRecalcs": 140,
"styleRecalcDurationMs": 17.80599999999999,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 20579.033,
"heapDeltaBytes": -51532508,
"heapUsedBytes": 155403584,
"domNodes": -8311,
"jsHeapTotalBytes": -13570048,
"scriptDurationMs": 882.838,
"eventListeners": -16376,
"totalBlockingTimeMs": 166,
"frameDurationMs": 18.329999999999927,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "vue-large-graph-pan",
"durationMs": 20307.67000000003,
"styleRecalcs": 139,
"styleRecalcDurationMs": 16.37500000000003,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 20275.414,
"heapDeltaBytes": -42201328,
"heapUsedBytes": 166017532,
"domNodes": -8311,
"jsHeapTotalBytes": -13832192,
"scriptDurationMs": 862.3040000000001,
"eventListeners": -16378,
"totalBlockingTimeMs": 137,
"frameDurationMs": 17.776666666666642,
"p95FrameDurationMs": 16.80000000000291
},
{
"name": "workflow-execution",
"durationMs": 479.99000000004344,
"styleRecalcs": 21,
"styleRecalcDurationMs": 23.592999999999996,
"layouts": 3,
"layoutDurationMs": 1.33,
"taskDurationMs": 131.33,
"heapDeltaBytes": -15682176,
"heapUsedBytes": 50941372,
"domNodes": 152,
"jsHeapTotalBytes": 7741440,
"scriptDurationMs": 10.508,
"eventListeners": 67,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "workflow-execution",
"durationMs": 458.59199999995326,
"styleRecalcs": 13,
"styleRecalcDurationMs": 18.307,
"layouts": 3,
"layoutDurationMs": 0.5730000000000002,
"taskDurationMs": 107.651,
"heapDeltaBytes": -16226616,
"heapUsedBytes": 50431936,
"domNodes": 119,
"jsHeapTotalBytes": 7741440,
"scriptDurationMs": 8.978000000000002,
"eventListeners": 65,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
}
]
} |
🌐 Website E2ETip All tests passed.
|
📝 WalkthroughWalkthroughMissing-model handling now detects gated Hugging Face repositories, caches access URLs, exposes gated access controls and hints, supports desktop-bridge delegation with browser fallback, and validates the behavior through unit and browser tests. ChangesMissing model access flow
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant MissingModelRow
participant useMissingModelDownload
participant ComfyDesktop2Bridge
participant openGatedRepoPage
MissingModelRow->>useMissingModelDownload: openModelAccessPage(repoUrl)
useMissingModelDownload->>ComfyDesktop2Bridge: request model access
ComfyDesktop2Bridge-->>useMissingModelDownload: resolve true, false, or reject
useMissingModelDownload->>openGatedRepoPage: open repo when bridge does not own request
Suggested reviewers: Important Pre-merge checks failedPlease resolve all errors before merging. Addressing warnings is optional. ❌ Failed checks (1 inconclusive)
✅ Passed checks (5 passed)
✨ 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 |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/platform/missingModel/components/MissingModelCard.test.ts`:
- Around line 311-327: Update the “Download all” test around MissingModelCard
and the shared missing-model download handler to exercise the real download
composable instead of asserting only mockDownloadModel. Use multiple
downloadable models and verify the observable browser anchor interaction or
Desktop bridge effect, preserving the expected “all models” 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: ASSERTIVE
Plan: Pro Plus
Run ID: 4d23b4a7-6c94-41b8-9e2d-ea45c9fa25ff
📒 Files selected for processing (15)
packages/comfyui-desktop-bridge-types/comfyDesktopBridge.d.tspackages/comfyui-desktop-bridge-types/package.jsonsrc/locales/en/main.jsonsrc/locales/ko/main.jsonsrc/platform/missingModel/components/MissingModelCard.test.tssrc/platform/missingModel/components/MissingModelCard.vuesrc/platform/missingModel/components/MissingModelRow.test.tssrc/platform/missingModel/components/MissingModelRow.vuesrc/platform/missingModel/composables/useMissingModelDownload.tssrc/platform/missingModel/missingModelDownload.test.tssrc/platform/missingModel/missingModelDownload.tssrc/platform/missingModel/missingModelPipeline.test.tssrc/platform/missingModel/missingModelPipeline.tssrc/platform/missingModel/missingModelStore.test.tssrc/platform/missingModel/missingModelStore.ts
Codecov Report❌ Patch coverage is
@@ Coverage Diff @@
## main #13742 +/- ##
==========================================
- Coverage 78.57% 77.75% -0.83%
==========================================
Files 1698 1699 +1
Lines 113608 99643 -13965
Branches 36753 35368 -1385
==========================================
- Hits 89269 77477 -11792
+ Misses 23712 21703 -2009
+ Partials 627 463 -164
Flags with carried forward coverage won't be shown. Click here to find out more.
... and 283 files with indirect coverage changes 🚀 New features to boost your workflow:
|
9a80859 to
c24ec68
Compare
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/locales/en/main.json`:
- Around line 3994-3995: Update the gatedModelsHint and gatedModelTooltip
translations to describe completing the model’s access requirements rather than
always accepting a license agreement, while preserving the sign-in guidance and
distinct hint/tooltip context.
In `@src/platform/missingModel/components/MissingModelCard.test.ts`:
- Around line 19-26: Update the partial mock for the missingModelDownload module
in MissingModelCard tests to also replace fetchModelMetadata with a default
resolved response of { fileSize: null, gatedRepoUrl: null }. Keep the existing
downloadModel mock and all other actual exports unchanged, ensuring
MissingModelRow mounts do not trigger network requests.
🪄 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: ASSERTIVE
Plan: Pro Plus
Run ID: ab2b917e-53e0-4e40-a50c-8c1dbf7f45c1
📒 Files selected for processing (14)
packages/comfyui-desktop-bridge-types/comfyDesktopBridge.d.tspackages/comfyui-desktop-bridge-types/package.jsonsrc/locales/en/main.jsonsrc/platform/missingModel/components/MissingModelCard.test.tssrc/platform/missingModel/components/MissingModelCard.vuesrc/platform/missingModel/components/MissingModelRow.test.tssrc/platform/missingModel/components/MissingModelRow.vuesrc/platform/missingModel/composables/useMissingModelDownload.tssrc/platform/missingModel/missingModelDownload.test.tssrc/platform/missingModel/missingModelDownload.tssrc/platform/missingModel/missingModelPipeline.test.tssrc/platform/missingModel/missingModelPipeline.tssrc/platform/missingModel/missingModelStore.test.tssrc/platform/missingModel/missingModelStore.ts
The bridge contract resolves `false` when the host declines to open a model access page, but the caller only branched on the method's presence and handled rejections, so a declined request left the shield button doing nothing. Await the result and fall through to the anchor fallback on both `false` and a thrown error. Align the card's gated banner with the row's shield action. The banner keyed off `gatedRepoUrls` alone while the shield also requires `isModelDownloadable`, and the pipeline populates `gatedRepoUrls` without checking allowed suffixes. A gated `.bin` or `.gguf` therefore rendered guidance to accept a license with no shield or download button on any row. Derive the banner from `downloadableModels` so both conditions share one authority. Also: - Use a polite live region for the gated guidance instead of an assertive one. - Document that `isRemote()` reports the backend server rather than the user's location, and why the access-page bridge is used regardless of it. - Document that the metadata HEAD is deliberately uncredentialed, so a gated result cannot be cleared by a later re-check. - Extract the duplicated anchor-click code behind one helper. - Drop the unreachable catch in `prefetchModelMetadata`. - Cover both 404 and 500 in the not-gated test, and drop the `setGatedRepoUrl` test that restated its own implementation.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/platform/missingModel/composables/useMissingModelDownload.ts`:
- Around line 31-34: Update the gated access flow in useMissingModelDownload to
invoke openModelAccessPage directly through window.__comfyDesktop2 instead of
extracting it into openInHost, preserving the bridge object's this context while
retaining the existing await and early-return 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: ASSERTIVE
Plan: Pro Plus
Run ID: c5786042-d75c-410d-a380-f5d014f84000
📒 Files selected for processing (9)
packages/comfyui-desktop-bridge-types/comfyDesktopBridge.d.tssrc/platform/missingModel/components/MissingModelCard.test.tssrc/platform/missingModel/components/MissingModelCard.vuesrc/platform/missingModel/components/MissingModelRow.test.tssrc/platform/missingModel/components/MissingModelRow.vuesrc/platform/missingModel/composables/useMissingModelDownload.tssrc/platform/missingModel/missingModelDownload.test.tssrc/platform/missingModel/missingModelDownload.tssrc/platform/missingModel/missingModelStore.test.ts
💤 Files with no reviewable changes (1)
- src/platform/missingModel/missingModelStore.test.ts
The gated flow had no coverage above unit level. The classification is driven purely by a HEAD response status and the shield, banner, and fallback are ordinary DOM, so the parts this feature adds are reachable from Playwright even though a real access grant is not. Add three tests to the OSS missing-models spec: - a 403 on the metadata HEAD classifies the model as gated and renders both the shield action and the card banner; - Download stays visible and enabled on a gated row, alongside the shield; - clicking the shield with no Desktop bridge opens the derived repository URL through the anchor fallback. The stubbed 403 carries `Access-Control-Allow-Origin` because the HEAD is cross-origin: without it the browser rejects the response, the classification never runs, and the tests would pass or fail for reasons unrelated to the code under test. Routing is registered on the browser context rather than the page so the fallback popup's navigation is stubbed too. Add a workflow fixture with a HuggingFace URL, since no existing one has a download source that survives `isModelDownloadable()`, and register the two new test IDs in the shared selectors.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@browser_tests/tests/propertiesPanel/errorsTabMissingModels.spec.ts`:
- Line 223: Replace the synchronous accessPage.url() equality assertion with
Playwright’s retrying await expect(accessPage).toHaveURL(...) assertion,
preserving GATED_MODEL_REPO_URL as the expected destination.
🪄 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: ASSERTIVE
Plan: Pro Plus
Run ID: fb1303ff-ef3f-42d5-9baa-2872085b2d10
📒 Files selected for processing (3)
browser_tests/assets/missing/missing_models_gated.jsonbrowser_tests/fixtures/selectors.tsbrowser_tests/tests/propertiesPanel/errorsTabMissingModels.spec.ts
The card's gated banner is derived from downloadable models, while the pipeline records gated repository URLs for any candidate that has a url and a directory. Nothing above unit level pinned that difference, so a regression would restore guidance to accept a license on rows that offer no way to act on it. Add an OSS case for a gated model whose extension is excluded from download: the row is listed as missing, the HuggingFace HEAD is still answered with 403, and neither the banner, the shield, nor Download appears. Waiting on the HEAD count before the absence assertions keeps them from passing merely because classification had not run yet.
The comment defending the bridge call justified skipping the isRemote() check by claiming the host session is reused by downloads. It is not: downloadModel only takes the bridge when isRemote() is false, so the rationale was false in the one branch it existed to defend. Skipping the check is still right, for a different reason, and the comment now gives that reason. The exclusion test polled a counter incremented when the route handler was entered, which is before the response is sent, before the store is written, and before a render. It caught the regression only because an earlier assertion happened to leave enough time. Serve the gated response first and gate a downloadable companion's metadata behind it, so a rendered size proves the gated response was already consumed before the absence assertions run. Await the popup URL instead of reading it off a page that may not have navigated yet, and require an explicit true from the bridge rather than any truthy value. Document what false obligates the caller to do, since the contract only described the accepting case.
|
Note Unit test generation is a beta feature. Expect some limitations and changes as we gather feedback and continue to improve it. Generating unit tests... This may take up to 20 minutes. |
|
@coderabbitai resume |
✅ Action performedReviews resumed. |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/platform/missingModel/composables/useMissingModelDownload.ts`:
- Around line 32-36: Update openModelAccessPage to invoke
window.__comfyDesktop2.openModelAccessPage directly rather than storing it in
openInHost, preserving the bridge object's this context while keeping the
existing result check and fallback flow unchanged.
🪄 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: ASSERTIVE
Plan: Pro Plus
Run ID: 11a5ccdb-470a-46b5-a5f5-cd455b03d8b8
📒 Files selected for processing (4)
browser_tests/assets/missing/missing_models_gated_unsupported.jsonbrowser_tests/tests/propertiesPanel/errorsTabMissingModels.spec.tspackages/comfyui-desktop-bridge-types/comfyDesktopBridge.d.tssrc/platform/missingModel/composables/useMissingModelDownload.ts
|
Request timed out after 900000ms (requestId=637e9953-bd70-4d79-ab9b-4805d478bbb1) |
Extracting openModelAccessPage into a local detached it from its receiver, so a host that reads this on the method would throw and the flow would fall to the system-browser anchor, stranding provider cookies. Call it on the bridge object. The Download all test drove a single downloadable model, so it could not tell "downloads all" from "downloads the first". Drive two models and assert each is dispatched once with its own arguments; reducing the component to the first model now fails the call count.
| size="sm" | ||
| class="shrink-0 focus-visible:ring-inset" | ||
| :aria-label="`${t('g.download')} ${model.name}`" | ||
| :title="gatedModelTooltip" |
There was a problem hiding this comment.
Could you clarify what this comment was pointing to? Component-level coverage already existed. If you meant that the composable itself was missing direct unit tests, I addressed that in 7caff34 by adding useMissingModelDownload.test.ts with coverage for the metadata guard, download dispatch, Desktop bridge behavior, browser fallback, and URL validation.
| GATED_STATUS_CODES.has(response.status) && | ||
| response.headers.get('x-error-code') === HUGGING_FACE_GATED_ERROR_CODE | ||
| ) { | ||
| return { fileSize: null, gatedRepoUrl: downloadUrlToHfRepoUrl(url) } |
There was a problem hiding this comment.
issue: The isComplete function below returns false when a URL fetches successfully but omits content-length -- producing { fileSize: null, gatedRepoUrl: null } which is never cached. fetchModelMetadata will re-fire a HEAD request on every call for these URLs (e.g. HF URLs that return 200 with no size header). Same root affects prefetchModelMetadata in the composable: the guard store.fileSizes[url] !== undefined || store.gatedRepoUrls[url] fails to skip for non-gated URLs with no content-length, so onMounted refetches on every component mount. Consider caching all results unconditionally, or adding a fetched: true sentinel field to ModelMetadata to distinguish "fetched but unknown" from "not fetched".
There was a problem hiding this comment.
Fixed in 7caff34. fetchHeadMetadata now returns metadata together with an explicit cacheability outcome. A successful HEAD response without content-length is cached, while thrown/network failures and non-OK responses remain uncached so they can be retried. The existing Civitai path is intentionally unchanged. I added regression coverage for successful empty-size caching and failed/non-OK retries.
| size="sm" | ||
| class="shrink-0 focus-visible:ring-inset" | ||
| :aria-label="`${t('g.download')} ${model.name}`" | ||
| :title="gatedModelTooltip" |
There was a problem hiding this comment.
suggestion: (non-blocking) When a model is gated, the Download button receives gatedModelTooltip ("This model is gated and requires you to be logged in...") as its :title. Since Download is still functional, this is contradictory -- the tooltip implies access is blocked while the button implies it is available. Consider removing gatedModelTooltip from the Download button's :title, or replacing it with a softer hint like "Download may require signing in to Hugging Face first".
There was a problem hiding this comment.
Updated in 7caff34. The Download button now uses the softer i18n-backed hint, “Download may require signing in to Hugging Face first.” The stronger gated/license message remains on the dedicated lock action.
| } | ||
|
|
||
| function openUrlInNewTab(url: string, downloadAs?: string): void { | ||
| const link = document.createElement('a') |
There was a problem hiding this comment.
issue: openUrlInNewTab sets link.href = url and calls link.click() with no scheme validation. Browsers execute javascript: URIs assigned to anchor .href when clicked programmatically. The current callers only pass https://huggingface.co/... URLs derived from downloadUrlToHfRepoUrl, so the path is safe today. But the function has no defense in depth -- a future caller or the catch { return url } fallback in downloadUrlToHfRepoUrl could introduce a javascript: URI. Suggest validating the scheme before navigation: if (new URL(url).protocol !== 'https:') return.
There was a problem hiding this comment.
Addressed in 7caff34 at the gated-navigation boundary. openGatedRepoPage now rejects any URL whose parsed origin is not exactly https://huggingface.co, with tests covering javascript: and non-Hugging-Face URLs. I kept the generic openUrlInNewTab helper unchanged because it also serves ordinary model download URLs; the stricter trust rule belongs to the gated repository action.
| const bridge = window.__comfyDesktop2 | ||
| if (bridge?.openModelAccessPage) { | ||
| try { | ||
| if ((await bridge.openModelAccessPage(repoUrl)) === true) return |
There was a problem hiding this comment.
issue: bridge.openModelAccessPage(repoUrl) passes the URL to Electron IPC without origin validation. If the Desktop-side implementation calls shell.openExternal(), a file:// or javascript: URI could access local files or execute code on the host. The renderer should validate the origin before delegating: if (!repoUrl.startsWith('https://huggingface.co/')) { openGatedRepoPage(repoUrl); return } (or reject entirely). Renderer-side validation should not rely on the host to sanitize.
There was a problem hiding this comment.
Fixed in 7caff34. The composable now validates the parsed URL origin with isTrustedHuggingFaceUrl before invoking either the Desktop bridge or the browser fallback. Only the exact https://huggingface.co origin is accepted; lookalike hosts, alternate schemes, and alternate ports are rejected. The Desktop side still performs its own validation as defense in depth.
| <div | ||
| v-if="showGatedModelsHint" | ||
| data-testid="missing-model-gated-hint" | ||
| role="status" |
There was a problem hiding this comment.
issue: role="status" is an ARIA live region (aria-live="polite"). On a banner rendered as static content when the component mounts, this causes screen readers to announce the gated-hint text as a dynamic status update on every render or re-render -- interrupting the user's current focus unexpectedly. For a persistent informational message, role="note" (no live behavior) or no role is appropriate. role="status" should be reserved for content that genuinely changes in response to user action. WCAG 4.1.3.
There was a problem hiding this comment.
Fixed in 7caff34. The persistent banner now uses role="note", avoiding repeated polite live-region announcements when the Errors tab remounts. The component test was updated accordingly.
| variant="secondary" | ||
| size="sm" | ||
| class="shrink-0 focus-visible:ring-inset" | ||
| :aria-label="`${t('g.download')} ${model.name}`" |
There was a problem hiding this comment.
suggestion: (non-blocking) When a model is gated, the Download button's aria-label is still just "Download [model name]" -- keyboard-only users (who don't receive title attribute values) get no indication that sign-in is required before the download will succeed. Consider adding context to the aria-label for gated models, e.g. Download ${model.name} (login required), or pointing to the hint banner via aria-describedby. WCAG 2.4.6.
There was a problem hiding this comment.
Fixed in 7caff34 using aria-describedby. The Download button keeps the concise accessible name Download [model name] and references a unique hidden description containing the sign-in hint when the model is gated. This preserves a clear command name while supplying the gated context to assistive technology.
| <i | ||
| aria-hidden="true" | ||
| class="mt-0.5 icon-[lucide--lock] size-4 shrink-0 text-warning-background" | ||
| /> |
There was a problem hiding this comment.
nitpick: (non-blocking) The gated-hint banner conveys its warning state through color alone (text-warning-background, border-warning-background). Users with color vision deficiencies may not distinguish it from ordinary informational content. A short text prefix ("Note:" or similar) or a non-color visual indicator alongside the lock icon would satisfy WCAG 1.4.1 Use of Color.
There was a problem hiding this comment.
The banner already has two non-color indicators: a visible lock icon and explicit text stating that some models are gated and require Hugging Face sign-in/license acceptance. Color is only supplemental styling here, so I kept the current copy rather than adding a generic “Note:” prefix.
|
|
||
| it('returns gatedRepoUrl for gated HuggingFace HEAD requests (403)', async () => { | ||
| fetchMock.mockResolvedValueOnce({ ok: false, status: 403 }) | ||
| it.for([401, 403, 451])( |
There was a problem hiding this comment.
suggestion: (non-blocking) metadataCache is a module-level Map in missingModelDownload.ts. vi.resetAllMocks() in beforeEach resets mock functions but does not re-import modules or clear module state, so the cache persists across tests. The testId counter prevents same-URL collisions within this file, but any future test using a hardcoded URL could read stale cached state. Consider exporting a clearMetadataCache() test hook, or using vi.resetModules() + dynamic import in beforeEach to guarantee isolation.
There was a problem hiding this comment.
I kept the module cache encapsulated rather than adding a production export solely for tests or converting the suite to dynamic imports. The existing testId strategy gives every cache-sensitive test a unique URL, and the new successful-empty and retry tests follow that same isolation rule. I agree this would need revisiting if future tests start sharing fixed cache keys.
| import type { ModelWithUrl } from '@/platform/missingModel/missingModelDownload' | ||
| import { useMissingModelStore } from '@/platform/missingModel/missingModelStore' | ||
|
|
||
| export function useMissingModelDownload() { |
There was a problem hiding this comment.
suggestion: (non-blocking) useMissingModelDownload.ts is a new file with three functions but no unit test file. The openModelAccessPage bridge/fallback paths are tested via component integration tests, but composable-level behavior -- especially the prefetchModelMetadata guard when gatedRepoUrls[url] is already set, and the openModelAccessPage fallback when the bridge returns false -- is not covered at the unit level. Worth adding a useMissingModelDownload.test.ts alongside the other composable tests.
There was a problem hiding this comment.
Added direct composable coverage in 7caff34. The new useMissingModelDownload.test.ts covers file and gated metadata storage, the existing-store guard, download dispatch, Desktop bridge receiver/URL handling, remote Desktop behavior, bridge false and rejection fallbacks, and untrusted URL rejection. The component tests now retain only the UI wiring responsibility.
| /** Opens a model provider access page in the hosted frontend's browser session. | ||
| * Resolves `true` when the host has taken ownership of the request. | ||
| * On `false` or rejection the frontend falls back to opening a new tab. */ | ||
| openModelAccessPage?: (url: string) => Promise<boolean> |
There was a problem hiding this comment.
nitpick: (non-blocking) The downloadUrlToHfRepoUrl utility used to derive gatedRepoUrl has a catch block that returns the raw url argument unchanged on parse failure. Since isHuggingFaceRepoUrl uses new URL() too and would fail on the same malformed input, this path is unreachable via fetchHeadMetadata. But the function's own contract is broken: it claims to return an HF repo URL but can return anything. A safer fallback would be return 'https://huggingface.co/' so future callers that skip the hostname check don't receive arbitrary values.
There was a problem hiding this comment.
I left the raw fallback unchanged because it is a pre-existing utility-contract issue and remains unreachable from the gated flow. In 7caff34, both the composable and openGatedRepoPage independently require the exact https://huggingface.co origin, so a malformed/raw fallback cannot reach browser navigation or Electron IPC. I would prefer to clean up the broader conversion utility contract in a focused follow-up if it gains additional callers.
Summary
This PR adds an explicit access hint for gated Hugging Face models in the missing-model UI without taking the normal download action away from the user.
When metadata prefetch receives an authorization-related response (
401,403, or451) with Hugging Face's explicitX-Error-Code: GatedRepomarker, the UI keeps the derived repository URL and shows a lock action beside Download. The lock opens the repository page so the user can sign in and accept any required access terms. Download continues to use the existing OSS or Desktop download path.This gives gated models a practical recovery path without adding a Hugging Face OAuth flow, storing an access token, or integrating another provider API.
Changes
useMissingModelDownload()so Row and Card use the same platform-level behavior.openModelAccessPage()Desktop bridge contract and retain the browser anchor fallback for OSS and older Desktop hosts.Why Download Remains Available
A failed cross-origin HEAD request can tell us that a Hugging Face URL appears to require authorization, but it cannot reliably tell us whether the user's browser already has a valid Hugging Face login cookie and access grant. Gated responses also do not expose a usable
content-length, so the missing-model UI cannot show the model size in this state.Treating the gated result as a hard redirect would therefore remove a working one-click download from users who are already authorized. This PR treats it as an access hint instead:
The lock may remain visible after access is granted because it represents metadata observed during the current missing-model scan, not a live authentication-state check.
User Flow
Desktop same-session navigation is implemented by the paired Comfy-Org/Comfy-Desktop#1275.
Intended Limitations
content-length.GatedRepoerror code, and other HTTP failures are not classified as gated repositories.Review Focus
Validation
pnpm format:checkpnpm lintpnpm typecheckpnpm test:unitpnpm test:browserpnpm knippnpm buildPlaywright E2E tests cover the deterministic browser-visible flow: a mocked Hugging Face
403response carryingX-Error-Code: GatedRepoclassifies a model as gated, renders the lock and guidance while keeping Download available, and opens the derived repository URL through the browser fallback.The provider-authenticated flow itself is not automated because it depends on a real third-party Hugging Face account, a gated repository access grant, cross-origin cookies, and the host browser or Electron session. That full sign-in, grant, return, and retry flow was verified manually in Comfy Desktop.
Screenshots
2026-07-17.2.31.33.mov