Compare commits

..

10 Commits

Author SHA1 Message Date
github-actions
806ea8e31b Update locales [skip ci] 2025-06-15 12:26:57 +00:00
Yiqun Xu
48e4856e4c feat: add Custom Launch Arguments 2025-06-15 05:16:35 -07:00
Christian Byrne
a6bd04f951 [Manager] Make dialog closeable with button and hotkey (#4179)
Co-authored-by: github-actions <github-actions@github.com>
2025-06-14 15:21:43 -07:00
Christian Byrne
5b32d2aad0 [Manager] Persist/Restore Manager UI state (#4180) 2025-06-14 15:19:56 -07:00
Christian Byrne
23ba7e6501 [Manager] Fix version selector popover not closing when selecting different pack (#4176) 2025-06-14 15:06:32 -07:00
Comfy Org PR Bot
1e2b16f14d 1.22.2 (#4170)
Co-authored-by: webfiltered <176114999+webfiltered@users.noreply.github.com>
2025-06-14 07:19:53 +00:00
Christian Byrne
ec27d50333 [Manager] Fix selection state race condition during pack data merge (#4165) 2025-06-13 23:46:53 -07:00
Christian Byrne
693e156ab2 [Manager] Update PackCard styling to match Figma design (#4164) 2025-06-13 22:27:34 -07:00
comfy-waifu
8274df5075 Fixed favicon some progress frames not found - by ComfyWaifu 🤍 (#4143) 2025-06-13 21:59:35 -07:00
Christian Byrne
55bf36564d [Manager] Fix card selection highlight z-index and border radius issues (#4160) 2025-06-13 21:19:11 -07:00
45 changed files with 486 additions and 411 deletions

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "@comfyorg/comfyui-frontend",
"version": "1.22.1",
"version": "1.22.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@comfyorg/comfyui-frontend",
"version": "1.22.1",
"version": "1.22.2",
"license": "GPL-3.0-only",
"dependencies": {
"@alloc/quick-lru": "^5.2.0",

View File

@@ -1,7 +1,7 @@
{
"name": "@comfyorg/comfyui-frontend",
"private": true,
"version": "1.22.1",
"version": "1.22.2",
"type": "module",
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
"homepage": "https://comfy.org",

View File

@@ -93,7 +93,14 @@
import { whenever } from '@vueuse/core'
import { merge } from 'lodash'
import Button from 'primevue/button'
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
import {
computed,
onBeforeUnmount,
onMounted,
onUnmounted,
ref,
watch
} from 'vue'
import { useI18n } from 'vue-i18n'
import ContentDivider from '@/components/common/ContentDivider.vue'
@@ -106,6 +113,7 @@ import PackCard from '@/components/dialog/content/manager/packCard/PackCard.vue'
import RegistrySearchBar from '@/components/dialog/content/manager/registrySearchBar/RegistrySearchBar.vue'
import GridSkeleton from '@/components/dialog/content/manager/skeleton/GridSkeleton.vue'
import { useResponsiveCollapse } from '@/composables/element/useResponsiveCollapse'
import { useManagerStatePersistence } from '@/composables/manager/useManagerStatePersistence'
import { useInstalledPacks } from '@/composables/nodePack/useInstalledPacks'
import { usePackUpdateStatus } from '@/composables/nodePack/usePackUpdateStatus'
import { useWorkflowPacks } from '@/composables/nodePack/useWorkflowPacks'
@@ -116,13 +124,15 @@ import type { TabItem } from '@/types/comfyManagerTypes'
import { ManagerTab } from '@/types/comfyManagerTypes'
import { components } from '@/types/comfyRegistryTypes'
const { initialTab = ManagerTab.All } = defineProps<{
initialTab: ManagerTab
const { initialTab } = defineProps<{
initialTab?: ManagerTab
}>()
const { t } = useI18n()
const comfyManagerStore = useComfyManagerStore()
const { getPackById } = useComfyRegistryStore()
const persistedState = useManagerStatePersistence()
const initialState = persistedState.loadStoredState()
const GRID_STYLE = {
display: 'grid',
@@ -156,8 +166,10 @@ const tabs = ref<TabItem[]>([
icon: 'pi-sync'
}
])
const initialTabId = initialTab ?? initialState.selectedTabId
const selectedTab = ref<TabItem>(
tabs.value.find((tab) => tab.id === initialTab) || tabs.value[0]
tabs.value.find((tab) => tab.id === initialTabId) || tabs.value[0]
)
const {
@@ -168,7 +180,11 @@ const {
searchMode,
sortField,
suggestions
} = useRegistrySearch()
} = useRegistrySearch({
initialSortField: initialState.sortField,
initialSearchMode: initialState.searchMode,
initialSearchQuery: initialState.searchQuery
})
pageNumber.value = 0
const onApproachEnd = () => {
pageNumber.value++
@@ -430,7 +446,13 @@ whenever(selectedNodePack, async () => {
if (data?.id === pack.id) {
lastFetchedPackId.value = pack.id
const mergedPack = merge({}, pack, data)
selectedNodePacks.value = [mergedPack]
// Update the pack in current selection without changing selection state
const packIndex = selectedNodePacks.value.findIndex(
(p) => p.id === mergedPack.id
)
if (packIndex !== -1) {
selectedNodePacks.value.splice(packIndex, 1, mergedPack)
}
// Replace pack in displayPacks so that children receive a fresh prop reference
const idx = displayPacks.value.findIndex((p) => p.id === mergedPack.id)
if (idx !== -1) {
@@ -450,6 +472,15 @@ watch(searchQuery, () => {
}
})
onBeforeUnmount(() => {
persistedState.persistState({
selectedTabId: selectedTab.value?.id,
searchQuery: searchQuery.value,
searchMode: searchMode.value,
sortField: sortField.value
})
})
onUnmounted(() => {
getPackById.cancel()
})

View File

@@ -62,6 +62,7 @@ describe('PackVersionBadge', () => {
return mount(PackVersionBadge, {
props: {
nodePack: mockNodePack,
isSelected: false,
...props
},
global: {
@@ -162,4 +163,58 @@ describe('PackVersionBadge', () => {
// Verify that the hide method was called
expect(mockHide).toHaveBeenCalled()
})
describe('selection state changes', () => {
it('closes the popover when card is deselected', async () => {
const wrapper = mountComponent({
props: { isSelected: true }
})
// Change isSelected from true to false
await wrapper.setProps({ isSelected: false })
await nextTick()
// Verify that the hide method was called
expect(mockHide).toHaveBeenCalled()
})
it('does not close the popover when card is selected', async () => {
const wrapper = mountComponent({
props: { isSelected: false }
})
// Change isSelected from false to true
await wrapper.setProps({ isSelected: true })
await nextTick()
// Verify that the hide method was NOT called
expect(mockHide).not.toHaveBeenCalled()
})
it('does not close the popover when isSelected remains false', async () => {
const wrapper = mountComponent({
props: { isSelected: false }
})
// Change isSelected from false to false (no change)
await wrapper.setProps({ isSelected: false })
await nextTick()
// Verify that the hide method was NOT called
expect(mockHide).not.toHaveBeenCalled()
})
it('does not close the popover when isSelected remains true', async () => {
const wrapper = mountComponent({
props: { isSelected: true }
})
// Change isSelected from true to true (no change)
await wrapper.setProps({ isSelected: true })
await nextTick()
// Verify that the hide method was NOT called
expect(mockHide).not.toHaveBeenCalled()
})
})
})

View File

@@ -33,7 +33,7 @@
<script setup lang="ts">
import Button from 'primevue/button'
import Popover from 'primevue/popover'
import { computed, ref } from 'vue'
import { computed, ref, watch } from 'vue'
import PackVersionSelectorPopover from '@/components/dialog/content/manager/PackVersionSelectorPopover.vue'
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
@@ -43,8 +43,9 @@ import { isSemVer } from '@/utils/formatUtil'
const TRUNCATED_HASH_LENGTH = 7
const { nodePack } = defineProps<{
const { nodePack, isSelected } = defineProps<{
nodePack: components['schemas']['Node']
isSelected: boolean
}>()
const popoverRef = ref()
@@ -69,4 +70,14 @@ const toggleVersionSelector = (event: Event) => {
const closeVersionSelector = () => {
popoverRef.value.hide()
}
// If the card is unselected, automatically close the version selector popover
watch(
() => isSelected,
(isSelected, wasSelected) => {
if (wasSelected && !isSelected) {
closeVersionSelector()
}
}
)
</script>

View File

@@ -191,6 +191,100 @@ describe('PackVersionSelectorPopover', () => {
expect(mockGetPackVersions).toHaveBeenCalledWith(newNodePack.id)
})
describe('nodePack.id changes', () => {
it('re-fetches versions when nodePack.id changes', async () => {
// Set up the mock for the initial fetch
mockGetPackVersions.mockResolvedValueOnce(defaultMockVersions)
const wrapper = mountComponent()
await waitForPromises()
// Verify initial fetch
expect(mockGetPackVersions).toHaveBeenCalledTimes(1)
expect(mockGetPackVersions).toHaveBeenCalledWith(mockNodePack.id)
// Set up the mock for the second fetch
const newVersions = [
{ version: '2.0.0', createdAt: '2023-06-01' },
{ version: '1.9.0', createdAt: '2023-05-01' }
]
mockGetPackVersions.mockResolvedValueOnce(newVersions)
// Update the nodePack with a new ID
const newNodePack = {
...mockNodePack,
id: 'different-pack',
name: 'Different Pack'
}
await wrapper.setProps({ nodePack: newNodePack })
await waitForPromises()
// Should fetch versions for the new nodePack
expect(mockGetPackVersions).toHaveBeenCalledTimes(2)
expect(mockGetPackVersions).toHaveBeenLastCalledWith(newNodePack.id)
// Check that new versions are displayed
const listbox = wrapper.findComponent(Listbox)
const options = listbox.props('options')!
expect(options.some((o) => o.value === '2.0.0')).toBe(true)
expect(options.some((o) => o.value === '1.9.0')).toBe(true)
})
it('does not re-fetch when nodePack changes but id remains the same', async () => {
// Set up the mock for the initial fetch
mockGetPackVersions.mockResolvedValueOnce(defaultMockVersions)
const wrapper = mountComponent()
await waitForPromises()
// Verify initial fetch
expect(mockGetPackVersions).toHaveBeenCalledTimes(1)
// Update the nodePack with same ID but different properties
const updatedNodePack = {
...mockNodePack,
name: 'Updated Test Pack',
description: 'New description'
}
await wrapper.setProps({ nodePack: updatedNodePack })
await waitForPromises()
// Should NOT fetch versions again
expect(mockGetPackVersions).toHaveBeenCalledTimes(1)
})
it('maintains selected version when switching to a new pack', async () => {
// Set up the mock for the initial fetch
mockGetPackVersions.mockResolvedValueOnce(defaultMockVersions)
const wrapper = mountComponent()
await waitForPromises()
// Select a specific version
const listbox = wrapper.findComponent(Listbox)
await listbox.setValue('0.9.0')
expect(listbox.props('modelValue')).toBe('0.9.0')
// Set up the mock for the second fetch
mockGetPackVersions.mockResolvedValueOnce([
{ version: '3.0.0', createdAt: '2023-07-01' },
{ version: '0.9.0', createdAt: '2023-04-01' }
])
// Update to a new pack that also has version 0.9.0
const newNodePack = {
id: 'another-pack',
name: 'Another Pack',
latest_version: { version: '3.0.0' }
}
await wrapper.setProps({ nodePack: newNodePack })
await waitForPromises()
// Selected version should remain the same if available
expect(listbox.props('modelValue')).toBe('0.9.0')
})
})
describe('Unclaimed GitHub packs handling', () => {
it('falls back to nightly when no versions exist', async () => {
// Set up the mock to return versions

View File

@@ -62,7 +62,7 @@ import { whenever } from '@vueuse/core'
import Button from 'primevue/button'
import Listbox from 'primevue/listbox'
import ProgressSpinner from 'primevue/progressspinner'
import { onMounted, onUnmounted, ref } from 'vue'
import { onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import ContentDivider from '@/components/common/ContentDivider.vue'
@@ -161,9 +161,11 @@ const onNodePackChange = async () => {
}
whenever(
() => nodePack,
() => {
void onNodePackChange()
() => nodePack.id,
(nodePackId, oldNodePackId) => {
if (nodePackId !== oldNodePackId) {
void onNodePackChange()
}
},
{ deep: true, immediate: true }
)
@@ -182,8 +184,4 @@ const handleSubmit = async () => {
isQueueing.value = false
emit('submit')
}
onUnmounted(() => {
managerStore.installPack.clear()
})
</script>

View File

@@ -32,7 +32,7 @@
/>
</MetadataRow>
<MetadataRow :label="t('manager.version')">
<PackVersionBadge :node-pack="nodePack" />
<PackVersionBadge :node-pack="nodePack" :is-selected="true" />
</MetadataRow>
</div>
<div class="mb-6 overflow-hidden">
@@ -118,7 +118,15 @@ const onNodePackChange = () => {
y.value = 0
}
whenever(() => nodePack, onNodePackChange, { immediate: true, deep: true })
whenever(
() => nodePack.id,
(nodePackId, oldNodePackId) => {
if (nodePackId !== oldNodePackId) {
onNodePackChange()
}
},
{ immediate: true }
)
</script>
<style scoped>
.hidden-scrollbar {

View File

@@ -1,13 +1,13 @@
<template>
<Card
class="w-full h-full inline-flex flex-col justify-between items-start overflow-hidden rounded-2xl shadow-elevation-3 dark-theme:bg-dark-elevation-2 transition-all duration-200"
class="w-full h-full inline-flex flex-col justify-between items-start overflow-hidden rounded-lg shadow-elevation-3 dark-theme:bg-dark-elevation-2 transition-all duration-200"
:class="{
'outline outline-[6px] outline-[var(--p-primary-color)]': isSelected,
'selected-card': isSelected,
'opacity-60': isDisabled
}"
:pt="{
body: { class: 'p-0 flex flex-col w-full h-full rounded-2xl gap-0' },
content: { class: 'flex-1 flex flex-col rounded-2xl min-h-0' },
body: { class: 'p-0 flex flex-col w-full h-full rounded-lg gap-0' },
content: { class: 'flex-1 flex flex-col rounded-lg min-h-0' },
title: { class: 'w-full h-full rounded-t-lg cursor-pointer' },
footer: { class: 'p-0 m-0' }
}"
@@ -70,7 +70,10 @@
>
<i class="pi pi-arrow-circle-up text-blue-600" />
</div>
<PackVersionBadge :node-pack="nodePack" />
<PackVersionBadge
:node-pack="nodePack"
:is-selected="isSelected"
/>
</div>
<div
v-if="formattedLatestVersionDate"
@@ -158,3 +161,22 @@ const formattedLatestVersionDate = computed(() => {
})
})
</script>
<style scoped>
.selected-card {
position: relative;
}
.selected-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
border: 3px solid var(--p-primary-color);
border-radius: 0.5rem;
pointer-events: none;
z-index: 100;
}
</style>

View File

@@ -71,6 +71,7 @@ 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'
@@ -188,46 +189,22 @@ watch(
}
)
// Update the progress of executing nodes
// Update the progress of the executing node
watch(
() => executionStore.nodeProgressStates,
(nodeProgressStates) => {
// Clear progress for all nodes first
() =>
[executionStore.executingNodeId, executionStore.executingNodeProgress] as [
NodeId | null,
number | null
],
([executingNodeId, executingNodeProgress]) => {
for (const node of comfyApp.graph.nodes) {
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
)
}
}
if (node.id == executingNodeId) {
node.progress = executingNodeProgress ?? undefined
} else {
node.progress = undefined
}
}
// TODO - Do we need to force canvas redraw here?
// comfyApp.graph.setDirtyCanvas(true, true)
},
{ deep: true }
}
)
// Update node slot errors

View File

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

View File

@@ -0,0 +1,54 @@
import {
ManagerState,
ManagerTab,
SortableAlgoliaField
} from '@/types/comfyManagerTypes'
const STORAGE_KEY = 'Comfy.Manager.UI.State'
export const useManagerStatePersistence = () => {
/**
* Load the UI state from localStorage.
*/
const loadStoredState = (): ManagerState => {
try {
const stored = localStorage.getItem(STORAGE_KEY)
if (stored) {
return JSON.parse(stored)
}
} catch (e) {
console.error('Failed to load manager UI state:', e)
}
return {
selectedTabId: ManagerTab.All,
searchQuery: '',
searchMode: 'packs',
sortField: SortableAlgoliaField.Downloads
}
}
/**
* Persist the UI state to localStorage.
*/
const persistState = (state: ManagerState) => {
localStorage.setItem(STORAGE_KEY, JSON.stringify(state))
}
/**
* Reset the UI state to the default values.
*/
const reset = () => {
persistState({
selectedTabId: ManagerTab.All,
searchQuery: '',
searchMode: 'packs',
sortField: SortableAlgoliaField.Downloads
})
}
return {
loadStoredState,
persistState,
reset
}
}

View File

@@ -1,7 +1,6 @@
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'
@@ -37,35 +36,11 @@ export const useBrowserTabTitle = () => {
: DEFAULT_TITLE
})
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 nodeExecutionTitle = computed(() =>
executionStore.executingNode && executionStore.executingNodeProgress
? `${executionText.value}[${Math.round(executionStore.executingNodeProgress * 100)}%] ${executionStore.executingNode.type}`
: ''
)
const workflowTitle = computed(
() =>

View File

@@ -658,19 +658,19 @@ export function useCoreCommands(): ComfyCommand[] {
{
id: 'Comfy.Manager.CustomNodesManager',
icon: 'pi pi-puzzle',
label: 'Custom Nodes Manager',
label: 'Toggle the Custom Nodes Manager',
versionAdded: '1.12.10',
function: () => {
dialogService.showManagerDialog()
dialogService.toggleManagerDialog()
}
},
{
id: 'Comfy.Manager.ToggleManagerProgressDialog',
icon: 'pi pi-spinner',
label: 'Toggle Progress Dialog',
label: 'Toggle the Custom Nodes Manager Progress Bar',
versionAdded: '1.13.9',
function: () => {
dialogService.showManagerProgressDialog()
dialogService.toggleManagerProgressDialog()
}
},
{

View File

@@ -15,7 +15,10 @@ export const useProgressFavicon = () => {
if (isIdle) {
favicon.value = defaultFavicon
} else {
const frame = Math.floor(progress * totalFrames)
const frame = Math.min(
Math.max(0, Math.floor(progress * totalFrames)),
totalFrames - 1
)
favicon.value = `/assets/images/favicon_progress_16x16/frame_${frame}.png`
}
}

View File

@@ -1,7 +1,7 @@
import { watchDebounced } from '@vueuse/core'
import type { Hit } from 'algoliasearch/dist/lite/browser'
import { memoize, orderBy } from 'lodash'
import { computed, onUnmounted, ref, watch } from 'vue'
import { computed, ref, watch } from 'vue'
import { useAlgoliaSearchService } from '@/services/algoliaSearchService'
import type {
@@ -14,7 +14,6 @@ import { SortableAlgoliaField } from '@/types/comfyManagerTypes'
const SEARCH_DEBOUNCE_TIME = 320
const DEFAULT_PAGE_SIZE = 64
const DEFAULT_SORT_FIELD = SortableAlgoliaField.Downloads // Set in the index configuration
const DEFAULT_MAX_CACHE_SIZE = 64
const SORT_DIRECTIONS: Record<SortableAlgoliaField, 'asc' | 'desc'> = {
[SortableAlgoliaField.Downloads]: 'desc',
[SortableAlgoliaField.Created]: 'desc',
@@ -30,18 +29,25 @@ const isDateField = (field: SortableAlgoliaField): boolean =>
/**
* Composable for managing UI state of Comfy Node Registry search.
*/
export function useRegistrySearch(
options: {
maxCacheSize?: number
} = {}
) {
const { maxCacheSize = DEFAULT_MAX_CACHE_SIZE } = options
export function useRegistrySearch(options: {
initialSortField?: SortableAlgoliaField
initialSearchMode?: 'nodes' | 'packs'
initialSearchQuery?: string
initialPageNumber?: number
}) {
const {
initialSortField = SortableAlgoliaField.Downloads,
initialSearchMode = 'packs',
initialSearchQuery = '',
initialPageNumber = 0
} = options
const isLoading = ref(false)
const sortField = ref<SortableAlgoliaField>(SortableAlgoliaField.Downloads)
const searchMode = ref<'nodes' | 'packs'>('packs')
const sortField = ref<SortableAlgoliaField>(initialSortField)
const searchMode = ref<'nodes' | 'packs'>(initialSearchMode)
const pageSize = ref(DEFAULT_PAGE_SIZE)
const pageNumber = ref(0)
const searchQuery = ref('')
const pageNumber = ref(initialPageNumber)
const searchQuery = ref(initialSearchQuery)
const results = ref<AlgoliaNodePack[]>([])
const suggestions = ref<NodesIndexSuggestion[]>([])
@@ -62,9 +68,7 @@ export function useRegistrySearch(
)
const { searchPacksCached, toRegistryPack, clearSearchPacksCache } =
useAlgoliaSearchService({
maxCacheSize
})
useAlgoliaSearchService()
const algoliaToRegistry = memoize(
toRegistryPack,
@@ -124,8 +128,6 @@ export function useRegistrySearch(
immediate: true
})
onUnmounted(clearSearchPacksCache)
return {
isLoading,
pageNumber,
@@ -135,6 +137,7 @@ export function useRegistrySearch(
searchQuery,
suggestions,
searchResults: resultsAsRegistryPacks,
nodeSearchResults: resultsAsNodes
nodeSearchResults: resultsAsNodes,
clearCache: clearSearchPacksCache
}
}

View File

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

View File

@@ -43,6 +43,15 @@ import { checkMirrorReachable } from '@/utils/networkUtil'
defaultValue: true,
onChange: onChangeRestartApp
},
{
id: 'Comfy-Desktop.LaunchOptions',
category: ['Comfy-Desktop', 'General', 'Launch Options'],
name: 'Custom Launch Arguments',
tooltip:
'Custom command line arguments for launching the backend (e.g., --cpu). Requires restart.',
type: 'text',
defaultValue: ''
},
{
id: 'Comfy-Desktop.WindowStyle',
category: ['Comfy-Desktop', 'General', 'Window Style'],

View File

@@ -9,7 +9,6 @@ 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'
@@ -1179,10 +1178,9 @@ export class GroupNodeHandler {
node.onDrawForeground = function (ctx) {
// @ts-expect-error fixme ts strict error
onDrawForeground?.apply?.(this, arguments)
const executionStore = useExecutionStore()
if (
executionStore.nodeProgressStates[this.id] &&
executionStore.nodeProgressStates[this.id].state === 'running' &&
// @ts-expect-error fixme ts strict error
+app.runningNodeId === this.id &&
this.runningInternalNodeId !== null
) {
// @ts-expect-error fixme ts strict error
@@ -1277,45 +1275,6 @@ 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,
@@ -1335,7 +1294,6 @@ 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)
}

View File

@@ -147,10 +147,10 @@
"label": "Load Default Workflow"
},
"Comfy_Manager_CustomNodesManager": {
"label": "Custom Nodes Manager"
"label": "Toggle the Custom Nodes Manager"
},
"Comfy_Manager_ToggleManagerProgressDialog": {
"label": "Toggle Progress Dialog"
"label": "Toggle the Custom Nodes Manager Progress Bar"
},
"Comfy_MaskEditor_OpenMaskEditor": {
"label": "Open Mask Editor for Selected Node"

View File

@@ -123,8 +123,7 @@
"copy": "Copy",
"imageUrl": "Image URL",
"clear": "Clear",
"copyURL": "Copy URL",
"nodesRunning": "nodes running"
"copyURL": "Copy URL"
},
"manager": {
"title": "Custom Nodes Manager",
@@ -829,8 +828,8 @@
"ComfyUI Issues": "ComfyUI Issues",
"Interrupt": "Interrupt",
"Load Default Workflow": "Load Default Workflow",
"Custom Nodes Manager": "Custom Nodes Manager",
"Toggle Progress Dialog": "Toggle Progress Dialog",
"Toggle the Custom Nodes Manager": "Toggle the Custom Nodes Manager",
"Toggle the Custom Nodes Manager Progress Bar": "Toggle the Custom Nodes Manager Progress Bar",
"Open Mask Editor for Selected Node": "Open Mask Editor for Selected Node",
"New": "New",
"Clipspace": "Clipspace",

View File

@@ -2,6 +2,10 @@
"Comfy-Desktop_AutoUpdate": {
"name": "Automatically check for updates"
},
"Comfy-Desktop_LaunchOptions": {
"name": "Custom Launch Arguments",
"tooltip": "Custom command line arguments for launching the backend (e.g., --cpu). Requires restart."
},
"Comfy-Desktop_SendStatistics": {
"name": "Send anonymous usage metrics"
},

View File

@@ -323,7 +323,6 @@
"noTasksFoundMessage": "No hay tareas en la cola.",
"noWorkflowsFound": "No se encontraron flujos de trabajo.",
"nodes": "Nodos",
"nodesRunning": "nodos en ejecución",
"ok": "OK",
"openNewIssue": "Abrir nuevo problema",
"overwrite": "Sobrescribir",
@@ -695,7 +694,6 @@
"ComfyUI Issues": "Problemas de ComfyUI",
"Contact Support": "Contactar soporte",
"Convert selected nodes to group node": "Convertir nodos seleccionados en nodo de grupo",
"Custom Nodes Manager": "Gestor de nodos personalizados",
"Delete Selected Items": "Eliminar elementos seleccionados",
"Desktop User Guide": "Guía de usuario de escritorio",
"Duplicate Current Workflow": "Duplicar flujo de trabajo actual",
@@ -749,12 +747,13 @@
"Toggle Logs Bottom Panel": "Alternar panel inferior de registros",
"Toggle Model Library Sidebar": "Alternar barra lateral de biblioteca de modelos",
"Toggle Node Library Sidebar": "Alternar barra lateral de biblioteca de nodos",
"Toggle Progress Dialog": "Alternar diálogo de progreso",
"Toggle Queue Sidebar": "Alternar barra lateral de cola",
"Toggle Search Box": "Alternar caja de búsqueda",
"Toggle Terminal Bottom Panel": "Alternar panel inferior de terminal",
"Toggle Theme (Dark/Light)": "Alternar tema (Oscuro/Claro)",
"Toggle Workflows Sidebar": "Alternar barra lateral de flujos de trabajo",
"Toggle the Custom Nodes Manager": "Alternar el Administrador de Nodos Personalizados",
"Toggle the Custom Nodes Manager Progress Bar": "Alternar la Barra de Progreso del Administrador de Nodos Personalizados",
"Undo": "Deshacer",
"Ungroup selected group nodes": "Desagrupar nodos de grupo seleccionados",
"Workflow": "Flujo de trabajo",

View File

@@ -2,6 +2,10 @@
"Comfy-Desktop_AutoUpdate": {
"name": "Verificar actualizaciones automáticamente"
},
"Comfy-Desktop_LaunchOptions": {
"name": "Argumentos de lanzamiento personalizados",
"tooltip": "Argumentos personalizados de línea de comandos para iniciar el backend (por ejemplo, --cpu). Requiere reinicio."
},
"Comfy-Desktop_SendStatistics": {
"name": "Enviar métricas de uso anónimas"
},

View File

@@ -323,7 +323,6 @@
"noTasksFoundMessage": "Il n'y a pas de tâches dans la file d'attente.",
"noWorkflowsFound": "Aucun flux de travail trouvé.",
"nodes": "Nœuds",
"nodesRunning": "nœuds en cours dexécution",
"ok": "OK",
"openNewIssue": "Ouvrir un nouveau problème",
"overwrite": "Écraser",
@@ -695,7 +694,6 @@
"ComfyUI Issues": "Problèmes de ComfyUI",
"Contact Support": "Contacter le support",
"Convert selected nodes to group node": "Convertir les nœuds sélectionnés en nœud de groupe",
"Custom Nodes Manager": "Gestionnaire de Nœuds Personnalisés",
"Delete Selected Items": "Supprimer les éléments sélectionnés",
"Desktop User Guide": "Guide de l'utilisateur de bureau",
"Duplicate Current Workflow": "Dupliquer le flux de travail actuel",
@@ -749,12 +747,13 @@
"Toggle Logs Bottom Panel": "Basculer le panneau inférieur des journaux",
"Toggle Model Library Sidebar": "Basculer la barre latérale de la bibliothèque de modèles",
"Toggle Node Library Sidebar": "Basculer la barre latérale de la bibliothèque de nœuds",
"Toggle Progress Dialog": "Basculer la boîte de dialogue de progression",
"Toggle Queue Sidebar": "Basculer la barre latérale de la file d'attente",
"Toggle Search Box": "Basculer la boîte de recherche",
"Toggle Terminal Bottom Panel": "Basculer le panneau inférieur du terminal",
"Toggle Theme (Dark/Light)": "Basculer le thème (Sombre/Clair)",
"Toggle Workflows Sidebar": "Basculer la barre latérale des flux de travail",
"Toggle the Custom Nodes Manager": "Basculer le gestionnaire de nœuds personnalisés",
"Toggle the Custom Nodes Manager Progress Bar": "Basculer la barre de progression du gestionnaire de nœuds personnalisés",
"Undo": "Annuler",
"Ungroup selected group nodes": "Dégrouper les nœuds de groupe sélectionnés",
"Workflow": "Flux de travail",

View File

@@ -2,6 +2,10 @@
"Comfy-Desktop_AutoUpdate": {
"name": "Vérifier automatiquement les mises à jour"
},
"Comfy-Desktop_LaunchOptions": {
"name": "Arguments de lancement personnalisés",
"tooltip": "Arguments de ligne de commande personnalisés pour lancer le backend (par exemple, --cpu). Nécessite un redémarrage."
},
"Comfy-Desktop_SendStatistics": {
"name": "Envoyer des métriques d'utilisation anonymes"
},

View File

@@ -323,7 +323,6 @@
"noTasksFoundMessage": "キューにタスクがありません。",
"noWorkflowsFound": "ワークフローが見つかりません。",
"nodes": "ノード",
"nodesRunning": "ノードが実行中",
"ok": "OK",
"openNewIssue": "新しい問題を開く",
"overwrite": "上書き",
@@ -695,7 +694,6 @@
"ComfyUI Issues": "ComfyUIの問題",
"Contact Support": "サポートに連絡",
"Convert selected nodes to group node": "選択したノードをグループノードに変換",
"Custom Nodes Manager": "カスタムノードマネージャ",
"Delete Selected Items": "選択したアイテムを削除",
"Desktop User Guide": "デスクトップユーザーガイド",
"Duplicate Current Workflow": "現在のワークフローを複製",
@@ -749,12 +747,13 @@
"Toggle Logs Bottom Panel": "ログパネル下部を切り替え",
"Toggle Model Library Sidebar": "モデルライブラリサイドバーを切り替え",
"Toggle Node Library Sidebar": "ノードライブラリサイドバーを切り替え",
"Toggle Progress Dialog": "進行状況ダイアログの切り替え",
"Toggle Queue Sidebar": "キューサイドバーを切り替え",
"Toggle Search Box": "検索ボックスの切り替え",
"Toggle Terminal Bottom Panel": "ターミナルパネル下部を切り替え",
"Toggle Theme (Dark/Light)": "テーマを切り替え(ダーク/ライト)",
"Toggle Workflows Sidebar": "ワークフローサイドバーを切り替え",
"Toggle the Custom Nodes Manager": "カスタムノードマネージャーを切り替え",
"Toggle the Custom Nodes Manager Progress Bar": "カスタムノードマネージャーの進行状況バーを切り替え",
"Undo": "元に戻す",
"Ungroup selected group nodes": "選択したグループノードのグループ解除",
"Workflow": "ワークフロー",

View File

@@ -2,6 +2,10 @@
"Comfy-Desktop_AutoUpdate": {
"name": "自動的に更新を確認する"
},
"Comfy-Desktop_LaunchOptions": {
"name": "カスタム起動引数",
"tooltip": "バックエンドを起動するためのカスタムコマンドライン引数(例: --cpu。再起動が必要です。"
},
"Comfy-Desktop_SendStatistics": {
"name": "匿名の使用統計を送信する"
},

View File

@@ -323,7 +323,6 @@
"noTasksFoundMessage": "대기열에 작업이 없습니다.",
"noWorkflowsFound": "워크플로를 찾을 수 없습니다.",
"nodes": "노드",
"nodesRunning": "노드 실행 중",
"ok": "확인",
"openNewIssue": "새 문제 열기",
"overwrite": "덮어쓰기",
@@ -695,7 +694,6 @@
"ComfyUI Issues": "ComfyUI 이슈 페이지",
"Contact Support": "고객 지원 문의",
"Convert selected nodes to group node": "선택한 노드를 그룹 노드로 변환",
"Custom Nodes Manager": "사용자 정의 노드 관리자",
"Delete Selected Items": "선택한 항목 삭제",
"Desktop User Guide": "데스크톱 사용자 가이드",
"Duplicate Current Workflow": "현재 워크플로 복제",
@@ -749,12 +747,13 @@
"Toggle Logs Bottom Panel": "로그 하단 패널 전환",
"Toggle Model Library Sidebar": "모델 라이브러리 사이드바 전환",
"Toggle Node Library Sidebar": "노드 라이브러리 사이드바 전환",
"Toggle Progress Dialog": "진행 상황 대화 상자 전환",
"Toggle Queue Sidebar": "실행 대기열 사이드바 전환",
"Toggle Search Box": "검색 상자 전환",
"Toggle Terminal Bottom Panel": "터미널 하단 패널 전환",
"Toggle Theme (Dark/Light)": "테마 전환 (어두운/밝은)",
"Toggle Workflows Sidebar": "워크플로 사이드바 전환",
"Toggle the Custom Nodes Manager": "커스텀 노드 매니저 전환",
"Toggle the Custom Nodes Manager Progress Bar": "커스텀 노드 매니저 진행률 표시줄 전환",
"Undo": "실행 취소",
"Ungroup selected group nodes": "선택한 그룹 노드 그룹 해제",
"Workflow": "워크플로",

View File

@@ -2,6 +2,10 @@
"Comfy-Desktop_AutoUpdate": {
"name": "자동 업데이트 확인"
},
"Comfy-Desktop_LaunchOptions": {
"name": "사용자 지정 실행 인수",
"tooltip": "백엔드 실행을 위한 사용자 지정 명령줄 인수입니다(예: --cpu). 재시작이 필요합니다."
},
"Comfy-Desktop_SendStatistics": {
"name": "익명 사용 통계 보내기"
},

View File

@@ -323,7 +323,6 @@
"noTasksFoundMessage": "В очереди нет задач.",
"noWorkflowsFound": "Рабочие процессы не найдены.",
"nodes": "Узлы",
"nodesRunning": "запущено узлов",
"ok": "ОК",
"openNewIssue": "Открыть новую проблему",
"overwrite": "Перезаписать",
@@ -695,7 +694,6 @@
"ComfyUI Issues": "Проблемы ComfyUI",
"Contact Support": "Связаться с поддержкой",
"Convert selected nodes to group node": "Преобразовать выбранные ноды в групповую ноду",
"Custom Nodes Manager": "Менеджер Пользовательских Узлов",
"Delete Selected Items": "Удалить выбранные элементы",
"Desktop User Guide": "Руководство пользователя для настольных ПК",
"Duplicate Current Workflow": "Дублировать текущий рабочий процесс",
@@ -749,12 +747,13 @@
"Toggle Logs Bottom Panel": "Переключение нижней панели журналов",
"Toggle Model Library Sidebar": "Переключение боковой панели библиотеки моделей",
"Toggle Node Library Sidebar": "Переключение боковой панели библиотеки нод",
"Toggle Progress Dialog": "Переключить диалоговое окно прогресса",
"Toggle Queue Sidebar": "Переключение боковой панели очереди",
"Toggle Search Box": "Переключить поисковую панель",
"Toggle Terminal Bottom Panel": "Переключение нижней панели терминала",
"Toggle Theme (Dark/Light)": "Переключение темы (Тёмная/Светлая)",
"Toggle Workflows Sidebar": "Переключение боковой панели рабочих процессов",
"Toggle the Custom Nodes Manager": "Переключить менеджер пользовательских узлов",
"Toggle the Custom Nodes Manager Progress Bar": "Переключить индикатор выполнения менеджера пользовательских узлов",
"Undo": "Отменить",
"Ungroup selected group nodes": "Разгруппировать выбранные групповые ноды",
"Workflow": "Рабочий процесс",

View File

@@ -2,6 +2,10 @@
"Comfy-Desktop_AutoUpdate": {
"name": "Автоматически проверять обновления"
},
"Comfy-Desktop_LaunchOptions": {
"name": "Пользовательские параметры запуска",
"tooltip": "Пользовательские аргументы командной строки для запуска backend (например, --cpu). Требуется перезапуск."
},
"Comfy-Desktop_SendStatistics": {
"name": "Отправлять анонимную статистику использования"
},

View File

@@ -323,7 +323,6 @@
"noTasksFoundMessage": "队列中没有任务。",
"noWorkflowsFound": "未找到工作流。",
"nodes": "节点",
"nodesRunning": "节点正在运行",
"ok": "确定",
"openNewIssue": "打开新问题",
"overwrite": "覆盖",
@@ -695,7 +694,6 @@
"ComfyUI Issues": "ComfyUI 问题",
"Contact Support": "联系支持",
"Convert selected nodes to group node": "将选中节点转换为组节点",
"Custom Nodes Manager": "自定义节点管理器",
"Delete Selected Items": "删除选定的项目",
"Desktop User Guide": "桌面端用户指南",
"Duplicate Current Workflow": "复制当前工作流",
@@ -749,12 +747,13 @@
"Toggle Logs Bottom Panel": "切换日志底部面板",
"Toggle Model Library Sidebar": "切换模型库侧边栏",
"Toggle Node Library Sidebar": "切换节点库侧边栏",
"Toggle Progress Dialog": "切换进度对话框",
"Toggle Queue Sidebar": "切换队列侧边栏",
"Toggle Search Box": "切换搜索框",
"Toggle Terminal Bottom Panel": "切换终端底部面板",
"Toggle Theme (Dark/Light)": "切换主题(暗/亮)",
"Toggle Workflows Sidebar": "切换工作流侧边栏",
"Toggle the Custom Nodes Manager": "切换自定义节点管理器",
"Toggle the Custom Nodes Manager Progress Bar": "切换自定义节点管理器进度条",
"Undo": "撤销",
"Ungroup selected group nodes": "解散选中组节点",
"Workflow": "工作流",

View File

@@ -2,6 +2,10 @@
"Comfy-Desktop_AutoUpdate": {
"name": "自动检查更新"
},
"Comfy-Desktop_LaunchOptions": {
"name": "自定义启动参数",
"tooltip": "用于启动后端的自定义命令行参数(例如:--cpu。需要重启。"
},
"Comfy-Desktop_SendStatistics": {
"name": "发送匿名使用情况统计"
},

View File

@@ -45,22 +45,6 @@ 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,
@@ -145,8 +129,6 @@ 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({
@@ -468,6 +450,7 @@ const zSettings = z.object({
'Comfy.TutorialCompleted': z.boolean(),
'Comfy.Node.AllowImageSizeDraw': z.boolean(),
'Comfy-Desktop.AutoUpdate': z.boolean(),
'Comfy-Desktop.LaunchOptions': z.string(),
'Comfy-Desktop.SendStatistics': z.boolean(),
'Comfy-Desktop.WindowStyle': z.string(),
'Comfy-Desktop.UV.PythonInstallMirror': z.string(),

View File

@@ -15,7 +15,6 @@ import type {
LogsRawResponse,
LogsWsMessage,
PendingTaskItem,
ProgressStateWsMessage,
ProgressTextWsMessage,
ProgressWsMessage,
PromptResponse,
@@ -104,17 +103,7 @@ 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
}
@@ -443,33 +432,6 @@ 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}`
@@ -499,7 +461,6 @@ export class ComfyApi extends EventTarget {
case 'execution_cached':
case 'execution_success':
case 'progress':
case 'progress_state':
case 'executed':
case 'graphChanged':
case 'promptQueued':

View File

@@ -193,8 +193,6 @@ 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
@@ -636,6 +634,10 @@ 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 }) => {
@@ -684,13 +686,15 @@ export class ComfyApp {
this.canvas.draw(true, true)
})
api.addEventListener('b_preview_with_metadata', ({ detail }) => {
// Enhanced preview with explicit node context
const { blob, displayNodeId } = detail
this.revokePreviews(displayNodeId)
api.addEventListener('b_preview', ({ detail }) => {
const id = this.runningNodeId
if (id == null) return
const blob = detail
const blobUrl = URL.createObjectURL(blob)
// Preview cleanup is now handled in progress_state event to support multiple concurrent previews
this.nodePreviewImages[displayNodeId] = [blobUrl]
// Ensure clean up if `executing` event is missed.
this.revokePreviews(id)
this.nodePreviewImages[id] = [blobUrl]
})
api.init()

View File

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

View File

@@ -40,12 +40,6 @@ const RETRIEVE_ATTRIBUTES: SearchAttribute[] = [
]
interface AlgoliaSearchServiceOptions {
/**
* Maximum number of search results to store in the cache.
* The cache is automatically cleared when the component is unmounted.
* @default 64
*/
maxCacheSize?: number
/**
* Minimum number of characters for suggestions. An additional query
* will be made to the suggestions/completions index for queries that
@@ -55,17 +49,15 @@ interface AlgoliaSearchServiceOptions {
minCharsForSuggestions?: number
}
const searchPacksCache = new QuickLRU<string, SearchPacksResult>({
maxSize: DEFAULT_MAX_CACHE_SIZE
})
export const useAlgoliaSearchService = (
options: AlgoliaSearchServiceOptions = {}
) => {
const {
maxCacheSize = DEFAULT_MAX_CACHE_SIZE,
minCharsForSuggestions = DEFAULT_MIN_CHARS_FOR_SUGGESTIONS
} = options
const { minCharsForSuggestions = DEFAULT_MIN_CHARS_FOR_SUGGESTIONS } = options
const searchClient = algoliasearch(__ALGOLIA_APP_ID__, __ALGOLIA_API_KEY__)
const searchPacksCache = new QuickLRU<string, SearchPacksResult>({
maxSize: maxCacheSize
})
const toRegistryLatestVersion = (
algoliaNode: AlgoliaNodePack

View File

@@ -21,7 +21,6 @@ import TemplateWorkflowsDialogHeader from '@/components/templates/TemplateWorkfl
import { t } from '@/i18n'
import type { ExecutionErrorWsMessage } from '@/schemas/apiSchema'
import { type ShowDialogOptions, useDialogStore } from '@/stores/dialogStore'
import { ManagerTab } from '@/types/comfyManagerTypes'
export type ConfirmationDialogType =
| 'default'
@@ -129,16 +128,14 @@ export const useDialogService = () => {
}
function showManagerDialog(
props: InstanceType<typeof ManagerDialogContent>['$props'] = {
initialTab: ManagerTab.All
}
props: InstanceType<typeof ManagerDialogContent>['$props'] = {}
) {
dialogStore.showDialog({
key: 'global-manager',
component: ManagerDialogContent,
headerComponent: ManagerHeader,
dialogComponentProps: {
closable: false,
closable: true,
pt: {
header: { class: '!p-0 !m-0' },
content: { class: '!px-0 h-[83vh] w-[90vw] overflow-y-hidden' }
@@ -397,6 +394,26 @@ export const useDialogService = () => {
}
}
function toggleManagerDialog(
props?: InstanceType<typeof ManagerDialogContent>['$props']
) {
if (dialogStore.isDialogOpen('global-manager')) {
dialogStore.closeDialog({ key: 'global-manager' })
} else {
showManagerDialog(props)
}
}
function toggleManagerProgressDialog(
props?: InstanceType<typeof ManagerProgressDialogContent>['$props']
) {
if (dialogStore.isDialogOpen('global-manager-progress-dialog')) {
dialogStore.closeDialog({ key: 'global-manager-progress-dialog' })
} else {
showManagerProgressDialog({ props })
}
}
return {
showLoadWorkflowWarning,
showMissingModelsWarning,
@@ -414,6 +431,8 @@ export const useDialogService = () => {
showUpdatePasswordDialog,
showExtensionDialog,
prompt,
confirm
confirm,
toggleManagerDialog,
toggleManagerProgressDialog
}
}

View File

@@ -29,7 +29,6 @@ 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'
@@ -97,12 +96,7 @@ export const useLitegraphService = () => {
*/
#setupStrokeStyles() {
this.strokeStyles['running'] = function (this: LGraphNode) {
const nodeProgressStates = useExecutionStore().nodeProgressStates
const nodeId = String(this.id)
if (
nodeProgressStates[nodeId] &&
nodeProgressStates[nodeId].state === 'running'
) {
if (this.id == app.runningNodeId) {
return { color: '#0f0' }
}
}

View File

@@ -169,11 +169,16 @@ export const useDialogStore = defineStore('dialog', () => {
return dialog
}
function isDialogOpen(key: string) {
return dialogStack.value.some((d) => d.key === key)
}
return {
dialogStack,
riseDialog,
showDialog,
closeDialog,
showExtensionDialog
showExtensionDialog,
isDialogOpen
}
})

View File

@@ -11,8 +11,6 @@ import type {
ExecutionErrorWsMessage,
ExecutionStartWsMessage,
NodeError,
NodeProgressState,
ProgressStateWsMessage,
ProgressTextWsMessage,
ProgressWsMessage
} from '@/schemas/apiSchema'
@@ -44,22 +42,7 @@ 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)
// 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 executingNodeId = ref<NodeId | null>(null)
const executingNode = computed<ComfyNode | null>(() => {
if (!executingNodeId.value) return null
@@ -77,7 +60,7 @@ export const useExecutionStore = defineStore('execution', () => {
)
})
// This is the progress of the currently executing node (for backward compatibility)
// This is the progress of the currently executing node, if any
const _executingNodeProgress = ref<ProgressWsMessage | null>(null)
const executingNodeProgress = computed(() =>
_executingNodeProgress.value
@@ -105,7 +88,7 @@ export const useExecutionStore = defineStore('execution', () => {
if (!activePrompt.value) return 0
const total = totalNodesToExecute.value
const done = nodesExecuted.value
return done / total
return total > 0 ? done / total : 0
})
function bindExecutionEvents() {
@@ -114,7 +97,6 @@ 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)
}
@@ -127,7 +109,6 @@ 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)
@@ -157,8 +138,12 @@ export const useExecutionStore = defineStore('execution', () => {
if (!activePrompt.value) return
// Update the executing nodes list
if (e.detail === null) {
if (executingNodeId.value && activePrompt.value) {
// Seems sometimes nodes that are cached fire executing but not executed
activePrompt.value.nodes[executingNodeId.value] = true
}
executingNodeId.value = e.detail
if (executingNodeId.value === null) {
if (activePromptId.value) {
delete queuedPrompts.value[activePromptId.value]
}
@@ -166,37 +151,6 @@ 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>) {
_executingNodeProgress.value = e.detail
}
@@ -284,13 +238,9 @@ export const useExecutionStore = defineStore('execution', () => {
*/
lastExecutionError,
/**
* The id of the node that is currently being executed (backward compatibility)
* The id of the node that is currently being executed
*/
executingNodeId,
/**
* The list of all nodes that are currently executing
*/
executingNodeIds,
/**
* The prompt that is currently being executed
*/
@@ -308,17 +258,13 @@ export const useExecutionStore = defineStore('execution', () => {
*/
executionProgress,
/**
* The node that is currently being executed (backward compatibility)
* The node that is currently being executed
*/
executingNode,
/**
* The progress of the executing node (backward compatibility)
* The progress of the executing node (if the node reports progress)
*/
executingNodeProgress,
/**
* All node progress states from progress_state events
*/
nodeProgressStates,
bindExecutionEvents,
unbindExecutionEvents,
storePrompt,

View File

@@ -39,7 +39,7 @@ export enum SortableAlgoliaField {
}
export interface TabItem {
id: string
id: ManagerTab
label: string
icon: string
}
@@ -234,3 +234,10 @@ export interface InstallPackParams extends ManagerPackInfo {
export interface UpdateAllPacksParams {
mode?: ManagerDatabaseSource
}
export interface ManagerState {
selectedTabId: ManagerTab
searchQuery: string
searchMode: 'nodes' | 'packs'
sortField: SortableAlgoliaField
}

View File

@@ -3,20 +3,12 @@ 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,
nodeProgressStates: {} as any,
activePrompt: null as any
executingNodeProgress: 0
})
vi.mock('@/stores/executionStore', () => ({
useExecutionStore: () => executionStore
@@ -45,8 +37,6 @@ 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')
@@ -107,41 +97,13 @@ describe('useBrowserTabTitle', () => {
expect(document.title).toBe('[30%]ComfyUI')
})
it('shows node execution title when executing a node using nodeProgressStates', async () => {
it('shows node execution title when executing a node', async () => {
executionStore.isIdle = false
executionStore.executionProgress = 0.4
executionStore.nodeProgressStates = {
'1': { state: 'running', value: 5, max: 10, node: '1', prompt_id: 'test' }
}
executionStore.activePrompt = {
workflow: {
changeTracker: {
activeState: {
nodes: [{ id: 1, type: 'Foo' }]
}
}
}
}
executionStore.executingNodeProgress = 0.5
executionStore.executingNode = { 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]')
})
})