diff --git a/src/components/dialog/content/SettingDialogContent.vue b/src/components/dialog/content/SettingDialogContent.vue
index 730dd2c8b..750f553f4 100644
--- a/src/components/dialog/content/SettingDialogContent.vue
+++ b/src/components/dialog/content/SettingDialogContent.vue
@@ -57,6 +57,9 @@
+
+
+
@@ -78,6 +81,7 @@ import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
import { flattenTree } from '@/utils/treeUtil'
import AboutPanel from './setting/AboutPanel.vue'
import KeybindingPanel from './setting/KeybindingPanel.vue'
+import ExtensionPanel from './setting/ExtensionPanel.vue'
interface ISettingGroup {
label: string
@@ -96,11 +100,24 @@ const keybindingPanelNode: SettingTreeNode = {
children: []
}
+const extensionPanelNode: SettingTreeNode = {
+ key: 'extension',
+ label: 'Extension',
+ children: []
+}
+
+const extensionPanelNodeList = computed(() => {
+ const settingStore = useSettingStore()
+ const showExtensionPanel = settingStore.get('Comfy.Settings.ExtensionPanel')
+ return showExtensionPanel ? [extensionPanelNode] : []
+})
+
const settingStore = useSettingStore()
const settingRoot = computed(() => settingStore.settingTree)
const categories = computed(() => [
...(settingRoot.value.children || []),
keybindingPanelNode,
+ ...extensionPanelNodeList.value,
aboutPanelNode
])
const activeCategory = ref(null)
@@ -226,4 +243,15 @@ const tabValue = computed(() =>
width: 100%;
}
}
+
+/* Show a separator line above the Keybinding tab */
+/* This indicates the start of custom setting panels */
+.settings-sidebar :deep(.p-listbox-option[aria-label='Keybinding']) {
+ position: relative;
+}
+
+.settings-sidebar :deep(.p-listbox-option[aria-label='Keybinding'])::before {
+ @apply content-[''] top-0 left-0 absolute w-full;
+ border-top: 1px solid var(--p-divider-border-color);
+}
diff --git a/src/components/dialog/content/setting/ExtensionPanel.vue b/src/components/dialog/content/setting/ExtensionPanel.vue
new file mode 100644
index 000000000..fc8be122a
--- /dev/null
+++ b/src/components/dialog/content/setting/ExtensionPanel.vue
@@ -0,0 +1,93 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+ {{ extensionStore.isExtensionEnabled(ext.name) ? '[-]' : '[+]' }}
+
+ {{ ext.name }}
+
+
+
+
+
+
+
+
+
diff --git a/src/extensions/core/nodeBadge.ts b/src/extensions/core/nodeBadge.ts
index 109bd75ea..93a95e9a9 100644
--- a/src/extensions/core/nodeBadge.ts
+++ b/src/extensions/core/nodeBadge.ts
@@ -9,7 +9,6 @@ import _ from 'lodash'
import { getColorPalette, defaultColorPalette } from './colorPalette'
import { BadgePosition } from '@comfyorg/litegraph'
import type { Palette } from '@/types/colorPalette'
-import type { ComfyNodeDef } from '@/types/apiTypes'
import { useNodeDefStore } from '@/stores/nodeDefStore'
function getNodeSource(node: LGraphNode): NodeSource | null {
diff --git a/src/i18n.ts b/src/i18n.ts
index 26fffee17..ab9b37d54 100644
--- a/src/i18n.ts
+++ b/src/i18n.ts
@@ -2,6 +2,8 @@ import { createI18n } from 'vue-i18n'
const messages = {
en: {
+ extensionName: 'Extension Name',
+ reloadToApplyChanges: 'Reload to apply changes',
insert: 'Insert',
systemInfo: 'System Info',
devices: 'Devices',
@@ -108,6 +110,8 @@ const messages = {
}
},
zh: {
+ extensionName: '扩展名称',
+ reloadToApplyChanges: '重新加载以应用更改',
insert: '插入',
systemInfo: '系统信息',
devices: '设备',
diff --git a/src/scripts/app.ts b/src/scripts/app.ts
index 56f2d6f4f..24b8623ba 100644
--- a/src/scripts/app.ts
+++ b/src/scripts/app.ts
@@ -52,8 +52,7 @@ import type { ToastMessageOptions } from 'primevue/toast'
import { useWorkspaceStore } from '@/stores/workspaceStateStore'
import { useExecutionStore } from '@/stores/executionStore'
import { IWidget } from '@comfyorg/litegraph'
-import { useKeybindingStore } from '@/stores/keybindingStore'
-import { useCommandStore } from '@/stores/commandStore'
+import { useExtensionStore } from '@/stores/extensionStore'
export const ANIM_PREVIEW_WIDGET = '$$comfy_animation_preview'
@@ -357,6 +356,13 @@ export class ComfyApp {
}
}
+ get enabledExtensions() {
+ if (!this.vueAppReady) {
+ return this.extensions
+ }
+ return useExtensionStore().enabledExtensions
+ }
+
/**
* Invoke an extension callback
* @param {keyof ComfyExtension} method The extension callback to execute
@@ -365,7 +371,7 @@ export class ComfyApp {
*/
#invokeExtensions(method, ...args) {
let results = []
- for (const ext of this.extensions) {
+ for (const ext of this.enabledExtensions) {
if (method in ext) {
try {
results.push(ext[method](...args, this))
@@ -391,7 +397,7 @@ export class ComfyApp {
*/
async #invokeExtensionsAsync(method, ...args) {
return await Promise.all(
- this.extensions.map(async (ext) => {
+ this.enabledExtensions.map(async (ext) => {
if (method in ext) {
try {
return await ext[method](...args, this)
@@ -1773,6 +1779,8 @@ export class ComfyApp {
* Loads all extensions from the API into the window in parallel
*/
async #loadExtensions() {
+ useExtensionStore().loadDisabledExtensionNames()
+
const extensions = await api.getExtensions()
this.logging.addEntry('Comfy.App', 'debug', { Extensions: extensions })
@@ -2943,22 +2951,12 @@ export class ComfyApp {
* @param {ComfyExtension} extension
*/
registerExtension(extension: ComfyExtension) {
- if (!extension.name) {
- throw new Error("Extensions must have a 'name' property.")
- }
- // https://github.com/Comfy-Org/litegraph.js/pull/117
- if (extension.name === 'pysssss.Locking') {
- console.log('pysssss.Locking is replaced by pin/unpin in ComfyUI core.')
- return
- }
- if (this.extensions.find((ext) => ext.name === extension.name)) {
- throw new Error(`Extension named '${extension.name}' already registered.`)
- }
if (this.vueAppReady) {
- useKeybindingStore().loadExtensionKeybindings(extension)
- useCommandStore().loadExtensionCommands(extension)
+ useExtensionStore().registerExtension(extension)
+ } else {
+ // For jest testing.
+ this.extensions.push(extension)
}
- this.extensions.push(extension)
}
/**
diff --git a/src/stores/coreSettings.ts b/src/stores/coreSettings.ts
index 84d044f98..72ad7f558 100644
--- a/src/stores/coreSettings.ts
+++ b/src/stores/coreSettings.ts
@@ -420,5 +420,20 @@ export const CORE_SETTINGS: SettingParams[] = [
type: 'hidden',
defaultValue: [] as Keybinding[],
versionAdded: '1.3.7'
+ },
+ {
+ id: 'Comfy.Extension.Disabled',
+ name: 'Disabled extension names',
+ type: 'hidden',
+ defaultValue: [] as string[],
+ versionAdded: '1.3.11'
+ },
+ {
+ id: 'Comfy.Settings.ExtensionPanel',
+ name: 'Show extension panel in settings dialog',
+ type: 'boolean',
+ defaultValue: false,
+ experimental: true,
+ versionAdded: '1.3.11'
}
]
diff --git a/src/stores/extensionStore.ts b/src/stores/extensionStore.ts
new file mode 100644
index 000000000..675f41cdb
--- /dev/null
+++ b/src/stores/extensionStore.ts
@@ -0,0 +1,81 @@
+import { ref, computed, markRaw } from 'vue'
+import { defineStore } from 'pinia'
+import type { ComfyExtension } from '@/types/comfy'
+import { useKeybindingStore } from './keybindingStore'
+import { useCommandStore } from './commandStore'
+import { useSettingStore } from './settingStore'
+import { app } from '@/scripts/app'
+
+export const useExtensionStore = defineStore('extension', () => {
+ // For legacy reasons, the name uniquely identifies an extension
+ const extensionByName = ref>({})
+ const extensions = computed(() => Object.values(extensionByName.value))
+ // Not using computed because disable extension requires reloading of the page.
+ // Dynamically update this list won't affect extensions that are already loaded.
+ const disabledExtensionNames = ref>(new Set())
+
+ // Disabled extension names that are currently not in the extension list.
+ // If a node pack is disabled in the backend, we shouldn't remove the configuration
+ // of the frontend extension disable list, in case the node pack is re-enabled.
+ const inactiveDisabledExtensionNames = computed(() => {
+ return Array.from(disabledExtensionNames.value).filter(
+ (name) => !(name in extensionByName.value)
+ )
+ })
+
+ const isExtensionEnabled = (name: string) =>
+ !disabledExtensionNames.value.has(name)
+ const enabledExtensions = computed(() => {
+ return extensions.value.filter((ext) => isExtensionEnabled(ext.name))
+ })
+
+ function registerExtension(extension: ComfyExtension) {
+ if (!extension.name) {
+ throw new Error("Extensions must have a 'name' property.")
+ }
+
+ if (extensionByName.value[extension.name]) {
+ throw new Error(`Extension named '${extension.name}' already registered.`)
+ }
+
+ if (disabledExtensionNames.value.has(extension.name)) {
+ console.log(`Extension ${extension.name} is disabled.`)
+ }
+
+ extensionByName.value[extension.name] = markRaw(extension)
+ useKeybindingStore().loadExtensionKeybindings(extension)
+ useCommandStore().loadExtensionCommands(extension)
+
+ /*
+ * Extensions are currently stored in both extensionStore and app.extensions.
+ * Legacy jest tests still depend on app.extensions being populated.
+ */
+ app.extensions.push(extension)
+ }
+
+ function loadDisabledExtensionNames() {
+ disabledExtensionNames.value = new Set(
+ useSettingStore().get('Comfy.Extension.Disabled')
+ )
+ // pysssss.Locking is replaced by pin/unpin in ComfyUI core.
+ // https://github.com/Comfy-Org/litegraph.js/pull/117
+ disabledExtensionNames.value.add('pysssss.Locking')
+ }
+
+ // Some core extensions are registered before the store is initialized, e.g.
+ // colorPalette.
+ // Register them manually here so the state of app.extensions and
+ // extensionByName are in sync.
+ for (const ext of app.extensions) {
+ extensionByName.value[ext.name] = markRaw(ext)
+ }
+
+ return {
+ extensions,
+ enabledExtensions,
+ inactiveDisabledExtensionNames,
+ isExtensionEnabled,
+ registerExtension,
+ loadDisabledExtensionNames
+ }
+})
diff --git a/src/types/apiTypes.ts b/src/types/apiTypes.ts
index abe72831a..db8073a84 100644
--- a/src/types/apiTypes.ts
+++ b/src/types/apiTypes.ts
@@ -507,7 +507,9 @@ const zSettings = z.record(z.any()).and(
'Comfy.NodeBadge.NodeLifeCycleBadgeMode': zNodeBadgeMode,
'Comfy.QueueButton.BatchCountLimit': z.number(),
'Comfy.Keybinding.UnsetBindings': z.array(zKeybinding),
- 'Comfy.Keybinding.NewBindings': z.array(zKeybinding)
+ 'Comfy.Keybinding.NewBindings': z.array(zKeybinding),
+ 'Comfy.Extension.Disabled': z.array(z.string()),
+ 'Comfy.Settings.ExtensionPanel': z.boolean()
})
.optional()
)
diff --git a/tests-ui/globalSetup.ts b/tests-ui/globalSetup.ts
index d8ff2d2ea..4217738f7 100644
--- a/tests-ui/globalSetup.ts
+++ b/tests-ui/globalSetup.ts
@@ -36,6 +36,15 @@ module.exports = async function () {
}
})
+ jest.mock('@/stores/extensionStore', () => {
+ return {
+ useExtensionStore: () => ({
+ registerExtension: jest.fn(),
+ loadDisabledExtensionNames: jest.fn()
+ })
+ }
+ })
+
jest.mock('vue-i18n', () => {
return {
useI18n: jest.fn()