Skip to content
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
8c74a03
fix: open assets tab when QPO V2 enabled
benceruleanlu Jan 23, 2026
9fb11a1
fix: toggle assets tab from queue button
benceruleanlu Jan 23, 2026
fd07245
Merge remote-tracking branch 'origin/main' into fix/qpo-v2-queue-butt…
benceruleanlu Jan 23, 2026
7609554
test: cover queue button behavior by QPO setting
benceruleanlu Jan 23, 2026
c63e27f
Add inline progress text and progress bar
benceruleanlu Jan 23, 2026
76b7137
Adjust spacing
benceruleanlu Jan 23, 2026
ce86b53
Fix location when floating
benceruleanlu Jan 23, 2026
aef4ddb
Perfectly align inline progress
benceruleanlu Jan 23, 2026
c60114f
fix: anchor floating inline progress summary
benceruleanlu Jan 23, 2026
aea78da
Add stacked button to list view
benceruleanlu Jan 23, 2026
8a50238
Move stacked button in list view correctly
benceruleanlu Jan 23, 2026
282f94a
Add stacked functionality to list view
benceruleanlu Jan 23, 2026
1e5e7f2
Fix zoom in
benceruleanlu Jan 23, 2026
ed4a230
[automated] Apply ESLint and Oxfmt fixes
actions-user Jan 23, 2026
82bc727
Extract useOutputStacks
benceruleanlu Jan 23, 2026
6f2b8d2
feat: centralize output asset resolution
benceruleanlu Jan 24, 2026
11e9cd3
Merge remote-tracking branch 'origin/bl-stacked-assets' into bl-stack…
benceruleanlu Jan 24, 2026
f55c621
knip
benceruleanlu Jan 24, 2026
b23d910
Collapse identical computed
benceruleanlu Jan 24, 2026
4cfafc5
Minor redundancy refactor
benceruleanlu Jan 24, 2026
832546a
Add type cast
benceruleanlu Jan 24, 2026
f9583dc
fix: keep selection consistent with visible assets
benceruleanlu Jan 24, 2026
0debd00
Add more testing
benceruleanlu Jan 24, 2026
bb2e9fb
Don't clear selected on view switch
benceruleanlu Jan 24, 2026
bafb13d
Deduplicate types
benceruleanlu Jan 24, 2026
a908193
Rely on asset IDs for selection state
benceruleanlu Jan 24, 2026
e909875
Add missing anchor clear test case
benceruleanlu Jan 24, 2026
20d6b43
Add aria-expanded
benceruleanlu Jan 24, 2026
82a30bc
Deduplicate some code
benceruleanlu Jan 24, 2026
2bbd7ad
test: fix job output cache instance check
benceruleanlu Jan 24, 2026
9becf66
fix: tighten result item guard
benceruleanlu Jan 25, 2026
cfed135
fix: include subfolder in output asset key
benceruleanlu Jan 24, 2026
c33ab46
fix: use toRef for story args
benceruleanlu Jan 25, 2026
9f4e82f
Update src/platform/assets/components/AssetsListItem.vue
benceruleanlu Jan 25, 2026
03535c8
Merge remote-tracking branch 'origin/bl-stacked-assets' into bl-stack…
benceruleanlu Jan 25, 2026
f0e65a3
Merge remote-tracking branch 'origin/main' into bl-stacked-assets
benceruleanlu Jan 29, 2026
6e5900d
Fix key usage
benceruleanlu Jan 30, 2026
0876efe
Centraliza output key
benceruleanlu Jan 30, 2026
32823d5
nit
benceruleanlu Jan 30, 2026
b7a320f
fix: type stack toggle as async
benceruleanlu Jan 30, 2026
68341a0
refactor: use function declaration for asset selection
benceruleanlu Jan 30, 2026
1e50b90
Update src/services/jobOutputCache.ts
benceruleanlu Jan 30, 2026
472733e
fix: allow output key nodeId 0
benceruleanlu Jan 30, 2026
80598a4
Merge remote-tracking branch 'origin/bl-stacked-assets' into bl-stack…
benceruleanlu Jan 30, 2026
4b90292
Merge remote-tracking branch 'origin/main' into bl-stacked-assets
benceruleanlu Jan 30, 2026
10fec20
fix: hide active jobs in folder view
benceruleanlu Jan 30, 2026
b32c977
fix: keep root output subfolders
benceruleanlu Jan 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 71 additions & 2 deletions src/components/TopMenuSection.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ import type {
JobListItem,
JobStatus
} from '@/platform/remote/comfyui/jobs/jobTypes'
import { useSettingStore } from '@/platform/settings/settingStore'
import { useCommandStore } from '@/stores/commandStore'
import { TaskItemImpl, useQueueStore } from '@/stores/queueStore'
import { useSidebarTabStore } from '@/stores/workspace/sidebarTabStore'
import { isElectron } from '@/utils/envUtil'

const mockData = vi.hoisted(() => ({ isLoggedIn: false }))
Expand All @@ -33,7 +36,7 @@ vi.mock('@/stores/firebaseAuthStore', () => ({
}))
}))

function createWrapper() {
function createWrapper(pinia = createTestingPinia({ createSpy: vi.fn })) {
const i18n = createI18n({
legacy: false,
locale: 'en',
Expand All @@ -53,10 +56,11 @@ function createWrapper() {

return mount(TopMenuSection, {
global: {
plugins: [createTestingPinia({ createSpy: vi.fn }), i18n],
plugins: [pinia, i18n],
stubs: {
SubgraphBreadcrumb: true,
QueueProgressOverlay: true,
QueueInlineProgressSummary: true,
CurrentUserButton: true,
LoginButton: true,
ContextMenu: {
Expand Down Expand Up @@ -142,6 +146,71 @@ describe('TopMenuSection', () => {
expect(queueButton.text()).toContain('3 active')
})

it('hides queue progress overlay when QPO V2 is enabled', async () => {
const pinia = createTestingPinia({ createSpy: vi.fn })
const settingStore = useSettingStore(pinia)
vi.mocked(settingStore.get).mockImplementation((key) =>
key === 'Comfy.Queue.QPOV2' ? true : undefined
)
const wrapper = createWrapper(pinia)

await nextTick()

expect(wrapper.find('[data-testid="queue-overlay-toggle"]').exists()).toBe(
true
)
expect(
wrapper.findComponent({ name: 'QueueProgressOverlay' }).exists()
).toBe(false)
})

it('toggles the queue progress overlay when QPO V2 is disabled', async () => {
const pinia = createTestingPinia({ createSpy: vi.fn, stubActions: false })
const settingStore = useSettingStore(pinia)
vi.mocked(settingStore.get).mockImplementation((key) =>
key === 'Comfy.Queue.QPOV2' ? false : undefined
)
const wrapper = createWrapper(pinia)
const commandStore = useCommandStore(pinia)

await wrapper.find('[data-testid="queue-overlay-toggle"]').trigger('click')

expect(commandStore.execute).toHaveBeenCalledWith(
'Comfy.Queue.ToggleOverlay'
)
})

it('opens the assets sidebar tab when QPO V2 is enabled', async () => {
const pinia = createTestingPinia({ createSpy: vi.fn, stubActions: false })
const settingStore = useSettingStore(pinia)
vi.mocked(settingStore.get).mockImplementation((key) =>
key === 'Comfy.Queue.QPOV2' ? true : undefined
)
const wrapper = createWrapper(pinia)
const sidebarTabStore = useSidebarTabStore(pinia)

await wrapper.find('[data-testid="queue-overlay-toggle"]').trigger('click')

expect(sidebarTabStore.activeSidebarTabId).toBe('assets')
})

it('toggles the assets sidebar tab when QPO V2 is enabled', async () => {
const pinia = createTestingPinia({ createSpy: vi.fn, stubActions: false })
const settingStore = useSettingStore(pinia)
vi.mocked(settingStore.get).mockImplementation((key) =>
key === 'Comfy.Queue.QPOV2' ? true : undefined
)
const wrapper = createWrapper(pinia)
const sidebarTabStore = useSidebarTabStore(pinia)
const toggleButton = wrapper.find('[data-testid="queue-overlay-toggle"]')

await toggleButton.trigger('click')
expect(sidebarTabStore.activeSidebarTabId).toBe('assets')

await toggleButton.trigger('click')
expect(sidebarTabStore.activeSidebarTabId).toBe(null)
})

it('disables the clear queue context menu item when no queued jobs exist', () => {
const wrapper = createWrapper()
const menu = wrapper.findComponent({ name: 'ContextMenu' })
Expand Down
217 changes: 150 additions & 67 deletions src/components/TopMenuSection.vue
Original file line number Diff line number Diff line change
@@ -1,90 +1,130 @@
<template>
<div
v-if="!workspaceStore.focusMode"
class="ml-1 flex gap-x-0.5 pt-1"
class="ml-1 flex flex-col gap-1 pt-1"
@mouseenter="isTopMenuHovered = true"
@mouseleave="isTopMenuHovered = false"
>
<div class="min-w-0 flex-1">
<SubgraphBreadcrumb />
</div>
<div class="flex gap-x-0.5">
<div class="min-w-0 flex-1">
<SubgraphBreadcrumb />
</div>

<div class="mx-1 flex flex-col items-end gap-1">
<div class="flex items-center gap-2">
<div
v-if="managerState.shouldShowManagerButtons.value"
class="pointer-events-auto flex h-12 shrink-0 items-center rounded-lg border border-interface-stroke bg-comfy-menu-bg px-2 shadow-interface"
>
<Button
v-tooltip.bottom="customNodesManagerTooltipConfig"
variant="secondary"
size="icon"
:aria-label="t('menu.customNodesManager')"
class="relative"
@click="openCustomNodeManager"
<div class="mx-1 flex flex-col items-end gap-1">
<div class="flex items-center gap-2">
<div
v-if="managerState.shouldShowManagerButtons.value"
class="pointer-events-auto flex h-12 shrink-0 items-center rounded-lg border border-interface-stroke bg-comfy-menu-bg px-2 shadow-interface"
>
<i class="icon-[lucide--puzzle] size-4" />
<span
v-if="shouldShowRedDot"
class="absolute top-0.5 right-1 size-2 rounded-full bg-red-500"
<Button
v-tooltip.bottom="customNodesManagerTooltipConfig"
variant="secondary"
size="icon"
:aria-label="t('menu.customNodesManager')"
class="relative"
@click="openCustomNodeManager"
>
<i class="icon-[lucide--puzzle] size-4" />
<span
v-if="shouldShowRedDot"
class="absolute top-0.5 right-1 size-2 rounded-full bg-red-500"
/>
</Button>
</div>

<div
ref="actionbarContainerRef"
class="actionbar-container relative pointer-events-auto flex gap-2 h-12 items-center rounded-lg border border-interface-stroke bg-comfy-menu-bg px-2 shadow-interface"
>
<ActionBarButtons />
<!-- Support for legacy topbar elements attached by custom scripts, hidden if no elements present -->
<div
ref="legacyCommandsContainerRef"
class="[&:not(:has(*>*:not(:empty)))]:hidden"
></div>
<ComfyActionbar
ref="actionbarRef"
:top-menu-container="actionbarContainerRef"
:queue-overlay-expanded="isQueueOverlayExpanded"
/>
<Button
v-tooltip.bottom="queueHistoryTooltipConfig"
type="destructive"
size="md"
:aria-pressed="
isQueuePanelV2Enabled
? activeSidebarTabId === 'assets'
: isQueueProgressOverlayVisible
? isQueueOverlayExpanded
: undefined
"
class="px-3"
data-testid="queue-overlay-toggle"
@click="toggleQueueOverlay"
@contextmenu.stop.prevent="showQueueContextMenu"
>
<span class="text-sm font-normal tabular-nums">
{{ activeJobsLabel }}
</span>
<span class="sr-only">
{{
isQueuePanelV2Enabled
? t('sideToolbar.queueProgressOverlay.viewJobHistory')
: t('sideToolbar.queueProgressOverlay.expandCollapsedQueue')
}}
</span>
</Button>
<ContextMenu
ref="queueContextMenu"
:model="queueContextMenuItems"
/>
<CurrentUserButton
v-if="isLoggedIn && !isIntegratedTabBar"
class="shrink-0"
/>
</Button>
<LoginButton v-else-if="isDesktop && !isIntegratedTabBar" />
<Button
v-if="!isRightSidePanelOpen"
v-tooltip.bottom="rightSidePanelTooltipConfig"
type="secondary"
size="icon"
:aria-label="t('rightSidePanel.togglePanel')"
@click="rightSidePanelStore.togglePanel"
>
<i class="icon-[lucide--panel-right] size-4" />
</Button>
</div>
</div>
<QueueProgressOverlay
v-if="isQueueProgressOverlayVisible"
v-model:expanded="isQueueOverlayExpanded"
:menu-hovered="isTopMenuHovered"
/>
</div>
</div>

<div>
<Teleport
v-if="inlineProgressSummaryTarget"
:to="inlineProgressSummaryTarget"
>
<div
class="actionbar-container pointer-events-auto flex gap-2 h-12 items-center rounded-lg border border-interface-stroke bg-comfy-menu-bg px-2 shadow-interface"
class="pointer-events-none absolute left-0 right-0 top-full mt-1 flex justify-end pr-1"
>
<ActionBarButtons />
<!-- Support for legacy topbar elements attached by custom scripts, hidden if no elements present -->
<div
ref="legacyCommandsContainerRef"
class="[&:not(:has(*>*:not(:empty)))]:hidden"
></div>
<ComfyActionbar />
<Button
v-tooltip.bottom="queueHistoryTooltipConfig"
type="destructive"
size="md"
:aria-pressed="isQueueOverlayExpanded"
class="px-3"
data-testid="queue-overlay-toggle"
@click="toggleQueueOverlay"
@contextmenu.stop.prevent="showQueueContextMenu"
>
<span class="text-sm font-normal tabular-nums">
{{ activeJobsLabel }}
</span>
<span class="sr-only">
{{ t('sideToolbar.queueProgressOverlay.expandCollapsedQueue') }}
</span>
</Button>
<ContextMenu ref="queueContextMenu" :model="queueContextMenuItems" />
<CurrentUserButton
v-if="isLoggedIn && !isIntegratedTabBar"
class="shrink-0"
/>
<LoginButton v-else-if="isDesktop && !isIntegratedTabBar" />
<Button
v-if="!isRightSidePanelOpen"
v-tooltip.bottom="rightSidePanelTooltipConfig"
type="secondary"
size="icon"
:aria-label="t('rightSidePanel.togglePanel')"
@click="rightSidePanelStore.togglePanel"
>
<i class="icon-[lucide--panel-right] size-4" />
</Button>
<QueueInlineProgressSummary :hidden="isQueueOverlayExpanded" />
</div>
</div>
<QueueProgressOverlay
v-model:expanded="isQueueOverlayExpanded"
:menu-hovered="isTopMenuHovered"
</Teleport>
<QueueInlineProgressSummary
v-else-if="shouldShowInlineProgressSummary && !isActionbarFloating"
class="pr-1"
:hidden="isQueueOverlayExpanded"
/>
</div>
</div>
</template>

<script setup lang="ts">
import { useLocalStorage } from '@vueuse/core'
import { storeToRefs } from 'pinia'
import ContextMenu from 'primevue/contextmenu'
import type { MenuItem } from 'primevue/menuitem'
Expand All @@ -93,6 +133,7 @@ import { useI18n } from 'vue-i18n'

import ComfyActionbar from '@/components/actionbar/ComfyActionbar.vue'
import SubgraphBreadcrumb from '@/components/breadcrumb/SubgraphBreadcrumb.vue'
import QueueInlineProgressSummary from '@/components/queue/QueueInlineProgressSummary.vue'
import QueueProgressOverlay from '@/components/queue/QueueProgressOverlay.vue'
import ActionBarButtons from '@/components/topbar/ActionBarButtons.vue'
import CurrentUserButton from '@/components/topbar/CurrentUserButton.vue'
Expand All @@ -108,6 +149,7 @@ import { useCommandStore } from '@/stores/commandStore'
import { useExecutionStore } from '@/stores/executionStore'
import { useQueueStore, useQueueUIStore } from '@/stores/queueStore'
import { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore'
import { useSidebarTabStore } from '@/stores/workspace/sidebarTabStore'
import { useWorkspaceStore } from '@/stores/workspaceStore'
import { isElectron } from '@/utils/envUtil'
import { useConflictAcknowledgment } from '@/workbench/extensions/manager/composables/useConflictAcknowledgment'
Expand All @@ -126,13 +168,24 @@ const commandStore = useCommandStore()
const queueStore = useQueueStore()
const executionStore = useExecutionStore()
const queueUIStore = useQueueUIStore()
const sidebarTabStore = useSidebarTabStore()
const { activeJobsCount } = storeToRefs(queueStore)
const { isOverlayExpanded: isQueueOverlayExpanded } = storeToRefs(queueUIStore)
const { activeSidebarTabId } = storeToRefs(sidebarTabStore)
const releaseStore = useReleaseStore()
const { shouldShowRedDot: showReleaseRedDot } = storeToRefs(releaseStore)
const { shouldShowRedDot: shouldShowConflictRedDot } =
useConflictAcknowledgment()
const isTopMenuHovered = ref(false)
const actionbarContainerRef = ref<HTMLElement>()
const isActionbarDocked = useLocalStorage('Comfy.MenuPosition.Docked', true)
const actionbarPosition = computed(() => settingStore.get('Comfy.UseNewMenu'))
const isActionbarEnabled = computed(
() => actionbarPosition.value !== 'Disabled'
)
const isActionbarFloating = computed(
() => isActionbarEnabled.value && !isActionbarDocked.value
)
const activeJobsLabel = computed(() => {
const count = activeJobsCount.value
return t(
Expand All @@ -144,6 +197,32 @@ const activeJobsLabel = computed(() => {
const isIntegratedTabBar = computed(
() => settingStore.get('Comfy.UI.TabBarLayout') === 'Integrated'
)
const isQueuePanelV2Enabled = computed(() =>
settingStore.get('Comfy.Queue.QPOV2')
)
const isQueueProgressOverlayVisible = computed(
() => !isQueuePanelV2Enabled.value
)
const isInlineProgressVisible = computed(() => isQueuePanelV2Enabled.value)
type PanelElementTarget = HTMLElement | { value?: HTMLElement | null } | null
const shouldShowInlineProgressSummary = computed(
() => isInlineProgressVisible.value && isActionbarEnabled.value
)
const actionbarRef = ref<{ panelElement: PanelElementTarget } | null>(null)
const resolvePanelElement = (target: PanelElementTarget) => {
if (!target) return null
if (target instanceof HTMLElement) return target
if (typeof target === 'object' && 'value' in target) {
return target.value ?? null
}
return null
}
const inlineProgressSummaryTarget = computed(() => {
if (!shouldShowInlineProgressSummary.value || !isActionbarFloating.value) {
return null
}
return resolvePanelElement(actionbarRef.value?.panelElement ?? null)
})
const queueHistoryTooltipConfig = computed(() =>
buildTooltipConfig(t('sideToolbar.queueProgressOverlay.viewJobHistory'))
)
Expand Down Expand Up @@ -185,6 +264,10 @@ onMounted(() => {
})

const toggleQueueOverlay = () => {
if (isQueuePanelV2Enabled.value) {
sidebarTabStore.toggleSidebarTab('assets')
return
}
commandStore.execute('Comfy.Queue.ToggleOverlay')
}

Expand Down
Loading