Update the frontend to support async nodes.

This commit is contained in:
Jacob Segal
2025-06-09 16:56:54 -07:00
parent 48ac4a2b36
commit 1eba468f55
13 changed files with 318 additions and 59 deletions

View File

@@ -71,7 +71,6 @@ import { useWorkflowAutoSave } from '@/composables/useWorkflowAutoSave'
import { useWorkflowPersistence } from '@/composables/useWorkflowPersistence' import { useWorkflowPersistence } from '@/composables/useWorkflowPersistence'
import { CORE_SETTINGS } from '@/constants/coreSettings' import { CORE_SETTINGS } from '@/constants/coreSettings'
import { i18n, t } from '@/i18n' import { i18n, t } from '@/i18n'
import type { NodeId } from '@/schemas/comfyWorkflowSchema'
import { UnauthorizedError, api } from '@/scripts/api' import { UnauthorizedError, api } from '@/scripts/api'
import { app as comfyApp } from '@/scripts/app' import { app as comfyApp } from '@/scripts/app'
import { ChangeTracker } from '@/scripts/changeTracker' import { ChangeTracker } from '@/scripts/changeTracker'
@@ -189,22 +188,46 @@ watch(
} }
) )
// Update the progress of the executing node // Update the progress of executing nodes
watch( watch(
() => () => executionStore.nodeProgressStates,
[executionStore.executingNodeId, executionStore.executingNodeProgress] as [ (nodeProgressStates) => {
NodeId | null, // Clear progress for all nodes first
number | null
],
([executingNodeId, executingNodeProgress]) => {
for (const node of comfyApp.graph.nodes) { for (const node of comfyApp.graph.nodes) {
if (node.id == executingNodeId) { node.progress = undefined
node.progress = executingNodeProgress ?? undefined }
} else {
node.progress = undefined // Then set progress for nodes with progress states
for (const nodeId in nodeProgressStates) {
const progressState = nodeProgressStates[nodeId]
const node = comfyApp.graph.getNodeById(progressState.display_node_id)
if (node && progressState) {
// Only show progress for running nodes
if (progressState.state === 'running') {
if (node.progress === undefined || node.progress === 0.0) {
console.log(
`${Date.now()} Setting progress for node ${node.id} to ${progressState.value / progressState.max}=${progressState.value}/${progressState.max} due to ${nodeId}`
)
node.progress = progressState.value / progressState.max
} else {
// Update progress if it was already set
console.log(
`${Date.now()} Setting progress for node ${node.id} to Math.min(${node.progress}, ${progressState.value / progressState.max}=${progressState.value}/${progressState.max}) due to ${nodeId}`
)
node.progress = Math.min(
node.progress,
progressState.value / progressState.max
)
}
}
} }
} }
}
// TODO - Do we need to force canvas redraw here?
// comfyApp.graph.setDirtyCanvas(true, true)
},
{ deep: true }
) )
// Update node slot errors // Update node slot errors

View File

@@ -20,33 +20,44 @@ import { useExecutionStore } from '@/stores/executionStore'
import { linkifyHtml, nl2br } from '@/utils/formatUtil' import { linkifyHtml, nl2br } from '@/utils/formatUtil'
const modelValue = defineModel<string>({ required: true }) const modelValue = defineModel<string>({ required: true })
defineProps<{ const props = defineProps<{
widget?: object widget?: object
nodeId: NodeId
}>() }>()
const executionStore = useExecutionStore() const executionStore = useExecutionStore()
const isParentNodeExecuting = ref(true) const isParentNodeExecuting = ref(true)
const formattedText = computed(() => nl2br(linkifyHtml(modelValue.value))) const formattedText = computed(() => nl2br(linkifyHtml(modelValue.value)))
let executingNodeId: NodeId | null = null let parentNodeId: NodeId | null = null
onMounted(() => { onMounted(() => {
executingNodeId = executionStore.executingNodeId // Get the parent node ID from props if provided
// For backward compatibility, fall back to the first executing node
parentNodeId = props.nodeId
}) })
// Watch for either a new node has starting execution or overall execution ending // Watch for either a new node has starting execution or overall execution ending
const stopWatching = watch( const stopWatching = watch(
[() => executionStore.executingNode, () => executionStore.isIdle], [() => executionStore.executingNodeIds, () => executionStore.isIdle],
() => { () => {
if (executionStore.isIdle) {
isParentNodeExecuting.value = false
stopWatching()
return
}
// Check if parent node is no longer in the executing nodes list
if ( if (
executionStore.isIdle || parentNodeId &&
(executionStore.executingNode && !executionStore.executingNodeIds.includes(parentNodeId)
executionStore.executingNode.id !== executingNodeId)
) { ) {
isParentNodeExecuting.value = false isParentNodeExecuting.value = false
stopWatching() stopWatching()
} }
if (!executingNodeId) {
executingNodeId = executionStore.executingNodeId // Set parent node ID if not set yet
if (!parentNodeId && executionStore.executingNodeIds.length > 0) {
parentNodeId = executionStore.executingNodeIds[0]
} }
} }
) )

View File

@@ -1,6 +1,7 @@
import { useTitle } from '@vueuse/core' import { useTitle } from '@vueuse/core'
import { computed } from 'vue' import { computed } from 'vue'
import { t } from '@/i18n'
import { useExecutionStore } from '@/stores/executionStore' import { useExecutionStore } from '@/stores/executionStore'
import { useSettingStore } from '@/stores/settingStore' import { useSettingStore } from '@/stores/settingStore'
import { useWorkflowStore } from '@/stores/workflowStore' import { useWorkflowStore } from '@/stores/workflowStore'
@@ -36,11 +37,35 @@ export const useBrowserTabTitle = () => {
: DEFAULT_TITLE : DEFAULT_TITLE
}) })
const nodeExecutionTitle = computed(() => const nodeExecutionTitle = computed(() => {
executionStore.executingNode && executionStore.executingNodeProgress // Check if any nodes are in progress
? `${executionText.value}[${Math.round(executionStore.executingNodeProgress * 100)}%] ${executionStore.executingNode.type}` const nodeProgressEntries = Object.entries(
: '' executionStore.nodeProgressStates
) )
const runningNodes = nodeProgressEntries.filter(
([_, state]) => state.state === 'running'
)
if (runningNodes.length > 0) {
// If multiple nodes are running
if (runningNodes.length > 1) {
return `${executionText.value}[${runningNodes.length} ${t('g.nodesRunning', 'nodes running')}]`
}
// If only one node is running
else {
const [nodeId, state] = runningNodes[0]
const progress = Math.round((state.value / state.max) * 100)
const nodeType =
executionStore.activePrompt?.workflow?.changeTracker?.activeState?.nodes.find(
(n) => String(n.id) === nodeId
)?.type || 'Node'
return `${executionText.value}[${progress}%] ${nodeType}`
}
}
return ''
})
const workflowTitle = computed( const workflowTitle = computed(
() => () =>

View File

@@ -23,6 +23,9 @@ export const useTextPreviewWidget = (
name: inputSpec.name, name: inputSpec.name,
component: TextPreviewWidget, component: TextPreviewWidget,
inputSpec, inputSpec,
componentProps: {
nodeId: node.id
},
options: { options: {
getValue: () => widgetValue.value, getValue: () => widgetValue.value,
setValue: (value: string | object) => { setValue: (value: string | object) => {

View File

@@ -9,6 +9,7 @@ import {
} from '@/schemas/comfyWorkflowSchema' } from '@/schemas/comfyWorkflowSchema'
import type { ComfyNodeDef } from '@/schemas/nodeDefSchema' import type { ComfyNodeDef } from '@/schemas/nodeDefSchema'
import { useDialogService } from '@/services/dialogService' import { useDialogService } from '@/services/dialogService'
import { useExecutionStore } from '@/stores/executionStore'
import { useNodeDefStore } from '@/stores/nodeDefStore' import { useNodeDefStore } from '@/stores/nodeDefStore'
import { useToastStore } from '@/stores/toastStore' import { useToastStore } from '@/stores/toastStore'
import { useWidgetStore } from '@/stores/widgetStore' import { useWidgetStore } from '@/stores/widgetStore'
@@ -1178,9 +1179,10 @@ export class GroupNodeHandler {
node.onDrawForeground = function (ctx) { node.onDrawForeground = function (ctx) {
// @ts-expect-error fixme ts strict error // @ts-expect-error fixme ts strict error
onDrawForeground?.apply?.(this, arguments) onDrawForeground?.apply?.(this, arguments)
const executionStore = useExecutionStore()
if ( if (
// @ts-expect-error fixme ts strict error executionStore.nodeProgressStates[this.id] &&
+app.runningNodeId === this.id && executionStore.nodeProgressStates[this.id].state === 'running' &&
this.runningInternalNodeId !== null this.runningInternalNodeId !== null
) { ) {
// @ts-expect-error fixme ts strict error // @ts-expect-error fixme ts strict error
@@ -1275,6 +1277,45 @@ export class GroupNodeHandler {
// @ts-expect-error fixme ts strict error // @ts-expect-error fixme ts strict error
(_, id) => id (_, id) => id
) )
/*
// Handle progress_state events for multiple executing nodes
const progress_state = handleEvent.call(
this,
'progress_state',
(d) => {
// Check if any of our inner nodes are in this progress state update
for (const nodeId in d.nodes) {
const innerNodeIndex = this.innerNodes?.findIndex((n) => n.id == nodeId);
if (innerNodeIndex > -1) return nodeId;
}
return null;
},
(d, id, node) => {
// Create a new progress_state event with just our group node
const newProgressState = { ...d };
newProgressState.nodes = { [id]: {
node: id,
state: 'running',
value: 0,
max: 1,
prompt_id: d.prompt_id
}};
// If we have a specific running internal node, update its state
if (node.runningInternalNodeId !== null) {
const innerNodeId = this.innerNodes[node.runningInternalNodeId].id;
if (d.nodes[innerNodeId]) {
newProgressState.nodes[id] = {
...d.nodes[innerNodeId],
node: id
};
}
}
return newProgressState;
}
);
*/
const executed = handleEvent.call( const executed = handleEvent.call(
this, this,
@@ -1294,6 +1335,7 @@ export class GroupNodeHandler {
this.node.onRemoved = function () { this.node.onRemoved = function () {
// @ts-expect-error fixme ts strict error // @ts-expect-error fixme ts strict error
onRemoved?.apply(this, arguments) onRemoved?.apply(this, arguments)
// api.removeEventListener('progress_state', progress_state)
api.removeEventListener('executing', executing) api.removeEventListener('executing', executing)
api.removeEventListener('executed', executed) api.removeEventListener('executed', executed)
} }

View File

@@ -123,7 +123,8 @@
"copy": "Copy", "copy": "Copy",
"imageUrl": "Image URL", "imageUrl": "Image URL",
"clear": "Clear", "clear": "Clear",
"copyURL": "Copy URL" "copyURL": "Copy URL",
"nodesRunning": "nodes running"
}, },
"manager": { "manager": {
"title": "Custom Nodes Manager", "title": "Custom Nodes Manager",

View File

@@ -45,6 +45,22 @@ const zProgressWsMessage = z.object({
node: zNodeId node: zNodeId
}) })
const zNodeProgressState = z.object({
value: z.number(),
max: z.number(),
state: z.enum(['pending', 'running', 'finished', 'error']),
node_id: zNodeId,
prompt_id: zPromptId,
display_node_id: zNodeId.optional(),
parent_node_id: zNodeId.optional(),
real_node_id: zNodeId.optional()
})
const zProgressStateWsMessage = z.object({
prompt_id: zPromptId,
nodes: z.record(zNodeId, zNodeProgressState)
})
const zExecutingWsMessage = z.object({ const zExecutingWsMessage = z.object({
node: zNodeId, node: zNodeId,
display_node: zNodeId, display_node: zNodeId,
@@ -129,6 +145,8 @@ export type ProgressTextWsMessage = z.infer<typeof zProgressTextWsMessage>
export type DisplayComponentWsMessage = z.infer< export type DisplayComponentWsMessage = z.infer<
typeof zDisplayComponentWsMessage typeof zDisplayComponentWsMessage
> >
export type NodeProgressState = z.infer<typeof zNodeProgressState>
export type ProgressStateWsMessage = z.infer<typeof zProgressStateWsMessage>
// End of ws messages // End of ws messages
const zPromptInputItem = z.object({ const zPromptInputItem = z.object({

View File

@@ -15,6 +15,7 @@ import type {
LogsRawResponse, LogsRawResponse,
LogsWsMessage, LogsWsMessage,
PendingTaskItem, PendingTaskItem,
ProgressStateWsMessage,
ProgressTextWsMessage, ProgressTextWsMessage,
ProgressWsMessage, ProgressWsMessage,
PromptResponse, PromptResponse,
@@ -103,7 +104,17 @@ interface BackendApiCalls {
logs: LogsWsMessage logs: LogsWsMessage
/** Binary preview/progress data */ /** Binary preview/progress data */
b_preview: Blob b_preview: Blob
/** Binary preview with metadata (node_id, prompt_id) */
b_preview_with_metadata: {
blob: Blob
nodeId: string
parentNodeId: string
displayNodeId: string
realNodeId: string
promptId: string
}
progress_text: ProgressTextWsMessage progress_text: ProgressTextWsMessage
progress_state: ProgressStateWsMessage
display_component: DisplayComponentWsMessage display_component: DisplayComponentWsMessage
} }
@@ -432,6 +443,33 @@ export class ComfyApi extends EventTarget {
}) })
this.dispatchCustomEvent('b_preview', imageBlob) this.dispatchCustomEvent('b_preview', imageBlob)
break break
case 4:
// PREVIEW_IMAGE_WITH_METADATA
const decoder4 = new TextDecoder()
const metadataLength = view.getUint32(4)
const metadataBytes = event.data.slice(8, 8 + metadataLength)
const metadata = JSON.parse(decoder4.decode(metadataBytes))
const imageData4 = event.data.slice(8 + metadataLength)
let imageMime4 = metadata.image_type
const imageBlob4 = new Blob([imageData4], {
type: imageMime4
})
// Dispatch enhanced preview event with metadata
this.dispatchCustomEvent('b_preview_with_metadata', {
blob: imageBlob4,
nodeId: metadata.node_id,
displayNodeId: metadata.display_node_id,
parentNodeId: metadata.parent_node_id,
realNodeId: metadata.real_node_id,
promptId: metadata.prompt_id
})
// Also dispatch legacy b_preview for backward compatibility
this.dispatchCustomEvent('b_preview', imageBlob4)
break
default: default:
throw new Error( throw new Error(
`Unknown binary websocket message of type ${eventType}` `Unknown binary websocket message of type ${eventType}`
@@ -461,6 +499,7 @@ export class ComfyApi extends EventTarget {
case 'execution_cached': case 'execution_cached':
case 'execution_success': case 'execution_success':
case 'progress': case 'progress':
case 'progress_state':
case 'executed': case 'executed':
case 'graphChanged': case 'graphChanged':
case 'promptQueued': case 'promptQueued':

View File

@@ -193,6 +193,8 @@ export class ComfyApp {
/** /**
* @deprecated Use useExecutionStore().executingNodeId instead * @deprecated Use useExecutionStore().executingNodeId instead
* TODO: Update to support multiple executing nodes. This getter returns only the first executing node.
* Consider updating consumers to handle multiple nodes or use executingNodeIds array.
*/ */
get runningNodeId(): NodeId | null { get runningNodeId(): NodeId | null {
return useExecutionStore().executingNodeId return useExecutionStore().executingNodeId
@@ -634,10 +636,6 @@ export class ComfyApp {
api.addEventListener('executing', () => { api.addEventListener('executing', () => {
this.graph.setDirtyCanvas(true, false) this.graph.setDirtyCanvas(true, false)
// @ts-expect-error fixme ts strict error
this.revokePreviews(this.runningNodeId)
// @ts-expect-error fixme ts strict error
delete this.nodePreviewImages[this.runningNodeId]
}) })
api.addEventListener('executed', ({ detail }) => { api.addEventListener('executed', ({ detail }) => {
@@ -686,15 +684,13 @@ export class ComfyApp {
this.canvas.draw(true, true) this.canvas.draw(true, true)
}) })
api.addEventListener('b_preview', ({ detail }) => { api.addEventListener('b_preview_with_metadata', ({ detail }) => {
const id = this.runningNodeId // Enhanced preview with explicit node context
if (id == null) return const { blob, displayNodeId } = detail
this.revokePreviews(displayNodeId)
const blob = detail
const blobUrl = URL.createObjectURL(blob) const blobUrl = URL.createObjectURL(blob)
// Ensure clean up if `executing` event is missed. // Preview cleanup is now handled in progress_state event to support multiple concurrent previews
this.revokePreviews(id) this.nodePreviewImages[displayNodeId] = [blobUrl]
this.nodePreviewImages[id] = [blobUrl]
}) })
api.init() api.init()

View File

@@ -237,6 +237,7 @@ export class ComponentWidgetImpl<
component: Component component: Component
inputSpec: InputSpec inputSpec: InputSpec
props?: P props?: P
componentProps?: Record<string, unknown>
options: DOMWidgetOptions<V> options: DOMWidgetOptions<V>
}) { }) {
super({ super({
@@ -245,7 +246,9 @@ export class ComponentWidgetImpl<
}) })
this.component = obj.component this.component = obj.component
this.inputSpec = obj.inputSpec this.inputSpec = obj.inputSpec
this.props = obj.props this.props = obj.componentProps
? ({ ...obj.props, ...obj.componentProps } as P)
: obj.props
} }
override computeLayoutSize() { override computeLayoutSize() {

View File

@@ -29,6 +29,7 @@ import type {
import type { ComfyNodeDef as ComfyNodeDefV1 } from '@/schemas/nodeDefSchema' import type { ComfyNodeDef as ComfyNodeDefV1 } from '@/schemas/nodeDefSchema'
import { ComfyApp, app } from '@/scripts/app' import { ComfyApp, app } from '@/scripts/app'
import { $el } from '@/scripts/ui' import { $el } from '@/scripts/ui'
import { useExecutionStore } from '@/stores/executionStore'
import { useCanvasStore } from '@/stores/graphStore' import { useCanvasStore } from '@/stores/graphStore'
import { useNodeOutputStore } from '@/stores/imagePreviewStore' import { useNodeOutputStore } from '@/stores/imagePreviewStore'
import { ComfyNodeDefImpl } from '@/stores/nodeDefStore' import { ComfyNodeDefImpl } from '@/stores/nodeDefStore'
@@ -96,7 +97,12 @@ export const useLitegraphService = () => {
*/ */
#setupStrokeStyles() { #setupStrokeStyles() {
this.strokeStyles['running'] = function (this: LGraphNode) { this.strokeStyles['running'] = function (this: LGraphNode) {
if (this.id == app.runningNodeId) { const nodeProgressStates = useExecutionStore().nodeProgressStates
const nodeId = String(this.id)
if (
nodeProgressStates[nodeId] &&
nodeProgressStates[nodeId].state === 'running'
) {
return { color: '#0f0' } return { color: '#0f0' }
} }
} }

View File

@@ -11,6 +11,8 @@ import type {
ExecutionErrorWsMessage, ExecutionErrorWsMessage,
ExecutionStartWsMessage, ExecutionStartWsMessage,
NodeError, NodeError,
NodeProgressState,
ProgressStateWsMessage,
ProgressTextWsMessage, ProgressTextWsMessage,
ProgressWsMessage ProgressWsMessage
} from '@/schemas/apiSchema' } from '@/schemas/apiSchema'
@@ -42,7 +44,22 @@ export const useExecutionStore = defineStore('execution', () => {
const queuedPrompts = ref<Record<NodeId, QueuedPrompt>>({}) const queuedPrompts = ref<Record<NodeId, QueuedPrompt>>({})
const lastNodeErrors = ref<Record<NodeId, NodeError> | null>(null) const lastNodeErrors = ref<Record<NodeId, NodeError> | null>(null)
const lastExecutionError = ref<ExecutionErrorWsMessage | null>(null) const lastExecutionError = ref<ExecutionErrorWsMessage | null>(null)
const executingNodeId = ref<NodeId | null>(null) // This is the progress of all nodes in the currently executing workflow
const nodeProgressStates = ref<Record<string, NodeProgressState>>({})
// Easily access all currently executing node IDs
const executingNodeIds = computed<NodeId[]>(() => {
return Object.entries(nodeProgressStates)
.filter(([_, state]) => state.state === 'running')
.map(([nodeId, _]) => nodeId)
})
// For backward compatibility - stores the primary executing node ID
const executingNodeId = computed<NodeId | null>(() => {
return executingNodeIds.value.length > 0 ? executingNodeIds.value[0] : null
})
// For backward compatibility - returns the primary executing node
const executingNode = computed<ComfyNode | null>(() => { const executingNode = computed<ComfyNode | null>(() => {
if (!executingNodeId.value) return null if (!executingNodeId.value) return null
@@ -60,7 +77,7 @@ export const useExecutionStore = defineStore('execution', () => {
) )
}) })
// This is the progress of the currently executing node, if any // This is the progress of the currently executing node (for backward compatibility)
const _executingNodeProgress = ref<ProgressWsMessage | null>(null) const _executingNodeProgress = ref<ProgressWsMessage | null>(null)
const executingNodeProgress = computed(() => const executingNodeProgress = computed(() =>
_executingNodeProgress.value _executingNodeProgress.value
@@ -97,6 +114,7 @@ export const useExecutionStore = defineStore('execution', () => {
api.addEventListener('executed', handleExecuted) api.addEventListener('executed', handleExecuted)
api.addEventListener('executing', handleExecuting) api.addEventListener('executing', handleExecuting)
api.addEventListener('progress', handleProgress) api.addEventListener('progress', handleProgress)
api.addEventListener('progress_state', handleProgressState)
api.addEventListener('status', handleStatus) api.addEventListener('status', handleStatus)
api.addEventListener('execution_error', handleExecutionError) api.addEventListener('execution_error', handleExecutionError)
} }
@@ -109,6 +127,7 @@ export const useExecutionStore = defineStore('execution', () => {
api.removeEventListener('executed', handleExecuted) api.removeEventListener('executed', handleExecuted)
api.removeEventListener('executing', handleExecuting) api.removeEventListener('executing', handleExecuting)
api.removeEventListener('progress', handleProgress) api.removeEventListener('progress', handleProgress)
api.removeEventListener('progress_state', handleProgressState)
api.removeEventListener('status', handleStatus) api.removeEventListener('status', handleStatus)
api.removeEventListener('execution_error', handleExecutionError) api.removeEventListener('execution_error', handleExecutionError)
api.removeEventListener('progress_text', handleProgressText) api.removeEventListener('progress_text', handleProgressText)
@@ -138,12 +157,8 @@ export const useExecutionStore = defineStore('execution', () => {
if (!activePrompt.value) return if (!activePrompt.value) return
if (executingNodeId.value && activePrompt.value) { // Update the executing nodes list
// Seems sometimes nodes that are cached fire executing but not executed if (e.detail === null) {
activePrompt.value.nodes[executingNodeId.value] = true
}
executingNodeId.value = e.detail
if (executingNodeId.value === null) {
if (activePromptId.value) { if (activePromptId.value) {
delete queuedPrompts.value[activePromptId.value] delete queuedPrompts.value[activePromptId.value]
} }
@@ -151,6 +166,37 @@ export const useExecutionStore = defineStore('execution', () => {
} }
} }
function handleProgressState(e: CustomEvent<ProgressStateWsMessage>) {
const { nodes } = e.detail
// Revoke previews for nodes that are starting to execute
for (const nodeId in nodes) {
const nodeState = nodes[nodeId]
if (nodeState.state === 'running' && !nodeProgressStates.value[nodeId]) {
// This node just started executing, revoke its previews
// Note that we're doing the *actual* node id instead of the display node id
// here intentionally. That way, we don't clear the preview every time a new node
// within an expanded graph starts executing.
app.revokePreviews(nodeId)
delete app.nodePreviewImages[nodeId]
}
}
// Update the progress states for all nodes
nodeProgressStates.value = nodes
// If we have progress for the currently executing node, update it for backwards compatibility
if (executingNodeId.value && nodes[executingNodeId.value]) {
const nodeState = nodes[executingNodeId.value]
_executingNodeProgress.value = {
value: nodeState.value,
max: nodeState.max,
prompt_id: nodeState.prompt_id,
node: nodeState.display_node_id || nodeState.node_id
}
}
}
function handleProgress(e: CustomEvent<ProgressWsMessage>) { function handleProgress(e: CustomEvent<ProgressWsMessage>) {
_executingNodeProgress.value = e.detail _executingNodeProgress.value = e.detail
} }
@@ -238,9 +284,13 @@ export const useExecutionStore = defineStore('execution', () => {
*/ */
lastExecutionError, lastExecutionError,
/** /**
* The id of the node that is currently being executed * The id of the node that is currently being executed (backward compatibility)
*/ */
executingNodeId, executingNodeId,
/**
* The list of all nodes that are currently executing
*/
executingNodeIds,
/** /**
* The prompt that is currently being executed * The prompt that is currently being executed
*/ */
@@ -258,13 +308,17 @@ export const useExecutionStore = defineStore('execution', () => {
*/ */
executionProgress, executionProgress,
/** /**
* The node that is currently being executed * The node that is currently being executed (backward compatibility)
*/ */
executingNode, executingNode,
/** /**
* The progress of the executing node (if the node reports progress) * The progress of the executing node (backward compatibility)
*/ */
executingNodeProgress, executingNodeProgress,
/**
* All node progress states from progress_state events
*/
nodeProgressStates,
bindExecutionEvents, bindExecutionEvents,
unbindExecutionEvents, unbindExecutionEvents,
storePrompt, storePrompt,

View File

@@ -3,12 +3,20 @@ import { nextTick, reactive } from 'vue'
import { useBrowserTabTitle } from '@/composables/useBrowserTabTitle' import { useBrowserTabTitle } from '@/composables/useBrowserTabTitle'
// Mock i18n module
vi.mock('@/i18n', () => ({
t: (key: string, fallback: string) =>
key === 'g.nodesRunning' ? 'nodes running' : fallback
}))
// Mock the execution store // Mock the execution store
const executionStore = reactive({ const executionStore = reactive({
isIdle: true, isIdle: true,
executionProgress: 0, executionProgress: 0,
executingNode: null as any, executingNode: null as any,
executingNodeProgress: 0 executingNodeProgress: 0,
nodeProgressStates: {} as any,
activePrompt: null as any
}) })
vi.mock('@/stores/executionStore', () => ({ vi.mock('@/stores/executionStore', () => ({
useExecutionStore: () => executionStore useExecutionStore: () => executionStore
@@ -37,6 +45,8 @@ describe('useBrowserTabTitle', () => {
executionStore.executionProgress = 0 executionStore.executionProgress = 0
executionStore.executingNode = null as any executionStore.executingNode = null as any
executionStore.executingNodeProgress = 0 executionStore.executingNodeProgress = 0
executionStore.nodeProgressStates = {}
executionStore.activePrompt = null
// reset setting and workflow stores // reset setting and workflow stores
;(settingStore.get as any).mockReturnValue('Enabled') ;(settingStore.get as any).mockReturnValue('Enabled')
@@ -97,13 +107,41 @@ describe('useBrowserTabTitle', () => {
expect(document.title).toBe('[30%]ComfyUI') expect(document.title).toBe('[30%]ComfyUI')
}) })
it('shows node execution title when executing a node', async () => { it('shows node execution title when executing a node using nodeProgressStates', async () => {
executionStore.isIdle = false executionStore.isIdle = false
executionStore.executionProgress = 0.4 executionStore.executionProgress = 0.4
executionStore.executingNodeProgress = 0.5 executionStore.nodeProgressStates = {
executionStore.executingNode = { type: 'Foo' } '1': { state: 'running', value: 5, max: 10, node: '1', prompt_id: 'test' }
}
executionStore.activePrompt = {
workflow: {
changeTracker: {
activeState: {
nodes: [{ id: 1, type: 'Foo' }]
}
}
}
}
useBrowserTabTitle() useBrowserTabTitle()
await nextTick() await nextTick()
expect(document.title).toBe('[40%][50%] Foo') expect(document.title).toBe('[40%][50%] Foo')
}) })
it('shows multiple nodes running when multiple nodes are executing', async () => {
executionStore.isIdle = false
executionStore.executionProgress = 0.4
executionStore.nodeProgressStates = {
'1': {
state: 'running',
value: 5,
max: 10,
node: '1',
prompt_id: 'test'
},
'2': { state: 'running', value: 8, max: 10, node: '2', prompt_id: 'test' }
}
useBrowserTabTitle()
await nextTick()
expect(document.title).toBe('[40%][2 nodes running]')
})
}) })