-
Notifications
You must be signed in to change notification settings - Fork 411
fix: dropdown widget fetching output files #6734
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
fix: dropdown widget fetching output files #6734
Conversation
🎭 Playwright Test Results⏰ Completed at: 11/21/2025, 10:23:10 PM UTC 📈 Summary
📊 Test Reports by Browser
🎉 Click on the links above to view detailed test results for each browser configuration. |
🎨 Storybook Build Status✅ Build completed successfully! ⏰ Completed at: 11/21/2025, 10:12:41 PM UTC 🔗 Links🎉 Your Storybook is ready for review! |
Bundle Size ReportSummary
Category Glance Per-category breakdownApp Entry Points — 3.13 MB (baseline 3.13 MB) • 🔴 +58 BMain entry bundles and manifests
Status: 3 added / 3 removed Graph Workspace — 945 kB (baseline 945 kB) • ⚪ 0 BGraph editor runtime, canvas, workflow orchestration
Status: 1 added / 1 removed Views & Navigation — 7.97 kB (baseline 7.97 kB) • ⚪ 0 BTop-level views, pages, and routed surfaces
Status: 1 added / 1 removed Panels & Settings — 306 kB (baseline 306 kB) • ⚪ 0 BConfiguration panels, inspectors, and settings screens
Status: 6 added / 6 removed UI Components — 141 kB (baseline 141 kB) • 🔴 +579 BReusable component library chunks
Status: 6 added / 6 removed Data & Services — 12.5 kB (baseline 12.5 kB) • ⚪ 0 BStores, services, APIs, and repositories
Status: 2 added / 2 removed Utilities & Hooks — 2.94 kB (baseline 2.94 kB) • ⚪ 0 BHelpers, composables, and utility bundles
Status: 1 added / 1 removed Vendor & Third-Party — 5.32 MB (baseline 5.32 MB) • ⚪ 0 BExternal libraries and shared vendor chunks
Other — 3.87 MB (baseline 3.87 MB) • ⚪ 0 BBundles that do not match a named category
Status: 18 added / 18 removed |
src/stores/outputsStore.ts
Outdated
| }) | ||
|
|
||
| if (!response.ok) { | ||
| console.error('Failed to fetch output files:', response.statusText) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[architecture] medium Priority
Issue: Missing error handling for API failures - store returns empty array on error
Context: Users won't know when output file fetching fails, and dropdown will appear empty
Suggestion: Add error state tracking and user notification for fetch failures
src/stores/outputsStore.ts
Outdated
|
|
||
| const outputImages = computed(() => { | ||
| return outputs.value.filter((filename) => { | ||
| const ext = filename.toLowerCase().split('.').pop() || '' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[performance] low Priority
Issue: Inefficient file extension parsing with repeated split operations
Context: Each computed property calls split('.').pop() for every file, causing O(n*m) complexity
Suggestion: Pre-process file extensions once or use more efficient parsing like filename.lastIndexOf('.')
src/stores/outputsStore.ts
Outdated
| return ['png', 'jpg', 'jpeg', 'webp', 'gif', 'bmp', 'tiff'].includes(ext) | ||
| }) | ||
| }) | ||
| const outputVideos = computed(() => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[quality] medium Priority
Issue: Code duplication across outputImages, outputVideos, and outputAudios computed properties
Context: Same filtering logic repeated three times, violating DRY principle
Suggestion: Extract to a helper function: getFilesByExtensions(extensions) or use a shared utility
src/stores/outputsStore.ts
Outdated
| } | ||
| } | ||
|
|
||
| const fetchOutputFiles = async () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[architecture] high Priority
Issue: Potential race condition with promise caching mechanism
Context: If multiple components call fetchOutputFiles() while promise is resolving, they all get the same promise but thePromise.value is set to null in finally block
Suggestion: Consider using a more robust caching mechanism or ensure promise is cleared only after all consumers are notified
| } | ||
| // Fetch output files on component mount | ||
| onMounted(() => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[performance] medium Priority
Issue: Unconditional output files fetch on component mount regardless of widget type
Context: Every dropdown widget will fetch output files even if not needed (non-media widgets)
Suggestion: Only fetch output files when assetKind indicates media types (image/video/audio)
| import { api } from '@/scripts/api' | ||
| import { useAssetsStore } from '@/stores/assetsStore' | ||
| import { useQueueStore } from '@/stores/queueStore' | ||
| import { useOutputsStore } from '@/stores/outputsStore' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[architecture] critical Priority
Issue: Breaking change - removed queue store dependency without data migration
Context: Old implementation extracted outputs from queueStore.historyTasks but new implementation uses fresh API calls, potentially losing user data
Suggestion: Add migration path or ensure existing workflows aren't broken
| } | ||
| })() | ||
| return outputFiles.map((filename, index) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[architecture] medium Priority
Issue: Simplified output handling may not preserve subfolder structure from original queue-based implementation
Context: Old code constructed paths with subfolder info, new code uses direct filenames from API
Suggestion: Verify API returns files with full path or add subfolder handling
| class="icon-[lucide--circle-off] size-30 text-zinc-500/20" | ||
| /> | ||
| </div> | ||
| <template v-if="items.length === 0"> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[quality] low Priority
Issue: Template structure change from div to template is good but missing semantic improvement
Context: Using template wrapper is more performant than div for conditional rendering
Suggestion: Consider adding proper ARIA attributes or role for better accessibility
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Comprehensive PR Review
This review is generated by Claude. It may not always be accurate, as with human reviewers. If you believe that any of the comments are invalid or incorrect, please state why for each. For others, please implement the changes in one way or another.
Review Summary
PR: fix: dropdown widget fetching output files (#6734)
Impact: 113 additions, 38 deletions across 3 files
Focus: Refactoring dropdown widget to use dedicated outputs store instead of queue store
Issue Distribution
- Critical: 1
- High: 1
- Medium: 4
- Low: 2
Category Breakdown
- Architecture: 5 issues
- Code Quality: 2 issues
- Performance: 1 issues
- Security: 0 issues
Key Findings
Architecture & Design
Critical: The migration from queue store to outputs store represents a breaking change that may lose existing user data or functionality. The old implementation extracted outputs from queue history with subfolder structure, while the new approach uses direct API calls.
High Priority: Race condition potential in the outputs store promise caching mechanism could lead to inconsistent state when multiple components fetch simultaneously.
Medium Priority: The new outputs store lacks proper error handling, which means users won't be notified when API calls fail, resulting in empty dropdowns without explanation.
Performance Impact
The unconditional fetching of output files on component mount for all dropdown widgets is inefficient - this should only happen for media-type widgets. The file extension filtering also uses inefficient string operations that could be optimized.
Code Quality
Several areas need improvement: code duplication in file type filtering logic, inconsistent type handling between string/number widget values and string-only output files, and missing proper error states.
Positive Observations
- Good architectural direction separating concerns with dedicated outputs store
- Improved template structure using template instead of div for conditional rendering
- Clean separation of file types with computed properties
- Proper TypeScript usage with explicit type definitions
References
- ComfyUI Frontend Repository Guide
- Vue 3 Best Practices
- Pinia Store Patterns
Next Steps
- Critical: Address the breaking change concern - ensure data migration path exists
- High: Fix race condition in outputs store promise handling
- Medium: Add proper error handling and user feedback for API failures
- Medium: Optimize performance by conditional fetching based on widget type
- Add tests to verify output file fetching behavior
- Consider backward compatibility implications
Testing Recommendations
- Test dropdown behavior with and without existing output files
- Verify subfolder structure preservation
- Test error scenarios (API failures, network issues)
- Verify performance with large numbers of output files
- Test concurrent widget mounting scenarios
This is a comprehensive automated review. For architectural decisions requiring human judgment, please request additional manual review.
| <div class="absolute inset-0 flex items-center justify-center"> | ||
| <i | ||
| title="No items" | ||
| class="icon-[lucide--circle-off] size-30 text-zinc-500/20" | ||
| /> | ||
| </div> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
note: We also have this
| <template #content> |
(not blocking this PR).
src/stores/outputsStore.ts
Outdated
| const outputImages = computed(() => { | ||
| return outputs.value.filter((filename) => { | ||
| const ext = filename.toLowerCase().split('.').pop() || '' | ||
| return ['png', 'jpg', 'jpeg', 'webp', 'gif', 'bmp', 'tiff'].includes(ext) | ||
| }) | ||
| }) | ||
| const outputVideos = computed(() => { | ||
| return outputs.value.filter((filename) => { | ||
| const ext = filename.toLowerCase().split('.').pop() || '' | ||
| return ['mp4', 'webm', 'mov', 'avi', 'mkv'].includes(ext) | ||
| }) | ||
| }) | ||
| const outputAudios = computed(() => { | ||
| return outputs.value.filter((filename) => { | ||
| const ext = filename.toLowerCase().split('.').pop() || '' | ||
| return ['mp3', 'ogg', 'wav', 'flac'].includes(ext) | ||
| }) | ||
| }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you think it could be optimized to only need a single traversal?
src/stores/outputsStore.ts
Outdated
| import { defineStore } from 'pinia' | ||
| import { computed, ref } from 'vue' | ||
|
|
||
| export const useOutputsStore = defineStore('outputs', () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/src/platform/assets/composables/media/useMediaAssets.ts
const outputAssets = useMediaAssets('output')
It looks like most of the code in src/stores/outputsStore.ts could be replaced with something like const outputAssets = useMediaAssets('output'). I think it would be better to use the existing store. What do you think?
| outputs.add(annotatedPath) | ||
| } | ||
| }) | ||
| const outputFiles = ((): string[] => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/src/platform/assets/composables/useMediaAssetFiltering.ts#L62
And you can reuse this format utility!
viva-jinyi
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It doesn’t seem to cover both Cloud and OSS. Since internal/files can only fetch output files in the OSS environment, I think it would be better to use useMediaAssets.ts. It’s already implemented to work for both OSS and Cloud, and we can update it later when the new API becomes available.
|
I tried this API, but it doesn't work. I don't know why. you can see this image, so I use the fetch to get the list However, I still did it according to your request. You can see it here #6809, and you can actually run the code to compare their differences.
|
…iles` (#6809) ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-6809-refactor-use-media-assets-in-fix-dropdown-widget-fetching-output-files-2b26d73d365081139a7af77a79693010) by [Unito](https://www.unito.io)
|
Note Other AI code review bot(s) detectedCodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review. 📝 WalkthroughWalkthroughOutput dropdown handling refactored to use media assets-based approach with client-side type filtering. FormDropdown's local state replaced with Vue defineModel for two-way binding. FormDropdownMenu's DOM structure adjusted for "No items" rendering. Changes
Sequence DiagramsequenceDiagram
participant User
participant WidgetSelectDropdown
participant useMediaAssets
participant Asset Store
User->>WidgetSelectDropdown: Opens dropdown
WidgetSelectDropdown->>WidgetSelectDropdown: handleIsOpenUpdate triggered
WidgetSelectDropdown->>useMediaAssets: Fetch media assets
useMediaAssets->>Asset Store: Query assets
Asset Store-->>useMediaAssets: Return asset list
useMediaAssets-->>WidgetSelectDropdown: Provide media assets
WidgetSelectDropdown->>WidgetSelectDropdown: Filter by media type<br/>(image/video/audio/model)
WidgetSelectDropdown->>WidgetSelectDropdown: Map to AssetKind<br/>via toAssertType
WidgetSelectDropdown->>WidgetSelectDropdown: Build output items<br/>with preview URLs
WidgetSelectDropdown-->>User: Render dropdown with<br/>filtered outputs
✨ Finishing touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
🧹 Nitpick comments (2)
src/renderer/extensions/vueNodes/widgets/components/form/dropdown/FormDropdown.vue (1)
58-58: Switch todefineModelforisOpenlooks good; consider external control behaviorUsing
defineModel('isOpen')cleanly exposes the open state so parents can observe it (as you do with@update:is-open), while keeping internal toggling logic unchanged.If you ever intend to let parents control the open state via
v-model:is-open, you'll likely want a small watcher that hides/shows thePopoverwhenisOpenchanges externally; right now, external updates wouldn’t drive the popover visibility, only internal handlers do.src/renderer/extensions/vueNodes/widgets/components/WidgetSelectDropdown.vue (1)
348-352: Open-state refresh hook is good; tighten conditions and checkloadingtypeWiring
handleIsOpenUpdateto@update:is-openis a nice way to refresh outputs only when the dropdown opens. A couple of details to tighten:
If
outputMediaAssets.loadingis aRef<boolean>, this check should use.value; otherwise it will always be truthy:if (isOpen && !outputMediaAssets.loading.value) { outputMediaAssets.refresh() }To fully honor the earlier feedback about avoiding unnecessary output fetches for non-media widgets, consider short-circuiting when
assetKinddoesn’t need outputs (e.g., not image/video/audio/model):function handleIsOpenUpdate(isOpen: boolean) {if (isOpen && !outputMediaAssets.loading) {
outputMediaAssets.refresh()
}
- const kind = props.assetKind
- if (
- !isOpen ||
- !kind ||
- !['image', 'video', 'audio', 'model'].includes(kind)
- ) {
- return
- }
- if (!outputMediaAssets.loading /* or .loading.value if it's a ref */) {
- outputMediaAssets.refresh()
- }
}This keeps the refresh localized to dropdown-open events and avoids hitting the outputs API for non-media widgets. Also applies to: 371-371 </blockquote></details> </blockquote></details> <details> <summary>📜 Review details</summary> **Configuration used**: CodeRabbit UI **Review profile**: CHILL **Plan**: Pro <details> <summary>📥 Commits</summary> Reviewing files that changed from the base of the PR and between 7a0302ba7ab1fc2a3f6c437ab6c3688cd8951c1d and 705947dfd7e41e80e88958979ef7af21da8c8acc. </details> <details> <summary>📒 Files selected for processing (3)</summary> * `src/renderer/extensions/vueNodes/widgets/components/WidgetSelectDropdown.vue` (6 hunks) * `src/renderer/extensions/vueNodes/widgets/components/form/dropdown/FormDropdown.vue` (1 hunks) * `src/renderer/extensions/vueNodes/widgets/components/form/dropdown/FormDropdownMenu.vue` (1 hunks) </details> <details> <summary>🧰 Additional context used</summary> <details> <summary>🪛 ESLint</summary> <details> <summary>src/renderer/extensions/vueNodes/widgets/components/WidgetSelectDropdown.vue</summary> [error] 8-8: Unable to resolve path to module '@/platform/assets/composables/media/useMediaAssets'. (import-x/no-unresolved) --- [error] 25-25: Unable to resolve path to module '@/utils/formatUtil'. (import-x/no-unresolved) </details> </details> </details> <details> <summary>⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)</summary> * GitHub Check: test * GitHub Check: lint-and-format * GitHub Check: setup </details> <details> <summary>🔇 Additional comments (2)</summary><blockquote> <details> <summary>src/renderer/extensions/vueNodes/widgets/components/form/dropdown/FormDropdownMenu.vue (1)</summary><blockquote> `70-78`: **Empty-state template restructuring looks good** Using a `<template v-if>` for the “No items” state plus the `min-h-50` spacer keeps the grid layout stable while avoiding an extra wrapper element. No functional or readability issues here. </blockquote></details> <details> <summary>src/renderer/extensions/vueNodes/widgets/components/WidgetSelectDropdown.vue (1)</summary><blockquote> `8-9`: **Import paths are correctly configured via vite aliases; review comment is incorrect.** The imports in WidgetSelectDropdown.vue are properly resolved. The vite configuration (vite.config.mts, lines 685–687) contains an alias that maps `@/utils/formatUtil` to `/packages/shared-frontend-utils/src/formatUtil.ts`, where `getMediaTypeFromFilename` is exported. Similarly, `useMediaAssets` and `useToastStore` both reference valid file paths that exist in the codebase. The ESLint import-x/no-unresolved errors referenced in the review appear to be false positives, likely due to ESLint not being configured to understand vite aliases. The imports themselves are not unresolved and follow the established pattern in the codebase. > Likely an incorrect or invalid review comment. </blockquote></details> </blockquote></details> </details> <!-- This is an auto-generated comment by CodeRabbit for review status -->
| if (!['image', 'video', 'audio'].includes(props.assetKind ?? '')) return [] | ||
| // Filter assets by media type using getMediaTypeFromFilename | ||
| const outputFiles = outputMediaAssets.media.value.filter((asset) => { | ||
| const mediaType = getMediaTypeFromFilename(asset.name) | ||
| return toAssertType(mediaType) === props.assetKind | ||
| }) | ||
| return Array.from(outputs).map((output, index) => ({ | ||
| id: `output-${index}`, | ||
| mediaSrc: getMediaUrl(output.replace(' [output]', ''), 'output'), | ||
| name: output, | ||
| label: getDisplayLabel(output), | ||
| metadata: '' | ||
| })) | ||
| return outputFiles.map((asset, index) => { | ||
| // Add [output] annotation so the preview component knows the type | ||
| const annotatedPath = `${asset.name} [output]` | ||
| return { | ||
| id: `output-${index}`, | ||
| mediaSrc: asset.preview_url || getMediaUrl(asset.name, 'output'), | ||
| name: annotatedPath, | ||
| label: getDisplayLabel(annotatedPath), | ||
| metadata: '' | ||
| } | ||
| }) | ||
| }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
outputItems guard prevents model outputs from ever showing
In outputItems, this guard:
if (!['image', 'video', 'audio'].includes(props.assetKind ?? '')) return []short-circuits for assetKind === 'model', even though toAssertType explicitly maps a '3D' media type to 'model'. That means 3D/model outputs will never appear in the dropdown, despite having logic to support them.
A minimal fix is to include 'model' in the allowed list so those outputs can be surfaced:
-const outputItems = computed<DropdownItem[]>(() => {
- if (!['image', 'video', 'audio'].includes(props.assetKind ?? '')) return []
+const outputItems = computed<DropdownItem[]>(() => {
+ if (!['image', 'video', 'audio', 'model'].includes(props.assetKind ?? '')) {
+ return []
+ }
@@
- const outputFiles = outputMediaAssets.media.value.filter((asset) => {
- const mediaType = getMediaTypeFromFilename(asset.name)
- return toAssertType(mediaType) === props.assetKind
- })
+ const assetKind = props.assetKind
+
+ const outputFiles = outputMediaAssets.media.value.filter((asset) => {
+ const mediaType = getMediaTypeFromFilename(asset.name)
+ return assetKind != null && toAssertType(mediaType) === assetKind
+ })That keeps the filtering behavior but actually allows model/3D outputs to show up. If assetKind: 'unknown' should also see outputs, you may want to relax the guard further.
Also applies to: 145-158, 340-346
I'll help you verify the review comment about the WidgetSelectDropdown.vue file. Let me examine the code and understand the context.
---
🏁 Script executed:
```shell
#!/bin/bash
# Examine the target file
cat src/renderer/extensions/vueNodes/widgets/components/WidgetSelectDropdown.vue | head -200
Length of output: 5887
🏁 Script executed:
#!/bin/bash
# Search for toAssertType function definition
rg -n "toAssertType" --type=ts --type=js --type=vue -A 5 -B 2Length of output: 27
🏁 Script executed:
#!/bin/bash
# Search for getMediaTypeFromFilename to understand media type mapping
rg -n "getMediaTypeFromFilename" --type=ts --type=js --type=vue -A 5 -B 2Length of output: 27
🏁 Script executed:
#!/bin/bash
# Look for assetKind prop definition
rg -n "assetKind" src/renderer/extensions/vueNodes/widgets/components/WidgetSelectDropdown.vue -B 3 -A 3Length of output: 1553
---
🏁 Script executed:
```shell
#!/bin/bash
# Find AssetKind type definition
rg -n "type AssetKind" -A 5 -B 2
Length of output: 311
🏁 Script executed:
#!/bin/bash
# Get full context around getMediaUrl function
sed -n '338,347p' src/renderer/extensions/vueNodes/widgets/components/WidgetSelectDropdown.vueLength of output: 240
🏁 Script executed:
#!/bin/bash
# Get the complete outputItems and toAssertType functions
sed -n '123,158p' src/renderer/extensions/vueNodes/widgets/components/WidgetSelectDropdown.vueLength of output: 1047
Fix guards to allow model outputs to display
The outputItems computed property currently returns empty for any assetKind not in ['image', 'video', 'audio']. However, toAssertType explicitly maps the '3D' media type to 'model' (line 145), and AssetKind type definition includes 'model' as valid, yet the guard at line 124 prevents model outputs from ever being processed.
Additionally, getMediaUrl (line 344) has a similar guard that excludes both 'audio' and 'model' from media URL generation.
Required fixes:
- Line 124: Add
'model'to the allowed asset kinds inoutputItems - Line 344: Update
getMediaUrlguard to include'audio'and'model'(or make it comprehensive based on intended support)
The suggested diff in the original review for line 124 is appropriate. Apply similar logic to the getMediaUrl guard to ensure model assets can display media previews.
🤖 Prompt for AI Agents
In src/renderer/extensions/vueNodes/widgets/components/WidgetSelectDropdown.vue
around lines 124-143 and line ~344, the outputItems computed property and
getMediaUrl guard currently exclude 'model' (and getMediaUrl also excludes
'audio'), preventing model outputs from being processed/displayed; update the
guard at ~124 to include 'model' in the allowed asset kinds (e.g.,
['image','video','audio','model']) and adjust the getMediaUrl guard at ~344 to
include 'audio' and 'model' (or broaden the guard to allow all AssetKind values
your UI supports) so model assets are passed through to preview URL generation
and rendering.

related #5827 (comment)
┆Issue is synchronized with this Notion page by Unito