diff --git a/browser_tests/tests/loadWorkflowInMedia.spec.ts-snapshots/workflow-avif-chromium-linux.png b/browser_tests/tests/loadWorkflowInMedia.spec.ts-snapshots/workflow-avif-chromium-linux.png
index 12e526ce63..0dbcabedaa 100644
Binary files a/browser_tests/tests/loadWorkflowInMedia.spec.ts-snapshots/workflow-avif-chromium-linux.png and b/browser_tests/tests/loadWorkflowInMedia.spec.ts-snapshots/workflow-avif-chromium-linux.png differ
diff --git a/src/components/common/DotSpinner.vue b/src/components/common/DotSpinner.vue
new file mode 100644
index 0000000000..548737bdf5
--- /dev/null
+++ b/src/components/common/DotSpinner.vue
@@ -0,0 +1,131 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/components/dialog/content/LoadWorkflowWarning.vue b/src/components/dialog/content/LoadWorkflowWarning.vue
index 41d422d20c..523e3a0f23 100644
--- a/src/components/dialog/content/LoadWorkflowWarning.vue
+++ b/src/components/dialog/content/LoadWorkflowWarning.vue
@@ -31,28 +31,44 @@
-
+
diff --git a/src/components/dialog/content/manager/ManagerHeader.test.ts b/src/components/dialog/content/manager/ManagerHeader.test.ts
new file mode 100644
index 0000000000..291020d1f8
--- /dev/null
+++ b/src/components/dialog/content/manager/ManagerHeader.test.ts
@@ -0,0 +1,82 @@
+import { mount } from '@vue/test-utils'
+import { createPinia } from 'pinia'
+import PrimeVue from 'primevue/config'
+import Tag from 'primevue/tag'
+import Tooltip from 'primevue/tooltip'
+import { describe, expect, it } from 'vitest'
+import { createI18n } from 'vue-i18n'
+
+import enMessages from '@/locales/en/main.json'
+
+import ManagerHeader from './ManagerHeader.vue'
+
+const i18n = createI18n({
+ legacy: false,
+ locale: 'en',
+ messages: {
+ en: enMessages
+ }
+})
+
+describe('ManagerHeader', () => {
+ const createWrapper = () => {
+ return mount(ManagerHeader, {
+ global: {
+ plugins: [createPinia(), PrimeVue, i18n],
+ directives: {
+ tooltip: Tooltip
+ },
+ components: {
+ Tag
+ }
+ }
+ })
+ }
+
+ it('renders the component title', () => {
+ const wrapper = createWrapper()
+
+ expect(wrapper.find('h2').text()).toBe(
+ enMessages.manager.discoverCommunityContent
+ )
+ })
+
+ it('displays the legacy manager UI tag', () => {
+ const wrapper = createWrapper()
+
+ const tag = wrapper.find('[data-pc-name="tag"]')
+ expect(tag.exists()).toBe(true)
+ expect(tag.text()).toContain(enMessages.manager.legacyManagerUI)
+ })
+
+ it('applies info severity to the tag', () => {
+ const wrapper = createWrapper()
+
+ const tag = wrapper.find('[data-pc-name="tag"]')
+ expect(tag.classes()).toContain('p-tag-info')
+ })
+
+ it('displays info icon in the tag', () => {
+ const wrapper = createWrapper()
+
+ const icon = wrapper.find('.pi-info-circle')
+ expect(icon.exists()).toBe(true)
+ })
+
+ it('has cursor-help class on the tag', () => {
+ const wrapper = createWrapper()
+
+ const tag = wrapper.find('[data-pc-name="tag"]')
+ expect(tag.classes()).toContain('cursor-help')
+ })
+
+ it('has proper structure with flex container', () => {
+ const wrapper = createWrapper()
+
+ const flexContainer = wrapper.find('.flex.justify-end.ml-auto.pr-4')
+ expect(flexContainer.exists()).toBe(true)
+
+ const tag = flexContainer.find('[data-pc-name="tag"]')
+ expect(tag.exists()).toBe(true)
+ })
+})
diff --git a/src/components/dialog/content/manager/ManagerHeader.vue b/src/components/dialog/content/manager/ManagerHeader.vue
index f6177c87b8..a4fbdfaa35 100644
--- a/src/components/dialog/content/manager/ManagerHeader.vue
+++ b/src/components/dialog/content/manager/ManagerHeader.vue
@@ -4,6 +4,22 @@
{{ $t('manager.discoverCommunityContent') }}
+
+
+
+
+
diff --git a/src/components/dialog/content/manager/button/PackInstallButton.vue b/src/components/dialog/content/manager/button/PackInstallButton.vue
index 7bba01ef0a..2f9751d0fd 100644
--- a/src/components/dialog/content/manager/button/PackInstallButton.vue
+++ b/src/components/dialog/content/manager/button/PackInstallButton.vue
@@ -10,7 +10,6 @@
:loading="isInstalling"
:loading-message="$t('g.installing')"
@action="installAllPacks"
- @click="onClick"
/>
@@ -37,10 +36,6 @@ const { nodePacks, variant, label } = defineProps<{
const isInstalling = inject(IsInstallingKey, ref(false))
-const onClick = (): void => {
- isInstalling.value = true
-}
-
const managerStore = useComfyManagerStore()
const createPayload = (installItem: NodePack) => {
@@ -65,8 +60,6 @@ const installPack = (item: NodePack) =>
const installAllPacks = async () => {
if (!nodePacks?.length) return
- isInstalling.value = true
-
const uninstalledPacks = nodePacks.filter(
(pack) => !managerStore.isPackInstalled(pack.id)
)
diff --git a/src/components/dialog/content/manager/packCard/PackCard.vue b/src/components/dialog/content/manager/packCard/PackCard.vue
index 08caeb29cb..ee2cef92de 100644
--- a/src/components/dialog/content/manager/packCard/PackCard.vue
+++ b/src/components/dialog/content/manager/packCard/PackCard.vue
@@ -84,10 +84,9 @@
diff --git a/src/components/topbar/CommandMenubar.vue b/src/components/topbar/CommandMenubar.vue
index 41a8b0ba0c..95289105e4 100644
--- a/src/components/topbar/CommandMenubar.vue
+++ b/src/components/topbar/CommandMenubar.vue
@@ -107,9 +107,12 @@ import SubgraphBreadcrumb from '@/components/breadcrumb/SubgraphBreadcrumb.vue'
import SettingDialogContent from '@/components/dialog/content/SettingDialogContent.vue'
import SettingDialogHeader from '@/components/dialog/header/SettingDialogHeader.vue'
import { useDialogService } from '@/services/dialogService'
-import { useAboutPanelStore } from '@/stores/aboutPanelStore'
import { useCommandStore } from '@/stores/commandStore'
import { useDialogStore } from '@/stores/dialogStore'
+import {
+ ManagerUIState,
+ useManagerStateStore
+} from '@/stores/managerStateStore'
import { useMenuItemStore } from '@/stores/menuItemStore'
import { useSettingStore } from '@/stores/settingStore'
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
@@ -121,7 +124,6 @@ const colorPaletteStore = useColorPaletteStore()
const menuItemsStore = useMenuItemStore()
const commandStore = useCommandStore()
const dialogStore = useDialogStore()
-const aboutPanelStore = useAboutPanelStore()
const settingStore = useSettingStore()
const { t } = useI18n()
@@ -157,23 +159,28 @@ const showSettings = (defaultPanel?: string) => {
})
}
-// Temporary duplicated from LoadWorkflowWarning.vue
-// Determines if ComfyUI-Manager is installed by checking for its badge in the about panel
-// This allows us to conditionally show the Manager button only when the extension is available
-// TODO: Remove this check when Manager functionality is fully migrated into core
-const isManagerInstalled = computed(() => {
- return aboutPanelStore.badges.some(
- (badge) =>
- badge.label.includes('ComfyUI-Manager') ||
- badge.url.includes('ComfyUI-Manager')
- )
-})
+const managerStateStore = useManagerStateStore()
+
+const showManageExtensions = async () => {
+ const state = await managerStateStore.getManagerUIState()
+
+ switch (state) {
+ case ManagerUIState.DISABLED:
+ showSettings('extension')
+ break
+
+ case ManagerUIState.LEGACY_UI:
+ try {
+ await commandStore.execute('Comfy.Manager.Menu.ToggleVisibility')
+ } catch {
+ // If legacy command doesn't exist, fall back to extensions panel
+ showSettings('extension')
+ }
+ break
-const showManageExtensions = () => {
- if (isManagerInstalled.value) {
- useDialogService().showManagerDialog()
- } else {
- showSettings('extension')
+ case ManagerUIState.NEW_UI:
+ useDialogService().showManagerDialog()
+ break
}
}
diff --git a/src/composables/useCoreCommands.ts b/src/composables/useCoreCommands.ts
index 9162af2c54..fb8e122d2a 100644
--- a/src/composables/useCoreCommands.ts
+++ b/src/composables/useCoreCommands.ts
@@ -19,10 +19,15 @@ import { useDialogService } from '@/services/dialogService'
import { useLitegraphService } from '@/services/litegraphService'
import { useWorkflowService } from '@/services/workflowService'
import type { ComfyCommand } from '@/stores/commandStore'
+import { useCommandStore } from '@/stores/commandStore'
import { useExecutionStore } from '@/stores/executionStore'
import { useCanvasStore, useTitleEditorStore } from '@/stores/graphStore'
import { useHelpCenterStore } from '@/stores/helpCenterStore'
import { useNodeOutputStore } from '@/stores/imagePreviewStore'
+import {
+ ManagerUIState,
+ useManagerStateStore
+} from '@/stores/managerStateStore'
import { useQueueSettingsStore, useQueueStore } from '@/stores/queueStore'
import { useSettingStore } from '@/stores/settingStore'
import { useSubgraphNavigationStore } from '@/stores/subgraphNavigationStore'
@@ -32,6 +37,7 @@ import { useBottomPanelStore } from '@/stores/workspace/bottomPanelStore'
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
import { useSearchBoxStore } from '@/stores/workspace/searchBoxStore'
import { useWorkspaceStore } from '@/stores/workspaceStore'
+import { ManagerTab } from '@/types/comfyManagerTypes'
import {
getAllNonIoNodesInSubgraph,
getExecutionIdsForSelectedNodes
@@ -710,12 +716,99 @@ export function useCoreCommands(): ComfyCommand[] {
}
},
{
- id: 'Comfy.Manager.CustomNodesManager',
- icon: 'pi pi-puzzle',
- label: 'Toggle the Custom Nodes Manager',
+ id: 'Comfy.Manager.CustomNodesManager.ShowCustomNodesMenu',
+ icon: 'pi pi-objects-column',
+ label: 'Custom Nodes Manager',
versionAdded: '1.12.10',
- function: () => {
- dialogService.toggleManagerDialog()
+ function: async () => {
+ const managerStore = useManagerStateStore()
+ const state = await managerStore.getManagerUIState()
+
+ switch (state) {
+ case ManagerUIState.DISABLED:
+ dialogService.showSettingsDialog('extension')
+ break
+
+ case ManagerUIState.LEGACY_UI:
+ useCommandStore()
+ .execute('Comfy.Manager.Menu.ToggleVisibility')
+ .catch(() => {
+ // If legacy command doesn't exist, fall back to extensions panel
+ dialogService.showSettingsDialog('extension')
+ })
+ break
+
+ case ManagerUIState.NEW_UI:
+ dialogService.showManagerDialog()
+ break
+ }
+ }
+ },
+ {
+ id: 'Comfy.Manager.ShowUpdateAvailablePacks',
+ icon: 'pi pi-sync',
+ label: 'Check for Custom Node Updates',
+ versionAdded: '1.17.0',
+ function: async () => {
+ const managerStore = useManagerStateStore()
+ const state = await managerStore.getManagerUIState()
+
+ switch (state) {
+ case ManagerUIState.DISABLED:
+ dialogService.showSettingsDialog('extension')
+ break
+
+ case ManagerUIState.LEGACY_UI:
+ try {
+ await useCommandStore().execute(
+ 'Comfy.Manager.Menu.ToggleVisibility'
+ )
+ } catch {
+ // If legacy command doesn't exist, fall back to extensions panel
+ dialogService.showSettingsDialog('extension')
+ }
+ break
+
+ case ManagerUIState.NEW_UI:
+ dialogService.showManagerDialog({
+ initialTab: ManagerTab.UpdateAvailable
+ })
+ break
+ }
+ }
+ },
+ {
+ id: 'Comfy.Manager.ShowMissingPacks',
+ icon: 'pi pi-exclamation-circle',
+ label: 'Install Missing Custom Nodes',
+ versionAdded: '1.17.0',
+ function: async () => {
+ const managerStore = useManagerStateStore()
+ const state = await managerStore.getManagerUIState()
+
+ switch (state) {
+ case ManagerUIState.DISABLED:
+ // When manager is disabled, open the extensions panel in settings
+ dialogService.showSettingsDialog('extension')
+ break
+
+ case ManagerUIState.LEGACY_UI:
+ try {
+ await useCommandStore().execute(
+ 'Comfy.Manager.Menu.ToggleVisibility'
+ )
+ } catch {
+ // If legacy command doesn't exist, fall back to extensions panel
+ dialogService.showSettingsDialog('extension')
+ }
+ break
+
+ case ManagerUIState.NEW_UI:
+ dialogService.showManagerDialog({
+ initialTab: ManagerTab.Missing
+ })
+ break
+ }
}
},
{
@@ -878,6 +971,84 @@ export function useCoreCommands(): ComfyCommand[] {
navigationStore.navigationStack.at(-2) ?? canvas.graph.rootGraph
)
}
+ },
+ {
+ id: 'Comfy.Manager.CustomNodesManager.ShowLegacyCustomNodesMenu',
+ icon: 'pi pi-bars',
+ label: 'Custom Nodes (Legacy)',
+ versionAdded: '1.16.4',
+ function: async () => {
+ try {
+ await useCommandStore().execute(
+ 'Comfy.Manager.CustomNodesManager.ToggleVisibility'
+ )
+ } catch (error) {
+ useToastStore().add({
+ severity: 'error',
+ summary: t('g.error'),
+ detail: t('manager.legacyMenuNotAvailable'),
+ life: 3000
+ })
+ }
+ }
+ },
+ {
+ id: 'Comfy.Manager.ShowLegacyManagerMenu',
+ icon: 'mdi mdi-puzzle',
+ label: 'Manager Menu (Legacy)',
+ versionAdded: '1.16.4',
+ function: async () => {
+ try {
+ await useCommandStore().execute('Comfy.Manager.Menu.ToggleVisibility')
+ } catch (error) {
+ useToastStore().add({
+ severity: 'error',
+ summary: t('g.error'),
+ detail: t('manager.legacyMenuNotAvailable'),
+ life: 3000
+ })
+ }
+ }
+ },
+ {
+ id: 'Comfy.Memory.UnloadModels',
+ icon: 'mdi mdi-vacuum-outline',
+ label: 'Unload Models',
+ versionAdded: '1.16.4',
+ function: async () => {
+ if (!useSettingStore().get('Comfy.Memory.AllowManualUnload')) {
+ useToastStore().add({
+ severity: 'error',
+ summary: t('g.error'),
+ detail: t('g.commandProhibited', {
+ command: 'Comfy.Memory.UnloadModels'
+ }),
+ life: 3000
+ })
+ return
+ }
+ await api.freeMemory({ freeExecutionCache: false })
+ }
+ },
+ {
+ id: 'Comfy.Memory.UnloadModelsAndExecutionCache',
+ icon: 'mdi mdi-vacuum-outline',
+ label: 'Unload Models and Execution Cache',
+ versionAdded: '1.16.4',
+ function: async () => {
+ if (!useSettingStore().get('Comfy.Memory.AllowManualUnload')) {
+ useToastStore().add({
+ severity: 'error',
+ summary: t('g.error'),
+ detail: t('g.commandProhibited', {
+ command: 'Comfy.Memory.UnloadModelsAndExecutionCache'
+ }),
+ life: 3000
+ })
+ return
+ }
+ await api.freeMemory({ freeExecutionCache: true })
+ }
}
]
diff --git a/src/composables/useFeatureFlags.ts b/src/composables/useFeatureFlags.ts
new file mode 100644
index 0000000000..a578eb8bf1
--- /dev/null
+++ b/src/composables/useFeatureFlags.ts
@@ -0,0 +1,40 @@
+import { computed, reactive, readonly } from 'vue'
+
+import { api } from '@/scripts/api'
+
+/**
+ * Known server feature flags (top-level, not extensions)
+ */
+export enum ServerFeatureFlag {
+ SUPPORTS_PREVIEW_METADATA = 'supports_preview_metadata',
+ MAX_UPLOAD_SIZE = 'max_upload_size',
+ MANAGER_SUPPORTS_V4 = 'extension.manager.supports_v4'
+}
+
+/**
+ * Composable for reactive access to feature flags
+ */
+export function useFeatureFlags() {
+ // Create reactive state that tracks server feature flags
+ const flags = reactive({
+ get supportsPreviewMetadata() {
+ return api.getServerFeature(ServerFeatureFlag.SUPPORTS_PREVIEW_METADATA)
+ },
+ get maxUploadSize() {
+ return api.getServerFeature(ServerFeatureFlag.MAX_UPLOAD_SIZE)
+ },
+ get supportsManagerV4() {
+ return api.getServerFeature(ServerFeatureFlag.MANAGER_SUPPORTS_V4)
+ }
+ })
+
+ // Create a reactive computed for any feature flag
+ const featureFlag =