mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-02 22:37:32 +00:00
`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)
1207 lines
37 KiB
TypeScript
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' }))
|
|
}
|