Merge main into austin/widgets-v2

This commit is contained in:
Austin Mroz
2025-09-11 13:04:34 -05:00
745 changed files with 44060 additions and 10711 deletions

View File

@@ -100,57 +100,64 @@ The following diagram illustrates the store architecture and data flow:
## Core Stores
The following table lists ALL stores in the system as of 2025-01-30:
The following table lists ALL 46 store instances in the system as of 2025-09-01:
### Main Stores
| Store | Description | Category |
|-------|-------------|----------|
| aboutPanelStore.ts | Manages the About panel state and badges | UI |
| apiKeyAuthStore.ts | Handles API key authentication | Auth |
| comfyManagerStore.ts | Manages ComfyUI application state | Core |
| comfyRegistryStore.ts | Handles extensions registry | Registry |
| commandStore.ts | Manages commands and command execution | Core |
| dialogStore.ts | Controls dialog/modal display and state | UI |
| domWidgetStore.ts | Manages DOM widget state | Widgets |
| electronDownloadStore.ts | Handles Electron-specific download operations | Platform |
| executionStore.ts | Tracks workflow execution state | Execution |
| extensionStore.ts | Manages extension registration and state | Extensions |
| firebaseAuthStore.ts | Handles Firebase authentication | Auth |
| graphStore.ts | Manages the graph canvas state | Core |
| imagePreviewStore.ts | Controls image preview functionality | Media |
| keybindingStore.ts | Manages keyboard shortcuts | Input |
| maintenanceTaskStore.ts | Handles system maintenance tasks | System |
| menuItemStore.ts | Handles menu items and their state | UI |
| modelStore.ts | Manages AI models information | Models |
| modelToNodeStore.ts | Maps models to compatible nodes | Models |
| nodeBookmarkStore.ts | Manages node bookmarks and favorites | Nodes |
| nodeDefStore.ts | Manages node definitions | Nodes |
| queueStore.ts | Handles the execution queue | Execution |
| releaseStore.ts | Manages application release information | System |
| serverConfigStore.ts | Handles server configuration | Config |
| settingStore.ts | Manages application settings | Config |
| subgraphNavigationStore.ts | Handles subgraph navigation state | Navigation |
| systemStatsStore.ts | Tracks system performance statistics | System |
| toastStore.ts | Manages toast notifications | UI |
| userFileStore.ts | Manages user file operations | Files |
| userStore.ts | Manages user data and preferences | User |
| versionCompatibilityStore.ts | Manages frontend/backend version compatibility warnings | Core |
| widgetStore.ts | Manages widget configurations | Widgets |
| workflowStore.ts | Handles workflow data and operations | Workflows |
| workflowTemplatesStore.ts | Manages workflow templates | Workflows |
| workspaceStore.ts | Manages overall workspace state | Workspace |
| File | Store | Description | Category |
|------|-------|-------------|----------|
| aboutPanelStore.ts | useAboutPanelStore | Manages the About panel state and badges | UI |
| apiKeyAuthStore.ts | useApiKeyAuthStore | Handles API key authentication | Auth |
| comfyManagerStore.ts | useComfyManagerStore | Manages ComfyUI application state | Core |
| comfyManagerStore.ts | useManagerProgressDialogStore | Manages manager progress dialog state | UI |
| comfyRegistryStore.ts | useComfyRegistryStore | Handles extensions registry | Registry |
| commandStore.ts | useCommandStore | Manages commands and command execution | Core |
| dialogStore.ts | useDialogStore | Controls dialog/modal display and state | UI |
| domWidgetStore.ts | useDomWidgetStore | Manages DOM widget state | Widgets |
| electronDownloadStore.ts | useElectronDownloadStore | Handles Electron-specific download operations | Platform |
| executionStore.ts | useExecutionStore | Tracks workflow execution state | Execution |
| extensionStore.ts | useExtensionStore | Manages extension registration and state | Extensions |
| firebaseAuthStore.ts | useFirebaseAuthStore | Handles Firebase authentication | Auth |
| graphStore.ts | useTitleEditorStore | Manages title editing for nodes and groups | UI |
| graphStore.ts | useCanvasStore | Manages the graph canvas state and interactions | Core |
| helpCenterStore.ts | useHelpCenterStore | Manages help center visibility and state | UI |
| imagePreviewStore.ts | useNodeOutputStore | Manages node outputs and execution results | Media |
| keybindingStore.ts | useKeybindingStore | Manages keyboard shortcuts | Input |
| maintenanceTaskStore.ts | useMaintenanceTaskStore | Handles system maintenance tasks | System |
| menuItemStore.ts | useMenuItemStore | Handles menu items and their state | UI |
| modelStore.ts | useModelStore | Manages AI models information | Models |
| modelToNodeStore.ts | useModelToNodeStore | Maps models to compatible nodes | Models |
| nodeBookmarkStore.ts | useNodeBookmarkStore | Manages node bookmarks and favorites | Nodes |
| nodeDefStore.ts | useNodeDefStore | Manages node definitions and schemas | Nodes |
| nodeDefStore.ts | useNodeFrequencyStore | Tracks node usage frequency | Nodes |
| queueStore.ts | useQueueStore | Manages execution queue and task history | Execution |
| queueStore.ts | useQueuePendingTaskCountStore | Tracks pending task counts | Execution |
| queueStore.ts | useQueueSettingsStore | Manages queue execution settings | Execution |
| releaseStore.ts | useReleaseStore | Manages application release information | System |
| serverConfigStore.ts | useServerConfigStore | Handles server configuration | Config |
| settingStore.ts | useSettingStore | Manages application settings | Config |
| subgraphNavigationStore.ts | useSubgraphNavigationStore | Handles subgraph navigation state | Navigation |
| systemStatsStore.ts | useSystemStatsStore | Tracks system performance statistics | System |
| toastStore.ts | useToastStore | Manages toast notifications | UI |
| userFileStore.ts | useUserFileStore | Manages user file operations | Files |
| userStore.ts | useUserStore | Manages user data and preferences | User |
| versionCompatibilityStore.ts | useVersionCompatibilityStore | Manages frontend/backend version compatibility warnings | Core |
| widgetStore.ts | useWidgetStore | Manages widget configurations | Widgets |
| workflowStore.ts | useWorkflowStore | Handles workflow data and operations | Workflows |
| workflowStore.ts | useWorkflowBookmarkStore | Manages workflow bookmarks and favorites | Workflows |
| workflowTemplatesStore.ts | useWorkflowTemplatesStore | Manages workflow templates | Workflows |
| workspaceStore.ts | useWorkspaceStore | Manages overall workspace state | Workspace |
### Workspace Stores
Located in `stores/workspace/`:
| Store | Description |
|-------|-------------|
| bottomPanelStore.ts | Controls bottom panel visibility and state |
| colorPaletteStore.ts | Manages color palette configurations |
| nodeHelpStore.ts | Handles node help and documentation display |
| searchBoxStore.ts | Manages search box functionality |
| sidebarTabStore.ts | Controls sidebar tab states and navigation |
| File | Store | Description | Category |
|------|-------|-------------|----------|
| bottomPanelStore.ts | useBottomPanelStore | Controls bottom panel visibility and state | UI |
| colorPaletteStore.ts | useColorPaletteStore | Manages color palette configurations | UI |
| nodeHelpStore.ts | useNodeHelpStore | Handles node help and documentation display | UI |
| searchBoxStore.ts | useSearchBoxStore | Manages search box functionality | UI |
| sidebarTabStore.ts | useSidebarTabStore | Controls sidebar tab states and navigation | UI |
## Store Development Guidelines
@@ -189,7 +196,7 @@ export const useExampleStore = defineStore('example', () => {
async function fetchItems() {
isLoading.value = true
error.value = null
try {
const response = await fetch('/api/items')
const data = await response.json()
@@ -207,11 +214,11 @@ export const useExampleStore = defineStore('example', () => {
items,
isLoading,
error,
// Getters
itemCount,
hasError,
// Actions
addItem,
fetchItems
@@ -238,7 +245,7 @@ export const useDataStore = defineStore('data', () => {
async function fetchData() {
loading.value = true
try {
const result = await api.getData()
const result = await api.getExtensions()
data.value = result
} catch (err) {
error.value = err.message
@@ -266,21 +273,21 @@ import { useOtherStore } from './otherStore'
export const useComposedStore = defineStore('composed', () => {
const otherStore = useOtherStore()
const { someData } = storeToRefs(otherStore)
// Local state
const localState = ref(0)
// Computed value based on other store
const derivedValue = computed(() => {
return computeFromOtherData(someData.value, localState.value)
})
// Action that uses another store
async function complexAction() {
await otherStore.someAction()
localState.value += 1
}
return {
localState,
derivedValue,
@@ -299,20 +306,20 @@ export const usePreferencesStore = defineStore('preferences', () => {
// Load from localStorage if available
const theme = ref(localStorage.getItem('theme') || 'light')
const fontSize = ref(parseInt(localStorage.getItem('fontSize') || '14'))
// Save to localStorage when changed
watch(theme, (newTheme) => {
localStorage.setItem('theme', newTheme)
})
watch(fontSize, (newSize) => {
localStorage.setItem('fontSize', newSize.toString())
})
function setTheme(newTheme) {
theme.value = newTheme
}
return {
theme,
fontSize,
@@ -347,7 +354,7 @@ describe('useExampleStore', () => {
// Create a fresh pinia instance and make it active
setActivePinia(createPinia())
store = useExampleStore()
// Clear all mocks
vi.clearAllMocks()
})
@@ -363,14 +370,14 @@ describe('useExampleStore', () => {
expect(store.items).toEqual(['test'])
expect(store.itemCount).toBe(1)
})
it('should fetch items', async () => {
// Setup mock response
vi.mocked(api.getData).mockResolvedValue(['item1', 'item2'])
// Call the action
await store.fetchItems()
// Verify state changes
expect(store.isLoading).toBe(false)
expect(store.items).toEqual(['item1', 'item2'])

View File

@@ -1,21 +1,30 @@
import { whenever } from '@vueuse/core'
import { useEventListener, whenever } from '@vueuse/core'
import { defineStore } from 'pinia'
import { v4 as uuidv4 } from 'uuid'
import { ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { useCachedRequest } from '@/composables/useCachedRequest'
import { useManagerQueue } from '@/composables/useManagerQueue'
import { useServerLogs } from '@/composables/useServerLogs'
import { api } from '@/scripts/api'
import { app } from '@/scripts/app'
import { useComfyManagerService } from '@/services/comfyManagerService'
import { useDialogService } from '@/services/dialogService'
import {
InstallPackParams,
InstalledPacksResponse,
ManagerPackInfo,
ManagerPackInstalled,
TaskLog,
UpdateAllPacksParams
} from '@/types/comfyManagerTypes'
import { TaskLog } from '@/types/comfyManagerTypes'
import { components } from '@/types/generatedManagerTypes'
import { normalizePackKeys } from '@/utils/packUtils'
type InstallPackParams = components['schemas']['InstallPackParams']
type InstalledPacksResponse = components['schemas']['InstalledPacksResponse']
type ManagerPackInfo = components['schemas']['ManagerPackInfo']
type ManagerPackInstalled = components['schemas']['ManagerPackInstalled']
type ManagerTaskHistory = Record<
string,
components['schemas']['TaskHistoryItem']
>
type ManagerTaskQueue = components['schemas']['TaskStateMessage']
type UpdateAllPacksParams = components['schemas']['UpdateAllPacksParams']
/**
* Store for state of installed node packs
@@ -29,16 +38,78 @@ export const useComfyManagerStore = defineStore('comfyManager', () => {
const enabledPacksIds = ref<Set<string>>(new Set())
const disabledPacksIds = ref<Set<string>>(new Set())
const installedPacksIds = ref<Set<string>>(new Set())
const installingPacksIds = ref<Set<string>>(new Set())
const isStale = ref(true)
const taskLogs = ref<TaskLog[]>([])
const succeededTasksLogs = ref<TaskLog[]>([])
const failedTasksLogs = ref<TaskLog[]>([])
const { statusMessage, allTasksDone, enqueueTask, uncompletedCount } =
useManagerQueue()
const taskHistory = ref<ManagerTaskHistory>({})
const succeededTasksIds = ref<string[]>([])
const failedTasksIds = ref<string[]>([])
const taskQueue = ref<ManagerTaskQueue>({
history: {},
running_queue: [],
pending_queue: [],
installed_packs: {}
})
// Track task ID to pack ID mapping for proper state cleanup
const taskIdToPackId = ref(new Map<string, string>())
const managerQueue = useManagerQueue(taskHistory, taskQueue, installedPacks)
// Listen for task completion events to clean up installing state
useEventListener(app.api, 'cm-task-completed', (event: any) => {
const taskId = event.detail?.ui_id
if (taskId && taskIdToPackId.value.has(taskId)) {
const packId = taskIdToPackId.value.get(taskId)!
installingPacksIds.value.delete(packId)
taskIdToPackId.value.delete(taskId)
}
})
const setStale = () => {
isStale.value = true
}
const partitionTaskLogs = () => {
const successTaskLogs: TaskLog[] = []
const failTaskLogs: TaskLog[] = []
for (const log of taskLogs.value) {
if (failedTasksIds.value.includes(log.taskId)) {
failTaskLogs.push(log)
} else {
successTaskLogs.push(log)
}
}
succeededTasksLogs.value = successTaskLogs
failedTasksLogs.value = failTaskLogs
}
const partitionTasks = () => {
const successTasksIds = []
const failTasksIds = []
for (const task of Object.values(taskHistory.value)) {
if (task.status?.status_str === 'success') {
successTasksIds.push(task.ui_id)
} else {
failTasksIds.push(task.ui_id)
}
}
succeededTasksIds.value = successTasksIds
failedTasksIds.value = failTasksIds
}
whenever(
taskHistory,
() => {
partitionTasks()
partitionTaskLogs()
},
{ deep: true }
)
const getPackId = (pack: ManagerPackInstalled) => pack.cnr_id || pack.aux_id
const isInstalledPackId = (packName: string | undefined): boolean =>
@@ -49,6 +120,9 @@ export const useComfyManagerStore = defineStore('comfyManager', () => {
isInstalledPackId(packName) &&
enabledPacksIds.value.has(packName)
const isInstallingPackId = (packName: string | undefined): boolean =>
!!packName && installingPacksIds.value.has(packName)
const packsToIdSet = (packs: ManagerPackInstalled[]) =>
packs.reduce((acc, pack) => {
const id = pack.cnr_id || pack.aux_id
@@ -110,28 +184,54 @@ export const useComfyManagerStore = defineStore('comfyManager', () => {
const refreshInstalledList = async () => {
const packs = await managerService.listInstalledPacks()
if (packs) installedPacks.value = packs
if (packs) {
// Normalize pack keys to ensure consistent access
installedPacks.value = normalizePackKeys(packs)
}
isStale.value = false
}
whenever(isStale, refreshInstalledList, { immediate: true })
whenever(uncompletedCount, () => showManagerProgressDialog())
const withLogs = (task: () => Promise<null>, taskName: string) => {
const { startListening, stopListening, logs } = useServerLogs()
const enqueueTaskWithLogs = async (
task: (taskId: string) => Promise<null>,
taskName: string
) => {
const taskId = uuidv4()
const { logs } = useServerLogs({
ui_id: taskId,
immediate: true
})
const loggedTask = async () => {
taskLogs.value.push({ taskName, logs: logs.value })
await startListening()
return task()
try {
// Show progress dialog immediately when task is queued
showManagerProgressDialog()
managerQueue.isProcessing.value = true
// Prepare logging hook
taskLogs.value.push({ taskName, taskId, logs: logs.value })
// Queue the task to the server
await task(taskId)
} catch (error) {
// Reset processing state on error
managerQueue.isProcessing.value = false
// The server has authority over task history in general, but in rare
// case of client-side error, we add that to failed tasks from the client side
taskHistory.value[taskId] = {
ui_id: taskId,
client_id: api.clientId || 'unknown',
kind: 'error',
result: 'failed',
status: {
status_str: 'error',
completed: false,
messages: [error instanceof Error ? error.message : String(error)]
},
timestamp: new Date().toISOString()
}
}
const onComplete = async () => {
await stopListening()
setStale()
}
return { task: loggedTask, onComplete }
}
const installPack = useCachedRequest<InstallPackParams, void>(
@@ -152,39 +252,69 @@ export const useComfyManagerStore = defineStore('comfyManager', () => {
}
}
const task = () => managerService.installPack(params, signal)
enqueueTask(withLogs(task, `${actionDescription} ${params.id}`))
installingPacksIds.value.add(params.id)
const task = (taskId: string) => {
taskIdToPackId.value.set(taskId, params.id)
return managerService.installPack(params, taskId, signal)
}
await enqueueTaskWithLogs(task, `${actionDescription} ${params.id}`)
},
{ maxSize: 1 }
)
const uninstallPack = (params: ManagerPackInfo, signal?: AbortSignal) => {
const uninstallPack = async (
params: ManagerPackInfo,
signal?: AbortSignal
) => {
installPack.clear()
installPack.cancel()
const task = () => managerService.uninstallPack(params, signal)
enqueueTask(withLogs(task, t('manager.uninstalling', { id: params.id })))
installingPacksIds.value.add(params.id)
const uninstallParams: components['schemas']['UninstallPackParams'] = {
node_name: params.id,
is_unknown: false
}
const task = (taskId: string) => {
taskIdToPackId.value.set(taskId, params.id)
return managerService.uninstallPack(uninstallParams, taskId, signal)
}
await enqueueTaskWithLogs(
task,
t('manager.uninstalling', { id: params.id })
)
}
const updatePack = useCachedRequest<ManagerPackInfo, void>(
async (params: ManagerPackInfo, signal?: AbortSignal) => {
updateAllPacks.cancel()
const task = () => managerService.updatePack(params, signal)
enqueueTask(withLogs(task, t('g.updating', { id: params.id })))
const updateParams: components['schemas']['UpdatePackParams'] = {
node_name: params.id,
node_ver: params.version
}
const task = (taskId: string) =>
managerService.updatePack(updateParams, taskId, signal)
await enqueueTaskWithLogs(task, t('g.updating', { id: params.id }))
},
{ maxSize: 1 }
)
const updateAllPacks = useCachedRequest<UpdateAllPacksParams, void>(
async (params: UpdateAllPacksParams, signal?: AbortSignal) => {
const task = () => managerService.updateAllPacks(params, signal)
enqueueTask(withLogs(task, t('manager.updatingAllPacks')))
const task = (taskId: string) =>
managerService.updateAllPacks(params, taskId, signal)
await enqueueTaskWithLogs(task, t('manager.updatingAllPacks'))
},
{ maxSize: 1 }
)
const disablePack = (params: ManagerPackInfo, signal?: AbortSignal) => {
const task = () => managerService.disablePack(params, signal)
enqueueTask(withLogs(task, t('g.disabling', { id: params.id })))
const disablePack = async (params: ManagerPackInfo, signal?: AbortSignal) => {
const disableParams: components['schemas']['DisablePackParams'] = {
node_name: params.id,
is_unknown: false
}
const task = (taskId: string) =>
managerService.disablePack(disableParams, taskId, signal)
await enqueueTaskWithLogs(task, t('g.disabling', { id: params.id }))
}
const getInstalledPackVersion = (packId: string) => {
@@ -196,15 +326,33 @@ export const useComfyManagerStore = defineStore('comfyManager', () => {
taskLogs.value = []
}
const resetTaskState = () => {
// Clear all task-related reactive state for fresh start after restart
taskLogs.value = []
taskHistory.value = {}
succeededTasksIds.value = []
failedTasksIds.value = []
succeededTasksLogs.value = []
failedTasksLogs.value = []
installingPacksIds.value.clear()
taskIdToPackId.value.clear()
// Reset task queue to initial state
taskQueue.value = {
history: {},
running_queue: [],
pending_queue: [],
installed_packs: {}
}
}
return {
// Manager state
isLoading: managerService.isLoading,
error: managerService.error,
statusMessage,
allTasksDone,
uncompletedCount,
taskLogs,
clearLogs,
resetTaskState,
setStale,
// Installed packs state
@@ -212,9 +360,20 @@ export const useComfyManagerStore = defineStore('comfyManager', () => {
installedPacksIds,
isPackInstalled: isInstalledPackId,
isPackEnabled: isEnabledPackId,
isPackInstalling: isInstallingPackId,
getInstalledPackVersion,
refreshInstalledList,
// Task queue state and actions
taskHistory,
taskQueue,
isProcessingTasks: managerQueue.isProcessing,
succeededTasksIds,
failedTasksIds,
succeededTasksLogs,
failedTasksLogs,
managerQueue,
// Pack actions
installPack,
uninstallPack,
@@ -234,6 +393,15 @@ export const useManagerProgressDialogStore = defineStore(
'managerProgressDialog',
() => {
const isExpanded = ref(false)
const activeTabIndex = ref(0)
const setActiveTabIndex = (index: number) => {
activeTabIndex.value = index
}
const getActiveTabIndex = () => {
return activeTabIndex.value
}
const toggle = () => {
isExpanded.value = !isExpanded.value
@@ -250,7 +418,9 @@ export const useManagerProgressDialogStore = defineStore(
isExpanded,
toggle,
collapse,
expand
expand,
setActiveTabIndex,
getActiveTabIndex
}
}
)

View File

@@ -0,0 +1,70 @@
import { defineStore } from 'pinia'
import { computed, ref } from 'vue'
import type { ConflictDetectionResult } from '@/types/conflictDetectionTypes'
export const useConflictDetectionStore = defineStore(
'conflictDetection',
() => {
// State
const conflictedPackages = ref<ConflictDetectionResult[]>([])
const isDetecting = ref(false)
const lastDetectionTime = ref<string | null>(null)
// Getters
const hasConflicts = computed(() =>
conflictedPackages.value.some((pkg) => pkg.has_conflict)
)
const getConflictsForPackageByID = computed(
() => (packageId: string) =>
conflictedPackages.value.find((pkg) => pkg.package_id === packageId)
)
const bannedPackages = computed(() =>
conflictedPackages.value.filter((pkg) =>
pkg.conflicts.some((conflict) => conflict.type === 'banned')
)
)
const securityPendingPackages = computed(() =>
conflictedPackages.value.filter((pkg) =>
pkg.conflicts.some((conflict) => conflict.type === 'pending')
)
)
// Actions
function setConflictedPackages(packages: ConflictDetectionResult[]) {
conflictedPackages.value = [...packages]
}
function clearConflicts() {
conflictedPackages.value = []
}
function setDetecting(detecting: boolean) {
isDetecting.value = detecting
}
function setLastDetectionTime(time: string) {
lastDetectionTime.value = time
}
return {
// State
conflictedPackages,
isDetecting,
lastDetectionTime,
// Getters
hasConflicts,
getConflictsForPackageByID,
bannedPackages,
securityPendingPackages,
// Actions
setConflictedPackages,
clearConflicts,
setDetecting,
setLastDetectionTime
}
}
)

View File

@@ -43,6 +43,7 @@ interface DialogInstance {
component: Component
contentProps: Record<string, any>
footerComponent?: Component
footerProps?: Record<string, any>
dialogComponentProps: DialogComponentProps
priority: number
}
@@ -54,6 +55,7 @@ export interface ShowDialogOptions {
footerComponent?: Component
component: Component
props?: Record<string, any>
footerProps?: Record<string, any>
dialogComponentProps?: DialogComponentProps
/**
* Optional priority for dialog stacking.
@@ -127,6 +129,7 @@ export const useDialogStore = defineStore('dialog', () => {
footerComponent?: Component
component: Component
props?: Record<string, any>
footerProps?: Record<string, any>
dialogComponentProps?: DialogComponentProps
priority?: number
}) {
@@ -146,6 +149,7 @@ export const useDialogStore = defineStore('dialog', () => {
: undefined,
component: markRaw(options.component),
contentProps: { ...options.props },
footerProps: { ...options.footerProps },
priority: options.priority ?? 1,
dialogComponentProps: {
maximizable: false,

View File

@@ -31,7 +31,7 @@ import { createNodeLocatorId } from '@/types/nodeIdentification'
import { useCanvasStore } from './graphStore'
import { ComfyWorkflow, useWorkflowStore } from './workflowStore'
export interface QueuedPrompt {
interface QueuedPrompt {
/**
* The nodes that are queued to be executed. The key is the node id and the
* value is a boolean indicating if the node has been executed.

View File

@@ -6,9 +6,9 @@ import type { ComfyExtension } from '@/types/comfy'
/**
* These extensions are always active, even if they are disabled in the setting.
*/
export const ALWAYS_ENABLED_EXTENSIONS: readonly string[] = []
const ALWAYS_ENABLED_EXTENSIONS: readonly string[] = []
export const ALWAYS_DISABLED_EXTENSIONS: readonly string[] = [
const ALWAYS_DISABLED_EXTENSIONS: readonly string[] = [
// pysssss.Locking is replaced by pin/unpin in ComfyUI core.
// https://github.com/Comfy-Org/litegraph.js/pull/117
'pysssss.Locking',

View File

@@ -8,6 +8,7 @@ import {
type UserCredential,
browserLocalPersistence,
createUserWithEmailAndPassword,
deleteUser,
onAuthStateChanged,
sendPasswordResetEmail,
setPersistence,
@@ -40,7 +41,7 @@ type AccessBillingPortalResponse =
type AccessBillingPortalReqBody =
operations['AccessBillingPortal']['requestBody']
export class FirebaseAuthStoreError extends Error {
class FirebaseAuthStoreError extends Error {
constructor(message: string) {
super(message)
this.name = 'FirebaseAuthStoreError'
@@ -287,6 +288,14 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
await updatePassword(currentUser.value, newPassword)
}
/** Delete the current user account */
const _deleteAccount = async (): Promise<void> => {
if (!currentUser.value) {
throw new FirebaseAuthStoreError(t('toastMessages.userNotAuthenticated'))
}
await deleteUser(currentUser.value)
}
const addCredits = async (
requestBodyContent: CreditPurchasePayload
): Promise<CreditPurchaseResponse> => {
@@ -385,6 +394,7 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
accessBillingPortal,
sendPasswordReset,
updatePassword: _updatePassword,
deleteAccount: _deleteAccount,
getAuthHeader
}
})

View File

@@ -1,6 +1,10 @@
import { defineStore } from 'pinia'
import { LGraphNode, SubgraphNode } from '@/lib/litegraph/src/litegraph'
import {
LGraphNode,
Subgraph,
SubgraphNode
} from '@/lib/litegraph/src/litegraph'
import {
ExecutedWsMessage,
ResultItem,
@@ -136,17 +140,22 @@ export const useNodeOutputStore = defineStore('nodeOutput', () => {
) {
if (!filenames || !node) return
const locatorId =
node.graph instanceof Subgraph
? nodeIdToNodeLocatorId(node.id, node.graph ?? undefined)
: `${node.id}`
if (!locatorId) return
if (typeof filenames === 'string') {
setNodeOutputsByNodeId(
node.id,
setOutputsByLocatorId(
locatorId,
createOutputs([filenames], folder, isAnimated)
)
} else if (!Array.isArray(filenames)) {
setNodeOutputsByNodeId(node.id, filenames)
setOutputsByLocatorId(locatorId, filenames)
} else {
const resultItems = createOutputs(filenames, folder, isAnimated)
if (!resultItems?.images?.length) return
setNodeOutputsByNodeId(node.id, resultItems)
setOutputsByLocatorId(locatorId, resultItems)
}
}
@@ -170,26 +179,6 @@ export const useNodeOutputStore = defineStore('nodeOutput', () => {
setOutputsByLocatorId(nodeLocatorId, outputs, options)
}
/**
* Set node outputs by node ID.
* Uses the current graph context to create the appropriate NodeLocatorId.
*
* @param nodeId - The node ID
* @param outputs - The outputs to store
* @param options - Options for setting outputs
* @param options.merge - If true, merge with existing outputs (arrays are concatenated)
*/
function setNodeOutputsByNodeId(
nodeId: string | number,
outputs: ExecutedWsMessage['output'] | ResultItem,
options: SetOutputOptions = {}
) {
const nodeLocatorId = nodeIdToNodeLocatorId(nodeId)
if (!nodeLocatorId) return
setOutputsByLocatorId(nodeLocatorId, outputs, options)
}
/**
* Set node preview images by execution ID (hierarchical ID from backend).
* Converts the execution ID to a NodeLocatorId before storing.
@@ -288,7 +277,6 @@ export const useNodeOutputStore = defineStore('nodeOutput', () => {
getNodePreviews,
setNodeOutputs,
setNodeOutputsByExecutionId,
setNodeOutputsByNodeId,
setNodePreviewsByExecutionId,
setNodePreviewsByNodeId,
revokePreviewsByExecutionId,

View File

@@ -1,7 +1,10 @@
import { defineStore } from 'pinia'
import { computed, ref } from 'vue'
import type { ModelFile } from '@/schemas/assetSchema'
import { api } from '@/scripts/api'
import { assetService } from '@/services/assetService'
import { useSettingStore } from '@/stores/settingStore'
/** (Internal helper) finds a value in a metadata object from any of a list of keys. */
function _findInMetadata(metadata: any, ...keys: string[]): string | null {
@@ -153,7 +156,10 @@ export class ModelFolder {
models: Record<string, ComfyModelDef> = {}
state: ResourceState = ResourceState.Uninitialized
constructor(public directory: string) {}
constructor(
public directory: string,
private getModelsFunc: (folder: string) => Promise<ModelFile[]>
) {}
get key(): string {
return this.directory + '/'
@@ -167,7 +173,7 @@ export class ModelFolder {
return this
}
this.state = ResourceState.Loading
const models = await api.getModels(this.directory)
const models = await this.getModelsFunc(this.directory)
for (const model of models) {
this.models[`${model.pathIndex}/${model.name}`] = new ComfyModelDef(
model.name,
@@ -182,6 +188,7 @@ export class ModelFolder {
/** Model store handler, wraps individual per-folder model stores */
export const useModelStore = defineStore('models', () => {
const settingStore = useSettingStore()
const modelFolderNames = ref<string[]>([])
const modelFolderByName = ref<Record<string, ModelFolder>>({})
const modelFolders = computed<ModelFolder[]>(() =>
@@ -197,11 +204,22 @@ export const useModelStore = defineStore('models', () => {
* Loads the model folders from the server
*/
async function loadModelFolders() {
const resData = await api.getModelFolders()
const useAssetAPI: boolean = settingStore.get('Comfy.Assets.UseAssetAPI')
const resData = useAssetAPI
? await assetService.getAssetModelFolders()
: await api.getModelFolders()
modelFolderNames.value = resData.map((folder) => folder.name)
modelFolderByName.value = {}
for (const folderName of modelFolderNames.value) {
modelFolderByName.value[folderName] = new ModelFolder(folderName)
const getModelsFunc = useAssetAPI
? (folder: string) => assetService.getAssetModels(folder)
: (folder: string) => api.getModels(folder)
modelFolderByName.value[folderName] = new ModelFolder(
folderName,
getModelsFunc
)
}
}

View File

@@ -1,5 +1,5 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { computed, ref } from 'vue'
import { ComfyNodeDefImpl, useNodeDefStore } from '@/stores/nodeDefStore'
@@ -22,6 +22,22 @@ export const useModelToNodeStore = defineStore('modelToNode', () => {
const modelToNodeMap = ref<Record<string, ModelNodeProvider[]>>({})
const nodeDefStore = useNodeDefStore()
const haveDefaultsLoaded = ref(false)
/** Internal computed for reactive caching of registered node types */
const registeredNodeTypes = computed(() => {
return new Set(
Object.values(modelToNodeMap.value)
.flat()
.map((provider) => provider.nodeDef.name)
)
})
/** Get set of all registered node types for efficient lookup */
function getRegisteredNodeTypes(): Set<string> {
registerDefaults()
return registeredNodeTypes.value
}
/**
* Get the node provider for the given model type name.
* @param modelType The name of the model type to get the node provider for.
@@ -83,10 +99,15 @@ export const useModelToNodeStore = defineStore('modelToNode', () => {
quickRegister('loras', 'LoraLoaderModelOnly', 'lora_name')
quickRegister('vae', 'VAELoader', 'vae_name')
quickRegister('controlnet', 'ControlNetLoader', 'control_net_name')
quickRegister('unet', 'UNETLoader', 'unet_name')
quickRegister('upscale_models', 'UpscaleModelLoader', 'model_name')
quickRegister('style_models', 'StyleModelLoader', 'style_model')
quickRegister('gligen', 'GLIGENLoader', 'gligen_name')
}
return {
modelToNodeMap,
getRegisteredNodeTypes,
getNodeProvider,
getAllNodeProviders,
registerNodeProvider,

View File

@@ -10,7 +10,7 @@ import { ComfyNodeDefImpl, createDummyFolderNodeDef } from './nodeDefStore'
import { buildNodeDefTree } from './nodeDefStore'
import { useSettingStore } from './settingStore'
export const BOOKMARK_SETTING_ID = 'Comfy.NodeLibrary.Bookmarks.V2'
const BOOKMARK_SETTING_ID = 'Comfy.NodeLibrary.Bookmarks.V2'
export const useNodeBookmarkStore = defineStore('nodeBookmark', () => {
const settingStore = useSettingStore()

View File

@@ -16,6 +16,7 @@ import type {
ComfyOutputTypesSpec as ComfyOutputSpecV1
} from '@/schemas/nodeDefSchema'
import { NodeSearchService } from '@/services/nodeSearchService'
import { useSubgraphStore } from '@/stores/subgraphStore'
import {
type NodeSource,
NodeSourceType,
@@ -223,7 +224,7 @@ export const SYSTEM_NODE_DEFS: Record<string, ComfyNodeDefV1> = {
}
}
export interface BuildNodeDefTreeOptions {
interface BuildNodeDefTreeOptions {
/**
* Custom function to extract the tree path from a node definition.
* If not provided, uses the default path based on nodeDef.nodePath.
@@ -291,8 +292,14 @@ export const useNodeDefStore = defineStore('nodeDef', () => {
const showDeprecated = ref(false)
const showExperimental = ref(false)
const nodeDefFilters = ref<NodeDefFilter[]>([])
const subgraphStore = useSubgraphStore()
const nodeDefs = computed(() => Object.values(nodeDefsByName.value))
const nodeDefs = computed(() => {
return [
...Object.values(nodeDefsByName.value),
...subgraphStore.subgraphBlueprints
]
})
const nodeDataTypes = computed(() => {
const types = new Set<string>()
for (const nodeDef of nodeDefs.value) {
@@ -383,7 +390,7 @@ export const useNodeDefStore = defineStore('nodeDef', () => {
})
// Subgraph nodes filter
// @todo Remove this filter when subgraph v2 is released
// Filter out litegraph typed subgraphs, saved blueprints are added in separately
registerNodeDefFilter({
id: 'core.subgraph',
name: 'Hide Subgraph Nodes',

View File

@@ -18,7 +18,7 @@ import { useExtensionService } from '@/services/extensionService'
import { useNodeOutputStore } from '@/stores/imagePreviewStore'
// Task type used in the API.
export type APITaskType = 'queue' | 'history'
type APITaskType = 'queue' | 'history'
export enum TaskItemDisplayStatus {
Running = 'Running',

View File

@@ -1,3 +1,4 @@
import { until } from '@vueuse/core'
import { defineStore } from 'pinia'
import { computed, ref } from 'vue'
@@ -240,7 +241,7 @@ export const useReleaseStore = defineStore('release', () => {
try {
// Ensure system stats are loaded
if (!systemStatsStore.systemStats) {
await systemStatsStore.fetchSystemStats()
await until(systemStatsStore.isInitialized)
}
const fetchedReleases = await releaseService.getReleases({

View File

@@ -3,7 +3,7 @@ import { computed, ref } from 'vue'
import { ServerConfig, ServerConfigValue } from '@/constants/serverConfig'
export type ServerConfigWithValue<T> = ServerConfig<T> & {
type ServerConfigWithValue<T> = ServerConfig<T> & {
/**
* Current value.
*/

View File

@@ -188,6 +188,48 @@ export const useSettingStore = defineStore('setting', () => {
)
}
settingValues.value = await api.getSettings()
// Migrate old zoom threshold setting to new font size setting
await migrateZoomThresholdToFontSize()
}
/**
* Migrate the old zoom threshold setting to the new font size setting.
* Preserves the exact zoom threshold behavior by converting it to equivalent font size.
*/
async function migrateZoomThresholdToFontSize() {
const oldKey = 'LiteGraph.Canvas.LowQualityRenderingZoomThreshold'
const newKey = 'LiteGraph.Canvas.MinFontSizeForLOD'
// Only migrate if old setting exists and new setting doesn't
if (
settingValues.value[oldKey] !== undefined &&
settingValues.value[newKey] === undefined
) {
const oldValue = settingValues.value[oldKey] as number
// Convert zoom threshold to equivalent font size to preserve exact behavior
// The threshold formula is: threshold = font_size / (14 * sqrt(DPR))
// For DPR=1: threshold = font_size / 14
// Therefore: font_size = threshold * 14
//
// Examples:
// - Old 0.6 threshold → 0.6 * 14 = 8.4px → rounds to 8px (preserves ~60% zoom threshold)
// - Old 0.5 threshold → 0.5 * 14 = 7px (preserves 50% zoom threshold)
// - Old 1.0 threshold → 1.0 * 14 = 14px (preserves 100% zoom threshold)
const mappedFontSize = Math.round(oldValue * 14)
const clampedFontSize = Math.max(1, Math.min(24, mappedFontSize))
// Set the new value
settingValues.value[newKey] = clampedFontSize
// Remove the old setting to prevent confusion
delete settingValues.value[oldKey]
// Store the migrated setting
await api.storeSetting(newKey, clampedFontSize)
await api.storeSetting(oldKey, undefined)
}
}
return {

321
src/stores/subgraphStore.ts Normal file
View File

@@ -0,0 +1,321 @@
import { defineStore } from 'pinia'
import { computed, ref } from 'vue'
import { t } from '@/i18n'
import { SubgraphNode } from '@/lib/litegraph/src/litegraph'
import type { NodeError } from '@/schemas/apiSchema'
import type {
ComfyNode,
ComfyWorkflowJSON,
NodeId
} from '@/schemas/comfyWorkflowSchema'
import type {
ComfyNodeDef as ComfyNodeDefV1,
InputSpec
} from '@/schemas/nodeDefSchema'
import { api } from '@/scripts/api'
import { useDialogService } from '@/services/dialogService'
import { useWorkflowService } from '@/services/workflowService'
import { useExecutionStore } from '@/stores/executionStore'
import { useCanvasStore } from '@/stores/graphStore'
import { ComfyNodeDefImpl } from '@/stores/nodeDefStore'
import { useSettingStore } from '@/stores/settingStore'
import { useToastStore } from '@/stores/toastStore'
import { UserFile } from '@/stores/userFileStore'
import {
ComfyWorkflow,
LoadedComfyWorkflow,
useWorkflowStore
} from '@/stores/workflowStore'
async function confirmOverwrite(name: string): Promise<boolean | null> {
return await useDialogService().confirm({
title: t('subgraphStore.overwriteBlueprintTitle'),
type: 'overwriteBlueprint',
message: t('subgraphStore.overwriteBlueprint'),
itemList: [name]
})
}
export const useSubgraphStore = defineStore('subgraph', () => {
class SubgraphBlueprint extends ComfyWorkflow {
static override readonly basePath = 'subgraphs/'
override readonly tintCanvasBg = '#22227740'
hasPromptedSave: boolean = false
constructor(
options: { path: string; modified: number; size: number },
confirmFirstSave: boolean = false
) {
super(options)
this.hasPromptedSave = !confirmFirstSave
}
validateSubgraph() {
if (!this.activeState?.definitions)
throw new Error(
'The root graph of a subgraph blueprint must consist of only a single subgraph node'
)
const { subgraphs } = this.activeState.definitions
const { nodes } = this.activeState
//Instanceof doesn't function as nodes are serialized
function isSubgraphNode(node: ComfyNode) {
return node && subgraphs.some((s) => s.id === node.type)
}
if (nodes.length == 1 && isSubgraphNode(nodes[0])) return
const errors: Record<NodeId, NodeError> = {}
//mark errors for all but first subgraph node
let firstSubgraphFound = false
for (let i = 0; i < nodes.length; i++) {
if (!firstSubgraphFound && isSubgraphNode(nodes[i])) {
firstSubgraphFound = true
continue
}
errors[nodes[i].id] = {
errors: [],
class_type: nodes[i].type,
dependent_outputs: []
}
}
useExecutionStore().lastNodeErrors = errors
useCanvasStore().getCanvas().draw(true, true)
throw new Error(
'The root graph of a subgraph blueprint must consist of only a single subgraph node'
)
}
override async save(): Promise<UserFile> {
this.validateSubgraph()
if (
!this.hasPromptedSave &&
useSettingStore().get('Comfy.Workflow.WarnBlueprintOverwrite')
) {
if (!(await confirmOverwrite(this.filename))) return this
this.hasPromptedSave = true
}
const ret = await super.save()
useSubgraphStore().updateDef(await this.load())
return ret
}
override async saveAs(path: string) {
this.validateSubgraph()
this.hasPromptedSave = true
const ret = await super.saveAs(path)
useSubgraphStore().updateDef(await this.load())
return ret
}
override async load({
force = false
}: { force?: boolean } = {}): Promise<LoadedComfyWorkflow> {
if (!force && this.isLoaded) return await super.load({ force })
const loaded = await super.load({ force })
const st = loaded.activeState
const sg = (st.definitions?.subgraphs ?? []).find(
(sg) => sg.id == st.nodes[0].type
)
if (!sg)
throw new Error(
'Loaded subgraph blueprint does not contain valid subgraph'
)
sg.name = st.nodes[0].title = this.filename
return loaded
}
override async promptSave(): Promise<string | null> {
return await useDialogService().prompt({
title: t('subgraphStore.saveBlueprint'),
message: t('subgraphStore.blueprintName') + ':',
defaultValue: this.filename
})
}
override unload(): void {
//Skip unloading. Even if a workflow is closed after editing,
//it must remain loaded in order to be added to the graph
}
}
const subgraphCache: Record<string, LoadedComfyWorkflow> = {}
const typePrefix = 'SubgraphBlueprint.'
const subgraphDefCache = ref<Map<string, ComfyNodeDefImpl>>(new Map())
const canvasStore = useCanvasStore()
const subgraphBlueprints = computed(() => [
...subgraphDefCache.value.values()
])
async function fetchSubgraphs() {
async function loadBlueprint(options: {
path: string
modified: number
size: number
}): Promise<void> {
const name = options.path.slice(0, -'.json'.length)
options.path = SubgraphBlueprint.basePath + options.path
const bp = await new SubgraphBlueprint(options, true).load()
useWorkflowStore().attachWorkflow(bp)
const nodeDef = convertToNodeDef(bp)
subgraphDefCache.value.set(name, nodeDef)
subgraphCache[name] = bp
}
const res = (
await api.listUserDataFullInfo(SubgraphBlueprint.basePath)
).filter((f) => f.path.endsWith('.json'))
const settled = await Promise.allSettled(res.map(loadBlueprint))
const errors = settled.filter((i) => 'reason' in i).map((i) => i.reason)
errors.forEach((e) => console.error('Failed to load subgraph blueprint', e))
if (errors.length > 0) {
useToastStore().add({
severity: 'error',
summary: t('subgraphStore.loadFailure'),
detail: errors.length > 3 ? `x${errors.length}` : `${errors}`,
life: 6000
})
}
}
function convertToNodeDef(workflow: LoadedComfyWorkflow): ComfyNodeDefImpl {
const name = workflow.filename
const subgraphNode = workflow.changeTracker.initialState.nodes[0]
if (!subgraphNode) throw new Error('Invalid Subgraph Blueprint')
subgraphNode.inputs ??= []
subgraphNode.outputs ??= []
//NOTE: Types are cast to string. This is only used for input coloring on previews
const inputs = Object.fromEntries(
subgraphNode.inputs.map((i) => [
i.name,
[`${i.type}`, undefined] satisfies InputSpec
])
)
let description = 'User generated subgraph blueprint'
if (workflow.initialState.extra?.BlueprintDescription)
description = `${workflow.initialState.extra.BlueprintDescription}`
const nodedefv1: ComfyNodeDefV1 = {
input: { required: inputs },
output: subgraphNode.outputs.map((o) => `${o.type}`),
output_name: subgraphNode.outputs.map((o) => o.name),
name: typePrefix + name,
display_name: name,
description,
category: 'Subgraph Blueprints',
output_node: false,
python_module: 'blueprint'
}
const nodeDefImpl = new ComfyNodeDefImpl(nodedefv1)
return nodeDefImpl
}
async function publishSubgraph() {
const canvas = canvasStore.getCanvas()
const subgraphNode = [...canvas.selectedItems][0]
if (
canvas.selectedItems.size !== 1 ||
!(subgraphNode instanceof SubgraphNode)
)
throw new TypeError('Must have single SubgraphNode selected to publish')
const { nodes = [], subgraphs = [] } = canvas._serializeItems([
subgraphNode
])
if (nodes.length != 1) {
throw new TypeError('Must have single SubgraphNode selected to publish')
}
//create minimal workflow
const workflowData = {
revision: 0,
last_node_id: subgraphNode.id,
last_link_id: 0,
nodes,
links: [],
version: 0.4,
definitions: { subgraphs }
}
//prompt name
const name = await useDialogService().prompt({
title: t('subgraphStore.saveBlueprint'),
message: t('subgraphStore.blueprintName') + ':',
defaultValue: subgraphNode.title
})
if (!name) return
if (subgraphDefCache.value.has(name) && !(await confirmOverwrite(name)))
//User has chosen not to overwrite.
return
//upload file
const path = SubgraphBlueprint.basePath + name + '.json'
const workflow = new SubgraphBlueprint({
path,
size: -1,
modified: Date.now()
})
workflow.originalContent = JSON.stringify(workflowData)
const loadedWorkflow = await workflow.load()
//Mark non-temporary
workflow.size = 1
await workflow.save()
//add to files list?
useWorkflowStore().attachWorkflow(loadedWorkflow)
subgraphDefCache.value.set(name, convertToNodeDef(loadedWorkflow))
subgraphCache[name] = loadedWorkflow
useToastStore().add({
severity: 'success',
summary: t('subgraphStore.publishSuccess'),
detail: t('subgraphStore.publishSuccessMessage'),
life: 4000
})
}
function updateDef(blueprint: LoadedComfyWorkflow) {
subgraphDefCache.value.set(blueprint.filename, convertToNodeDef(blueprint))
}
async function editBlueprint(nodeType: string) {
const name = nodeType.slice(typePrefix.length)
if (!(name in subgraphCache))
//As loading is blocked on in startup, this can likely be changed to invalid type
throw new Error('not yet loaded')
useWorkflowStore().attachWorkflow(subgraphCache[name])
await useWorkflowService().openWorkflow(subgraphCache[name])
const canvas = useCanvasStore().getCanvas()
if (canvas.graph && 'subgraph' in canvas.graph.nodes[0])
canvas.setGraph(canvas.graph.nodes[0].subgraph)
}
function getBlueprint(nodeType: string): ComfyWorkflowJSON {
const name = nodeType.slice(typePrefix.length)
if (!(name in subgraphCache))
//As loading is blocked on in startup, this can likely be changed to invalid type
throw new Error('not yet loaded')
return subgraphCache[name].changeTracker.initialState
}
async function deleteBlueprint(nodeType: string) {
const name = nodeType.slice(typePrefix.length)
if (!(name in subgraphCache))
//As loading is blocked on in startup, this can likely be changed to invalid type
throw new Error('not yet loaded')
if (
!(await useDialogService().confirm({
title: t('subgraphStore.confirmDeleteTitle'),
type: 'delete',
message: t('subgraphStore.confirmDelete'),
itemList: [name]
}))
)
return
await subgraphCache[name].delete()
delete subgraphCache[name]
subgraphDefCache.value.delete(name)
}
function isSubgraphBlueprint(
workflow: unknown
): workflow is SubgraphBlueprint {
return workflow instanceof SubgraphBlueprint
}
return {
deleteBlueprint,
editBlueprint,
fetchSubgraphs,
getBlueprint,
isSubgraphBlueprint,
publishSubgraph,
subgraphBlueprints,
typePrefix,
updateDef
}
})

View File

@@ -1,32 +1,34 @@
import { useAsyncState } from '@vueuse/core'
import { defineStore } from 'pinia'
import { ref } from 'vue'
import type { SystemStats } from '@/schemas/apiSchema'
import { api } from '@/scripts/api'
import { isElectron } from '@/utils/envUtil'
export const useSystemStatsStore = defineStore('systemStats', () => {
const systemStats = ref<SystemStats | null>(null)
const isLoading = ref(false)
const error = ref<string | null>(null)
async function fetchSystemStats() {
isLoading.value = true
error.value = null
const fetchSystemStatsData = async () => {
try {
systemStats.value = await api.getSystemStats()
return await api.getSystemStats()
} catch (err) {
error.value =
err instanceof Error
? err.message
: 'An error occurred while fetching system stats'
console.error('Error fetching system stats:', err)
} finally {
isLoading.value = false
throw err
}
}
const {
state: systemStats,
isLoading,
error,
isReady: isInitialized,
execute: refetchSystemStats
} = useAsyncState<SystemStats | null>(
fetchSystemStatsData,
null, // initial value
{
immediate: true
}
)
function getFormFactor(): string {
if (!systemStats.value?.system?.os) {
return 'other'
@@ -62,7 +64,8 @@ export const useSystemStatsStore = defineStore('systemStats', () => {
systemStats,
isLoading,
error,
fetchSystemStats,
isInitialized,
refetchSystemStats,
getFormFactor
}
})

View File

@@ -182,7 +182,7 @@ export class UserFile {
}
}
export interface LoadedUserFile extends UserFile {
interface LoadedUserFile extends UserFile {
isLoaded: true
originalContent: string
content: string

View File

@@ -1,4 +1,4 @@
import { useStorage } from '@vueuse/core'
import { until, useStorage } from '@vueuse/core'
import { defineStore } from 'pinia'
import * as semver from 'semver'
import { computed } from 'vue'
@@ -103,7 +103,7 @@ export const useVersionCompatibilityStore = defineStore(
async function checkVersionCompatibility() {
if (!systemStatsStore.systemStats) {
await systemStatsStore.fetchSystemStats()
await until(systemStatsStore.isInitialized)
}
}

View File

@@ -2,6 +2,7 @@ import _ from 'es-toolkit/compat'
import { defineStore } from 'pinia'
import { type Raw, computed, markRaw, ref, shallowRef, watch } from 'vue'
import { t } from '@/i18n'
import type { LGraph, Subgraph } from '@/lib/litegraph/src/litegraph'
import { useWorkflowThumbnail } from '@/renderer/thumbnail/composables/useWorkflowThumbnail'
import { ComfyWorkflowJSON } from '@/schemas/comfyWorkflowSchema'
@@ -10,6 +11,7 @@ import { api } from '@/scripts/api'
import { app as comfyApp } from '@/scripts/app'
import { ChangeTracker } from '@/scripts/changeTracker'
import { defaultGraphJSON } from '@/scripts/defaultGraph'
import { useDialogService } from '@/services/dialogService'
import type { NodeExecutionId, NodeLocatorId } from '@/types/nodeIdentification'
import {
createNodeExecutionId,
@@ -24,7 +26,8 @@ import { isSubgraph } from '@/utils/typeGuardUtil'
import { UserFile } from './userFileStore'
export class ComfyWorkflow extends UserFile {
static readonly basePath = 'workflows/'
static readonly basePath: string = 'workflows/'
readonly tintCanvasBg?: string
/**
* The change tracker for the workflow. Non-reactive raw object.
@@ -120,6 +123,14 @@ export class ComfyWorkflow extends UserFile {
this.content = JSON.stringify(this.activeState)
return await super.saveAs(path)
}
async promptSave(): Promise<string | null> {
return await useDialogService().prompt({
title: t('workflowService.saveWorkflow'),
message: t('workflowService.enterFilename') + ':',
defaultValue: this.filename
})
}
}
export interface LoadedComfyWorkflow extends ComfyWorkflow {
@@ -137,8 +148,9 @@ export interface LoadedComfyWorkflow extends ComfyWorkflow {
* error TS7056: The inferred type of this node exceeds the maximum length the
* compiler will serialize. An explicit type annotation is needed.
*/
export interface WorkflowStore {
interface WorkflowStore {
activeWorkflow: LoadedComfyWorkflow | null
attachWorkflow: (workflow: ComfyWorkflow, openIndex?: number) => void
isActive: (workflow: ComfyWorkflow) => boolean
openWorkflows: ComfyWorkflow[]
openedWorkflowIndexShift: (shift: number) => ComfyWorkflow | null
@@ -290,6 +302,7 @@ export const useWorkflowStore = defineStore('workflow', () => {
}
const loadedWorkflow = await workflow.load()
activeWorkflow.value = loadedWorkflow
comfyApp.canvas.bg_tint = loadedWorkflow.tintCanvasBg
console.debug('[workflowStore] open workflow', workflow.path)
return loadedWorkflow
}
@@ -304,11 +317,37 @@ export const useWorkflowStore = defineStore('workflow', () => {
}
return newPath
}
const saveAs = (
existingWorkflow: ComfyWorkflow,
path: string
): ComfyWorkflow => {
const workflow: ComfyWorkflow = new (existingWorkflow.constructor as any)({
path,
modified: Date.now(),
size: -1
})
workflow.originalContent = workflow.content = existingWorkflow.content
workflowLookup.value[workflow.path] = workflow
return workflow
}
const createTemporary = (path?: string, workflowData?: ComfyWorkflowJSON) => {
const fullPath = getUnconflictedPath(
ComfyWorkflow.basePath + (path ?? 'Unsaved Workflow.json')
)
const existingWorkflow = workflows.value.find((w) => w.fullFilename == path)
if (
path &&
workflowData &&
existingWorkflow?.changeTracker &&
!existingWorkflow.directory.startsWith(
ComfyWorkflow.basePath.slice(0, -1)
)
) {
existingWorkflow.changeTracker.reset(workflowData)
return existingWorkflow
}
const workflow = new ComfyWorkflow({
path: fullPath,
modified: Date.now(),
@@ -357,7 +396,10 @@ export const useWorkflowStore = defineStore('workflow', () => {
}
const persistedWorkflows = computed(() =>
Array.from(workflows.value).filter((workflow) => workflow.isPersisted)
Array.from(workflows.value).filter(
(workflow) =>
workflow.isPersisted && !workflow.path.startsWith('subgraphs/')
)
)
const syncWorkflows = async (dir: string = '') => {
await syncEntities(
@@ -647,6 +689,7 @@ export const useWorkflowStore = defineStore('workflow', () => {
return {
activeWorkflow,
attachWorkflow,
isActive,
openWorkflows,
openedWorkflowIndexShift,
@@ -658,6 +701,7 @@ export const useWorkflowStore = defineStore('workflow', () => {
createTemporary,
renameWorkflow,
deleteWorkflow,
saveAs,
saveWorkflow,
reorderWorkflows,