Skip to content

[integration][do-not-merge] M1 FE asset stack for BE integration testing#12411

Draft
dante01yoon wants to merge 47 commits into
mainfrom
jaewon/m1-fe-integration
Draft

[integration][do-not-merge] M1 FE asset stack for BE integration testing#12411
dante01yoon wants to merge 47 commits into
mainfrom
jaewon/m1-fe-integration

Conversation

@dante01yoon
Copy link
Copy Markdown
Collaborator

@dante01yoon dante01yoon commented May 22, 2026

Purpose

Test-only branch — do not merge. Stacks every open M1 Assets Foundation FE PR onto latest main so backend can run integration tests against the unified FE state without waiting for each PR to merge sequentially.

Stacked PRs (current)

# Ticket PR Author Merge commit
1 FE-733 #12287 dante01yoon 77514a4c8
2 FE-729 #12322 dante01yoon 3051c0025
3 FE-730 #12335 dante01yoon 4837307fa
4 FE-731 #12375 dante01yoon c244b53d0
5 FE-746 #12398 dante01yoon 117d01190 + d8422691c (Case B + soft-degrade + e2e follow-up)
6 FE-749 #12328 mattmillerai 4b0f6dc29
7 FE-750 #12318 mattmillerai d45e64197
8 FE-747 + FE-748 #12399 (bundled) dante01yoon b50510f7d
9 FE-732 #12417 dante01yoon 752452b90

Base: main. 13 merge commits total.

M1 scope coverage (per Assets FE Divergence Survey)

L1 — Remove !isCloud asset guards (10 sites)

Site Ticket PR In stack
useMediaAssets.ts:14 (deleted) FE-728 #12283 MERGED already on main
assetService.ts:385 FE-729 #12322
assetsStore.ts:121,324 FE-730 #12335 draft
useComboWidget.ts:216 + useAssetWidgetData.ts:1-80 FE-731 #12375
MediaAssetFilterBar.vue:12,30 + AssetBrowserModal.vue + AssetsSidebarTab.vue:287 + useMediaAssetActions.ts:96 + MediaAssetContextMenu.vue:124 FE-732 #12417 ✅ NEW
(prereq) identifier helpers + mockFeatureFlags FE-733 #12287

L2 — Capability flags (6 sites)

Site Ticket PR In stack
useMediaAssetActions.ts:120 (ZIP export) FE-781 ❌ BE-996/997 pending
useModelUpload.ts + useUploadModelWizard.ts + missingModelPipeline.ts:114,136,164 + MissingModelCard.vue:61,66,87,142 + MissingModelRow.vue:35,151 FE-780 ❌ BE-995/997 pending

L3 — Asset Identity RFC (3 sites)

Site Ticket PR In stack
missingMediaScan.ts FE-746 #12398 draft
assetMetadataUtils.ts:66-68,164-201 FE-747 #12399 (bundle) ✅ NEW
useMissingMediaInteractions.ts:92 FE-748 #12399 (bundle) ✅ NEW

L4 — OSS filter/sort param parity

Handled as residual inside FE-732. BE-886/BE-891 light up job_ids / asset_hash / include_public on OSS.

L5 — Typed metadata (1 site)

Site Ticket PR In stack
MediaAssetCard.vue:286 image dimensions FE-749 #12328

L6 — Field renames

Localized at helper layer by FE-733. No FE rename ticket needed; helpers absorb asset_hashhash, prompt_idjob_id aliasing.

L7 — Image-editor upload (1 site)

Site Ticket PR In stack
usePainter.ts:634,635 + useMaskEditorSaver.ts FE-750 #12318

N1 — Out of M1 scope (3 sites)

Widget value formats — useMediaAssetActions.ts:46,302,446 + useComboWidget.ts:186 + usePainter.ts:673 + useMaskEditorSaver.ts:329.

Verified

  • pnpm typecheck → exit 0 (after FE-732 merge)
  • Knip pre-push hook bypassed: mockFeatureFlags.ts flagged as unused due to stacking-order false-positive.

BE gate matrix

Group BE blocker
FE-729 / 730 / 731 / 732 (L1) BE-786
FE-746 / 748 (L3 file_path) BE-933 + BE-934
FE-747 (L3 metadata helpers) BE-1043 + BE-1044
FE-749 (L5) BE-15
FE-750 (L7) none
FE-733 (prereq) none
FE-780 / 781 (L2) BE-995 / BE-996 / BE-997

dante01yoon and others added 30 commits May 15, 2026 18:04
…ureFlags test util

L1 prerequisite cleanup. Two type-preserving refactors that make
subsequent L1 sub-issues mechanical.

1. Extract getAssetStoredFilename(asset) helper to collapse the
   duplicated `isCloud && asset.asset_hash ? asset.asset_hash :
   asset.name` branch from useMediaAssetActions (lines 302, 446)
   into a single helper. Once BE-933/934 emit file_path and the
   cloud spec sync brings it into generated types, only the helper
   changes.

2. Add mockFeatureFlags(overrides) test utility under src/test-utils/.
   L1 sub-issues will use this consistently instead of re-inventing
   the mock shape each time. Includes a FeatureFlags type export
   from useFeatureFlags.ts so the mock stays in sync.

Also auto-fixed unrelated tailwind class-order lint errors in four
files (VirtualGrid, RightSidePanel, Textarea, ModelInfoPanel) to
keep CI green.

No behavior change.
Collapse the OSS vs Cloud branching in the mask editor and painter so both
runtimes use the same upload contract: POST to /upload/image with type "input"
and no subfolder, then reference the result by filename only.

- maskeditor: replace separate uploadMask + uploadImage helpers with a single
  uploadLayer; drop the /upload/mask call, the original_ref form field, and
  the clipspace subfolder
- painter: drop the isCloud branch on type/subfolder/result string; always
  upload as type "input" with no subfolder
- update painter test to match the unified result shape
Both uploadLayer (maskeditor) and the painter's serialize path previously
fell through to the pre-upload layer.ref when the server returned 200 with
no name or with a non-JSON body, logging only a console.warn. The caller
then assigned the stale ref onto the saved widget value as if the upload
had succeeded, producing graphs that pointed at files that were never
written.

- maskeditor: rewrite uploadLayer to throw on JSON parse failure and on
  missing data.name (was: console.warn + return layer.ref)
- painter: add a missing-name guard alongside the existing JSON parse
  guard, surfacing the same toast/error path the JSON failure already uses
- painter test: assert the upload FormData carries type=input with no
  subfolder, and add coverage for the two new error branches
After the upload-contract unification, the mask editor saves all four
layers through /upload/image. The Playwright route interceptors for
/upload/mask in the save-success and save-failure tests no longer
fire.

- save-success: remove the mask interceptor, tighten the count assertion
  from "> 0" to "=== 4" (one upload per layer)
- save-failure: drop the unused mask interceptor; the image-route 500
  is sufficient to keep the dialog open
Replace the hand-rolled { name?: string ... } shapes in the painter
and mask editor with UploadImageResponse from the codegen'd
ingest-types package. The runtime shape is unchanged - the type now
flows from the spec via the existing codegen pipeline instead of
being declared inline locally.

The runtime check for data.name remains: the spec marks the 200
response properties as optional, so the imported type still has
name?: string. The check enforces what the server actually returns
in practice; tightening the spec is a separate follow-up.
FE-729 (L1.2 — L1 central). Asset API will be always available
post-BE-786 (`--enable-assets` removed from OSS). Delete the gating
function, the user-facing setting, the experimental warning, and the
toggle command. Simplify all caller sites to assume the asset API
is available.

Changes:
- Delete isAssetAPIEnabled() from assetService.ts; simplify
  shouldUseAssetBrowser() to call isAssetBrowserEligible() directly.
- Remove the EXPERIMENTAL_WARNING prefix from four asset-load error
  messages.
- Drop the Comfy.Assets.UseAssetAPI setting from coreSettings.ts and
  its schema entry in apiSchema.ts.
- Remove the Comfy.ToggleAssetAPI command outright. Keep
  Comfy.BrowseModelAssets but drop its setting-check / setting-set
  preamble and the "Experimental:" label prefix; the command now
  just opens the asset browser dialog.
- modelStore: drop the useAssetAPI fork in createGetModelsFunc and
  loadModelFolders; always route through assetService.
- sidebarTabStore: model-library tab click always routes through
  Comfy.BrowseModelAssets (no more setting check).
- assetPreviewUtil.isAssetPreviewSupported() returns true
  unconditionally; caller-site simplification deferred.
- WidgetSelect.vue: simplify isAssetMode to drop the
  isAssetAPIEnabled() arm of the disjunction.
- Test mocks/expectations updated.

Blocked-merge by BE-786 (OSS `--enable-assets` removal). PR opened
as draft.

Also auto-fixed unrelated tailwind class-order lint errors in four
files (VirtualGrid, RightSidePanel, Textarea, ModelInfoPanel,
WidgetSelectDefault) to keep CI green.
… callers

Follow-up to the previous FE-729 commit. After deleting
isAssetAPIEnabled, isAssetPreviewSupported() became a wrapper that
always returned true. Remove the function and simplify all callers.

Changes:
- Delete isAssetPreviewSupported() from assetPreviewUtil.ts.
- Media3DTop.vue: drop the isAssetPreviewSupported() arm of the
  loadThumbnail guard (asset.name check is still required).
- saveMesh.ts: unwrap two `if (isAssetPreviewSupported()) { ... }`
  blocks in applySaveGLBOutput and the SaveGLB beforeRegisterNodeDef
  extension callback.
- FormDropdownMenuItem.vue: drop the early return from
  resolveMeshPreview.
- useLoad3d.ts: drop the isAssetPreviewSupported() arm of the
  modelReady guard.
- Tests: remove the dead "asset preview is unsupported" branches
  (useLoad3d, Media3DTop, FormDropdownMenuItem) and clean up the
  associated mocks and hoisted state.

Auto-fixed unrelated tailwind class-order lint errors in five files
(VirtualGrid, RightSidePanel, Textarea, ModelInfoPanel,
WidgetSelectDefault) to keep CI green.
Asset cards now render image dimensions from the asset response's
metadata field (`asset.metadata.width` / `asset.metadata.height`)
when available, falling back to the locally-computed naturalWidth /
naturalHeight from the rendered <img> tag when not. This unifies
the dimension display across runtimes that serve the original file
vs. runtimes that serve a downscaled preview — the rendered <img>
on the latter reports the preview size, not the source asset's
size, so a metadata-first read is what makes the displayed label
match the source on both runtimes.

No behavior change in the absence of metadata: cards continue to
display the locally-computed dimensions as they do today.

Drops the now-unused isCloud import; the conditional it guarded
collapses into the unified read-then-fallback path.
…ming

Tightens the metadata-dimension read added in the previous commit
based on review feedback:

- Adds isValidDimension() guard: rejects NaN, Infinity, 0, negatives,
  and fractional values. typeof v === 'number' alone accepts all of
  those, any of which would render as garbage in the dimension label
  (e.g. "NaNxNaN", "0x0", "1024.5x768").
- Renames originalImageDimensions to displayImageDimensions. The
  fallback branch may return preview-sized values from the rendered
  <img>'s naturalWidth/Height — calling that "original" invites the
  next caller to wire it into a place where source-of-truth
  dimensions are required.
- Restores meaning of metaInfo's existing truthy check by allowing
  displayImageDimensions to return undefined when neither path
  produces usable values (e.g. invalid metadata + image not yet
  loaded). Previously the truthy check was dead code because the
  computed always returned something.

No behavior change in the happy path (valid metadata, valid local).
Move isValidDimension + the metadata-vs-fallback logic into
getAssetMetadataDimensions in assetMetadataUtils, leaving the SFC with a
one-line computed. Switches the block comments to JSDoc on the helper
(per review feedback) and adds unit coverage for the validation cases —
addresses codecov/patch failure on the previously SFC-internal logic.
Temporary unblock for FE-729 e2e while BE-786 is in flight. FE-729
removes the legacy /api/models fallback in modelStore, so the assets
endpoint must be reachable in CI. BE-786 will make assets always-on
in OSS core; once that ships in the CI ComfyUI image, this flag (and
this commit) MUST be reverted before merging FE-729 to main.
After FE-729 the model-library sidebar tab routes to the Asset Browser
dialog and never renders ModelLibrarySidebarTab.vue. The legacy
sidebar-tree tests now validate dead code paths:

- delete browser_tests/tests/sidebar/modelLibrary.spec.ts entirely
  (tab/folders/search/refresh/empty-state — all dead)
- drop the model-library entry from the defaultKeybindings sidebar
  toggle table (KeyM no longer toggles a panel)
- remove the Refresh-clears-resolved-missing-model case from
  errorsTabMissingModels.spec.ts — it waited for the legacy
  /experiment/models endpoint that modelStore no longer hits

Dialog-flow coverage for the new entry point belongs to FE-732.
… fetcher

FE-730 (L1.3). Both Cloud and OSS will serve /api/assets post-BE-786,
so assetsStore collapses to a single asset-API code path.

- Drop fetchInputFilesFromAPI (legacy /files/input caller); rename
  fetchInputFilesFromCloud to fetchInputFiles.
- Remove the isCloud ternary on the input fetcher swap.
- Unwrap getModelState()'s if(isCloud) body and delete its OSS no-op
  return object; the model pagination subsystem now runs unconditionally.
- Drop now-dead isCloud + mapInputFileToAssetItem imports.
- Delete mapInputFileToAssetItem + stripDirectoryAnnotation from
  assetMappers.ts (no remaining callers in src/); delete its test file.
- Simplify assetsStore.test.ts: hardcode the distribution mock to
  isCloud: true, drop the mockIsCloud toggle helper and the dead
  mapper mock, rename the now-unconditional "(Cloud)" describe blocks.

Blocked-merge by BE-786 (OSS --enable-assets removal) + FE-729
(isAssetAPIEnabled deletion). Opened as a Draft on the FE-729 stack.

Auto-fixed unrelated tailwind class-order lint errors in five files
via pnpm lint:fix to keep CI green.
FE-729 routes model widgets (ckpt_name, lora_name, etc.) through
WidgetSelectDropdown on OSS as well as Cloud. That dropdown's trigger
button has no aria-label — the widget name lives in a sibling label
div ([data-testid="widget-layout-field-label"]). Grid-mode widgets
(WidgetSelectDefault) still carry aria-label on the input directly.

Update the helper to .or() the two strategies so existing aria-label
lookups keep working while asset-mode dropdowns are reached via the
visible label text + sibling trigger button.
- useComboWidget: drop the `if (isCloud)` wrap so shouldUseAssetBrowser
  and NODE_MEDIA_TYPE_MAP paths run unconditionally. Rename
  resolveCloudDefault -> resolveAssetDefault.
- useAssetWidgetData: drop the `if (isCloud)` branch and its empty-data
  fallback; remove the "Cloud-only" docstring.
- Tests: drop the isCloud mock + per-test toggles. Delete the
  isCloud=false desktop spec and the OSS LoadImage fall-through case
  that asserted the now-removed branch.
…Widget

Previous .or() implementation acted as a union — both branches resolved
for number widgets (e.g. 'seed' has aria-label on wrapper + a Decrement
button under widget-layout-field-label), causing Playwright strict mode
violations on tests that previously passed.

Switch to a count-based check: prefer the aria-label match when it
exists, otherwise fall back to the dropdown-trigger button under the
visible label row. This restores grid/number widget behaviour while
keeping asset-mode dropdown support.
…746)

- Add `file_path` to AssetItem schema (nullable per BE-933/BE-934 wire shape)
- Rewrite `getAssetDetectionNames`: use `file_path` alone when emitted,
  fall back to the legacy `asset_hash` / `name` / `subfolder + name` union
  when null (hash-only Core registrations, tagless Cloud rows, legacy data)
- Drop `isCloud` parameter from `scanAllMediaCandidates`,
  `scanNodeMediaCandidates`, and `MediaVerificationOptions` — both backends
  consult the same asset listing oracle post-BE-933/934
- Remove the OSS synchronous-shortcut branch; all candidates now resolve
  asynchronously through `verifyMediaCandidates` against the unified listing
- Rename `MediaVerificationOptions.isCloud` to `allowCompactSuffix`
  (N1-deferred annotation grammar flag, no longer a backend identity field)
- Always call `assetService.getInputAssetsIncludingPublic`; retain internal
  `isCloud` only for the generated-assets oracle (Cloud /api/assets vs OSS
  history) — unifying that path is tracked separately
- Update callers in app.ts, useErrorClearingHooks.ts, markDeletedAssetsAsMissingMedia.ts
- Update tests with module-level `isCloud` mock for generated-asset oracle
  control; add `file_path`-primary matching test case

Per RFC BE-808 v2 (Asset Identity Semantics). DO NOT MERGE until BE-933
(Comfy-Org/ComfyUI#14005) and BE-934 (Comfy-Org/cloud#3744) merge
and ship — both backends must emit `file_path` for the new path to fire.

Fixes FE-746
…, FE-748)

FE-748: drop the isCloud branch in getMediaDisplayName. Resolve through
the shared getAssetDisplayFilename helper so the missingMedia surface
uses the same fallback chain (user_metadata.filename ->
metadata.filename -> display_name -> asset.name) as the asset card and
browser surfaces.

FE-747: align assetMetadataUtils helper docstrings with the unified
display_name shape emitted by both Core and Cloud per the BE-808 Asset
Identity RFC. Add fixture tests covering the Cloud (hash + display_name)
and OSS (filename + nullable display_name) response shapes.
…DisplayName

Adds 4 cases against the in-file consumer of getMediaDisplayName that was
previously untested: empty-on-missing-widget, Cloud-hash combo values
mapped through the shared helper chain (display_name and
metadata.filename), OSS filename pass-through, and the candidate-name
filter. Caught by the FE-747/748 PR review pass.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 22, 2026

Review Change Stack

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 18f5d1f4-0d68-45f1-8e7a-fe46ff11e919

📥 Commits

Reviewing files that changed from the base of the PR and between 752452b and 4991802.

📒 Files selected for processing (6)
  • browser_tests/tests/sidebar/assets.spec.ts
  • src/renderer/extensions/vueNodes/widgets/components/WidgetSelectDropdown.test.ts
  • src/renderer/extensions/vueNodes/widgets/components/WidgetSelectDropdown.vue
  • src/renderer/extensions/vueNodes/widgets/composables/useAssetWidgetData.test.ts
  • src/renderer/extensions/vueNodes/widgets/composables/useAssetWidgetData.ts
  • src/renderer/extensions/vueNodes/widgets/composables/useWidgetSelectItems.test.ts

📝 Walkthrough

Walkthrough

Removes cloud/setting gating, unifies uploads to /upload/image and upload-response parsing, makes thumbnail persistence unconditional, defers missing-media detection to verification (allowCompactSuffix), adds asset filename/dimension helpers, refactors stores/composables/components, updates CI start flag, and updates/adds many tests.

Changes

Unified asset & media pipeline

Layer / File(s) Summary
Unified asset & media pipeline (single checkpoint)
src/platform/*, src/composables/*, src/stores/*, browser_tests/*, .github/actions/start-comfyui-server/action.yaml
CI start now includes --enable-assets. Image uploads unified to /upload/image with UploadImageResponse parsing and stronger error handling; preview/thumbnail capability guards removed so thumbnails are always captured/persisted (errors swallowed); missing-media scanning defers missingness and verification now accepts { allowCompactSuffix }; schema and utils add file_path, stored-filename and validated image-dimensions helpers; assetService, assets/model stores, and widget/composables refactored to runtime-driven asset API usage (removed isCloud/Comfy.Assets.UseAssetAPI gating); numerous components updated to remove cloud-only guards; broad test updates and new browser/Vitest specs added.

Estimated code review effort:
🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs:

Suggested labels:
size:XXL, core/1.44, cloud/1.44

Suggested reviewers:

  • benceruleanlu
  • AustinMroz
  • pythongosssss
  • christian-byrne

"A rabbit hopped in with a patch so neat,
Unified uploads, one endpoint to meet,
Thumbnails captured, no gates in the way,
Tests reworked, stores tidy today,
Hooray for assets — nibble, hop, repeat!"

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch jaewon/m1-fe-integration

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 22, 2026

🎨 Storybook: ✅ Built — View Storybook

Details

⏰ Completed at: 05/26/2026, 04:02:53 PM UTC

Links

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 22, 2026

🎭 Playwright: ❌ 1367 passed, 130 failed · 1 flaky

❌ Failed Tests

📊 Browser Reports

@codecov
Copy link
Copy Markdown

codecov Bot commented May 22, 2026

Codecov Report

❌ Patch coverage is 89.13043% with 35 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/stores/assetsStore.ts 92.30% 13 Missing ⚠️
src/components/graph/GraphCanvas.vue 0.00% 5 Missing ⚠️
src/composables/maskeditor/useMaskEditorSaver.ts 75.00% 4 Missing ⚠️
src/platform/assets/components/MediaAssetCard.vue 0.00% 4 Missing ⚠️
src/test-utils/mockFeatureFlags.ts 0.00% 4 Missing ⚠️
src/extensions/core/saveMesh.ts 83.33% 2 Missing ⚠️
src/platform/assets/services/assetService.ts 66.66% 1 Missing ⚠️
src/scripts/app.ts 0.00% 1 Missing ⚠️
src/stores/workspace/sidebarTabStore.ts 0.00% 1 Missing ⚠️
@@             Coverage Diff             @@
##             main   #12411       +/-   ##
===========================================
- Coverage   75.07%   60.37%   -14.70%     
===========================================
  Files        1526     1417      -109     
  Lines       96710    72519    -24191     
  Branches    28279    19250     -9029     
===========================================
- Hits        72602    43785    -28817     
- Misses      23214    28260     +5046     
+ Partials      894      474      -420     
Flag Coverage Δ
e2e ?
unit 60.37% <89.13%> (+0.44%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
src/components/load3d/Load3D.vue 100.00% <ø> (ø)
src/components/load3d/Load3DScene.vue 86.95% <ø> (-10.27%) ⬇️
src/components/sidebar/tabs/AssetsSidebarTab.vue 3.34% <ø> (-66.75%) ⬇️
src/composables/graph/useErrorClearingHooks.ts 93.71% <100.00%> (-3.24%) ⬇️
src/composables/node/useNodeDragToCanvas.ts 100.00% <100.00%> (ø)
src/composables/painter/usePainter.ts 50.97% <100.00%> (-39.85%) ⬇️
src/composables/useCoreCommands.ts 25.00% <ø> (-34.69%) ⬇️
src/composables/useFeatureFlags.ts 67.64% <ø> (-10.62%) ⬇️
src/composables/useLoad3d.ts 84.24% <100.00%> (-1.58%) ⬇️
src/extensions/core/load3d/SceneManager.ts 94.75% <100.00%> (+3.33%) ⬆️
... and 31 more

... and 1011 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/stores/workspace/sidebarTabStore.ts (1)

78-83: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Avoid silent no-op when browse command is unavailable.

At Line 82, the early return runs even when Comfy.BrowseModelAssets is not
registered, so model-library won’t toggle at all in that case. Add a fallback
to toggleSidebarTab(tab.id).

💡 Suggested change
-        if (tab.id === 'model-library') {
-          await commandStore.commands
-            .find((cmd) => cmd.id === 'Comfy.BrowseModelAssets')
-            ?.function?.()
-          return
-        }
+        if (tab.id === 'model-library') {
+          const browseModelAssetsCommand = commandStore.commands.find(
+            (cmd) => cmd.id === 'Comfy.BrowseModelAssets'
+          )
+          if (browseModelAssetsCommand?.function) {
+            await browseModelAssetsCommand.function()
+            return
+          }
+        }

         toggleSidebarTab(tab.id)
🤖 Prompt for 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.

In `@src/stores/workspace/sidebarTabStore.ts` around lines 78 - 83, The current
branch for tab.id === 'model-library' returns unconditionally, causing a silent
no-op when the Comfy.BrowseModelAssets command is not registered; update the
logic in the sidebarTabStore handler that checks commandStore.commands for id
'Comfy.BrowseModelAssets' so that you only return after successfully invoking
its .function(), and otherwise call toggleSidebarTab(tab.id) as a fallback
(i.e., if .find(...) or .function is undefined, invoke toggleSidebarTab with the
tab id).
🧹 Nitpick comments (9)
src/platform/assets/services/assetService.ts (1)

230-232: ⚡ Quick win

Keep Zod diagnostics in logs, not thrown user messages.

These thrown errors include full schema diagnostics. Prefer logging fromZodError(...) and throwing a localized, user-friendly message instead.

Proposed change
 function validateAssetResponse(data: unknown): AssetResponse {
   const result = assetResponseSchema.safeParse(data)
   if (result.success) return result.data

-  const error = fromZodError(result.error)
-  throw new Error(`Invalid asset response against zod schema:\n${error}`)
+  console.error('Invalid asset response:', fromZodError(result.error))
+  throw new Error(
+    st(
+      'assetBrowser.errorInvalidAssetResponse',
+      'Received an invalid asset response. Please try again.'
+    )
+  )
 }
@@
-  const error = result.error
-    ? fromZodError(result.error)
-    : 'Unknown validation error'
-  throw new Error(`Invalid asset response against zod schema:\n${error}`)
+  console.error(
+    'Invalid asset details response:',
+    result.error ? fromZodError(result.error) : 'Unknown validation error'
+  )
+  throw new Error(
+    st(
+      'assetBrowser.errorInvalidAssetResponse',
+      'Received an invalid asset response. Please try again.'
+    )
+  )

Based on learnings: "In asset service files under src/platform/assets/services, prefer using schema.safeParse()... convert the error with fromZodError(result.error) for logging while throwing user-friendly errors that do not reveal internal schema details."

Also applies to: 447-450

🤖 Prompt for 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.

In `@src/platform/assets/services/assetService.ts` around lines 230 - 232, Replace
throwing the full Zod diagnostics created by fromZodError(result.error) with
logging the diagnostics and throwing a user-friendly message: call your logger
(e.g., processLogger.error or existing logger) with fromZodError(result.error)
to record full schema details, then throw a generic/localized Error like "Asset
response validation failed" (do this in the block that currently uses
fromZodError(result.error)). Apply the same change to the other occurrence
around the second validation (the block referencing fromZodError at the later
location).
src/platform/missingMedia/missingMediaAssetResolver.test.ts (1)

23-27: ⚡ Quick win

Remove redundant mock-explanation comments.

The surrounding test naming already communicates intent; these comments are extra upkeep without added signal.

♻️ Proposed cleanup
-// Mutable holder so each test can flip the runtime `isCloud` to drive the
-// resolver's generated-assets oracle selection (Cloud /api/assets vs OSS
-// job history). The named-import binding into the resolver re-reads the
-// getter on each access (ESM live binding semantics).
 const isCloudHolder = vi.hoisted(() => ({ value: false }))
As per coding guidelines: `**/*.{ts,tsx,vue,js,jsx}`: "Avoid new usage of code comments; do not add or retain redundant comments".
🤖 Prompt for 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.

In `@src/platform/missingMedia/missingMediaAssetResolver.test.ts` around lines 23
- 27, Remove the redundant explanatory comment block above the test helper and
leave only the code; specifically delete the multi-line comment describing the
mutable holder and ESM live binding semantics that precedes isCloudHolder, so
keep the const isCloudHolder = vi.hoisted(() => ({ value: false })) line but
remove the surrounding comment text.
src/platform/missingMedia/missingMediaScan.test.ts (2)

864-870: ⚡ Quick win

Drop the redundant "no pending candidates" test case.

This scenario is already covered by the prior "already resolved as true" test and does not add new behavioral coverage.

♻️ Proposed cleanup
-  it('skips entirely when no pending candidates', async () => {
-    const candidates = [makeCandidate('1', missingHash, { isMissing: true })]
-
-    await verifyMediaCandidates(candidates, { allowCompactSuffix: true })
-
-    expect(mockGetInputAssetsIncludingPublic).not.toHaveBeenCalled()
-  })
As per coding guidelines: `src/**/*.test.ts`: "Do not write redundant tests; follow composable testing patterns".
🤖 Prompt for 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.

In `@src/platform/missingMedia/missingMediaScan.test.ts` around lines 864 - 870,
Remove the redundant test case 'skips entirely when no pending candidates' in
missingMediaScan.test.ts because its behavior is already covered by the earlier
"already resolved as true" test; delete the it(...) block that constructs
candidates with makeCandidate('1', missingHash, { isMissing: true }) and calls
verifyMediaCandidates(...), and ensure any expectations against
mockGetInputAssetsIncludingPublic in that block are removed so the suite relies
on the existing test covering the same behavior.

31-34: ⚡ Quick win

Remove redundant inline comments in test bodies.

These comments duplicate intent already captured by test names/assertions and add maintenance noise.

♻️ Proposed cleanup
-// Mutable runtime `isCloud` holder for tests that exercise the default
-// resolver's generated-assets oracle (Cloud /api/assets vs OSS history).
-// Tests with their own `resolveAssetSources` mock can ignore this.
 const isCloudHolder = vi.hoisted(() => ({ value: false }))
@@
-      // Legacy `name` and `asset_hash` deliberately diverge from the
-      // widget value; `file_path` is the sole reason the match succeeds.
       name: 'unrelated.png',
As per coding guidelines: `**/*.{ts,tsx,vue,js,jsx}`: "Avoid new usage of code comments; do not add or retain redundant comments".

Also applies to: 492-494

🤖 Prompt for 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.

In `@src/platform/missingMedia/missingMediaScan.test.ts` around lines 31 - 34,
Remove the redundant inline comments in test bodies that duplicate test
names/assertions: delete the comment block above the isCloudHolder declaration
(the mutable runtime `isCloud` holder) and remove the similar inline comments
around the tests referenced near the bottom (the ones at 492-494) while leaving
the isCloudHolder constant and the tests themselves unchanged so
behavior/assertions remain intact.
browser_tests/fixtures/helpers/BuilderSelectHelper.ts (1)

123-127: ⚡ Quick win

Remove newly added implementation comment block

Please encode this intent in selector naming/helpers instead of inline comments to stay aligned with repo guidance.

As per coding guidelines, "Avoid new usage of code comments; do not add or retain redundant comments."

🤖 Prompt for 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.

In `@browser_tests/fixtures/helpers/BuilderSelectHelper.ts` around lines 123 -
127, Remove the inline implementation comment block in BuilderSelectHelper.ts
and instead encode the intent in a descriptive selector/helper name; e.g.,
replace the explanatory comment about grid-mode vs asset-mode widgets with a
clear helper or selector like clickWidgetDropdownTriggerInRow or
findWidgetByLabelOrDropdown that encapsulates the branching (WidgetSelectDefault
vs WidgetSelectDropdown) and uses the data-testid "widget-layout-field-label"
fallback internally, so the reasoning lives in the helper name and tests call
that helper rather than relying on comments.
src/renderer/extensions/vueNodes/widgets/composables/useComboWidget.ts (1)

108-111: ⚡ Quick win

Remove newly introduced explanatory comments in this TS module.

Please keep the intent in naming/tests and drop these new comments to match repo conventions.

As per coding guidelines: "Avoid new usage of code comments; do not add or retain redundant comments."

Also applies to: 215-217

🤖 Prompt for 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.

In `@src/renderer/extensions/vueNodes/widgets/composables/useComboWidget.ts`
around lines 108 - 111, Remove the newly added explanatory comment blocks in the
useComboWidget module: delete the comment that begins "Resolve the default value
for an asset widget. Priority: inputSpec.default..." and the other recently
introduced explanatory block later in the same file, leaving only intent
expressed via clear naming and tests (i.e., do not add or retain these redundant
comments in the useComboWidget/composables code).
src/composables/useFeatureFlags.ts (1)

46-49: ⚡ Quick win

Drop the newly added doc comment block here to match TS repo conventions.

Keep the exported type, but remove this new explanatory comment.

As per coding guidelines: "Avoid new usage of code comments; do not add or retain redundant comments."

🤖 Prompt for 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.

In `@src/composables/useFeatureFlags.ts` around lines 46 - 49, Remove the newly
added doc comment block above the exported type returned by useFeatureFlags so
the file follows TS repo conventions; keep the exported type itself and any
export statements intact, locating the comment immediately above the
useFeatureFlags return type declaration and delete only that explanatory comment
text.
src/test-utils/mockFeatureFlags.ts (2)

8-18: ⚡ Quick win

Remove the new explanatory block comment and keep the helper self-documenting.

This comment block is redundant with the function name/signature and adds maintenance overhead.

As per coding guidelines, "Avoid new usage of code comments; do not add or retain redundant comments."

🤖 Prompt for 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.

In `@src/test-utils/mockFeatureFlags.ts` around lines 8 - 18, Remove the redundant
block comment above the mockFeatureFlags helper: delete the entire explanatory
comment block that documents useFeatureFlags/mockFeatureFlags so the helper
remains self-documenting via its function name and signature; locate the comment
immediately preceding the mockFeatureFlags function in
src/test-utils/mockFeatureFlags.ts and remove it, leaving only the
implementation and any minimal inline comments that add non-redundant value.

42-47: ⚡ Quick win

Clarify mockFeatureFlags featureFlag mock (current tests don’t use it)

  • src/test-utils/mockFeatureFlags.ts is not imported/called by the test suite; existing tests mock useFeatureFlags directly with flags only, so the current featureFlag: vi.fn() (returning undefined) won’t distort current assertions.
  • If this helper is intended for future use, make featureFlag return a computed consistent with useFeatureFlags (use flags when the path matches, otherwise return the provided defaultValue).
🤖 Prompt for 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.

In `@src/test-utils/mockFeatureFlags.ts` around lines 42 - 47, The
mockFeatureFlags helper currently returns featureFlag as vi.fn() which yields
undefined; change mockFeatureFlags so its featureFlag returns a Vue computed
that mirrors useFeatureFlags behavior: when called with a path string, return a
computed that resolves to the value in the provided flags for that path (support
nested lookup), and when a defaultValue is passed return that default when the
path is missing; update references to featureFlag in the returned object so
callers get a computed ref consistent with useFeatureFlags's featureFlag
implementation.
🤖 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/fixtures/helpers/BuilderSelectHelper.ts`:
- Around line 128-137: In selectInputWidget, tighten ambiguous locators: change
byAriaLabel to use .first() (e.g., const byAriaLabel =
node.getByLabel(widgetName, { exact: true }).first()) so it always returns a
single locator, and make the fallback filter use exact text matching for the
row/button (e.g., locate the field-label row by matching the label/button text
exactly via getByText(..., { exact: true }) or a anchored RegExp like ^…$ then
.locator('..').locator('button').first()). Remove the explanatory comment block
above this logic so the code remains self-documenting; update references to
byAriaLabel and widgetLocator in the click call accordingly.

In `@src/composables/useCoreCommands.ts`:
- Line 1286: The command label is hardcoded as 'Browse Model Assets'—replace it
with a vue-i18n translation call (use the local t function from useI18n or the
i18n instance used in useCoreCommands) so the command object's label property
uses t('commands.browseModelAssets') (or equivalent) instead of the string, and
add the key "commands.browseModelAssets": "Browse Model Assets" to
src/locales/en/main.json; locate the label in the command definition inside
useCoreCommands and update the import/usage of useI18n if needed to ensure t is
available.

In `@src/stores/assetsStore.ts`:
- Around line 632-653: The functions updateAssetMetadata (and similarly
updateAssetTags) currently catch errors, perform a rollback via
updateAssetInCache, and then swallow the error; change them to rethrow the
original error (or return a typed failure) after the rollback so callers can
handle/display the failure. Concretely, inside the catch block for
updateAssetMetadata (the assetService.updateAsset call and its
updateAssetInCache rollback) preserve the caught error and throw it (or return a
standardized error result) after restoring originalMetadata; apply the same
pattern to updateAssetTags where assetService.updateAssetTags and its rollback
via updateAssetInCache are handled.

---

Outside diff comments:
In `@src/stores/workspace/sidebarTabStore.ts`:
- Around line 78-83: The current branch for tab.id === 'model-library' returns
unconditionally, causing a silent no-op when the Comfy.BrowseModelAssets command
is not registered; update the logic in the sidebarTabStore handler that checks
commandStore.commands for id 'Comfy.BrowseModelAssets' so that you only return
after successfully invoking its .function(), and otherwise call
toggleSidebarTab(tab.id) as a fallback (i.e., if .find(...) or .function is
undefined, invoke toggleSidebarTab with the tab id).

---

Nitpick comments:
In `@browser_tests/fixtures/helpers/BuilderSelectHelper.ts`:
- Around line 123-127: Remove the inline implementation comment block in
BuilderSelectHelper.ts and instead encode the intent in a descriptive
selector/helper name; e.g., replace the explanatory comment about grid-mode vs
asset-mode widgets with a clear helper or selector like
clickWidgetDropdownTriggerInRow or findWidgetByLabelOrDropdown that encapsulates
the branching (WidgetSelectDefault vs WidgetSelectDropdown) and uses the
data-testid "widget-layout-field-label" fallback internally, so the reasoning
lives in the helper name and tests call that helper rather than relying on
comments.

In `@src/composables/useFeatureFlags.ts`:
- Around line 46-49: Remove the newly added doc comment block above the exported
type returned by useFeatureFlags so the file follows TS repo conventions; keep
the exported type itself and any export statements intact, locating the comment
immediately above the useFeatureFlags return type declaration and delete only
that explanatory comment text.

In `@src/platform/assets/services/assetService.ts`:
- Around line 230-232: Replace throwing the full Zod diagnostics created by
fromZodError(result.error) with logging the diagnostics and throwing a
user-friendly message: call your logger (e.g., processLogger.error or existing
logger) with fromZodError(result.error) to record full schema details, then
throw a generic/localized Error like "Asset response validation failed" (do this
in the block that currently uses fromZodError(result.error)). Apply the same
change to the other occurrence around the second validation (the block
referencing fromZodError at the later location).

In `@src/platform/missingMedia/missingMediaAssetResolver.test.ts`:
- Around line 23-27: Remove the redundant explanatory comment block above the
test helper and leave only the code; specifically delete the multi-line comment
describing the mutable holder and ESM live binding semantics that precedes
isCloudHolder, so keep the const isCloudHolder = vi.hoisted(() => ({ value:
false })) line but remove the surrounding comment text.

In `@src/platform/missingMedia/missingMediaScan.test.ts`:
- Around line 864-870: Remove the redundant test case 'skips entirely when no
pending candidates' in missingMediaScan.test.ts because its behavior is already
covered by the earlier "already resolved as true" test; delete the it(...) block
that constructs candidates with makeCandidate('1', missingHash, { isMissing:
true }) and calls verifyMediaCandidates(...), and ensure any expectations
against mockGetInputAssetsIncludingPublic in that block are removed so the suite
relies on the existing test covering the same behavior.
- Around line 31-34: Remove the redundant inline comments in test bodies that
duplicate test names/assertions: delete the comment block above the
isCloudHolder declaration (the mutable runtime `isCloud` holder) and remove the
similar inline comments around the tests referenced near the bottom (the ones at
492-494) while leaving the isCloudHolder constant and the tests themselves
unchanged so behavior/assertions remain intact.

In `@src/renderer/extensions/vueNodes/widgets/composables/useComboWidget.ts`:
- Around line 108-111: Remove the newly added explanatory comment blocks in the
useComboWidget module: delete the comment that begins "Resolve the default value
for an asset widget. Priority: inputSpec.default..." and the other recently
introduced explanatory block later in the same file, leaving only intent
expressed via clear naming and tests (i.e., do not add or retain these redundant
comments in the useComboWidget/composables code).

In `@src/test-utils/mockFeatureFlags.ts`:
- Around line 8-18: Remove the redundant block comment above the
mockFeatureFlags helper: delete the entire explanatory comment block that
documents useFeatureFlags/mockFeatureFlags so the helper remains
self-documenting via its function name and signature; locate the comment
immediately preceding the mockFeatureFlags function in
src/test-utils/mockFeatureFlags.ts and remove it, leaving only the
implementation and any minimal inline comments that add non-redundant value.
- Around line 42-47: The mockFeatureFlags helper currently returns featureFlag
as vi.fn() which yields undefined; change mockFeatureFlags so its featureFlag
returns a Vue computed that mirrors useFeatureFlags behavior: when called with a
path string, return a computed that resolves to the value in the provided flags
for that path (support nested lookup), and when a defaultValue is passed return
that default when the path is missing; update references to featureFlag in the
returned object so callers get a computed ref consistent with useFeatureFlags's
featureFlag implementation.
🪄 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: 8ac99015-b849-4d11-a4ac-dbb7ddd38426

📥 Commits

Reviewing files that changed from the base of the PR and between 0a07781 and d45e641.

📒 Files selected for processing (55)
  • .github/actions/start-comfyui-server/action.yaml
  • browser_tests/fixtures/helpers/BuilderSelectHelper.ts
  • browser_tests/tests/defaultKeybindings.spec.ts
  • browser_tests/tests/maskEditor.spec.ts
  • browser_tests/tests/propertiesPanel/errorsTabMissingModels.spec.ts
  • browser_tests/tests/sidebar/modelLibrary.spec.ts
  • src/composables/graph/useErrorClearingHooks.test.ts
  • src/composables/graph/useErrorClearingHooks.ts
  • src/composables/maskeditor/useMaskEditorSaver.ts
  • src/composables/painter/usePainter.test.ts
  • src/composables/painter/usePainter.ts
  • src/composables/useCoreCommands.ts
  • src/composables/useFeatureFlags.ts
  • src/composables/useLoad3d.test.ts
  • src/composables/useLoad3d.ts
  • src/extensions/core/saveMesh.test.ts
  • src/extensions/core/saveMesh.ts
  • src/platform/assets/components/Media3DTop.test.ts
  • src/platform/assets/components/Media3DTop.vue
  • src/platform/assets/components/MediaAssetCard.vue
  • src/platform/assets/composables/media/assetMappers.test.ts
  • src/platform/assets/composables/media/assetMappers.ts
  • src/platform/assets/composables/useMediaAssetActions.ts
  • src/platform/assets/schemas/assetSchema.ts
  • src/platform/assets/services/assetService.test.ts
  • src/platform/assets/services/assetService.ts
  • src/platform/assets/utils/assetMetadataUtils.test.ts
  • src/platform/assets/utils/assetMetadataUtils.ts
  • src/platform/assets/utils/assetPreviewUtil.test.ts
  • src/platform/assets/utils/assetPreviewUtil.ts
  • src/platform/assets/utils/markDeletedAssetsAsMissingMedia.test.ts
  • src/platform/assets/utils/markDeletedAssetsAsMissingMedia.ts
  • src/platform/missingMedia/missingMediaAssetResolver.test.ts
  • src/platform/missingMedia/missingMediaAssetResolver.ts
  • src/platform/missingMedia/missingMediaScan.test.ts
  • src/platform/missingMedia/missingMediaScan.ts
  • src/platform/settings/constants/coreSettings.ts
  • src/renderer/extensions/vueNodes/widgets/components/WidgetSelect.asset-mode.test.ts
  • src/renderer/extensions/vueNodes/widgets/components/WidgetSelect.test.ts
  • src/renderer/extensions/vueNodes/widgets/components/WidgetSelect.vue
  • src/renderer/extensions/vueNodes/widgets/components/form/dropdown/FormDropdownMenuItem.test.ts
  • src/renderer/extensions/vueNodes/widgets/components/form/dropdown/FormDropdownMenuItem.vue
  • src/renderer/extensions/vueNodes/widgets/composables/useAssetWidgetData.desktop.test.ts
  • src/renderer/extensions/vueNodes/widgets/composables/useAssetWidgetData.test.ts
  • src/renderer/extensions/vueNodes/widgets/composables/useAssetWidgetData.ts
  • src/renderer/extensions/vueNodes/widgets/composables/useComboWidget.test.ts
  • src/renderer/extensions/vueNodes/widgets/composables/useComboWidget.ts
  • src/schemas/apiSchema.ts
  • src/scripts/app.ts
  • src/stores/assetsStore.test.ts
  • src/stores/assetsStore.ts
  • src/stores/modelStore.test.ts
  • src/stores/modelStore.ts
  • src/stores/workspace/sidebarTabStore.ts
  • src/test-utils/mockFeatureFlags.ts
💤 Files with no reviewable changes (10)
  • src/platform/assets/composables/media/assetMappers.test.ts
  • src/extensions/core/saveMesh.test.ts
  • src/platform/assets/utils/assetPreviewUtil.ts
  • src/platform/settings/constants/coreSettings.ts
  • browser_tests/tests/defaultKeybindings.spec.ts
  • browser_tests/tests/sidebar/modelLibrary.spec.ts
  • browser_tests/tests/propertiesPanel/errorsTabMissingModels.spec.ts
  • src/schemas/apiSchema.ts
  • src/renderer/extensions/vueNodes/widgets/composables/useAssetWidgetData.desktop.test.ts
  • src/platform/assets/composables/media/assetMappers.ts

Comment on lines +128 to +137
const byAriaLabel = node.getByLabel(widgetName, { exact: true })
const widgetLocator =
(await byAriaLabel.count()) > 0
? byAriaLabel
: node
.getByTestId('widget-layout-field-label')
.filter({ hasText: widgetName })
.locator('..')
.locator('button')
.first()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="browser_tests/fixtures/helpers/BuilderSelectHelper.ts"
if [ ! -f "$FILE" ]; then
  echo "Missing file: $FILE" >&2
  exit 1
fi

echo "== Lines 90-170 =="
nl -ba "$FILE" | sed -n '90,170p'
echo
echo "== Extract function selectInputWidget (best-effort) =="
# Try to locate the function body without dumping huge output
rg -n "selectInputWidget" "$FILE" || true
START=$(rg -n "selectInputWidget" "$FILE" | head -n1 | cut -d: -f1 || true)
if [ -n "${START:-}" ]; then
  # Print from start to next closing brace-ish region; keep bounded
  END=$((START+140))
  nl -ba "$FILE" | sed -n "${START},${END}p"
fi

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 151


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="browser_tests/fixtures/helpers/BuilderSelectHelper.ts"
if [ ! -f "$FILE" ]; then
  echo "Missing file: $FILE" >&2
  exit 1
fi

echo "== Showing lines 110-160 =="
awk 'NR>=110 && NR<=160 {printf "%5d\t%s\n", NR, $0}' "$FILE"
echo

echo "== Finding selectInputWidget and widgetLocator usage =="
rg -n "selectInputWidget|widgetLocator" "$FILE" || true
echo

echo "== Finding any widgetLocator.click() usage =="
rg -n "widgetLocator\.click\(" "$FILE" || true
echo

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 2840


Fix strict-mode/ambiguity risks in selectInputWidget locator selection (and avoid extra comment)

  • When byAriaLabel.count() > 0, widgetLocator is set to byAriaLabel without narrowing to a single element; if the label matches multiple nodes, await widgetLocator.click({ force: true }) can throw strict-mode errors.
  • The fallback filter({ hasText: widgetName }) can match the wrong “field-label” when widget names overlap; use exact text matching for the intended row/button.
  • Remove or refactor the explanatory comment block (123–127) to keep the code self-documenting per repo guidelines.
🤖 Prompt for 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.

In `@browser_tests/fixtures/helpers/BuilderSelectHelper.ts` around lines 128 -
137, In selectInputWidget, tighten ambiguous locators: change byAriaLabel to use
.first() (e.g., const byAriaLabel = node.getByLabel(widgetName, { exact: true
}).first()) so it always returns a single locator, and make the fallback filter
use exact text matching for the row/button (e.g., locate the field-label row by
matching the label/button text exactly via getByText(..., { exact: true }) or a
anchored RegExp like ^…$ then .locator('..').locator('button').first()). Remove
the explanatory comment block above this logic so the code remains
self-documenting; update references to byAriaLabel and widgetLocator in the
click call accordingly.

id: 'Comfy.BrowseModelAssets',
icon: 'pi pi-folder-open',
label: 'Experimental: Browse Model Assets',
label: 'Browse Model Assets',
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Localize the new command label via vue-i18n.

This command label is user-facing text and should use a translation key rather than a hardcoded string.

Proposed change
-      label: 'Browse Model Assets',
+      label: () => t('commands.browseModelAssets'),

As per coding guidelines: "Use vue-i18n for ALL user-facing strings, configured in src/locales/en/main.json."

🤖 Prompt for 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.

In `@src/composables/useCoreCommands.ts` at line 1286, The command label is
hardcoded as 'Browse Model Assets'—replace it with a vue-i18n translation call
(use the local t function from useI18n or the i18n instance used in
useCoreCommands) so the command object's label property uses
t('commands.browseModelAssets') (or equivalent) instead of the string, and add
the key "commands.browseModelAssets": "Browse Model Assets" to
src/locales/en/main.json; locate the label in the command definition inside
useCoreCommands and update the import/usage of useI18n if needed to ensure t is
available.

Comment thread src/stores/assetsStore.ts
Comment on lines +632 to +653
async function updateAssetMetadata(
asset: AssetItem,
userMetadata: Record<string, unknown>,
cacheKey?: string
) {
const originalMetadata = asset.user_metadata
updateAssetInCache(asset.id, { user_metadata: userMetadata }, cacheKey)

try {
const updatedAsset = await assetService.updateAsset(asset.id, {
user_metadata: userMetadata
})
updateAssetInCache(asset.id, updatedAsset, cacheKey)
} catch (error) {
console.error('Failed to update asset metadata:', error)
updateAssetInCache(
asset.id,
{ user_metadata: originalMetadata },
cacheKey
)
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Propagate mutation failures to callers after rollback.

updateAssetMetadata and updateAssetTags currently log and swallow
failures. Callers can’t surface an error state/toast or trigger higher-level
recovery. After rollback/compensation, rethrow (or return a typed failure
result).

💡 Suggested change
     async function updateAssetMetadata(
       asset: AssetItem,
       userMetadata: Record<string, unknown>,
       cacheKey?: string
     ) {
       const originalMetadata = asset.user_metadata
       updateAssetInCache(asset.id, { user_metadata: userMetadata }, cacheKey)

       try {
         const updatedAsset = await assetService.updateAsset(asset.id, {
           user_metadata: userMetadata
         })
         updateAssetInCache(asset.id, updatedAsset, cacheKey)
       } catch (error) {
         console.error('Failed to update asset metadata:', error)
         updateAssetInCache(
           asset.id,
           { user_metadata: originalMetadata },
           cacheKey
         )
+        throw error instanceof Error
+          ? error
+          : new Error('Failed to update asset metadata')
       }
     }
@@
     async function updateAssetTags(
       asset: AssetItem,
       newTags: string[],
       cacheKey?: string
     ) {
@@
       } catch (error) {
         console.error('Failed to update asset tags:', error)
         updateAssetInCache(asset.id, { tags: originalTags }, cacheKey)
@@
         }
+        throw error instanceof Error
+          ? error
+          : new Error('Failed to update asset tags')
       }
     }

As per coding guidelines: "Implement proper error propagation".

Also applies to: 661-725

🤖 Prompt for 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.

In `@src/stores/assetsStore.ts` around lines 632 - 653, The functions
updateAssetMetadata (and similarly updateAssetTags) currently catch errors,
perform a rollback via updateAssetInCache, and then swallow the error; change
them to rethrow the original error (or return a typed failure) after the
rollback so callers can handle/display the failure. Concretely, inside the catch
block for updateAssetMetadata (the assetService.updateAsset call and its
updateAssetInCache rollback) preserve the caught error and throw it (or return a
standardized error result) after restoring originalMetadata; apply the same
pattern to updateAssetTags where assetService.updateAssetTags and its rollback
via updateAssetInCache are handled.

…ase B)

`getAssetDetectionNames` previously short-circuited on `asset.file_path`,
returning only that key when emitted. That premise — `file_path` as the
primary match key — doesn't match RFC BE-808 v2: §3 names `id` as the
identity field and §4 explicitly calls `file_path` "a standalone
locator/display string" emitted on a BEST EFFORT basis.

Workflow widget values predate the BE-933 / BE-934 `file_path` rollout
and may still be bare filenames, hashes, or annotated paths. A pre-BE
workflow paired with a post-BE asset would false-negative under the
early-return: the asset emits `file_path: "input/sub/photo.png"` while
the widget value is still `"photo.png"`, and the previous code would
mark the reference missing even though the file is present.

Union `file_path`, `asset_hash`, `name`, and `subfolder + name` variants
so workflows authored against any historical asset shape keep matching.
Both backends round-trip `name` through the BE-792 deprecation window,
so the legacy keys stay valid for the duration of the rollout.

Adversarial review (Codex) flagged the false-negative; RFC re-read
confirmed `id` (not `file_path`) is the identity field. Plan and
docblock terminology updated to drop the misleading "canonical match
key" framing.

Adds a regression test for the bare-filename × file_path-emitting asset
case.
…se.allSettled)

`resolveMissingMediaAssetSources` previously ran the input-asset fetch
(`/api/assets`) and the generated-asset fetch (Cloud `/api/assets` or
OSS `/history`) under `Promise.all` with `abortSiblingsOnFailure`, so a
single rejection took down both. With the FE-746 unification, the input
fetch now always hits `assetService.getInputAssetsIncludingPublic` —
including against backends that may legitimately fail (pre-BE-786 OSS
without `/api/assets`, BE-934 partial deploys, transient network errors).

Switch to `Promise.allSettled` and per-branch soft-degrade. Each oracle
is independent; failure in one shouldn't cancel the other. On rejection,
return an empty list from that branch; abort errors stay silent, real
errors get a one-line console warning so we can spot them without
swallowing the rest of the verification.

Caller (`verifyMediaCandidates`) already treats an empty asset list as
"nothing matches this candidate," which surfaces affected candidates as
missing rather than silently dropping them via the previous toast-warn
path. False-positive missing is recoverable (user sees the issue in the
Errors tab); silent-fail was not.

Adversarial review (Codex) flagged the cascade. RFC §4's BEST EFFORT
framing for `file_path` already implies the contract has soft-fail
expectations, so this aligns the resolver shape with the RFC's stated
tolerance.

Removes the now-unused `abortSiblingsOnFailure` helper. Replaces the
abort-cascade regression test with two independence tests covering each
oracle failing in isolation.
…ests (FE-746)

Adds an e2e regression spec for the FE-746 behavior changes that unit
tests can only assert with mocks:

1. Case B union — a workflow stored before BE-933/934 keeps its widget
   value as a bare filename. Once the backend starts emitting `file_path`
   on the same asset (a namespace-rooted locator that diverges from
   `name`), detection must still match via the `name` arm. The
   pre-correction Case A early-return would false-negative here.
2. BE-933 hash-only registration — assets registered via
   POST /assets/from-hash come back with `file_path: null` and
   `display_name: null`. The legacy `name` arm has to keep working
   through the BE-792 deprecation window.
3. Negative path — a listing that does not cover the widget value via
   any key still surfaces the missing-media overlay. Guards against an
   accidental "match everything" regression when the early-return was
   removed.
4. Soft-degrade — when `/api/assets` returns 500 (pre-BE-786 OSS without
   the endpoint, partial BE-934 deploys, transient errors),
   `Promise.allSettled` lets the verifier finish and mark the candidate
   missing instead of leaving `isMissing` stuck at undefined behind a
   silent toast.

`Asset` from `@comfyorg/ingest-types` does not yet carry `file_path` /
`display_name` (regen is BE-932). Spec extends the type locally so the
mocked wire shape stays strongly typed.

The companion workflow asset is the minimum LoadImage graph needed to
fire the missing-media pipeline; widget value is a fixed sentinel
filename used by every test.
# Conflicts:
#	src/platform/assets/utils/assetMetadataUtils.test.ts
#	src/platform/assets/utils/assetMetadataUtils.ts
Remove isCloud guards from the filter bar, asset browser modal, sidebar
delete site, context-menu delete site, and input-asset delete throw —
all five UI surfaces enumerated in FE-732 now render and execute
unconditionally against the unified /api/assets surface.

Drop the dead cloud-only docstring on AssetBrowserModal.overrideAssets
and the dead shouldShowDeleteButton computed in AssetsSidebarTab (the
latter was the last caller of the :show-delete-button pass-through).
Fix the latent Vue Boolean prop-default-false trap in
MediaAssetContextMenu by defaulting showDeleteButton to true via
destructure default; the previous "propAllows = showDeleteButton ?? true"
never fired because Vue normalizes absent Boolean props to false. The
bug was masked while AssetsSidebarTab explicitly passed the prop.

Drop the now-unused mediaAsset.deletingImportedFilesCloudOnly i18n
key from all 12 locales.

Tests: add OSS + Cloud input-asset deletion paths in
useMediaAssetActions, add input-mode delete visibility coverage in
MediaAssetContextMenu, add an @oss describe block in
assetDeleteClearsLoadImage so both Playwright projects run the
deletion flow. Refresh stale cloud-only comments in the filter and
sort sidebar specs; their @cloud tags stay because /api/jobs is
still cloud-only.

L4 residual: filter-bar query params job_ids, asset_hash, and
include_public will start being accepted by OSS once BE-886 and
BE-891 merge — no FE change needed at that point.

Stacked on top of FE-731. Depends on BE-786 (OSS removes
--enable-assets) before this can ship to production.

- Fixes FE-732
Lock the post-FE-732 contracts surfaced by the latent Boolean
prop-default-true fix and the unconditional filter rendering:

- MediaAssetContextMenu.test.ts — add an explicit
  showDeleteButton: false case so the opt-out path of the prop is
  asserted, complementing the input-mode default-true case from the
  parent commit. Reshape mountComponent to take an opts object.

- MediaAssetFilterBar.test.ts — new component test verifies the
  filter button renders unconditionally and that the settings menu
  receives show-sort-options=true after the constant pin.
The pre-FE-732 sidebar logic hid the bulk Delete footer button on the
Imported tab when isCloud was false. After removing the gate the button
must render in both Cloud and OSS builds. Add a Playwright case that
runs on the default chromium project — i.e. the OSS build — to lock the
new invariant, mirroring the existing output-asset case in the same
describe block.
…ode dropdown open (FE-732 fix) (#12465)

https://github.com/user-attachments/assets/1baf8283-8170-4d50-aa22-25c05598874d



Stacks on #12417 (FE-732). The isCloud guard removal in the M1 stack
(FE-731/FE-732) exposed two latent regressions on the inline
`FormDropdown` asset path; this PR fixes both.

## Symptoms (verified via CDP against `pr-3809.testenvs.comfy.org` cloud
BE and a local `synap5e/assets-m1 --enable-assets` OSS BE)

Reported by Simon in #m1-fe-integration testing against PR #12411:

1. **Model dropdown stale list** — `r` to refresh doesn't update the
dropdown; the model list is fetched once on first dropdown open
(`/api/assets?include_tags=models,checkpoints&limit=500&exclude_tags=missing`)
and cached. Reopening shows stale data. Newly imported models don't
appear until full page reload.
2. **`/api/jobs` per dropdown open** — each model dropdown open hits
`/api/jobs?status=completed,failed,cancelled&limit=200&offset=0` for no
reason. This is the OSS history path being triggered by the output-media
refresh hook in the dropdown.

## Root cause

- `useAssetWidgetData.watch(immediate:true)` is the only trigger for
model fetch; once `assetsStore.hasAssetKey(...)` returns true, the watch
short-circuits forever. Dropdown reopen has no refresh hook.
- `WidgetSelectDropdown.handleIsOpenUpdate` calls
`outputMediaAssets.refresh()` on every open regardless of widget kind.
For asset-mode (model) dropdowns this is irrelevant — but the call still
routes through `useAssetsApi('output')` → `assetsStore.updateHistory` →
`api.getHistory` → `/jobs?status=completed,failed,cancelled` on OSS.
Cloud distribution swaps the provider for `useFlatOutputAssets` which
hits `/api/assets?include_tags=output` — still wasted work, just a
different URL.

The handler and cache guard are pre-existing (#6734, #8090), but were
only reachable from cloud distributions before FE-731 unwrapped `if
(isCloud)` in `useAssetWidgetData`.

## Fix

- `useAssetWidgetData`: expose `refresh()` that re-invokes
`assetsStore.updateModelsForNodeType` for the current node type, guarded
against re-entry while a fetch is in flight.
- `WidgetSelectDropdown.handleIsOpenUpdate`: branch on
`props.isAssetMode` — asset-mode dropdowns refresh model assets via
`assetData.refresh()`; non-asset-mode dropdowns continue to refresh
`outputMediaAssets` as before (preserved cloud / OSS behavior).

## Verification

**CDP — Vue Nodes 2.0 ON**

Same dropdown opened twice (`qwen_image_vae` VAELoader widget):

| Open # | `/api/jobs?status=...` |
`/api/assets?include_tags=models,vae` |
|---|---|---|
| 1st | 0 | 1 (fresh fetch) |
| 2nd | 0 | 1 (fresh fetch — newly added models would appear) |

**Before the fix** (same setup): 1st open fired both `/api/jobs` and the
model fetch; 2nd open fired only `/api/jobs` (model list stale).

**Other paths preserved (verified empirically + by code)**:
- OSS + non-asset-mode dropdown → `/api/jobs` still fires (existing OSS
behavior).
- Cloud + non-asset-mode dropdown → `/api/assets?include_tags=output`
still fires (existing cloud behavior; branch in
`WidgetSelectDropdown.vue` outputMediaAssets ternary is untouched).
- WS-status queue/history polling (`limit=64` `/api/jobs`) still fires
on page load — unrelated to dropdown.

## Test plan

- [x] `pnpm vitest run
src/renderer/extensions/vueNodes/widgets/composables/useAssetWidgetData.test.ts`
— 10/10 (3 new tests for `refresh()`).
- [x] `pnpm vitest run
src/renderer/extensions/vueNodes/widgets/components/WidgetSelectDropdown.test.ts`
— 9/9 (4 new tests cover handleIsOpenUpdate branches: asset-mode skips
outputMediaAssets.refresh, asset-mode reopen refetches model assets,
non-asset-mode preserves outputMediaAssets.refresh, close event is
no-op).
- [x] `pnpm typecheck` clean.
- [ ] Re-test in `m1-fe-integration` once stacked PRs merge: import a
new model via cloud import flow → reopen dropdown → new model appears
without page reload.

Uploading Screen Recording 2026-05-27 at 12.43.13 AM.mov…
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants