mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-28 16:27:32 +00:00
Compare commits
2 Commits
test/stand
...
feat/tab-e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
63929f1802 | ||
|
|
71fccf3d8b |
47
src/components/topbar/WorkflowExecutionIndicator.test.ts
Normal file
47
src/components/topbar/WorkflowExecutionIndicator.test.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import type { WorkflowExecutionState } from '@/stores/executionStore'
|
||||
|
||||
import WorkflowExecutionIndicator from './WorkflowExecutionIndicator.vue'
|
||||
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: 'en',
|
||||
messages: {
|
||||
en: {
|
||||
workflowExecution: {
|
||||
running: 'Workflow is running',
|
||||
completed: 'Workflow completed successfully',
|
||||
error: 'Workflow execution failed'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
describe('WorkflowExecutionIndicator', () => {
|
||||
const mountWithI18n = (props: { state: WorkflowExecutionState }) =>
|
||||
mount(WorkflowExecutionIndicator, {
|
||||
props,
|
||||
global: {
|
||||
plugins: [i18n]
|
||||
}
|
||||
})
|
||||
|
||||
it('renders nothing for idle state', () => {
|
||||
const wrapper = mountWithI18n({ state: 'idle' })
|
||||
expect(wrapper.find('i').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it.each<{ state: WorkflowExecutionState; label: string }>([
|
||||
{ state: 'running', label: 'Workflow is running' },
|
||||
{ state: 'completed', label: 'Workflow completed successfully' },
|
||||
{ state: 'error', label: 'Workflow execution failed' }
|
||||
])('renders accessible icon for $state state', ({ state, label }) => {
|
||||
const wrapper = mountWithI18n({ state })
|
||||
const icon = wrapper.find('i')
|
||||
expect(icon.exists()).toBe(true)
|
||||
expect(icon.attributes('aria-label')).toBe(label)
|
||||
})
|
||||
})
|
||||
25
src/components/topbar/WorkflowExecutionIndicator.vue
Normal file
25
src/components/topbar/WorkflowExecutionIndicator.vue
Normal file
@@ -0,0 +1,25 @@
|
||||
<template>
|
||||
<i
|
||||
v-if="state === 'running'"
|
||||
class="icon-[lucide--loader-circle] size-4 shrink-0 animate-spin text-muted-foreground"
|
||||
:aria-label="$t('workflowExecution.running')"
|
||||
/>
|
||||
<i
|
||||
v-else-if="state === 'completed'"
|
||||
class="icon-[lucide--circle-check] size-4 shrink-0 text-jade-600"
|
||||
:aria-label="$t('workflowExecution.completed')"
|
||||
/>
|
||||
<i
|
||||
v-else-if="state === 'error'"
|
||||
class="icon-[lucide--circle-alert] size-4 shrink-0 text-coral-600"
|
||||
:aria-label="$t('workflowExecution.error')"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { WorkflowExecutionState } from '@/stores/executionStore'
|
||||
|
||||
const { state } = defineProps<{
|
||||
state: WorkflowExecutionState
|
||||
}>()
|
||||
</script>
|
||||
@@ -20,6 +20,10 @@
|
||||
>
|
||||
{{ workflowOption.workflow.filename }}
|
||||
</span>
|
||||
<WorkflowExecutionIndicator
|
||||
v-if="showExecutionIndicator"
|
||||
:state="executionState"
|
||||
/>
|
||||
<div class="relative">
|
||||
<span
|
||||
v-if="shouldShowStatusIndicator"
|
||||
@@ -68,11 +72,12 @@ import {
|
||||
ContextMenuSeparator,
|
||||
ContextMenuTrigger
|
||||
} from 'reka-ui'
|
||||
import { computed, onUnmounted, ref } from 'vue'
|
||||
import { computed, onUnmounted, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import WorkflowActionsList from '@/components/common/WorkflowActionsList.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { useWorkflowExecutionState } from '@/composables/useWorkflowExecutionState'
|
||||
import {
|
||||
usePragmaticDraggable,
|
||||
usePragmaticDroppable
|
||||
@@ -87,6 +92,7 @@ import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||
import type { WorkflowMenuItem } from '@/types/workflowMenuItem'
|
||||
|
||||
import WorkflowExecutionIndicator from './WorkflowExecutionIndicator.vue'
|
||||
import WorkflowTabPopover from './WorkflowTabPopover.vue'
|
||||
|
||||
interface WorkflowOption {
|
||||
@@ -116,6 +122,33 @@ const workflowTabRef = ref<HTMLElement | null>(null)
|
||||
const popoverRef = ref<InstanceType<typeof WorkflowTabPopover> | null>(null)
|
||||
const workflowThumbnail = useWorkflowThumbnail()
|
||||
|
||||
const workflowId = computed(() => {
|
||||
const activeState = props.workflowOption.workflow.activeState
|
||||
const initialState = props.workflowOption.workflow.initialState
|
||||
return activeState?.id ?? initialState?.id
|
||||
})
|
||||
|
||||
const { state: executionState, clearResult } =
|
||||
useWorkflowExecutionState(workflowId)
|
||||
|
||||
const clearTimeoutId = ref<ReturnType<typeof setTimeout> | null>(null)
|
||||
|
||||
watch(executionState, (newState) => {
|
||||
if (clearTimeoutId.value) {
|
||||
clearTimeout(clearTimeoutId.value)
|
||||
clearTimeoutId.value = null
|
||||
}
|
||||
|
||||
if (newState === 'completed') {
|
||||
clearTimeoutId.value = setTimeout(() => {
|
||||
clearResult()
|
||||
clearTimeoutId.value = null
|
||||
}, 5000)
|
||||
}
|
||||
})
|
||||
|
||||
const showExecutionIndicator = computed(() => executionState.value !== 'idle')
|
||||
|
||||
// Use computed refs to cache autosave settings
|
||||
const autoSaveSetting = computed(() =>
|
||||
settingStore.get('Comfy.Workflow.AutoSave')
|
||||
@@ -287,6 +320,9 @@ usePragmaticDroppable(tabGetter, {
|
||||
|
||||
onUnmounted(() => {
|
||||
popoverRef.value?.hidePopover()
|
||||
if (clearTimeoutId.value) {
|
||||
clearTimeout(clearTimeoutId.value)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
71
src/composables/useWorkflowExecutionState.test.ts
Normal file
71
src/composables/useWorkflowExecutionState.test.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { createTestingPinia } from '@pinia/testing'
|
||||
import { setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import type { WorkflowExecutionState } from '@/stores/executionStore'
|
||||
|
||||
import { useWorkflowExecutionState } from './useWorkflowExecutionState'
|
||||
|
||||
const _workflowExecutionStates = ref(new Map<string, WorkflowExecutionState>())
|
||||
const _clearWorkflowExecutionResult = vi.fn()
|
||||
|
||||
vi.mock('@/stores/executionStore', () => ({
|
||||
useExecutionStore: () => ({
|
||||
getWorkflowExecutionState: (wid: string | undefined) => {
|
||||
if (!wid) return 'idle'
|
||||
return _workflowExecutionStates.value.get(wid) ?? 'idle'
|
||||
},
|
||||
clearWorkflowExecutionResult: _clearWorkflowExecutionResult
|
||||
})
|
||||
}))
|
||||
|
||||
describe('useWorkflowExecutionState', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createTestingPinia({ stubActions: false }))
|
||||
vi.clearAllMocks()
|
||||
_workflowExecutionStates.value = new Map()
|
||||
})
|
||||
|
||||
it('returns idle when workflowId is undefined', () => {
|
||||
const { state } = useWorkflowExecutionState(undefined)
|
||||
expect(state.value).toBe('idle')
|
||||
})
|
||||
|
||||
it('returns idle when no execution data exists', () => {
|
||||
const { state } = useWorkflowExecutionState('workflow-1')
|
||||
expect(state.value).toBe('idle')
|
||||
})
|
||||
|
||||
it('returns state from execution store map', () => {
|
||||
_workflowExecutionStates.value = new Map([['workflow-1', 'running']])
|
||||
const { state } = useWorkflowExecutionState('workflow-1')
|
||||
expect(state.value).toBe('running')
|
||||
})
|
||||
|
||||
it('reacts to workflowId ref changes', () => {
|
||||
const wfId = ref<string | undefined>('workflow-1')
|
||||
_workflowExecutionStates.value = new Map([
|
||||
['workflow-1', 'running'],
|
||||
['workflow-2', 'error']
|
||||
])
|
||||
|
||||
const { state } = useWorkflowExecutionState(wfId)
|
||||
expect(state.value).toBe('running')
|
||||
|
||||
wfId.value = 'workflow-2'
|
||||
expect(state.value).toBe('error')
|
||||
})
|
||||
|
||||
it('clearResult delegates to executionStore', () => {
|
||||
const { clearResult } = useWorkflowExecutionState('workflow-1')
|
||||
clearResult()
|
||||
expect(_clearWorkflowExecutionResult).toHaveBeenCalledWith('workflow-1')
|
||||
})
|
||||
|
||||
it('clearResult does nothing when workflowId is undefined', () => {
|
||||
const { clearResult } = useWorkflowExecutionState(undefined)
|
||||
clearResult()
|
||||
expect(_clearWorkflowExecutionResult).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
27
src/composables/useWorkflowExecutionState.ts
Normal file
27
src/composables/useWorkflowExecutionState.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import type { MaybeRefOrGetter } from 'vue'
|
||||
import { computed, toValue } from 'vue'
|
||||
|
||||
import type { WorkflowExecutionState } from '@/stores/executionStore'
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
|
||||
export function useWorkflowExecutionState(
|
||||
workflowId: MaybeRefOrGetter<string | undefined>
|
||||
) {
|
||||
const executionStore = useExecutionStore()
|
||||
|
||||
const state = computed<WorkflowExecutionState>(() =>
|
||||
executionStore.getWorkflowExecutionState(toValue(workflowId))
|
||||
)
|
||||
|
||||
function clearResult() {
|
||||
const wid = toValue(workflowId)
|
||||
if (wid) {
|
||||
executionStore.clearWorkflowExecutionResult(wid)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
state,
|
||||
clearResult
|
||||
}
|
||||
}
|
||||
@@ -3589,5 +3589,10 @@
|
||||
"builderMenu": {
|
||||
"enterAppMode": "Enter app mode",
|
||||
"exitAppBuilder": "Exit app builder"
|
||||
},
|
||||
"workflowExecution": {
|
||||
"running": "Workflow is running",
|
||||
"completed": "Workflow completed successfully",
|
||||
"error": "Workflow execution failed"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1626,6 +1626,18 @@ export class ComfyApp {
|
||||
}
|
||||
}
|
||||
|
||||
const activeWorkflow = useWorkspaceStore().workflow
|
||||
.activeWorkflow as ComfyWorkflow | undefined
|
||||
const wid =
|
||||
activeWorkflow?.activeState?.id ??
|
||||
activeWorkflow?.initialState?.id
|
||||
if (wid) {
|
||||
executionStore.setWorkflowExecutionResultByWorkflowId(
|
||||
String(wid),
|
||||
'error'
|
||||
)
|
||||
}
|
||||
|
||||
if (useSettingStore().get('Comfy.RightSidePanel.ShowErrorsTab')) {
|
||||
executionErrorStore.showErrorOverlay()
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { app } from '@/scripts/app'
|
||||
import type { NodeProgressState } from '@/schemas/apiSchema'
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
import { useExecutionErrorStore } from '@/stores/executionErrorStore'
|
||||
import { executionIdToNodeLocatorId } from '@/utils/graphTraversalUtil'
|
||||
@@ -11,6 +12,7 @@ const mockNodeIdToNodeLocatorId = vi.fn()
|
||||
const mockNodeLocatorIdToNodeExecutionId = vi.fn()
|
||||
|
||||
import type * as WorkflowStoreModule from '@/platform/workflow/management/stores/workflowStore'
|
||||
import type { ComfyWorkflow } from '@/platform/workflow/management/stores/workflowStore'
|
||||
import { createMockLGraphNode } from '@/utils/__tests__/litegraphTestUtils'
|
||||
import { createTestingPinia } from '@pinia/testing'
|
||||
|
||||
@@ -442,3 +444,111 @@ describe('useExecutionErrorStore - setMissingNodeTypes', () => {
|
||||
expect(store.missingNodesError?.nodeTypes).toEqual(input)
|
||||
})
|
||||
})
|
||||
|
||||
describe('useExecutionStore - Workflow Execution State', () => {
|
||||
let store: ReturnType<typeof useExecutionStore>
|
||||
|
||||
function mockWorkflow(id: string) {
|
||||
return { activeState: { id } } as unknown as ComfyWorkflow
|
||||
}
|
||||
|
||||
function mockNodeProgress(
|
||||
state: NodeProgressState['state']
|
||||
): NodeProgressState {
|
||||
return { value: 0, max: 1, state, node_id: '1', prompt_id: 'job-1' }
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
setActivePinia(createTestingPinia({ stubActions: false }))
|
||||
store = useExecutionStore()
|
||||
})
|
||||
|
||||
describe('workflowExecutionStates', () => {
|
||||
it('should return empty map when no workflows are running or have results', () => {
|
||||
expect(store.workflowExecutionStates.size).toBe(0)
|
||||
})
|
||||
|
||||
it('should return running state for workflows with active jobs', () => {
|
||||
store.storeJob({
|
||||
nodes: ['1', '2'],
|
||||
id: 'job-1',
|
||||
workflow: mockWorkflow('wf-1')
|
||||
})
|
||||
|
||||
store.nodeProgressStatesByJob = {
|
||||
'job-1': { '1': mockNodeProgress('running') }
|
||||
}
|
||||
|
||||
expect(store.workflowExecutionStates.get('wf-1')).toBe('running')
|
||||
})
|
||||
|
||||
it('should return completed state from last execution result', () => {
|
||||
const next = new Map(store.lastExecutionResultByWorkflowId)
|
||||
next.set('wf-1', {
|
||||
state: 'completed',
|
||||
timestamp: Date.now(),
|
||||
promptId: 'job-1'
|
||||
})
|
||||
store.lastExecutionResultByWorkflowId = next
|
||||
|
||||
expect(store.workflowExecutionStates.get('wf-1')).toBe('completed')
|
||||
})
|
||||
|
||||
it('should return error state from last execution result', () => {
|
||||
const next = new Map(store.lastExecutionResultByWorkflowId)
|
||||
next.set('wf-1', {
|
||||
state: 'error',
|
||||
timestamp: Date.now(),
|
||||
promptId: 'job-1'
|
||||
})
|
||||
store.lastExecutionResultByWorkflowId = next
|
||||
|
||||
expect(store.workflowExecutionStates.get('wf-1')).toBe('error')
|
||||
})
|
||||
|
||||
it('should prioritize running over completed/error', () => {
|
||||
store.storeJob({
|
||||
nodes: ['1', '2'],
|
||||
id: 'job-2',
|
||||
workflow: mockWorkflow('wf-1')
|
||||
})
|
||||
|
||||
store.nodeProgressStatesByJob = {
|
||||
'job-2': { '1': mockNodeProgress('running') }
|
||||
}
|
||||
|
||||
const next = new Map(store.lastExecutionResultByWorkflowId)
|
||||
next.set('wf-1', {
|
||||
state: 'completed',
|
||||
timestamp: Date.now(),
|
||||
promptId: 'job-1'
|
||||
})
|
||||
store.lastExecutionResultByWorkflowId = next
|
||||
|
||||
expect(store.workflowExecutionStates.get('wf-1')).toBe('running')
|
||||
})
|
||||
})
|
||||
|
||||
describe('getWorkflowExecutionState', () => {
|
||||
it('should return idle for undefined workflowId', () => {
|
||||
expect(store.getWorkflowExecutionState(undefined)).toBe('idle')
|
||||
})
|
||||
|
||||
it('should return idle for unknown workflowId', () => {
|
||||
expect(store.getWorkflowExecutionState('unknown')).toBe('idle')
|
||||
})
|
||||
|
||||
it('should return state from workflowExecutionStates map', () => {
|
||||
const next = new Map(store.lastExecutionResultByWorkflowId)
|
||||
next.set('wf-1', {
|
||||
state: 'completed',
|
||||
timestamp: Date.now(),
|
||||
promptId: 'job-1'
|
||||
})
|
||||
store.lastExecutionResultByWorkflowId = next
|
||||
|
||||
expect(store.getWorkflowExecutionState('wf-1')).toBe('completed')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -46,6 +46,14 @@ interface QueuedJob {
|
||||
workflow?: ComfyWorkflow
|
||||
}
|
||||
|
||||
export type WorkflowExecutionResult = {
|
||||
state: 'completed' | 'error'
|
||||
timestamp: number
|
||||
promptId?: string
|
||||
}
|
||||
|
||||
export type WorkflowExecutionState = 'idle' | 'running' | 'completed' | 'error'
|
||||
|
||||
export const useExecutionStore = defineStore('execution', () => {
|
||||
const workflowStore = useWorkflowStore()
|
||||
const canvasStore = useCanvasStore()
|
||||
@@ -73,6 +81,95 @@ export const useExecutionStore = defineStore('execution', () => {
|
||||
|
||||
const initializingJobIds = ref<Set<string>>(new Set())
|
||||
|
||||
/**
|
||||
* Map of workflow ID to last execution result for UI state display.
|
||||
*/
|
||||
const lastExecutionResultByWorkflowId = ref<
|
||||
Map<string, WorkflowExecutionResult>
|
||||
>(new Map())
|
||||
|
||||
function clearWorkflowExecutionResult(workflowId: string) {
|
||||
if (!lastExecutionResultByWorkflowId.value.has(workflowId)) return
|
||||
const next = new Map(lastExecutionResultByWorkflowId.value)
|
||||
next.delete(workflowId)
|
||||
lastExecutionResultByWorkflowId.value = next
|
||||
}
|
||||
|
||||
function setWorkflowExecutionResult(
|
||||
jobId: string,
|
||||
state: 'completed' | 'error'
|
||||
) {
|
||||
const wid = jobIdToWorkflowId.value.get(jobId)
|
||||
if (!wid) {
|
||||
console.warn(
|
||||
`[executionStore] No workflow mapping for job ${jobId}, execution result '${state}' dropped`
|
||||
)
|
||||
return
|
||||
}
|
||||
setWorkflowExecutionResultByWorkflowId(wid, state, jobId)
|
||||
}
|
||||
|
||||
function setWorkflowExecutionResultByWorkflowId(
|
||||
workflowId: string,
|
||||
state: 'completed' | 'error',
|
||||
promptId?: string
|
||||
) {
|
||||
const next = new Map(lastExecutionResultByWorkflowId.value)
|
||||
next.set(workflowId, {
|
||||
state,
|
||||
timestamp: Date.now(),
|
||||
promptId
|
||||
})
|
||||
lastExecutionResultByWorkflowId.value = next
|
||||
}
|
||||
|
||||
function batchSetWorkflowExecutionResults(
|
||||
results: Map<string, WorkflowExecutionResult>
|
||||
) {
|
||||
if (results.size === 0) return
|
||||
const next = new Map(lastExecutionResultByWorkflowId.value)
|
||||
for (const [workflowId, result] of results) {
|
||||
next.set(workflowId, result)
|
||||
}
|
||||
lastExecutionResultByWorkflowId.value = next
|
||||
}
|
||||
|
||||
/**
|
||||
* Computed map of workflow ID to execution state for reactive UI updates.
|
||||
*/
|
||||
const workflowExecutionStates = computed<Map<string, WorkflowExecutionState>>(
|
||||
() => {
|
||||
const states = new Map<string, WorkflowExecutionState>()
|
||||
|
||||
// Mark running workflows
|
||||
for (const jobId of runningJobIds.value) {
|
||||
const workflowId = jobIdToWorkflowId.value.get(jobId)
|
||||
if (workflowId) {
|
||||
states.set(workflowId, 'running')
|
||||
}
|
||||
}
|
||||
|
||||
// Add completed/error states for workflows not currently running
|
||||
for (const [
|
||||
workflowId,
|
||||
result
|
||||
] of lastExecutionResultByWorkflowId.value) {
|
||||
if (!states.has(workflowId)) {
|
||||
states.set(workflowId, result.state)
|
||||
}
|
||||
}
|
||||
|
||||
return states
|
||||
}
|
||||
)
|
||||
|
||||
function getWorkflowExecutionState(
|
||||
workflowId: string | undefined
|
||||
): WorkflowExecutionState {
|
||||
if (!workflowId) return 'idle'
|
||||
return workflowExecutionStates.value.get(workflowId) ?? 'idle'
|
||||
}
|
||||
|
||||
const mergeExecutionProgressStates = (
|
||||
currentState: NodeProgressState | undefined,
|
||||
newState: NodeProgressState
|
||||
@@ -254,12 +351,13 @@ export const useExecutionStore = defineStore('execution', () => {
|
||||
}
|
||||
|
||||
function handleExecutionSuccess(e: CustomEvent<ExecutionSuccessWsMessage>) {
|
||||
const jobId = e.detail.prompt_id
|
||||
setWorkflowExecutionResult(jobId, 'completed')
|
||||
if (isCloud && activeJobId.value) {
|
||||
useTelemetry()?.trackExecutionSuccess({
|
||||
jobId: activeJobId.value
|
||||
})
|
||||
}
|
||||
const jobId = e.detail.prompt_id
|
||||
resetExecutionState(jobId)
|
||||
}
|
||||
|
||||
@@ -328,9 +426,11 @@ export const useExecutionStore = defineStore('execution', () => {
|
||||
}
|
||||
|
||||
function handleExecutionError(e: CustomEvent<ExecutionErrorWsMessage>) {
|
||||
const jobId = e.detail.prompt_id
|
||||
setWorkflowExecutionResult(jobId, 'error')
|
||||
if (isCloud) {
|
||||
useTelemetry()?.trackExecutionError({
|
||||
jobId: e.detail.prompt_id,
|
||||
jobId,
|
||||
nodeId: String(e.detail.node_id),
|
||||
nodeType: e.detail.node_type,
|
||||
error: e.detail.exception_message
|
||||
@@ -591,6 +691,13 @@ export const useExecutionStore = defineStore('execution', () => {
|
||||
nodeLocatorIdToExecutionId,
|
||||
jobIdToWorkflowId,
|
||||
jobIdToSessionWorkflowPath,
|
||||
ensureSessionWorkflowPath
|
||||
ensureSessionWorkflowPath,
|
||||
// Workflow execution result tracking
|
||||
lastExecutionResultByWorkflowId,
|
||||
clearWorkflowExecutionResult,
|
||||
setWorkflowExecutionResultByWorkflowId,
|
||||
batchSetWorkflowExecutionResults,
|
||||
workflowExecutionStates,
|
||||
getWorkflowExecutionState
|
||||
}
|
||||
})
|
||||
|
||||
@@ -20,6 +20,7 @@ import type { ComfyApp } from '@/scripts/app'
|
||||
import { useExtensionService } from '@/services/extensionService'
|
||||
import { getJobDetail } from '@/services/jobOutputCache'
|
||||
import { useNodeOutputStore } from '@/stores/nodeOutputStore'
|
||||
import type { WorkflowExecutionResult } from '@/stores/executionStore'
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { getMediaTypeFromFilename } from '@/utils/formatUtil'
|
||||
@@ -556,6 +557,31 @@ export const useQueueStore = defineStore('queue', () => {
|
||||
.sort((a, b) => b.create_time - a.create_time)
|
||||
.slice(0, toValue(maxHistoryItems))
|
||||
|
||||
const seenWorkflows = new Set<string>()
|
||||
const batchResults = new Map<string, WorkflowExecutionResult>()
|
||||
for (const job of sortedHistory) {
|
||||
const workflowId = job.workflow_id
|
||||
if (!workflowId || seenWorkflows.has(workflowId)) continue
|
||||
seenWorkflows.add(workflowId)
|
||||
const state =
|
||||
job.status === 'failed'
|
||||
? 'error'
|
||||
: job.status === 'completed'
|
||||
? 'completed'
|
||||
: undefined
|
||||
if (!state) continue
|
||||
const existing =
|
||||
executionStore.lastExecutionResultByWorkflowId.get(workflowId)
|
||||
const jobTimestamp = job.create_time * 1000
|
||||
if (existing && existing.timestamp > jobTimestamp) continue
|
||||
batchResults.set(workflowId, {
|
||||
state,
|
||||
timestamp: jobTimestamp,
|
||||
promptId: job.id
|
||||
})
|
||||
}
|
||||
executionStore.batchSetWorkflowExecutionResults(batchResults)
|
||||
|
||||
// Reuse existing TaskItemImpl instances or create new
|
||||
// Must recreate if outputs_count changed (e.g., API started returning it)
|
||||
const existingByJobId = new Map(
|
||||
|
||||
Reference in New Issue
Block a user