Files
ComfyUI_frontend/src/composables/useCoreCommands.ts
AustinMroz 431594a6fc Fix partial execution inside subgraphs (#6487)
`getExecutionIdsForSelectedNodes` is only used for partial execution.
The prior implementation solved the wrong problem. Given a list of
nodes, it would explore into subgraphs and return a list of partial
ExecutionIds for all contained nodes. Because this does not resolve the
partial execution path to the current subgraph, this is incorrect when
the current graph is not the root graph. Woefully, this incorrect
functionality is never useful because the recursive exploration only
applies to subgraph nodes which never satisfy the outputNode filter
applied by the parent function.

An extra function is used to correctly append the parent execution path,
but the existing, probably never useful code for recursively collecting
children is otherwise left in place.

Resolves #6480

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6487-Fix-partial-execution-inside-subgraphs-29d6d73d36508197924bfb3a0fb6699e)
by [Unito](https://www.unito.io)
2025-11-01 16:08:57 -07:00

1207 lines
37 KiB
TypeScript

import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
import { useSelectedLiteGraphItems } from '@/composables/canvas/useSelectedLiteGraphItems'
import { useModelSelectorDialog } from '@/composables/useModelSelectorDialog'
import {
DEFAULT_DARK_COLOR_PALETTE,
DEFAULT_LIGHT_COLOR_PALETTE
} from '@/constants/coreColorPalettes'
import { tryToggleWidgetPromotion } from '@/core/graph/subgraph/proxyWidgetUtils'
import { showSubgraphNodeDialog } from '@/core/graph/subgraph/useSubgraphNodeDialog'
import { t } from '@/i18n'
import {
LGraphEventMode,
LGraphGroup,
LGraphNode,
LiteGraph,
SubgraphNode
} from '@/lib/litegraph/src/litegraph'
import type { Point } from '@/lib/litegraph/src/litegraph'
import { useAssetBrowserDialog } from '@/platform/assets/composables/useAssetBrowserDialog'
import { createModelNodeFromAsset } from '@/platform/assets/utils/createModelNodeFromAsset'
import { useSubscription } from '@/platform/cloud/subscription/composables/useSubscription'
import { useSettingStore } from '@/platform/settings/settingStore'
import { SUPPORT_URL } from '@/platform/support/config'
import { useTelemetry } from '@/platform/telemetry'
import { useToastStore } from '@/platform/updates/common/toastStore'
import { useWorkflowService } from '@/platform/workflow/core/services/workflowService'
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
import type { ComfyWorkflow } from '@/platform/workflow/management/stores/workflowStore'
import {
useCanvasStore,
useTitleEditorStore
} from '@/renderer/core/canvas/canvasStore'
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
import { selectionBounds } from '@/renderer/core/layout/utils/layoutMath'
import { api } from '@/scripts/api'
import { app } from '@/scripts/app'
import { useDialogService } from '@/services/dialogService'
import { useLitegraphService } from '@/services/litegraphService'
import type { ComfyCommand } from '@/stores/commandStore'
import { useExecutionStore } from '@/stores/executionStore'
import { useHelpCenterStore } from '@/stores/helpCenterStore'
import { useNodeOutputStore } from '@/stores/imagePreviewStore'
import { useQueueSettingsStore, useQueueStore } from '@/stores/queueStore'
import { useSubgraphNavigationStore } from '@/stores/subgraphNavigationStore'
import { useSubgraphStore } from '@/stores/subgraphStore'
import { useBottomPanelStore } from '@/stores/workspace/bottomPanelStore'
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
import { useSearchBoxStore } from '@/stores/workspace/searchBoxStore'
import { useWorkspaceStore } from '@/stores/workspaceStore'
import {
getAllNonIoNodesInSubgraph,
getExecutionIdsForSelectedNodes
} from '@/utils/graphTraversalUtil'
import { filterOutputNodes } from '@/utils/nodeFilterUtil'
import {
ManagerUIState,
useManagerState
} from '@/workbench/extensions/manager/composables/useManagerState'
import { ManagerTab } from '@/workbench/extensions/manager/types/comfyManagerTypes'
import { useWorkflowTemplateSelectorDialog } from './useWorkflowTemplateSelectorDialog'
const { isActiveSubscription, showSubscriptionDialog } = useSubscription()
const moveSelectedNodesVersionAdded = '1.22.2'
export function useCoreCommands(): ComfyCommand[] {
const workflowService = useWorkflowService()
const workflowStore = useWorkflowStore()
const dialogService = useDialogService()
const colorPaletteStore = useColorPaletteStore()
const firebaseAuthActions = useFirebaseAuthActions()
const toastStore = useToastStore()
const canvasStore = useCanvasStore()
const executionStore = useExecutionStore()
const telemetry = useTelemetry()
const bottomPanelStore = useBottomPanelStore()
const { getSelectedNodes, toggleSelectedNodesMode } =
useSelectedLiteGraphItems()
const getTracker = () => workflowStore.activeWorkflow?.changeTracker
const moveSelectedNodes = (
positionUpdater: (pos: Point, gridSize: number) => Point
) => {
const selectedNodes = getSelectedNodes()
if (selectedNodes.length === 0) return
const gridSize = useSettingStore().get('Comfy.SnapToGrid.GridSize')
selectedNodes.forEach((node) => {
node.pos = positionUpdater(node.pos, gridSize)
})
app.canvas.state.selectionChanged = true
app.canvas.setDirty(true, true)
}
const commands = [
{
id: 'Comfy.NewBlankWorkflow',
icon: 'pi pi-plus',
label: 'New Blank Workflow',
menubarLabel: 'New',
category: 'essentials' as const,
function: async () => {
const previousWorkflowHadNodes = app.graph._nodes.length > 0
await workflowService.loadBlankWorkflow()
telemetry?.trackWorkflowCreated({
workflow_type: 'blank',
previous_workflow_had_nodes: previousWorkflowHadNodes
})
}
},
{
id: 'Comfy.OpenWorkflow',
icon: 'pi pi-folder-open',
label: 'Open Workflow',
menubarLabel: 'Open',
category: 'essentials' as const,
function: () => {
app.ui.loadFile()
}
},
{
id: 'Comfy.LoadDefaultWorkflow',
icon: 'pi pi-code',
label: 'Load Default Workflow',
function: async () => {
const previousWorkflowHadNodes = app.graph._nodes.length > 0
await workflowService.loadDefaultWorkflow()
telemetry?.trackWorkflowCreated({
workflow_type: 'default',
previous_workflow_had_nodes: previousWorkflowHadNodes
})
}
},
{
id: 'Comfy.SaveWorkflow',
icon: 'pi pi-save',
label: 'Save Workflow',
menubarLabel: 'Save',
category: 'essentials' as const,
function: async () => {
const workflow = useWorkflowStore().activeWorkflow as ComfyWorkflow
if (!workflow) return
await workflowService.saveWorkflow(workflow)
}
},
{
id: 'Comfy.PublishSubgraph',
icon: 'pi pi-save',
label: 'Publish Subgraph',
menubarLabel: 'Publish',
function: async () => {
await useSubgraphStore().publishSubgraph()
}
},
{
id: 'Comfy.SaveWorkflowAs',
icon: 'pi pi-save',
label: 'Save Workflow As',
menubarLabel: 'Save As',
category: 'essentials' as const,
function: async () => {
const workflow = useWorkflowStore().activeWorkflow as ComfyWorkflow
if (!workflow) return
await workflowService.saveWorkflowAs(workflow)
}
},
{
id: 'Comfy.ExportWorkflow',
icon: 'pi pi-download',
label: 'Export Workflow',
menubarLabel: 'Export',
category: 'essentials' as const,
function: async () => {
await workflowService.exportWorkflow('workflow', 'workflow')
}
},
{
id: 'Comfy.ExportWorkflowAPI',
icon: 'pi pi-download',
label: 'Export Workflow (API Format)',
menubarLabel: 'Export (API)',
function: async () => {
await workflowService.exportWorkflow('workflow_api', 'output')
}
},
{
id: 'Comfy.Undo',
icon: 'pi pi-undo',
label: 'Undo',
category: 'essentials' as const,
function: async () => {
await getTracker()?.undo?.()
}
},
{
id: 'Comfy.Redo',
icon: 'pi pi-refresh',
label: 'Redo',
category: 'essentials' as const,
function: async () => {
await getTracker()?.redo?.()
}
},
{
id: 'Comfy.ClearWorkflow',
icon: 'pi pi-trash',
label: 'Clear Workflow',
category: 'essentials' as const,
function: () => {
const settingStore = useSettingStore()
if (
!settingStore.get('Comfy.ConfirmClear') ||
confirm('Clear workflow?')
) {
app.clean()
if (app.canvas.subgraph) {
// `clear` is not implemented on subgraphs and the parent class's
// (`LGraph`) `clear` breaks the subgraph structure. For subgraphs,
// just clear the nodes but preserve input/output nodes and structure
const subgraph = app.canvas.subgraph
const nonIoNodes = getAllNonIoNodesInSubgraph(subgraph)
nonIoNodes.forEach((node) => subgraph.remove(node))
}
api.dispatchCustomEvent('graphCleared')
}
}
},
{
id: 'Comfy.Canvas.ResetView',
icon: 'pi pi-expand',
label: 'Reset View',
function: () => {
useLitegraphService().resetView()
}
},
{
id: 'Comfy.OpenClipspace',
icon: 'pi pi-clipboard',
label: 'Clipspace',
function: () => {
app.openClipspace()
}
},
{
id: 'Comfy.RefreshNodeDefinitions',
icon: 'pi pi-refresh',
label: 'Refresh Node Definitions',
category: 'essentials' as const,
function: async () => {
await app.refreshComboInNodes()
}
},
{
id: 'Comfy.Interrupt',
icon: 'pi pi-stop',
label: 'Interrupt',
category: 'essentials' as const,
function: async () => {
await api.interrupt(executionStore.activePromptId)
toastStore.add({
severity: 'info',
summary: t('g.interrupted'),
detail: t('toastMessages.interrupted'),
life: 1000
})
}
},
{
id: 'Comfy.ClearPendingTasks',
icon: 'pi pi-stop',
label: 'Clear Pending Tasks',
category: 'essentials' as const,
function: async () => {
await useQueueStore().clear(['queue'])
toastStore.add({
severity: 'info',
summary: t('g.confirmed'),
detail: t('toastMessages.pendingTasksDeleted'),
life: 3000
})
}
},
{
id: 'Comfy.BrowseTemplates',
icon: 'pi pi-folder-open',
label: 'Browse Templates',
function: () => {
useWorkflowTemplateSelectorDialog().show()
}
},
{
id: 'Comfy.Canvas.ZoomIn',
icon: 'pi pi-plus',
label: 'Zoom In',
category: 'view-controls' as const,
function: () => {
const ds = app.canvas.ds
ds.changeScale(
ds.scale * 1.1,
ds.element ? [ds.element.width / 2, ds.element.height / 2] : undefined
)
app.canvas.setDirty(true, true)
}
},
{
id: 'Comfy.Canvas.ZoomOut',
icon: 'pi pi-minus',
label: 'Zoom Out',
category: 'view-controls' as const,
function: () => {
const ds = app.canvas.ds
ds.changeScale(
ds.scale / 1.1,
ds.element ? [ds.element.width / 2, ds.element.height / 2] : undefined
)
app.canvas.setDirty(true, true)
}
},
{
id: 'Experimental.ToggleVueNodes',
label: () =>
`Experimental: ${
useSettingStore().get('Comfy.VueNodes.Enabled') ? 'Disable' : 'Enable'
} Vue Nodes`,
function: async () => {
const settingStore = useSettingStore()
const current = settingStore.get('Comfy.VueNodes.Enabled') ?? false
await settingStore.set('Comfy.VueNodes.Enabled', !current)
}
},
{
id: 'Comfy.Canvas.FitView',
icon: 'pi pi-expand',
label: 'Fit view to selected nodes',
menubarLabel: 'Zoom to fit',
category: 'view-controls' as const,
function: () => {
const vueNodesEnabled = useSettingStore().get('Comfy.VueNodes.Enabled')
if (vueNodesEnabled) {
// Get nodes from Vue stores
const canvasStore = useCanvasStore()
const selectedNodeIds = canvasStore.selectedNodeIds
const allNodes = layoutStore.getAllNodes().value
// Get nodes to fit - selected if any, otherwise all
const nodesToFit =
selectedNodeIds.size > 0
? Array.from(selectedNodeIds)
.map((id) => allNodes.get(id))
.filter((node) => node != null)
: Array.from(allNodes.values())
// Use Vue nodes bounds calculation
const bounds = selectionBounds(nodesToFit)
if (!bounds) {
toastStore.add({
severity: 'error',
summary: t('toastMessages.emptyCanvas'),
life: 3000
})
return
}
// Convert to LiteGraph format and animate
const lgBounds = [
bounds.x,
bounds.y,
bounds.width,
bounds.height
] as const
const setDirty = () => app.canvas.setDirty(true, true)
app.canvas.ds.animateToBounds(lgBounds, setDirty)
} else {
if (app.canvas.empty) {
toastStore.add({
severity: 'error',
summary: t('toastMessages.emptyCanvas'),
life: 3000
})
return
}
app.canvas.fitViewToSelectionAnimated()
}
}
},
{
id: 'Comfy.Canvas.ToggleLock',
icon: 'pi pi-lock',
label: 'Canvas Toggle Lock',
category: 'view-controls' as const,
function: () => {
app.canvas.state.readOnly = !app.canvas.state.readOnly
}
},
{
id: 'Comfy.Canvas.Lock',
icon: 'pi pi-lock',
label: 'Lock Canvas',
category: 'view-controls' as const,
function: () => {
app.canvas.state.readOnly = true
}
},
{
id: 'Comfy.Canvas.Unlock',
icon: 'pi pi-lock-open',
label: 'Unlock Canvas',
function: () => {
app.canvas.state.readOnly = false
}
},
{
id: 'Comfy.Canvas.ToggleLinkVisibility',
icon: 'pi pi-eye',
label: 'Canvas Toggle Link Visibility',
menubarLabel: 'Node Links',
versionAdded: '1.3.6',
function: (() => {
const settingStore = useSettingStore()
let lastLinksRenderMode = LiteGraph.SPLINE_LINK
return async () => {
const currentMode = settingStore.get('Comfy.LinkRenderMode')
if (currentMode === LiteGraph.HIDDEN_LINK) {
// If links are hidden, restore the last positive value or default to spline mode
await settingStore.set('Comfy.LinkRenderMode', lastLinksRenderMode)
} else {
// If links are visible, store the current mode and hide links
lastLinksRenderMode = currentMode
await settingStore.set(
'Comfy.LinkRenderMode',
LiteGraph.HIDDEN_LINK
)
}
}
})(),
active: () =>
useSettingStore().get('Comfy.LinkRenderMode') !== LiteGraph.HIDDEN_LINK
},
{
id: 'Comfy.Canvas.ToggleMinimap',
icon: 'pi pi-map',
label: 'Canvas Toggle Minimap',
menubarLabel: 'Minimap',
versionAdded: '1.24.1',
function: async () => {
const settingStore = useSettingStore()
await settingStore.set(
'Comfy.Minimap.Visible',
!settingStore.get('Comfy.Minimap.Visible')
)
},
active: () => useSettingStore().get('Comfy.Minimap.Visible')
},
{
id: 'Comfy.QueuePrompt',
icon: 'pi pi-play',
label: 'Queue Prompt',
versionAdded: '1.3.7',
category: 'essentials' as const,
function: async () => {
if (!isActiveSubscription.value) {
showSubscriptionDialog()
return
}
const batchCount = useQueueSettingsStore().batchCount
useTelemetry()?.trackWorkflowExecution()
await app.queuePrompt(0, batchCount)
}
},
{
id: 'Comfy.QueuePromptFront',
icon: 'pi pi-play',
label: 'Queue Prompt (Front)',
versionAdded: '1.3.7',
category: 'essentials' as const,
function: async () => {
if (!isActiveSubscription.value) {
showSubscriptionDialog()
return
}
const batchCount = useQueueSettingsStore().batchCount
useTelemetry()?.trackWorkflowExecution()
await app.queuePrompt(-1, batchCount)
}
},
{
id: 'Comfy.QueueSelectedOutputNodes',
icon: 'pi pi-play',
label: 'Queue Selected Output Nodes',
versionAdded: '1.19.6',
function: async () => {
if (!isActiveSubscription.value) {
showSubscriptionDialog()
return
}
const batchCount = useQueueSettingsStore().batchCount
const selectedNodes = getSelectedNodes()
const selectedOutputNodes = filterOutputNodes(selectedNodes)
if (selectedOutputNodes.length === 0) {
toastStore.add({
severity: 'error',
summary: t('toastMessages.nothingToQueue'),
detail: t('toastMessages.pleaseSelectOutputNodes'),
life: 3000
})
return
}
// Get execution IDs for all selected output nodes and their descendants
const executionIds =
getExecutionIdsForSelectedNodes(selectedOutputNodes)
if (executionIds.length === 0) {
toastStore.add({
severity: 'error',
summary: t('toastMessages.failedToQueue'),
detail: t('toastMessages.failedExecutionPathResolution'),
life: 3000
})
return
}
await app.queuePrompt(0, batchCount, executionIds)
}
},
{
id: 'Comfy.ShowSettingsDialog',
icon: 'pi pi-cog',
label: 'Show Settings Dialog',
versionAdded: '1.3.7',
category: 'view-controls' as const,
function: () => {
dialogService.showSettingsDialog()
}
},
{
id: 'Comfy.Graph.GroupSelectedNodes',
icon: 'pi pi-sitemap',
label: 'Group Selected Nodes',
versionAdded: '1.3.7',
category: 'essentials' as const,
function: () => {
const { canvas } = app
if (!canvas.selectedItems?.size) {
toastStore.add({
severity: 'error',
summary: t('toastMessages.nothingToGroup'),
detail: t('toastMessages.pleaseSelectNodesToGroup'),
life: 3000
})
return
}
const group = new LGraphGroup()
const padding = useSettingStore().get(
'Comfy.GroupSelectedNodes.Padding'
)
group.resizeTo(canvas.selectedItems, padding)
canvas.graph?.add(group)
group.recomputeInsideNodes()
useTitleEditorStore().titleEditorTarget = group
}
},
{
id: 'Workspace.NextOpenedWorkflow',
icon: 'pi pi-step-forward',
label: 'Next Opened Workflow',
versionAdded: '1.3.9',
function: async () => {
await workflowService.loadNextOpenedWorkflow()
}
},
{
id: 'Workspace.PreviousOpenedWorkflow',
icon: 'pi pi-step-backward',
label: 'Previous Opened Workflow',
versionAdded: '1.3.9',
function: async () => {
await workflowService.loadPreviousOpenedWorkflow()
}
},
{
id: 'Comfy.Canvas.ToggleSelectedNodes.Mute',
icon: 'pi pi-volume-off',
label: 'Mute/Unmute Selected Nodes',
versionAdded: '1.3.11',
category: 'essentials' as const,
function: () => {
toggleSelectedNodesMode(LGraphEventMode.NEVER)
app.canvas.setDirty(true, true)
}
},
{
id: 'Comfy.Canvas.ToggleSelectedNodes.Bypass',
icon: 'pi pi-shield',
label: 'Bypass/Unbypass Selected Nodes',
versionAdded: '1.3.11',
category: 'essentials' as const,
function: () => {
toggleSelectedNodesMode(LGraphEventMode.BYPASS)
app.canvas.setDirty(true, true)
}
},
{
id: 'Comfy.Canvas.ToggleSelectedNodes.Pin',
icon: 'pi pi-pin',
label: 'Pin/Unpin Selected Nodes',
versionAdded: '1.3.11',
category: 'essentials' as const,
function: () => {
getSelectedNodes().forEach((node) => {
node.pin(!node.pinned)
})
app.canvas.setDirty(true, true)
}
},
{
id: 'Comfy.Canvas.ToggleSelected.Pin',
icon: 'pi pi-pin',
label: 'Pin/Unpin Selected Items',
versionAdded: '1.3.33',
function: () => {
for (const item of app.canvas.selectedItems) {
if (item instanceof LGraphNode || item instanceof LGraphGroup) {
item.pin(!item.pinned)
}
}
app.canvas.setDirty(true, true)
}
},
{
id: 'Comfy.Canvas.Resize',
icon: 'pi pi-minus',
label: 'Resize Selected Nodes',
versionAdded: '',
function: () => {
getSelectedNodes().forEach((node) => {
const optimalSize = node.computeSize()
node.setSize([optimalSize[0], optimalSize[1]])
})
app.canvas.setDirty(true, true)
}
},
{
id: 'Comfy.Canvas.ToggleSelectedNodes.Collapse',
icon: 'pi pi-minus',
label: 'Collapse/Expand Selected Nodes',
versionAdded: '1.3.11',
function: () => {
getSelectedNodes().forEach((node) => {
node.collapse()
})
app.canvas.setDirty(true, true)
}
},
{
id: 'Comfy.ToggleTheme',
icon: 'pi pi-moon',
label: 'Toggle Theme (Dark/Light)',
versionAdded: '1.3.12',
function: (() => {
let previousDarkTheme: string = DEFAULT_DARK_COLOR_PALETTE.id
let previousLightTheme: string = DEFAULT_LIGHT_COLOR_PALETTE.id
return async () => {
const settingStore = useSettingStore()
const theme = colorPaletteStore.completedActivePalette
if (theme.light_theme) {
previousLightTheme = theme.id
await settingStore.set('Comfy.ColorPalette', previousDarkTheme)
} else {
previousDarkTheme = theme.id
await settingStore.set('Comfy.ColorPalette', previousLightTheme)
}
}
})()
},
{
id: 'Workspace.ToggleBottomPanel',
icon: 'pi pi-list',
label: 'Toggle Bottom Panel',
menubarLabel: 'Bottom Panel',
versionAdded: '1.3.22',
category: 'view-controls' as const,
function: () => {
bottomPanelStore.toggleBottomPanel()
},
active: () => bottomPanelStore.bottomPanelVisible
},
{
id: 'Workspace.ToggleFocusMode',
icon: 'pi pi-eye',
label: 'Toggle Focus Mode',
menubarLabel: 'Focus Mode',
versionAdded: '1.3.27',
category: 'view-controls' as const,
function: () => {
useWorkspaceStore().toggleFocusMode()
},
active: () => useWorkspaceStore().focusMode
},
{
id: 'Comfy.Graph.FitGroupToContents',
icon: 'pi pi-expand',
label: 'Fit Group To Contents',
versionAdded: '1.4.9',
function: () => {
for (const group of app.canvas.selectedItems) {
if (group instanceof LGraphGroup) {
group.recomputeInsideNodes()
const padding = useSettingStore().get(
'Comfy.GroupSelectedNodes.Padding'
)
group.resizeTo(group.children, padding)
app.graph.change()
}
}
}
},
{
id: 'Comfy.Help.OpenComfyUIIssues',
icon: 'pi pi-github',
label: 'Open ComfyUI Issues',
menubarLabel: 'ComfyUI Issues',
versionAdded: '1.5.5',
function: () => {
telemetry?.trackHelpResourceClicked({
resource_type: 'github',
is_external: true,
source: 'menu'
})
window.open(
'https://github.com/comfyanonymous/ComfyUI/issues',
'_blank'
)
}
},
{
id: 'Comfy.Help.OpenComfyUIDocs',
icon: 'pi pi-info-circle',
label: 'Open ComfyUI Docs',
menubarLabel: 'ComfyUI Docs',
versionAdded: '1.5.5',
function: () => {
telemetry?.trackHelpResourceClicked({
resource_type: 'docs',
is_external: true,
source: 'menu'
})
window.open('https://docs.comfy.org/', '_blank')
}
},
{
id: 'Comfy.Help.OpenComfyOrgDiscord',
icon: 'pi pi-discord',
label: 'Open Comfy-Org Discord',
menubarLabel: 'Comfy-Org Discord',
versionAdded: '1.5.5',
function: () => {
telemetry?.trackHelpResourceClicked({
resource_type: 'discord',
is_external: true,
source: 'menu'
})
window.open('https://www.comfy.org/discord', '_blank')
}
},
{
id: 'Workspace.SearchBox.Toggle',
icon: 'pi pi-search',
label: 'Toggle Search Box',
versionAdded: '1.5.7',
function: () => {
useSearchBoxStore().toggleVisible()
}
},
{
id: 'Comfy.Help.AboutComfyUI',
icon: 'pi pi-info-circle',
label: 'Open About ComfyUI',
menubarLabel: 'About ComfyUI',
versionAdded: '1.6.4',
function: () => {
dialogService.showSettingsDialog('about')
}
},
{
id: 'Comfy.DuplicateWorkflow',
icon: 'pi pi-clone',
label: 'Duplicate Current Workflow',
versionAdded: '1.6.15',
function: async () => {
await workflowService.duplicateWorkflow(workflowStore.activeWorkflow!)
}
},
{
id: 'Workspace.CloseWorkflow',
icon: 'pi pi-times',
label: 'Close Current Workflow',
versionAdded: '1.7.3',
function: async () => {
if (workflowStore.activeWorkflow)
await workflowService.closeWorkflow(workflowStore.activeWorkflow)
}
},
{
id: 'Comfy.ContactSupport',
icon: 'pi pi-question',
label: 'Contact Support',
versionAdded: '1.17.8',
function: () => {
window.open(SUPPORT_URL, '_blank')
}
},
{
id: 'Comfy.Help.OpenComfyUIForum',
icon: 'pi pi-comments',
label: 'Open ComfyUI Forum',
menubarLabel: 'ComfyUI Forum',
versionAdded: '1.8.2',
function: () => {
telemetry?.trackHelpResourceClicked({
resource_type: 'help_feedback',
is_external: true,
source: 'menu'
})
window.open('https://forum.comfy.org/', '_blank')
}
},
{
id: 'Comfy.Canvas.DeleteSelectedItems',
icon: 'pi pi-trash',
label: 'Delete Selected Items',
versionAdded: '1.10.5',
function: () => {
app.canvas.deleteSelected()
app.canvas.setDirty(true, true)
}
},
{
id: 'Comfy.Manager.CustomNodesManager.ShowCustomNodesMenu',
icon: 'pi pi-puzzle',
label: 'Custom Nodes Manager',
versionAdded: '1.12.10',
function: async () => {
await useManagerState().openManager({
showToastOnLegacyError: true
})
}
},
{
id: 'Comfy.Manager.ShowUpdateAvailablePacks',
icon: 'pi pi-sync',
label: 'Check for Custom Node Updates',
versionAdded: '1.17.0',
function: async () => {
const managerState = useManagerState()
const state = managerState.managerUIState.value
// For DISABLED state, show error toast instead of opening settings
if (state === ManagerUIState.DISABLED) {
toastStore.add({
severity: 'error',
summary: t('g.error'),
detail: t('manager.notAvailable'),
life: 3000
})
return
}
await managerState.openManager({
initialTab: ManagerTab.UpdateAvailable,
showToastOnLegacyError: false
})
}
},
{
id: 'Comfy.Manager.ShowMissingPacks',
icon: 'pi pi-exclamation-circle',
label: 'Install Missing Custom Nodes',
versionAdded: '1.17.0',
function: async () => {
await useManagerState().openManager({
initialTab: ManagerTab.Missing,
showToastOnLegacyError: false
})
}
},
{
id: 'Comfy.Manager.ToggleManagerProgressDialog',
icon: 'pi pi-spinner',
label: 'Toggle the Custom Nodes Manager Progress Bar',
versionAdded: '1.13.9',
function: () => {
dialogService.toggleManagerProgressDialog()
}
},
{
id: 'Comfy.User.OpenSignInDialog',
icon: 'pi pi-user',
label: 'Open Sign In Dialog',
versionAdded: '1.17.6',
function: async () => {
await dialogService.showSignInDialog()
}
},
{
id: 'Comfy.User.SignOut',
icon: 'pi pi-sign-out',
label: 'Sign Out',
versionAdded: '1.18.1',
function: async () => {
await firebaseAuthActions.logout()
}
},
{
id: 'Comfy.Canvas.MoveSelectedNodes.Up',
icon: 'pi pi-arrow-up',
label: 'Move Selected Nodes Up',
versionAdded: moveSelectedNodesVersionAdded,
function: () => moveSelectedNodes(([x, y], gridSize) => [x, y - gridSize])
},
{
id: 'Comfy.Canvas.MoveSelectedNodes.Down',
icon: 'pi pi-arrow-down',
label: 'Move Selected Nodes Down',
versionAdded: moveSelectedNodesVersionAdded,
function: () => moveSelectedNodes(([x, y], gridSize) => [x, y + gridSize])
},
{
id: 'Comfy.Canvas.MoveSelectedNodes.Left',
icon: 'pi pi-arrow-left',
label: 'Move Selected Nodes Left',
versionAdded: moveSelectedNodesVersionAdded,
function: () => moveSelectedNodes(([x, y], gridSize) => [x - gridSize, y])
},
{
id: 'Comfy.Canvas.MoveSelectedNodes.Right',
icon: 'pi pi-arrow-right',
label: 'Move Selected Nodes Right',
versionAdded: moveSelectedNodesVersionAdded,
function: () => moveSelectedNodes(([x, y], gridSize) => [x + gridSize, y])
},
{
id: 'Comfy.Graph.ConvertToSubgraph',
icon: 'icon-[lucide--shrink]',
label: 'Convert Selection to Subgraph',
versionAdded: '1.20.1',
category: 'essentials' as const,
function: () => {
const canvas = canvasStore.getCanvas()
const graph = canvas.subgraph ?? canvas.graph
if (!graph) throw new TypeError('Canvas has no graph or subgraph set.')
const res = graph.convertToSubgraph(canvas.selectedItems)
if (!res) {
toastStore.add({
severity: 'error',
summary: t('toastMessages.cannotCreateSubgraph'),
detail: t('toastMessages.failedToConvertToSubgraph'),
life: 3000
})
return
}
const { node } = res
canvas.select(node)
canvasStore.updateSelectedItems()
}
},
{
id: 'Comfy.Graph.UnpackSubgraph',
icon: 'icon-[lucide--expand]',
label: 'Unpack the selected Subgraph',
versionAdded: '1.26.3',
function: () => {
const canvas = canvasStore.getCanvas()
const graph = canvas.subgraph ?? canvas.graph
if (!graph) throw new TypeError('Canvas has no graph or subgraph set.')
const subgraphNode = app.canvas.selectedItems.values().next().value
if (!(subgraphNode instanceof SubgraphNode)) return
useNodeOutputStore().revokeSubgraphPreviews(subgraphNode)
graph.unpackSubgraph(subgraphNode)
}
},
{
id: 'Comfy.Graph.EditSubgraphWidgets',
label: 'Edit Subgraph Widgets',
icon: 'icon-[lucide--settings-2]',
versionAdded: '1.28.5',
function: showSubgraphNodeDialog
},
{
id: 'Comfy.Graph.ToggleWidgetPromotion',
icon: 'icon-[lucide--arrow-left-right]',
label: 'Toggle promotion of hovered widget',
versionAdded: '1.30.1',
function: tryToggleWidgetPromotion
},
{
id: 'Comfy.OpenManagerDialog',
icon: 'mdi mdi-puzzle-outline',
label: 'Manager',
function: async () => {
await useManagerState().openManager({
initialTab: ManagerTab.All,
showToastOnLegacyError: false
})
}
},
{
id: 'Comfy.ToggleHelpCenter',
icon: 'pi pi-question-circle',
label: 'Help Center',
function: () => {
useHelpCenterStore().toggle()
},
active: () => useHelpCenterStore().isVisible
},
{
id: 'Comfy.ToggleCanvasInfo',
icon: 'pi pi-info-circle',
label: 'Canvas Performance',
function: async () => {
const settingStore = useSettingStore()
const currentValue = settingStore.get('Comfy.Graph.CanvasInfo')
await settingStore.set('Comfy.Graph.CanvasInfo', !currentValue)
},
active: () => useSettingStore().get('Comfy.Graph.CanvasInfo')
},
{
id: 'Workspace.ToggleBottomPanel.Shortcuts',
icon: 'pi pi-key',
label: 'Show Keybindings Dialog',
versionAdded: '1.24.1',
category: 'view-controls' as const,
function: () => {
bottomPanelStore.togglePanel('shortcuts')
}
},
{
id: 'Comfy.Graph.ExitSubgraph',
icon: 'pi pi-arrow-up',
label: 'Exit Subgraph',
versionAdded: '1.20.1',
function: () => {
const canvas = useCanvasStore().getCanvas()
const navigationStore = useSubgraphNavigationStore()
if (!canvas.graph) return
canvas.setGraph(
navigationStore.navigationStack.at(-2) ?? canvas.graph.rootGraph
)
}
},
{
id: 'Comfy.Dev.ShowModelSelector',
icon: 'pi pi-box',
label: 'Show Model Selector (Dev)',
versionAdded: '1.26.2',
category: 'view-controls' as const,
function: () => {
const modelSelectorDialog = useModelSelectorDialog()
modelSelectorDialog.show()
}
},
{
id: 'Comfy.Manager.CustomNodesManager.ShowLegacyCustomNodesMenu',
icon: 'pi pi-bars',
label: 'Custom Nodes (Legacy)',
versionAdded: '1.16.4',
function: async () => {
await useManagerState().openManager({
legacyCommand: 'Comfy.Manager.CustomNodesManager.ToggleVisibility',
showToastOnLegacyError: true,
isLegacyOnly: true
})
}
},
{
id: 'Comfy.Manager.ShowLegacyManagerMenu',
icon: 'mdi mdi-puzzle',
label: 'Manager Menu (Legacy)',
versionAdded: '1.16.4',
function: async () => {
await useManagerState().openManager({
showToastOnLegacyError: true,
isLegacyOnly: true
})
}
},
{
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 })
}
},
{
id: 'Comfy.BrowseModelAssets',
icon: 'pi pi-folder-open',
label: 'Experimental: Browse Model Assets',
versionAdded: '1.28.3',
function: async () => {
if (!useSettingStore().get('Comfy.Assets.UseAssetAPI')) {
const confirmed = await dialogService.confirm({
title: 'Enable Asset API',
message:
'The Asset API is currently disabled. Would you like to enable it?',
type: 'default'
})
if (!confirmed) return
const settingStore = useSettingStore()
await settingStore.set('Comfy.Assets.UseAssetAPI', true)
await workflowService.reloadCurrentWorkflow()
}
const assetBrowserDialog = useAssetBrowserDialog()
await assetBrowserDialog.browse({
assetType: 'models',
title: t('sideToolbar.modelLibrary'),
onAssetSelected: (asset) => {
const result = createModelNodeFromAsset(asset)
if (!result.success) {
toastStore.add({
severity: 'error',
summary: t('g.error'),
detail: t('assetBrowser.failedToCreateNode')
})
console.error('Node creation failed:', result.error)
}
}
})
}
},
{
id: 'Comfy.ToggleAssetAPI',
icon: 'pi pi-database',
label: () =>
`Experimental: ${
useSettingStore().get('Comfy.Assets.UseAssetAPI')
? 'Disable'
: 'Enable'
} AssetAPI`,
function: async () => {
const settingStore = useSettingStore()
const current = settingStore.get('Comfy.Assets.UseAssetAPI') ?? false
await settingStore.set('Comfy.Assets.UseAssetAPI', !current)
await useWorkflowService().reloadCurrentWorkflow() // ensure changes take effect immediately
}
}
]
return commands.map((command) => ({ ...command, source: 'System' }))
}