mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-05 15:40:10 +00:00
Update the frontend to support async nodes.
This commit is contained in:
@@ -72,7 +72,6 @@ import { useWorkflowAutoSave } from '@/composables/useWorkflowAutoSave'
|
||||
import { useWorkflowPersistence } from '@/composables/useWorkflowPersistence'
|
||||
import { CORE_SETTINGS } from '@/constants/coreSettings'
|
||||
import { i18n, t } from '@/i18n'
|
||||
import type { NodeId } from '@/schemas/comfyWorkflowSchema'
|
||||
import { UnauthorizedError, api } from '@/scripts/api'
|
||||
import { app as comfyApp } from '@/scripts/app'
|
||||
import { ChangeTracker } from '@/scripts/changeTracker'
|
||||
@@ -191,22 +190,46 @@ watch(
|
||||
}
|
||||
)
|
||||
|
||||
// Update the progress of the executing node
|
||||
// Update the progress of executing nodes
|
||||
watch(
|
||||
() =>
|
||||
[
|
||||
executionStore.executingNodeId,
|
||||
executionStore.executingNodeProgress
|
||||
] satisfies [NodeId | null, number | null],
|
||||
([executingNodeId, executingNodeProgress]) => {
|
||||
() => executionStore.nodeProgressStates,
|
||||
(nodeProgressStates) => {
|
||||
// Clear progress for all nodes first
|
||||
for (const node of comfyApp.graph.nodes) {
|
||||
if (node.id == executingNodeId) {
|
||||
node.progress = executingNodeProgress ?? undefined
|
||||
} else {
|
||||
node.progress = undefined
|
||||
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
|
||||
|
||||
@@ -20,33 +20,44 @@ import { useExecutionStore } from '@/stores/executionStore'
|
||||
import { linkifyHtml, nl2br } from '@/utils/formatUtil'
|
||||
|
||||
const modelValue = defineModel<string>({ required: true })
|
||||
defineProps<{
|
||||
const props = defineProps<{
|
||||
widget?: object
|
||||
nodeId: NodeId
|
||||
}>()
|
||||
|
||||
const executionStore = useExecutionStore()
|
||||
const isParentNodeExecuting = ref(true)
|
||||
const formattedText = computed(() => nl2br(linkifyHtml(modelValue.value)))
|
||||
|
||||
let executingNodeId: NodeId | null = null
|
||||
let parentNodeId: NodeId | null = null
|
||||
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
|
||||
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 (
|
||||
executionStore.isIdle ||
|
||||
(executionStore.executingNode &&
|
||||
executionStore.executingNode.id !== executingNodeId)
|
||||
parentNodeId &&
|
||||
!executionStore.executingNodeIds.includes(parentNodeId)
|
||||
) {
|
||||
isParentNodeExecuting.value = false
|
||||
stopWatching()
|
||||
}
|
||||
if (!executingNodeId) {
|
||||
executingNodeId = executionStore.executingNodeId
|
||||
|
||||
// Set parent node ID if not set yet
|
||||
if (!parentNodeId && executionStore.executingNodeIds.length > 0) {
|
||||
parentNodeId = executionStore.executingNodeIds[0]
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useTitle } from '@vueuse/core'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { t } from '@/i18n'
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useWorkflowStore } from '@/stores/workflowStore'
|
||||
@@ -36,11 +37,35 @@ export const useBrowserTabTitle = () => {
|
||||
: DEFAULT_TITLE
|
||||
})
|
||||
|
||||
const nodeExecutionTitle = computed(() =>
|
||||
executionStore.executingNode && executionStore.executingNodeProgress
|
||||
? `${executionText.value}[${Math.round(executionStore.executingNodeProgress * 100)}%] ${executionStore.executingNode.type}`
|
||||
: ''
|
||||
)
|
||||
const nodeExecutionTitle = computed(() => {
|
||||
// Check if any nodes are in progress
|
||||
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(
|
||||
() =>
|
||||
|
||||
@@ -23,6 +23,9 @@ export const useTextPreviewWidget = (
|
||||
name: inputSpec.name,
|
||||
component: TextPreviewWidget,
|
||||
inputSpec,
|
||||
componentProps: {
|
||||
nodeId: node.id
|
||||
},
|
||||
options: {
|
||||
getValue: () => widgetValue.value,
|
||||
setValue: (value: string | object) => {
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
} from '@/schemas/comfyWorkflowSchema'
|
||||
import type { ComfyNodeDef } from '@/schemas/nodeDefSchema'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
import { useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
import { useToastStore } from '@/stores/toastStore'
|
||||
import { useWidgetStore } from '@/stores/widgetStore'
|
||||
@@ -1178,9 +1179,10 @@ export class GroupNodeHandler {
|
||||
node.onDrawForeground = function (ctx) {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
onDrawForeground?.apply?.(this, arguments)
|
||||
const executionStore = useExecutionStore()
|
||||
if (
|
||||
// @ts-expect-error fixme ts strict error
|
||||
+app.runningNodeId === this.id &&
|
||||
executionStore.nodeProgressStates[this.id] &&
|
||||
executionStore.nodeProgressStates[this.id].state === 'running' &&
|
||||
this.runningInternalNodeId !== null
|
||||
) {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
@@ -1275,6 +1277,45 @@ export class GroupNodeHandler {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
(_, 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(
|
||||
this,
|
||||
@@ -1294,6 +1335,7 @@ export class GroupNodeHandler {
|
||||
this.node.onRemoved = function () {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
onRemoved?.apply(this, arguments)
|
||||
// api.removeEventListener('progress_state', progress_state)
|
||||
api.removeEventListener('executing', executing)
|
||||
api.removeEventListener('executed', executed)
|
||||
}
|
||||
|
||||
@@ -133,7 +133,8 @@
|
||||
"copyURL": "Copy URL",
|
||||
"releaseTitle": "{package} {version} Release",
|
||||
"progressCountOf": "of",
|
||||
"keybindingAlreadyExists": "Keybinding already exists on"
|
||||
"keybindingAlreadyExists": "Keybinding already exists on",
|
||||
"nodesRunning": "nodes running"
|
||||
},
|
||||
"manager": {
|
||||
"title": "Custom Nodes Manager",
|
||||
@@ -1490,4 +1491,4 @@
|
||||
"whatsNewPopup": {
|
||||
"learnMore": "Learn more"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,6 +48,22 @@ const zProgressWsMessage = z.object({
|
||||
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({
|
||||
node: zNodeId,
|
||||
display_node: zNodeId,
|
||||
@@ -132,6 +148,8 @@ export type ProgressTextWsMessage = z.infer<typeof zProgressTextWsMessage>
|
||||
export type DisplayComponentWsMessage = z.infer<
|
||||
typeof zDisplayComponentWsMessage
|
||||
>
|
||||
export type NodeProgressState = z.infer<typeof zNodeProgressState>
|
||||
export type ProgressStateWsMessage = z.infer<typeof zProgressStateWsMessage>
|
||||
// End of ws messages
|
||||
|
||||
const zPromptInputItem = z.object({
|
||||
|
||||
@@ -15,6 +15,7 @@ import type {
|
||||
LogsRawResponse,
|
||||
LogsWsMessage,
|
||||
PendingTaskItem,
|
||||
ProgressStateWsMessage,
|
||||
ProgressTextWsMessage,
|
||||
ProgressWsMessage,
|
||||
PromptResponse,
|
||||
@@ -103,7 +104,17 @@ interface BackendApiCalls {
|
||||
logs: LogsWsMessage
|
||||
/** Binary preview/progress data */
|
||||
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_state: ProgressStateWsMessage
|
||||
display_component: DisplayComponentWsMessage
|
||||
}
|
||||
|
||||
@@ -432,6 +443,33 @@ export class ComfyApi extends EventTarget {
|
||||
})
|
||||
this.dispatchCustomEvent('b_preview', imageBlob)
|
||||
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:
|
||||
throw new Error(
|
||||
`Unknown binary websocket message of type ${eventType}`
|
||||
@@ -461,6 +499,7 @@ export class ComfyApi extends EventTarget {
|
||||
case 'execution_cached':
|
||||
case 'execution_success':
|
||||
case 'progress':
|
||||
case 'progress_state':
|
||||
case 'executed':
|
||||
case 'graphChanged':
|
||||
case 'promptQueued':
|
||||
|
||||
@@ -194,6 +194,8 @@ export class ComfyApp {
|
||||
|
||||
/**
|
||||
* @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 {
|
||||
return useExecutionStore().executingNodeId
|
||||
@@ -635,10 +637,6 @@ export class ComfyApp {
|
||||
|
||||
api.addEventListener('executing', () => {
|
||||
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 }) => {
|
||||
@@ -689,15 +687,13 @@ export class ComfyApp {
|
||||
this.canvas.draw(true, true)
|
||||
})
|
||||
|
||||
api.addEventListener('b_preview', ({ detail }) => {
|
||||
const id = this.runningNodeId
|
||||
if (id == null) return
|
||||
|
||||
const blob = detail
|
||||
api.addEventListener('b_preview_with_metadata', ({ detail }) => {
|
||||
// Enhanced preview with explicit node context
|
||||
const { blob, displayNodeId } = detail
|
||||
this.revokePreviews(displayNodeId)
|
||||
const blobUrl = URL.createObjectURL(blob)
|
||||
// Ensure clean up if `executing` event is missed.
|
||||
this.revokePreviews(id)
|
||||
this.nodePreviewImages[id] = [blobUrl]
|
||||
// Preview cleanup is now handled in progress_state event to support multiple concurrent previews
|
||||
this.nodePreviewImages[displayNodeId] = [blobUrl]
|
||||
})
|
||||
|
||||
api.init()
|
||||
|
||||
@@ -237,6 +237,7 @@ export class ComponentWidgetImpl<
|
||||
component: Component
|
||||
inputSpec: InputSpec
|
||||
props?: P
|
||||
componentProps?: Record<string, unknown>
|
||||
options: DOMWidgetOptions<V>
|
||||
}) {
|
||||
super({
|
||||
@@ -245,7 +246,9 @@ export class ComponentWidgetImpl<
|
||||
})
|
||||
this.component = obj.component
|
||||
this.inputSpec = obj.inputSpec
|
||||
this.props = obj.props
|
||||
this.props = obj.componentProps
|
||||
? ({ ...obj.props, ...obj.componentProps } as P)
|
||||
: obj.props
|
||||
}
|
||||
|
||||
override computeLayoutSize() {
|
||||
|
||||
@@ -33,6 +33,7 @@ import type {
|
||||
import type { ComfyNodeDef as ComfyNodeDefV1 } from '@/schemas/nodeDefSchema'
|
||||
import { ComfyApp, app } from '@/scripts/app'
|
||||
import { $el } from '@/scripts/ui'
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
import { useNodeOutputStore } from '@/stores/imagePreviewStore'
|
||||
import { ComfyNodeDefImpl } from '@/stores/nodeDefStore'
|
||||
@@ -362,7 +363,12 @@ export const useLitegraphService = () => {
|
||||
*/
|
||||
#setupStrokeStyles() {
|
||||
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' }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ import type {
|
||||
ExecutionErrorWsMessage,
|
||||
ExecutionStartWsMessage,
|
||||
NodeError,
|
||||
NodeProgressState,
|
||||
ProgressStateWsMessage,
|
||||
ProgressTextWsMessage,
|
||||
ProgressWsMessage
|
||||
} from '@/schemas/apiSchema'
|
||||
@@ -46,7 +48,22 @@ export const useExecutionStore = defineStore('execution', () => {
|
||||
const queuedPrompts = ref<Record<NodeId, QueuedPrompt>>({})
|
||||
const lastNodeErrors = ref<Record<NodeId, NodeError> | 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>(() => {
|
||||
if (!executingNodeId.value) return null
|
||||
|
||||
@@ -116,7 +133,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 = computed(() =>
|
||||
_executingNodeProgress.value
|
||||
@@ -153,6 +170,7 @@ export const useExecutionStore = defineStore('execution', () => {
|
||||
api.addEventListener('executed', handleExecuted)
|
||||
api.addEventListener('executing', handleExecuting)
|
||||
api.addEventListener('progress', handleProgress)
|
||||
api.addEventListener('progress_state', handleProgressState)
|
||||
api.addEventListener('status', handleStatus)
|
||||
api.addEventListener('execution_error', handleExecutionError)
|
||||
}
|
||||
@@ -165,6 +183,7 @@ export const useExecutionStore = defineStore('execution', () => {
|
||||
api.removeEventListener('executed', handleExecuted)
|
||||
api.removeEventListener('executing', handleExecuting)
|
||||
api.removeEventListener('progress', handleProgress)
|
||||
api.removeEventListener('progress_state', handleProgressState)
|
||||
api.removeEventListener('status', handleStatus)
|
||||
api.removeEventListener('execution_error', handleExecutionError)
|
||||
api.removeEventListener('progress_text', handleProgressText)
|
||||
@@ -194,19 +213,41 @@ export const useExecutionStore = defineStore('execution', () => {
|
||||
|
||||
if (!activePrompt.value) return
|
||||
|
||||
if (executingNodeId.value && activePrompt.value) {
|
||||
// Seems sometimes nodes that are cached fire executing but not executed
|
||||
activePrompt.value.nodes[executingNodeId.value] = true
|
||||
// Update the executing nodes list
|
||||
if (e.detail === null) {
|
||||
if (activePromptId.value) {
|
||||
delete queuedPrompts.value[activePromptId.value]
|
||||
}
|
||||
}
|
||||
if (typeof e.detail === 'string') {
|
||||
executingNodeId.value = executionIdToCurrentId(e.detail) ?? null
|
||||
} else {
|
||||
executingNodeId.value = e.detail
|
||||
if (executingNodeId.value === null) {
|
||||
if (activePromptId.value) {
|
||||
delete queuedPrompts.value[activePromptId.value]
|
||||
}
|
||||
activePromptId.value = null
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -310,9 +351,13 @@ export const useExecutionStore = defineStore('execution', () => {
|
||||
*/
|
||||
lastExecutionError,
|
||||
/**
|
||||
* The id of the node that is currently being executed
|
||||
* The id of the node that is currently being executed (backward compatibility)
|
||||
*/
|
||||
executingNodeId,
|
||||
/**
|
||||
* The list of all nodes that are currently executing
|
||||
*/
|
||||
executingNodeIds,
|
||||
/**
|
||||
* The prompt that is currently being executed
|
||||
*/
|
||||
@@ -330,13 +375,17 @@ export const useExecutionStore = defineStore('execution', () => {
|
||||
*/
|
||||
executionProgress,
|
||||
/**
|
||||
* The node that is currently being executed
|
||||
* The node that is currently being executed (backward compatibility)
|
||||
*/
|
||||
executingNode,
|
||||
/**
|
||||
* The progress of the executing node (if the node reports progress)
|
||||
* The progress of the executing node (backward compatibility)
|
||||
*/
|
||||
executingNodeProgress,
|
||||
/**
|
||||
* All node progress states from progress_state events
|
||||
*/
|
||||
nodeProgressStates,
|
||||
bindExecutionEvents,
|
||||
unbindExecutionEvents,
|
||||
storePrompt,
|
||||
|
||||
@@ -3,12 +3,20 @@ import { nextTick, reactive } from 'vue'
|
||||
|
||||
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
|
||||
const executionStore = reactive({
|
||||
isIdle: true,
|
||||
executionProgress: 0,
|
||||
executingNode: null as any,
|
||||
executingNodeProgress: 0
|
||||
executingNodeProgress: 0,
|
||||
nodeProgressStates: {} as any,
|
||||
activePrompt: null as any
|
||||
})
|
||||
vi.mock('@/stores/executionStore', () => ({
|
||||
useExecutionStore: () => executionStore
|
||||
@@ -37,6 +45,8 @@ describe('useBrowserTabTitle', () => {
|
||||
executionStore.executionProgress = 0
|
||||
executionStore.executingNode = null as any
|
||||
executionStore.executingNodeProgress = 0
|
||||
executionStore.nodeProgressStates = {}
|
||||
executionStore.activePrompt = null
|
||||
|
||||
// reset setting and workflow stores
|
||||
;(settingStore.get as any).mockReturnValue('Enabled')
|
||||
@@ -97,13 +107,41 @@ describe('useBrowserTabTitle', () => {
|
||||
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.executionProgress = 0.4
|
||||
executionStore.executingNodeProgress = 0.5
|
||||
executionStore.executingNode = { type: 'Foo' }
|
||||
executionStore.nodeProgressStates = {
|
||||
'1': { state: 'running', value: 5, max: 10, node: '1', prompt_id: 'test' }
|
||||
}
|
||||
executionStore.activePrompt = {
|
||||
workflow: {
|
||||
changeTracker: {
|
||||
activeState: {
|
||||
nodes: [{ id: 1, type: 'Foo' }]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
useBrowserTabTitle()
|
||||
await nextTick()
|
||||
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]')
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user