Skip to content

Commit 2415b97

Browse files
[feat] Add managerStateStore for three-state manager UI logic
- Create managerStateStore to determine manager UI state (disabled, legacy, new) - Check command line args, feature flags, and legacy API endpoints - Update useCoreCommands to use the new store instead of async API calls - Initialize manager state after system stats are loaded in GraphView - Add comprehensive tests for all manager state scenarios 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent 6426861 commit 2415b97

File tree

4 files changed

+291
-16
lines changed

4 files changed

+291
-16
lines changed

src/composables/useCoreCommands.ts

Lines changed: 28 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import {
1616
import { Point } from '@/lib/litegraph/src/litegraph'
1717
import { api } from '@/scripts/api'
1818
import { app } from '@/scripts/app'
19-
import { useComfyManagerService } from '@/services/comfyManagerService'
2019
import { useDialogService } from '@/services/dialogService'
2120
import { useLitegraphService } from '@/services/litegraphService'
2221
import { useWorkflowService } from '@/services/workflowService'
@@ -26,6 +25,10 @@ import { useExecutionStore } from '@/stores/executionStore'
2625
import { useCanvasStore, useTitleEditorStore } from '@/stores/graphStore'
2726
import { useHelpCenterStore } from '@/stores/helpCenterStore'
2827
import { useNodeOutputStore } from '@/stores/imagePreviewStore'
28+
import {
29+
ManagerUIState,
30+
useManagerStateStore
31+
} from '@/stores/managerStateStore'
2932
import { useQueueSettingsStore, useQueueStore } from '@/stores/queueStore'
3033
import { useSettingStore } from '@/stores/settingStore'
3134
import { useSubgraphNavigationStore } from '@/stores/subgraphNavigationStore'
@@ -739,27 +742,36 @@ export function useCoreCommands(): ComfyCommand[] {
739742
icon: 'pi pi-objects-column',
740743
label: 'Custom Nodes Manager',
741744
versionAdded: '1.12.10',
742-
function: async () => {
743-
const { is_legacy_manager_ui } =
744-
(await useComfyManagerService().isLegacyManagerUI()) ?? {}
745+
function: () => {
746+
const managerStore = useManagerStateStore()
747+
const state = managerStore.managerUIState
745748

746-
if (is_legacy_manager_ui === true) {
747-
try {
748-
await useCommandStore().execute(
749-
'Comfy.Manager.Menu.ToggleVisibility' // This command is registered by legacy manager FE extension
750-
)
751-
} catch (error) {
752-
console.error('error', error)
753-
useToastStore().add({
749+
switch (state) {
750+
case ManagerUIState.DISABLED:
751+
toastStore.add({
754752
severity: 'error',
755753
summary: t('g.error'),
756-
detail: t('manager.legacyMenuNotAvailable'),
754+
detail: t('manager.notAvailable'),
757755
life: 3000
758756
})
757+
break
758+
759+
case ManagerUIState.LEGACY_UI:
760+
useCommandStore()
761+
.execute('Comfy.Manager.Menu.ToggleVisibility')
762+
.catch(() => {
763+
toastStore.add({
764+
severity: 'error',
765+
summary: t('g.error'),
766+
detail: t('manager.legacyMenuNotAvailable'),
767+
life: 3000
768+
})
769+
})
770+
break
771+
772+
case ManagerUIState.NEW_UI:
759773
dialogService.showManagerDialog()
760-
}
761-
} else {
762-
dialogService.showManagerDialog()
774+
break
763775
}
764776
}
765777
},

src/stores/managerStateStore.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { defineStore } from 'pinia'
2+
import { readonly, ref } from 'vue'
3+
4+
import { useFeatureFlags } from '@/composables/useFeatureFlags'
5+
import { api } from '@/scripts/api'
6+
import { useComfyManagerService } from '@/services/comfyManagerService'
7+
import { useSystemStatsStore } from '@/stores/systemStatsStore'
8+
9+
export enum ManagerUIState {
10+
DISABLED = 'disabled',
11+
LEGACY_UI = 'legacy',
12+
NEW_UI = 'new'
13+
}
14+
15+
export const useManagerStateStore = defineStore('managerState', () => {
16+
const managerUIState = ref<ManagerUIState | null>(null)
17+
const isInitialized = ref(false)
18+
19+
const initializeManagerState = async () => {
20+
if (isInitialized.value) return
21+
22+
const systemStats = useSystemStatsStore().systemStats
23+
const { flags } = useFeatureFlags()
24+
const clientSupportsV4 =
25+
api.getClientFeatureFlags().supports_manager_v4_ui ?? false
26+
27+
// Check command line args first
28+
if (systemStats?.system?.argv?.includes('--disable-manager')) {
29+
managerUIState.value = ManagerUIState.DISABLED
30+
} else if (
31+
systemStats?.system?.argv?.includes('--enable-manager-legacy-ui')
32+
) {
33+
managerUIState.value = ManagerUIState.LEGACY_UI
34+
} else {
35+
// Check if we can use new UI
36+
if (clientSupportsV4 && flags.supportsManagerV4) {
37+
managerUIState.value = ManagerUIState.NEW_UI
38+
} else {
39+
// For old frontend, we need to check if legacy manager exists
40+
try {
41+
await useComfyManagerService().isLegacyManagerUI()
42+
// Route exists but we can't use v4
43+
managerUIState.value = ManagerUIState.LEGACY_UI
44+
} catch {
45+
// Route doesn't exist = old manager OR no manager
46+
// Old frontend will handle this itself
47+
managerUIState.value = ManagerUIState.LEGACY_UI
48+
}
49+
}
50+
}
51+
52+
isInitialized.value = true
53+
}
54+
55+
return {
56+
managerUIState: readonly(managerUIState),
57+
initializeManagerState
58+
}
59+
})

src/views/GraphView.vue

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ import { setupAutoQueueHandler } from '@/services/autoQueueService'
5353
import { useKeybindingService } from '@/services/keybindingService'
5454
import { useCommandStore } from '@/stores/commandStore'
5555
import { useExecutionStore } from '@/stores/executionStore'
56+
import { useManagerStateStore } from '@/stores/managerStateStore'
5657
import { useMenuItemStore } from '@/stores/menuItemStore'
5758
import { useModelStore } from '@/stores/modelStore'
5859
import { useNodeDefStore, useNodeFrequencyStore } from '@/stores/nodeDefStore'
@@ -250,6 +251,13 @@ void nextTick(() => {
250251
versionCompatibilityStore.initialize().catch((error) => {
251252
console.warn('Version compatibility check failed:', error)
252253
})
254+
255+
// Initialize manager state after system stats are loaded
256+
useManagerStateStore()
257+
.initializeManagerState()
258+
.catch((error) => {
259+
console.warn('Manager state initialization failed:', error)
260+
})
253261
})
254262
255263
const onGraphReady = () => {
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
import { createPinia, setActivePinia } from 'pinia'
2+
import { beforeEach, describe, expect, it, vi } from 'vitest'
3+
4+
import { useFeatureFlags } from '@/composables/useFeatureFlags'
5+
import { api } from '@/scripts/api'
6+
import { useComfyManagerService } from '@/services/comfyManagerService'
7+
import {
8+
ManagerUIState,
9+
useManagerStateStore
10+
} from '@/stores/managerStateStore'
11+
import { useSystemStatsStore } from '@/stores/systemStatsStore'
12+
13+
// Mock dependencies
14+
vi.mock('@/scripts/api', () => ({
15+
api: {
16+
getClientFeatureFlags: vi.fn()
17+
}
18+
}))
19+
20+
vi.mock('@/composables/useFeatureFlags', () => ({
21+
useFeatureFlags: vi.fn(() => ({
22+
flags: { supportsManagerV4: false },
23+
featureFlag: vi.fn()
24+
}))
25+
}))
26+
27+
vi.mock('@/services/comfyManagerService', () => ({
28+
useComfyManagerService: vi.fn()
29+
}))
30+
31+
vi.mock('@/stores/systemStatsStore', () => ({
32+
useSystemStatsStore: vi.fn()
33+
}))
34+
35+
describe('useManagerStateStore', () => {
36+
beforeEach(() => {
37+
setActivePinia(createPinia())
38+
vi.clearAllMocks()
39+
})
40+
41+
describe('initializeManagerState', () => {
42+
it('should set DISABLED state when --disable-manager is present', async () => {
43+
vi.mocked(useSystemStatsStore).mockReturnValue({
44+
systemStats: {
45+
system: { argv: ['python', 'main.py', '--disable-manager'] }
46+
}
47+
} as any)
48+
vi.mocked(api.getClientFeatureFlags).mockReturnValue({})
49+
50+
const store = useManagerStateStore()
51+
await store.initializeManagerState()
52+
53+
expect(store.managerUIState).toBe(ManagerUIState.DISABLED)
54+
})
55+
56+
it('should set LEGACY_UI state when --enable-manager-legacy-ui is present', async () => {
57+
vi.mocked(useSystemStatsStore).mockReturnValue({
58+
systemStats: {
59+
system: { argv: ['python', 'main.py', '--enable-manager-legacy-ui'] }
60+
}
61+
} as any)
62+
vi.mocked(api.getClientFeatureFlags).mockReturnValue({})
63+
64+
const store = useManagerStateStore()
65+
await store.initializeManagerState()
66+
67+
expect(store.managerUIState).toBe(ManagerUIState.LEGACY_UI)
68+
})
69+
70+
it('should set NEW_UI state when client and server both support v4', async () => {
71+
vi.mocked(useSystemStatsStore).mockReturnValue({
72+
systemStats: { system: { argv: ['python', 'main.py'] } }
73+
} as any)
74+
vi.mocked(api.getClientFeatureFlags).mockReturnValue({
75+
supports_manager_v4_ui: true
76+
})
77+
vi.mocked(useFeatureFlags).mockReturnValue({
78+
flags: { supportsManagerV4: true },
79+
featureFlag: vi.fn()
80+
} as any)
81+
vi.mocked(useComfyManagerService).mockReturnValue({
82+
isLegacyManagerUI: vi.fn().mockResolvedValue({})
83+
} as any)
84+
85+
const store = useManagerStateStore()
86+
await store.initializeManagerState()
87+
88+
expect(store.managerUIState).toBe(ManagerUIState.NEW_UI)
89+
})
90+
91+
it('should set LEGACY_UI state when client does not support v4', async () => {
92+
vi.mocked(useSystemStatsStore).mockReturnValue({
93+
systemStats: { system: { argv: ['python', 'main.py'] } }
94+
} as any)
95+
vi.mocked(api.getClientFeatureFlags).mockReturnValue({
96+
supports_manager_v4_ui: false
97+
})
98+
vi.mocked(useFeatureFlags).mockReturnValue({
99+
flags: { supportsManagerV4: true },
100+
featureFlag: vi.fn()
101+
} as any)
102+
vi.mocked(useComfyManagerService).mockReturnValue({
103+
isLegacyManagerUI: vi.fn().mockResolvedValue({})
104+
} as any)
105+
106+
const store = useManagerStateStore()
107+
await store.initializeManagerState()
108+
109+
expect(store.managerUIState).toBe(ManagerUIState.LEGACY_UI)
110+
})
111+
112+
it('should set LEGACY_UI state when server does not support v4', async () => {
113+
vi.mocked(useSystemStatsStore).mockReturnValue({
114+
systemStats: { system: { argv: ['python', 'main.py'] } }
115+
} as any)
116+
vi.mocked(api.getClientFeatureFlags).mockReturnValue({
117+
supports_manager_v4_ui: true
118+
})
119+
vi.mocked(useFeatureFlags).mockReturnValue({
120+
flags: { supportsManagerV4: false },
121+
featureFlag: vi.fn()
122+
} as any)
123+
vi.mocked(useComfyManagerService).mockReturnValue({
124+
isLegacyManagerUI: vi.fn().mockResolvedValue({})
125+
} as any)
126+
127+
const store = useManagerStateStore()
128+
await store.initializeManagerState()
129+
130+
expect(store.managerUIState).toBe(ManagerUIState.LEGACY_UI)
131+
})
132+
133+
it('should set LEGACY_UI state when isLegacyManagerUI route does not exist', async () => {
134+
vi.mocked(useSystemStatsStore).mockReturnValue({
135+
systemStats: { system: { argv: ['python', 'main.py'] } }
136+
} as any)
137+
vi.mocked(api.getClientFeatureFlags).mockReturnValue({})
138+
vi.mocked(useFeatureFlags).mockReturnValue({
139+
flags: { supportsManagerV4: false },
140+
featureFlag: vi.fn()
141+
} as any)
142+
vi.mocked(useComfyManagerService).mockReturnValue({
143+
isLegacyManagerUI: vi.fn().mockRejectedValue(new Error('404'))
144+
} as any)
145+
146+
const store = useManagerStateStore()
147+
await store.initializeManagerState()
148+
149+
expect(store.managerUIState).toBe(ManagerUIState.LEGACY_UI)
150+
})
151+
152+
it('should not re-initialize if already initialized', async () => {
153+
vi.mocked(useSystemStatsStore).mockReturnValue({
154+
systemStats: {
155+
system: { argv: ['python', 'main.py', '--disable-manager'] }
156+
}
157+
} as any)
158+
159+
const store = useManagerStateStore()
160+
await store.initializeManagerState()
161+
expect(store.managerUIState).toBe(ManagerUIState.DISABLED)
162+
163+
// Change the mock to return different value
164+
vi.mocked(useSystemStatsStore).mockReturnValue({
165+
systemStats: { system: { argv: ['python', 'main.py'] } }
166+
} as any)
167+
168+
// Try to initialize again
169+
await store.initializeManagerState()
170+
171+
// Should still be DISABLED from first initialization
172+
expect(store.managerUIState).toBe(ManagerUIState.DISABLED)
173+
})
174+
175+
it('should handle null systemStats gracefully', async () => {
176+
vi.mocked(useSystemStatsStore).mockReturnValue({
177+
systemStats: null
178+
} as any)
179+
vi.mocked(api.getClientFeatureFlags).mockReturnValue({
180+
supports_manager_v4_ui: true
181+
})
182+
vi.mocked(useFeatureFlags).mockReturnValue({
183+
flags: { supportsManagerV4: true },
184+
featureFlag: vi.fn()
185+
} as any)
186+
vi.mocked(useComfyManagerService).mockReturnValue({
187+
isLegacyManagerUI: vi.fn().mockResolvedValue({})
188+
} as any)
189+
190+
const store = useManagerStateStore()
191+
await store.initializeManagerState()
192+
193+
expect(store.managerUIState).toBe(ManagerUIState.NEW_UI)
194+
})
195+
})
196+
})

0 commit comments

Comments
 (0)