From d9e5b07c738255b2ec38155dab5aa6ac1e689786 Mon Sep 17 00:00:00 2001 From: Jin Yi Date: Wed, 21 Jan 2026 14:15:58 +0900 Subject: [PATCH] [bugfix] Clear queue button now properly removes initializing jobs from UI (#8203) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Fix: Initializing jobs now properly disappear from UI when cancelled or cleared - Add `clearInitializationByPromptIds` batch function for optimized Set operations - Handle Cloud vs local environment correctly (use `api.deleteItem` for Cloud, `api.interrupt` for local) ## Problem When clicking 'Clear queue' button or X button on initializing jobs, the jobs remained visible in both AssetsSidebarListView and JobQueue components until page refresh. ## Root Cause 1. `initializingPromptIds` in `executionStore` was not being cleared when jobs were cancelled/deleted 2. Cloud environment requires `api.deleteItem()` instead of `api.interrupt()` for cancellation ## Changes - `src/stores/executionStore.ts`: Export `clearInitializationByPromptId` and add batch `clearInitializationByPromptIds` function - `src/composables/queue/useJobMenu.ts`: Add Cloud branch handling and initialization cleanup - `src/components/queue/QueueProgressOverlay.vue`: Fix `onCancelItem()`, `cancelQueuedWorkflows()`, `interruptAll()` - `src/components/sidebar/tabs/AssetsSidebarTab.vue`: Add initialization cleanup to `handleClearQueue()` [screen-capture.webm](https://github.com/user-attachments/assets/0bf911c2-d8f4-427c-96e0-4784e8fe0f08) 🤖 Generated with [Claude Code](https://claude.com/claude-code) ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-8203-bugfix-Clear-queue-button-now-properly-removes-initializing-jobs-from-UI-2ef6d73d36508162a55bd84ad39ab49c) by [Unito](https://www.unito.io) --------- Co-authored-by: Claude Opus 4.5 --- src/components/queue/QueueProgressOverlay.vue | 20 ++++++++++++++++++- .../sidebar/tabs/AssetsSidebarTab.vue | 8 ++++++++ src/composables/queue/useJobMenu.test.ts | 14 ++++++++++++- src/composables/queue/useJobMenu.ts | 10 +++++++++- src/stores/executionStore.ts | 14 +++++++++++++ 5 files changed, 63 insertions(+), 3 deletions(-) diff --git a/src/components/queue/QueueProgressOverlay.vue b/src/components/queue/QueueProgressOverlay.vue index d6b3edcd8..626709593 100644 --- a/src/components/queue/QueueProgressOverlay.vue +++ b/src/components/queue/QueueProgressOverlay.vue @@ -200,7 +200,13 @@ const onCancelItem = wrapWithErrorHandlingAsync(async (item: JobListItem) => { if (item.state === 'running' || item.state === 'initialization') { // Running/initializing jobs: interrupt execution - await api.interrupt(promptId) + // Cloud backend uses deleteItem, local uses interrupt + if (isCloud) { + await api.deleteItem('queue', promptId) + } else { + await api.interrupt(promptId) + } + executionStore.clearInitializationByPromptId(promptId) await queueStore.update() } else if (item.state === 'pending') { // Pending jobs: remove from queue @@ -268,7 +274,15 @@ const inspectJobAsset = wrapWithErrorHandlingAsync( ) const cancelQueuedWorkflows = wrapWithErrorHandlingAsync(async () => { + // Capture pending promptIds before clearing + const pendingPromptIds = queueStore.pendingTasks + .map((task) => task.promptId) + .filter((id): id is string => typeof id === 'string' && id.length > 0) + await commandStore.execute('Comfy.ClearPendingTasks') + + // Clear initialization state for removed prompts + executionStore.clearInitializationByPromptIds(pendingPromptIds) }) const interruptAll = wrapWithErrorHandlingAsync(async () => { @@ -284,10 +298,14 @@ const interruptAll = wrapWithErrorHandlingAsync(async () => { // on cloud to ensure we cancel the workflow the user clicked. if (isCloud) { await Promise.all(promptIds.map((id) => api.deleteItem('queue', id))) + executionStore.clearInitializationByPromptIds(promptIds) + await queueStore.update() return } await Promise.all(promptIds.map((id) => api.interrupt(id))) + executionStore.clearInitializationByPromptIds(promptIds) + await queueStore.update() }) const showClearHistoryDialog = () => { diff --git a/src/components/sidebar/tabs/AssetsSidebarTab.vue b/src/components/sidebar/tabs/AssetsSidebarTab.vue index d481b0b9a..8db99e96f 100644 --- a/src/components/sidebar/tabs/AssetsSidebarTab.vue +++ b/src/components/sidebar/tabs/AssetsSidebarTab.vue @@ -244,6 +244,7 @@ import { useSettingStore } from '@/platform/settings/settingStore' import { getJobDetail } from '@/services/jobOutputCache' import { useCommandStore } from '@/stores/commandStore' import { useDialogStore } from '@/stores/dialogStore' +import { useExecutionStore } from '@/stores/executionStore' import { ResultItemImpl, useQueueStore } from '@/stores/queueStore' import { formatDuration, getMediaTypeFromFilename } from '@/utils/formatUtil' import { cn } from '@/utils/tailwindUtil' @@ -257,6 +258,7 @@ interface JobOutputItem { const { t, n } = useI18n() const commandStore = useCommandStore() const queueStore = useQueueStore() +const executionStore = useExecutionStore() const settingStore = useSettingStore() const activeTab = ref<'input' | 'output'>('output') @@ -511,7 +513,13 @@ const handleBulkDelete = async (assets: AssetItem[]) => { } const handleClearQueue = async () => { + const pendingPromptIds = queueStore.pendingTasks + .map((task) => task.promptId) + .filter((id): id is string => typeof id === 'string' && id.length > 0) + await commandStore.execute('Comfy.ClearPendingTasks') + + executionStore.clearInitializationByPromptIds(pendingPromptIds) } const handleBulkAddToWorkflow = async (assets: AssetItem[]) => { diff --git a/src/composables/queue/useJobMenu.test.ts b/src/composables/queue/useJobMenu.test.ts index 1b339d09c..9913b3b3e 100644 --- a/src/composables/queue/useJobMenu.test.ts +++ b/src/composables/queue/useJobMenu.test.ts @@ -5,6 +5,10 @@ import type { Ref } from 'vue' import type { JobListItem } from '@/composables/queue/useJobList' import type { MenuEntry } from '@/composables/queue/useJobMenu' +vi.mock('@/platform/distribution/types', () => ({ + isCloud: false +})) + const downloadFileMock = vi.fn() vi.mock('@/base/common/downloadUtil', () => ({ downloadFile: (...args: any[]) => downloadFileMock(...args) @@ -55,7 +59,8 @@ const workflowStoreMock = { createTemporary: vi.fn() } vi.mock('@/platform/workflow/management/stores/workflowStore', () => ({ - useWorkflowStore: () => workflowStoreMock + useWorkflowStore: () => workflowStoreMock, + ComfyWorkflow: class {} })) const interruptMock = vi.fn() @@ -104,6 +109,13 @@ vi.mock('@/stores/queueStore', () => ({ useQueueStore: () => queueStoreMock })) +const executionStoreMock = { + clearInitializationByPromptId: vi.fn() +} +vi.mock('@/stores/executionStore', () => ({ + useExecutionStore: () => executionStoreMock +})) + const getJobWorkflowMock = vi.fn() vi.mock('@/services/jobOutputCache', () => ({ getJobWorkflow: (...args: any[]) => getJobWorkflowMock(...args) diff --git a/src/composables/queue/useJobMenu.ts b/src/composables/queue/useJobMenu.ts index b4208304e..b50a24846 100644 --- a/src/composables/queue/useJobMenu.ts +++ b/src/composables/queue/useJobMenu.ts @@ -6,6 +6,7 @@ import { useCopyToClipboard } from '@/composables/useCopyToClipboard' import { st, t } from '@/i18n' import { mapTaskOutputToAssetItem } from '@/platform/assets/composables/media/assetMappers' import { useMediaAssetActions } from '@/platform/assets/composables/useMediaAssetActions' +import { isCloud } from '@/platform/distribution/types' import { useSettingStore } from '@/platform/settings/settingStore' import { useWorkflowService } from '@/platform/workflow/core/services/workflowService' import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore' @@ -15,6 +16,7 @@ import { downloadBlob } from '@/scripts/utils' import { useDialogService } from '@/services/dialogService' import { getJobWorkflow } from '@/services/jobOutputCache' import { useLitegraphService } from '@/services/litegraphService' +import { useExecutionStore } from '@/stores/executionStore' import { useNodeDefStore } from '@/stores/nodeDefStore' import { useQueueStore } from '@/stores/queueStore' import type { ResultItemImpl, TaskItemImpl } from '@/stores/queueStore' @@ -44,6 +46,7 @@ export function useJobMenu( const workflowStore = useWorkflowStore() const workflowService = useWorkflowService() const queueStore = useQueueStore() + const executionStore = useExecutionStore() const { copyToClipboard } = useCopyToClipboard() const litegraphService = useLitegraphService() const nodeDefStore = useNodeDefStore() @@ -72,10 +75,15 @@ export function useJobMenu( const target = resolveItem(item) if (!target) return if (target.state === 'running' || target.state === 'initialization') { - await api.interrupt(target.id) + if (isCloud) { + await api.deleteItem('queue', target.id) + } else { + await api.interrupt(target.id) + } } else if (target.state === 'pending') { await api.deleteItem('queue', target.id) } + executionStore.clearInitializationByPromptId(target.id) await queueStore.update() } diff --git a/src/stores/executionStore.ts b/src/stores/executionStore.ts index 429adb807..dda732af5 100644 --- a/src/stores/executionStore.ts +++ b/src/stores/executionStore.ts @@ -425,6 +425,18 @@ export const useExecutionStore = defineStore('execution', () => { initializingPromptIds.value = next } + function clearInitializationByPromptIds(promptIds: string[]) { + if (!promptIds.length) return + const current = initializingPromptIds.value + const toRemove = promptIds.filter((id) => current.has(id)) + if (!toRemove.length) return + const next = new Set(current) + for (const id of toRemove) { + next.delete(id) + } + initializingPromptIds.value = next + } + function isPromptInitializing( promptId: string | number | undefined ): boolean { @@ -650,6 +662,8 @@ export const useExecutionStore = defineStore('execution', () => { runningWorkflowCount, initializingPromptIds, isPromptInitializing, + clearInitializationByPromptId, + clearInitializationByPromptIds, bindExecutionEvents, unbindExecutionEvents, storePrompt,