fix: add run progress toggle to job history menu (#9176)

Summary
- Add hidden setting `Comfy.Queue.ShowRunProgressBar` (default `true`).
- Add `Show run progress bar` toggle to the shared `...` job history
menu (`JobHistoryActionsMenu`), placed next to `Docked Job History`.
- Use that setting to control both the inline run progress bar and the
inline summary text under it.
- Keep queue button right-click context menu focused on queue actions.
- Add/update tests for the new toggle behavior and summary visibility.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9176-fix-add-run-progress-toggle-to-job-history-menu-3116d73d365081118202d8d67a857367)
by [Unito](https://www.unito.io)
This commit is contained in:
Benjamin Lu
2026-03-04 14:15:11 -08:00
committed by GitHub
parent bcc470642f
commit 1cee6272c1
11 changed files with 339 additions and 40 deletions

View File

@@ -296,11 +296,13 @@ describe('TopMenuSection', () => {
describe('inline progress summary', () => {
const configureSettings = (
pinia: ReturnType<typeof createTestingPinia>,
qpoV2Enabled: boolean
qpoV2Enabled: boolean,
showRunProgressBar = true
) => {
const settingStore = useSettingStore(pinia)
vi.mocked(settingStore.get).mockImplementation((key) => {
if (key === 'Comfy.Queue.QPOV2') return qpoV2Enabled
if (key === 'Comfy.Queue.ShowRunProgressBar') return showRunProgressBar
if (key === 'Comfy.UseNewMenu') return 'Top'
return undefined
})
@@ -332,6 +334,19 @@ describe('TopMenuSection', () => {
).toBe(false)
})
it('does not render inline progress summary when run progress bar is disabled', async () => {
const pinia = createTestingPinia({ createSpy: vi.fn })
configureSettings(pinia, true, false)
const wrapper = createWrapper({ pinia })
await nextTick()
expect(
wrapper.findComponent({ name: 'QueueInlineProgressSummary' }).exists()
).toBe(false)
})
it('teleports inline progress summary when actionbar is floating', async () => {
localStorage.setItem('Comfy.MenuPosition.Docked', 'false')
const actionbarTarget = document.createElement('div')

View File

@@ -125,6 +125,7 @@ import CurrentUserButton from '@/components/topbar/CurrentUserButton.vue'
import LoginButton from '@/components/topbar/LoginButton.vue'
import Button from '@/components/ui/button/Button.vue'
import { useCurrentUser } from '@/composables/auth/useCurrentUser'
import { useQueueFeatureFlags } from '@/composables/queue/useQueueFeatureFlags'
import { useErrorHandling } from '@/composables/useErrorHandling'
import { buildTooltipConfig } from '@/composables/useTooltipConfig'
import { useSettingStore } from '@/platform/settings/settingStore'
@@ -164,14 +165,16 @@ const isActionbarFloating = computed(
const isIntegratedTabBar = computed(
() => settingStore.get('Comfy.UI.TabBarLayout') === 'Integrated'
)
const isQueuePanelV2Enabled = computed(() =>
settingStore.get('Comfy.Queue.QPOV2')
)
const { isQueuePanelV2Enabled, isRunProgressBarEnabled } =
useQueueFeatureFlags()
const isQueueProgressOverlayEnabled = computed(
() => !isQueuePanelV2Enabled.value
)
const shouldShowInlineProgressSummary = computed(
() => isQueuePanelV2Enabled.value && isActionbarEnabled.value
() =>
isQueuePanelV2Enabled.value &&
isActionbarEnabled.value &&
isRunProgressBarEnabled.value
)
const shouldShowQueueNotificationBanners = computed(
() => isActionbarEnabled.value

View File

@@ -0,0 +1,101 @@
import { createTestingPinia } from '@pinia/testing'
import { mount } from '@vue/test-utils'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { nextTick } from 'vue'
import ComfyActionbar from '@/components/actionbar/ComfyActionbar.vue'
import { i18n } from '@/i18n'
import { useSettingStore } from '@/platform/settings/settingStore'
const configureSettings = (
pinia: ReturnType<typeof createTestingPinia>,
showRunProgressBar: boolean
) => {
const settingStore = useSettingStore(pinia)
vi.mocked(settingStore.get).mockImplementation((key) => {
if (key === 'Comfy.UseNewMenu') return 'Top'
if (key === 'Comfy.Queue.QPOV2') return true
if (key === 'Comfy.Queue.ShowRunProgressBar') return showRunProgressBar
return undefined
})
}
const mountActionbar = (showRunProgressBar: boolean) => {
const topMenuContainer = document.createElement('div')
document.body.appendChild(topMenuContainer)
const pinia = createTestingPinia({ createSpy: vi.fn })
configureSettings(pinia, showRunProgressBar)
const wrapper = mount(ComfyActionbar, {
attachTo: document.body,
props: {
topMenuContainer,
queueOverlayExpanded: false
},
global: {
plugins: [pinia, i18n],
stubs: {
ContextMenu: {
name: 'ContextMenu',
template: '<div />'
},
Panel: {
name: 'Panel',
template: '<div><slot /></div>'
},
StatusBadge: true,
ComfyRunButton: {
name: 'ComfyRunButton',
template: '<button type="button">Run</button>'
},
QueueInlineProgress: true
},
directives: {
tooltip: () => {}
}
}
})
return {
wrapper,
topMenuContainer
}
}
describe('ComfyActionbar', () => {
beforeEach(() => {
i18n.global.locale.value = 'en'
localStorage.clear()
})
it('teleports inline progress when run progress bar is enabled', async () => {
const { wrapper, topMenuContainer } = mountActionbar(true)
try {
await nextTick()
expect(
topMenuContainer.querySelector('[data-testid="queue-inline-progress"]')
).not.toBeNull()
} finally {
wrapper.unmount()
topMenuContainer.remove()
}
})
it('does not teleport inline progress when run progress bar is disabled', async () => {
const { wrapper, topMenuContainer } = mountActionbar(false)
try {
await nextTick()
expect(
topMenuContainer.querySelector('[data-testid="queue-inline-progress"]')
).toBeNull()
} finally {
wrapper.unmount()
topMenuContainer.remove()
}
})
})

View File

@@ -107,6 +107,7 @@ import { useI18n } from 'vue-i18n'
import StatusBadge from '@/components/common/StatusBadge.vue'
import QueueInlineProgress from '@/components/queue/QueueInlineProgress.vue'
import Button from '@/components/ui/button/Button.vue'
import { useQueueFeatureFlags } from '@/composables/queue/useQueueFeatureFlags'
import { buildTooltipConfig } from '@/composables/useTooltipConfig'
import { useSettingStore } from '@/platform/settings/settingStore'
import { useTelemetry } from '@/platform/telemetry'
@@ -127,7 +128,7 @@ const emit = defineEmits<{
(event: 'update:progressTarget', target: HTMLElement | null): void
}>()
const settingsStore = useSettingStore()
const settingStore = useSettingStore()
const commandStore = useCommandStore()
const executionStore = useExecutionStore()
const queueStore = useQueueStore()
@@ -137,11 +138,10 @@ const { isIdle: isExecutionIdle } = storeToRefs(executionStore)
const { activeJobsCount } = storeToRefs(queueStore)
const { activeSidebarTabId } = storeToRefs(sidebarTabStore)
const position = computed(() => settingsStore.get('Comfy.UseNewMenu'))
const position = computed(() => settingStore.get('Comfy.UseNewMenu'))
const visible = computed(() => position.value !== 'Disabled')
const isQueuePanelV2Enabled = computed(() =>
settingsStore.get('Comfy.Queue.QPOV2')
)
const { isQueuePanelV2Enabled, isRunProgressBarEnabled } =
useQueueFeatureFlags()
const panelRef = ref<ComponentPublicInstance | null>(null)
const panelElement = computed<HTMLElement | null>(() => {
@@ -325,7 +325,13 @@ const onMouseLeaveDropZone = () => {
}
const inlineProgressTarget = computed(() => {
if (!visible.value || !isQueuePanelV2Enabled.value) return null
if (
!visible.value ||
!isQueuePanelV2Enabled.value ||
!isRunProgressBarEnabled.value
) {
return null
}
if (isDocked.value) return topMenuContainer ?? null
return panelElement.value
})

View File

@@ -0,0 +1,123 @@
import { mount } from '@vue/test-utils'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { defineComponent, h } from 'vue'
import { i18n } from '@/i18n'
import JobHistoryActionsMenu from '@/components/queue/JobHistoryActionsMenu.vue'
const popoverCloseSpy = vi.fn()
vi.mock('@/components/ui/Popover.vue', () => {
const PopoverStub = defineComponent({
name: 'Popover',
setup(_, { slots }) {
return () =>
h('div', [
slots.button?.(),
slots.default?.({
close: () => {
popoverCloseSpy()
}
})
])
}
})
return { default: PopoverStub }
})
vi.mock('@/platform/distribution/types', () => ({
isCloud: false
}))
const mockGetSetting = vi.fn<(key: string) => boolean | undefined>((key) =>
key === 'Comfy.Queue.QPOV2' || key === 'Comfy.Queue.ShowRunProgressBar'
? true
: undefined
)
const mockSetSetting = vi.fn()
const mockSetMany = vi.fn()
const mockSidebarTabStore = {
activeSidebarTabId: null as string | null
}
vi.mock('@/platform/settings/settingStore', () => ({
useSettingStore: () => ({
get: mockGetSetting,
set: mockSetSetting,
setMany: mockSetMany
})
}))
vi.mock('@/stores/workspace/sidebarTabStore', () => ({
useSidebarTabStore: () => mockSidebarTabStore
}))
const mountMenu = () =>
mount(JobHistoryActionsMenu, {
global: {
plugins: [i18n],
directives: { tooltip: () => {} }
}
})
describe('JobHistoryActionsMenu', () => {
beforeEach(() => {
i18n.global.locale.value = 'en'
popoverCloseSpy.mockClear()
mockSetSetting.mockClear()
mockSetMany.mockClear()
mockSidebarTabStore.activeSidebarTabId = null
mockGetSetting.mockImplementation((key: string) =>
key === 'Comfy.Queue.QPOV2' || key === 'Comfy.Queue.ShowRunProgressBar'
? true
: undefined
)
})
it('toggles show run progress bar setting from the menu', async () => {
const wrapper = mountMenu()
const showRunProgressBarButton = wrapper.get(
'[data-testid="show-run-progress-bar-action"]'
)
await showRunProgressBarButton.trigger('click')
expect(mockSetSetting).toHaveBeenCalledTimes(1)
expect(mockSetSetting).toHaveBeenCalledWith(
'Comfy.Queue.ShowRunProgressBar',
false
)
})
it('opens docked job history sidebar when enabling from the menu', async () => {
mockGetSetting.mockImplementation((key: string) => {
if (key === 'Comfy.Queue.QPOV2') return false
if (key === 'Comfy.Queue.ShowRunProgressBar') return true
return undefined
})
const wrapper = mountMenu()
const dockedJobHistoryButton = wrapper.get(
'[data-testid="docked-job-history-action"]'
)
await dockedJobHistoryButton.trigger('click')
expect(popoverCloseSpy).toHaveBeenCalledTimes(1)
expect(mockSetSetting).toHaveBeenCalledTimes(1)
expect(mockSetSetting).toHaveBeenCalledWith('Comfy.Queue.QPOV2', true)
expect(mockSetMany).not.toHaveBeenCalled()
expect(mockSidebarTabStore.activeSidebarTabId).toBe('job-history')
})
it('emits clear history from the menu', async () => {
const wrapper = mountMenu()
const clearHistoryButton = wrapper.get(
'[data-testid="clear-history-action"]'
)
await clearHistoryButton.trigger('click')
expect(popoverCloseSpy).toHaveBeenCalledTimes(1)
expect(wrapper.emitted('clearHistory')).toHaveLength(1)
})
})

View File

@@ -19,7 +19,7 @@
data-testid="docked-job-history-action"
class="w-full justify-between text-sm font-light"
variant="textonly"
size="sm"
size="md"
@click="onToggleDockedJobHistory(close)"
>
<span class="flex items-center gap-2">
@@ -35,14 +35,32 @@
class="icon-[lucide--check] size-4"
/>
</Button>
<Button
data-testid="show-run-progress-bar-action"
class="w-full justify-between text-sm font-light"
variant="textonly"
size="md"
@click="onToggleRunProgressBar"
>
<span class="flex items-center gap-2">
<i class="icon-[lucide--hourglass] size-4 text-text-secondary" />
<span>{{
t('sideToolbar.queueProgressOverlay.showRunProgressBar')
}}</span>
</span>
<i
v-if="isRunProgressBarEnabled"
class="icon-[lucide--check] size-4"
/>
</Button>
<!-- TODO: Bug in assets sidebar panel derives assets from history, so despite this not deleting the assets, it still effectively shows to the user as deleted -->
<template v-if="showClearHistoryAction">
<div class="my-1 border-t border-interface-stroke" />
<Button
data-testid="clear-history-action"
class="h-auto min-h-0 w-full items-start justify-start whitespace-normal"
class="h-auto min-h-8 w-full items-start justify-start whitespace-normal"
variant="textonly"
size="sm"
size="md"
@click="onClearHistoryFromMenu(close)"
>
<i
@@ -76,6 +94,7 @@ import { useI18n } from 'vue-i18n'
import Popover from '@/components/ui/Popover.vue'
import Button from '@/components/ui/button/Button.vue'
import { useQueueFeatureFlags } from '@/composables/queue/useQueueFeatureFlags'
import { buildTooltipConfig } from '@/composables/useTooltipConfig'
import { isCloud } from '@/platform/distribution/types'
import { useSettingStore } from '@/platform/settings/settingStore'
@@ -90,9 +109,8 @@ const settingStore = useSettingStore()
const sidebarTabStore = useSidebarTabStore()
const moreTooltipConfig = computed(() => buildTooltipConfig(t('g.more')))
const isQueuePanelV2Enabled = computed(() =>
settingStore.get('Comfy.Queue.QPOV2')
)
const { isQueuePanelV2Enabled, isRunProgressBarEnabled } =
useQueueFeatureFlags()
const showClearHistoryAction = computed(() => !isCloud)
const onClearHistoryFromMenu = (close: () => void) => {
@@ -118,4 +136,11 @@ const onToggleDockedJobHistory = async (close: () => void) => {
return
}
}
const onToggleRunProgressBar = async () => {
await settingStore.set(
'Comfy.Queue.ShowRunProgressBar',
!isRunProgressBarEnabled.value
)
}
</script>

View File

@@ -1,8 +1,9 @@
import { mount } from '@vue/test-utils'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { createI18n } from 'vue-i18n'
import { defineComponent, h } from 'vue'
import { i18n } from '@/i18n'
const popoverCloseSpy = vi.fn()
vi.mock('@/components/ui/Popover.vue', () => {
@@ -24,7 +25,9 @@ vi.mock('@/components/ui/Popover.vue', () => {
})
const mockGetSetting = vi.fn<(key: string) => boolean | undefined>((key) =>
key === 'Comfy.Queue.QPOV2' ? true : undefined
key === 'Comfy.Queue.QPOV2' || key === 'Comfy.Queue.ShowRunProgressBar'
? true
: undefined
)
const mockSetSetting = vi.fn()
const mockSetMany = vi.fn()
@@ -52,27 +55,6 @@ const tooltipDirectiveStub = {
updated: vi.fn()
}
const i18n = createI18n({
legacy: false,
locale: 'en',
messages: {
en: {
g: { more: 'More' },
sideToolbar: {
queueProgressOverlay: {
queuedSuffix: 'queued',
clearQueued: 'Clear queued',
clearQueueTooltip: 'Clear queue',
clearAllJobsTooltip: 'Cancel all running jobs',
moreOptions: 'More options',
clearHistory: 'Clear history',
dockedJobHistory: 'Docked Job History'
}
}
}
}
})
const mountHeader = (props = {}) =>
mount(QueueOverlayHeader, {
props: {
@@ -88,6 +70,7 @@ const mountHeader = (props = {}) =>
describe('QueueOverlayHeader', () => {
beforeEach(() => {
i18n.global.locale.value = 'en'
popoverCloseSpy.mockClear()
mockSetSetting.mockClear()
mockSetMany.mockClear()
@@ -207,4 +190,19 @@ describe('QueueOverlayHeader', () => {
'Comfy.Queue.History.Expanded': true
})
})
it('toggles show run progress bar setting from the menu', async () => {
const wrapper = mountHeader()
const showRunProgressBarButton = wrapper.get(
'[data-testid="show-run-progress-bar-action"]'
)
await showRunProgressBarButton.trigger('click')
expect(mockSetSetting).toHaveBeenCalledTimes(1)
expect(mockSetSetting).toHaveBeenCalledWith(
'Comfy.Queue.ShowRunProgressBar',
false
)
})
})

View File

@@ -0,0 +1,19 @@
import { computed } from 'vue'
import { useSettingStore } from '@/platform/settings/settingStore'
export function useQueueFeatureFlags() {
const settingStore = useSettingStore()
const isQueuePanelV2Enabled = computed(() =>
settingStore.get('Comfy.Queue.QPOV2')
)
const isRunProgressBarEnabled = computed(
() => settingStore.get('Comfy.Queue.ShowRunProgressBar') !== false
)
return {
isQueuePanelV2Enabled,
isRunProgressBarEnabled
}
}

View File

@@ -854,6 +854,7 @@
"clearQueued": "Clear queued",
"clearHistory": "Clear job history",
"dockedJobHistory": "Docked Job History",
"showRunProgressBar": "Show run progress bar",
"clearHistoryMenuAssetsNote": "Media assets won't be deleted.",
"filterJobs": "Filter jobs",
"filterBy": "Filter by",

View File

@@ -1207,6 +1207,13 @@ export const CORE_SETTINGS: SettingParams[] = [
defaultValue: false,
experimental: true
},
{
id: 'Comfy.Queue.ShowRunProgressBar',
name: 'Show run progress bar',
type: 'hidden',
defaultValue: true,
versionAdded: '1.41.3'
},
{
id: 'Comfy.Node.AlwaysShowAdvancedWidgets',
category: ['LiteGraph', 'Node Widget', 'AlwaysShowAdvancedWidgets'],

View File

@@ -411,6 +411,7 @@ const zSettings = z.object({
'Comfy.VueNodes.AutoScaleLayout': z.boolean(),
'Comfy.Assets.UseAssetAPI': z.boolean(),
'Comfy.Queue.QPOV2': z.boolean(),
'Comfy.Queue.ShowRunProgressBar': z.boolean(),
'Comfy-Desktop.AutoUpdate': z.boolean(),
'Comfy-Desktop.SendStatistics': z.boolean(),
'Comfy-Desktop.WindowStyle': z.string(),