Skip to content

Add Subgraphs #3905

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 83 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
83 commits
Select commit Hold shift + click to select a range
b3042d3
[Nope] Add Zod recursive schema requirements
webfiltered Apr 28, 2025
9620f83
[Nope] Zod schema recursive II
webfiltered Apr 28, 2025
31ab027
[Refactor] Generic TS type dedupe
webfiltered May 3, 2025
0dd308d
Add i18n for nothing selected notification
webfiltered May 15, 2025
d88a227
Add subgraph workflow fields to change tracker
webfiltered Apr 30, 2025
f95f014
[TS] Remove unintended type assertion
webfiltered May 3, 2025
6f8a91b
Fix corruption when app.graph is changed
webfiltered May 3, 2025
71bbca6
[PARTIAL] Add Subgraph
webfiltered May 10, 2025
bff802e
Fix DOM widgets after rebase
webfiltered May 10, 2025
09d17fe
nit - prevent duplicate subgraph registrations
webfiltered May 12, 2025
77968fe
Fix nested subgraph breadcrumbs
webfiltered May 12, 2025
99c7ecf
Remove unnecessary copy of litegraph objects
webfiltered May 12, 2025
5129cfa
Prune DOM widgets outside of current subgraph
webfiltered May 12, 2025
1300a13
[TS] Remove unnecessary assertion
webfiltered May 12, 2025
f41ae1d
Fix desync of litegraph type and nodedefs
webfiltered May 12, 2025
a5d0bc3
nit
webfiltered May 12, 2025
6918aa8
[TS] Replace ts-ignore w/error
webfiltered May 13, 2025
b65440e
Track subgraph open history for breadcrumbs
webfiltered May 14, 2025
b0fc736
nit
webfiltered May 14, 2025
bbd1ca2
Fix DOM widgets disappear
webfiltered May 14, 2025
15a2b37
Add convert to subgraph command
webfiltered May 15, 2025
dba8716
[Debug] Include more items in graph diff output
webfiltered May 15, 2025
7623711
Clear DOM widgets instead of trying to manage refs
webfiltered May 15, 2025
2cd315a
Keep subgraph nav state when swapping workflows
webfiltered May 15, 2025
0fe0519
[Test] Update expectations
webfiltered May 15, 2025
a3615b3
Add simpler interface for active DOM widgets
webfiltered May 15, 2025
4232e05
Use subgraph npm
webfiltered May 16, 2025
359e928
Update locales [skip ci]
invalid-email-address May 16, 2025
20833e5
Fix breadcrumb reactivity
webfiltered May 16, 2025
c76635c
Update litegraph 0.16.0-sub.1
webfiltered May 19, 2025
f73be5d
Fix crash on graph load - reactive proxy leak
webfiltered May 19, 2025
fc191a1
Update litegraph 0.16.0-sub.2
webfiltered May 19, 2025
5685cb6
Fix invalid links in nested subgraph conversion
webfiltered May 19, 2025
518faeb
[chore] Update litegraph 0.16.0-sub.3
webfiltered May 19, 2025
4388cbe
Update node create to use active subgraph
webfiltered May 21, 2025
9d4537e
[Cleanup] Remove redundant LinkConnector call
webfiltered May 21, 2025
0a40c11
[chore] Update litegraph 0.16.0-sub.4
webfiltered May 21, 2025
4ca5a92
Fix unwarranted workflow validation warning
webfiltered May 22, 2025
b3b0b95
Fix active subgraph requires breadcrumb component
webfiltered May 22, 2025
b2550f6
Fix breadcrumbs overlap 2nd row tabs
webfiltered May 22, 2025
98f5216
Fix edit mask button shown when non-nodes selected
webfiltered May 22, 2025
12e1508
Fix delete button shown for io nodes
webfiltered May 22, 2025
bdc1ac1
[chore] Update litegraph 0.16.0-sub.5
webfiltered May 22, 2025
6f9c481
Add convert to subgraph toolbox button
webfiltered May 22, 2025
c84218d
Remove deprecated group node conversion menu
webfiltered May 22, 2025
d018a69
nit
webfiltered May 22, 2025
962834e
Add subgraph nodes to before/after exec
webfiltered May 22, 2025
060540a
Reimpl. escape key handling in frontend
webfiltered May 22, 2025
842ec58
[chore] Update litegraph 0.16.0-sub.6
webfiltered Jun 4, 2025
b0fc8ef
[Test] Revert invalid test generation
webfiltered Jun 4, 2025
cb91c37
Fix Vue unwrap using markRaw
webfiltered Jun 4, 2025
c57c391
Fix convert to subgraph shown on IO node
webfiltered Jun 5, 2025
7d568e1
[Test] Remove failing test (auto-generated)
webfiltered Jun 5, 2025
97faee8
[chore] Update litegraph 0.16.0-sub.7
webfiltered Jun 5, 2025
6e89b19
[chore] Update litegraph 0.16.0-sub.8
webfiltered Jun 12, 2025
d755210
[chore] Update litegraph 0.16.0-sub.9
webfiltered Jun 12, 2025
2cdf547
[TS] Remove type assertion
webfiltered Jun 13, 2025
b02408f
[Refactor] Prefer canvas store over app.canvas
webfiltered Jun 13, 2025
66fbdad
Add subgraph functionality to execution store
webfiltered Jun 13, 2025
2afd295
[chore] Update litegraph 0.16.0-sub.10
webfiltered Jun 14, 2025
d2369c8
[chore] Update litegraph 0.16.0-sub.11
webfiltered Jun 14, 2025
1aac2d5
[chore] Update litegraph 0.16.0-sub.12
webfiltered Jun 14, 2025
a5a1f8c
Fix subgraphs linked to each other corrupt execution
webfiltered Jun 16, 2025
c3065ff
Fix execution fails when slot numbers don't match
webfiltered Jun 16, 2025
9b488da
[chore] Update litegraph 0.16.0-sub.13
webfiltered Jun 17, 2025
27d33c2
Subgraph testing release v1.22.2-sub.8 (#4212)
webfiltered Jun 18, 2025
b8740c6
Update to match upstream litegraph change
webfiltered Jun 20, 2025
76d6911
[chore] Update litegraph 0.16.0-sub.14
webfiltered Jun 20, 2025
9f0b22a
Subgraph testing release v1.22.2-sub.9 (#4226)
webfiltered Jun 20, 2025
ee93e36
Add default badge for subgraph nodes
webfiltered Jun 20, 2025
cf9af94
Add breadcrumb text outline + shadow
webfiltered Jun 18, 2025
aefc5eb
[chore] Update litegraph 0.16.0-sub.15
webfiltered Jun 22, 2025
5acfe4a
Subgraph testing release v1.22.2-sub.10 (#4248)
webfiltered Jun 22, 2025
586314e
Fix error discarded by error toast handler
webfiltered Jun 23, 2025
11b71bb
[TS] Remove unnecessary type redeclaration
webfiltered Jun 23, 2025
6926d44
[TS] Fix implicit any in augmented type
webfiltered Jun 25, 2025
9bc4f66
Upstream graph execution traversal logic to litegraph
webfiltered Jun 25, 2025
6b3d89e
[chore] Update litegraph 0.16.0-sub.16
webfiltered Jun 25, 2025
c462d35
Subgraph testing release v1.22.2-sub.11 (#4272)
webfiltered Jun 25, 2025
4df20a3
Subgraph testing release v1.23.2-sub.12 (#4273)
webfiltered Jun 25, 2025
586f882
[Test] Skip tests on removed group node menu
webfiltered Jun 26, 2025
683d818
[chore] Update litegraph 0.16.0-sub.17
webfiltered Jun 26, 2025
55cf65e
Subgraph testing release v1.23.2-sub.13 (#4282)
webfiltered Jun 26, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions browser_tests/tests/groupNode.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@ test.describe('Group Node', () => {
await libraryTab.open()
})

test('Is added to node library sidebar', async ({ comfyPage }) => {
test.skip('Is added to node library sidebar', async ({ comfyPage }) => {
expect(await libraryTab.getFolder('group nodes').count()).toBe(1)
})

test('Can be added to canvas using node library sidebar', async ({
test.skip('Can be added to canvas using node library sidebar', async ({
comfyPage
}) => {
const initialNodeCount = await comfyPage.getGraphNodesCount()
Expand All @@ -34,7 +34,7 @@ test.describe('Group Node', () => {
expect(await comfyPage.getGraphNodesCount()).toBe(initialNodeCount + 1)
})

test('Can be bookmarked and unbookmarked', async ({ comfyPage }) => {
test.skip('Can be bookmarked and unbookmarked', async ({ comfyPage }) => {
await libraryTab.getFolder(groupNodeCategory).click()
await libraryTab
.getNode(groupNodeName)
Expand All @@ -61,7 +61,7 @@ test.describe('Group Node', () => {
).toHaveLength(0)
})

test('Displays preview on bookmark hover', async ({ comfyPage }) => {
test.skip('Displays preview on bookmark hover', async ({ comfyPage }) => {
await libraryTab.getFolder(groupNodeCategory).click()
await libraryTab
.getNode(groupNodeName)
Expand Down Expand Up @@ -95,7 +95,7 @@ test.describe('Group Node', () => {
)
})

test('Displays tooltip on title hover', async ({ comfyPage }) => {
test.skip('Displays tooltip on title hover', async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.EnableTooltips', true)
await comfyPage.convertAllNodesToGroupNode('Group Node')
await comfyPage.page.mouse.move(47, 173)
Expand All @@ -104,7 +104,7 @@ test.describe('Group Node', () => {
await expect(comfyPage.page.locator('.node-tooltip')).toBeVisible()
})

test('Manage group opens with the correct group selected', async ({
test.skip('Manage group opens with the correct group selected', async ({
comfyPage
}) => {
const makeGroup = async (name, type1, type2) => {
Expand Down Expand Up @@ -165,7 +165,7 @@ test.describe('Group Node', () => {
expect(visibleInputCount).toBe(2)
})

test('Reconnects inputs after configuration changed via manage dialog save', async ({
test.skip('Reconnects inputs after configuration changed via manage dialog save', async ({
comfyPage
}) => {
const expectSingleNode = async (type: string) => {
Expand Down
2 changes: 1 addition & 1 deletion browser_tests/tests/rightClickMenu.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ test.describe('Canvas Right Click Menu', () => {
await expect(comfyPage.canvas).toHaveScreenshot('add-group-group-added.png')
})

test('Can convert to group node', async ({ comfyPage }) => {
test.skip('Can convert to group node', async ({ comfyPage }) => {
await comfyPage.select2Nodes()
await expect(comfyPage.canvas).toHaveScreenshot('selected-2-nodes.png')
await comfyPage.rightClickCanvas()
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 6 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@comfyorg/comfyui-frontend",
"private": true,
"version": "1.23.2",
"version": "1.23.2-sub.13",
"type": "module",
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
"homepage": "https://comfy.org",
Expand Down Expand Up @@ -76,7 +76,7 @@
"@alloc/quick-lru": "^5.2.0",
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
"@comfyorg/comfyui-electron-types": "^0.4.43",
"@comfyorg/litegraph": "^0.15.15",
"@comfyorg/litegraph": "^0.16.0-sub.17",
"@primevue/forms": "^4.2.5",
"@primevue/themes": "^4.2.5",
"@sentry/vue": "^8.48.0",
Expand Down
53 changes: 31 additions & 22 deletions src/components/breadcrumb/SubgraphBreadcrumb.vue
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
<template>
<div
v-if="workflowStore.isSubgraphActive"
class="fixed top-[var(--comfy-topbar-height)] left-[var(--sidebar-width)] p-2 subgraph-breadcrumb"
>
<div v-if="workflowStore.isSubgraphActive" class="p-2 subgraph-breadcrumb">
<Breadcrumb
class="bg-transparent"
:home="home"
Expand All @@ -14,36 +11,38 @@
</template>

<script setup lang="ts">
import { useEventListener, whenever } from '@vueuse/core'
import { useEventListener } from '@vueuse/core'
import Breadcrumb from 'primevue/breadcrumb'
import type { MenuItem, MenuItemCommandEvent } from 'primevue/menuitem'
import { computed } from 'vue'

import { useWorkflowService } from '@/services/workflowService'
import { useCanvasStore } from '@/stores/graphStore'
import { useSubgraphNavigationStore } from '@/stores/subgraphNavigationStore'
import { useWorkflowStore } from '@/stores/workflowStore'

const workflowService = useWorkflowService()
const workflowStore = useWorkflowStore()
const navigationStore = useSubgraphNavigationStore()

const workflowName = computed(() => workflowStore.activeWorkflow?.filename)

const items = computed(() => {
if (!workflowStore.subgraphNamePath.length) return []
if (!navigationStore.navigationStack.length) return []

return workflowStore.subgraphNamePath.map<MenuItem>((name) => ({
label: name,
command: async () => {
const workflow = workflowStore.getWorkflowByPath(name)
if (workflow) await workflowService.openWorkflow(workflow)
return navigationStore.navigationStack.map<MenuItem>((subgraph) => ({
label: subgraph.name,
command: () => {
const canvas = useCanvasStore().getCanvas()
if (!canvas.graph) throw new TypeError('Canvas has no graph')

canvas.setGraph(subgraph)
}
}))
})

const home = computed(() => ({
label: workflowName.value,
icon: 'pi pi-home',
command: async () => {
command: () => {
const canvas = useCanvasStore().getCanvas()
if (!canvas.graph) throw new TypeError('Canvas has no graph')

Expand All @@ -55,22 +54,32 @@ const handleItemClick = (event: MenuItemCommandEvent) => {
event.item.command?.(event)
}

whenever(
() => useCanvasStore().canvas,
(canvas) => {
useEventListener(canvas.canvas, 'litegraph:set-graph', () => {
useWorkflowStore().updateActiveGraph()
})
// Escape exits from the current subgraph.
useEventListener(document, 'keydown', (event) => {
if (event.key === 'Escape') {
const canvas = useCanvasStore().getCanvas()
if (!canvas.graph) throw new TypeError('Canvas has no graph')

canvas.setGraph(
navigationStore.navigationStack.at(-2) ?? canvas.graph.rootGraph
)
}
)
})
</script>

<style>
.subgraph-breadcrumb {
.p-breadcrumb-item-link,
.p-breadcrumb-item-icon {
@apply select-none;

color: #d26565;
user-select: none;
text-shadow:
1px 1px 0 #000,
-1px -1px 0 #000,
1px -1px 0 #000,
-1px 1px 0 #000,
0 0 0.375rem #000;
}
}
</style>
6 changes: 2 additions & 4 deletions src/components/graph/DomWidgets.vue
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,14 @@ import { useDomWidgetStore } from '@/stores/domWidgetStore'
import { useCanvasStore } from '@/stores/graphStore'

const domWidgetStore = useDomWidgetStore()
const widgetStates = computed(() =>
Array.from(domWidgetStore.widgetStates.values())
)
const widgetStates = computed(() => domWidgetStore.activeWidgetStates)

const updateWidgets = () => {
const lgCanvas = canvasStore.canvas
if (!lgCanvas) return

const lowQuality = lgCanvas.low_quality
for (const widgetState of domWidgetStore.widgetStates.values()) {
for (const widgetState of widgetStates.value) {
const widget = widgetState.widget
const node = widget.node as LGraphNode

Expand Down
32 changes: 22 additions & 10 deletions src/components/graph/GraphCanvas.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@
<BottomPanel />
</template>
<template #graph-canvas-panel>
<SecondRowWorkflowTabs
v-if="workflowTabsPosition === 'Topbar (2nd-row)'"
class="pointer-events-auto"
/>
<div class="absolute top-0 left-0 w-auto max-w-full pointer-events-auto">
<SecondRowWorkflowTabs
v-if="workflowTabsPosition === 'Topbar (2nd-row)'"
/>
<SubgraphBreadcrumb />
</div>
<GraphCanvasMenu v-if="canvasMenuEnabled" class="pointer-events-auto" />
</template>
</LiteGraphCanvasSplitterOverlay>
Expand All @@ -39,12 +41,11 @@
</SelectionOverlay>
<DomWidgets />
</template>
<SubgraphBreadcrumb />
</template>

<script setup lang="ts">
import type { LGraphNode } from '@comfyorg/litegraph'
import { useEventListener } from '@vueuse/core'
import { useEventListener, whenever } from '@vueuse/core'
import { computed, onMounted, ref, watch, watchEffect } from 'vue'

import LiteGraphCanvasSplitterOverlay from '@/components/LiteGraphCanvasSplitterOverlay.vue'
Expand Down Expand Up @@ -84,6 +85,7 @@ import { useCanvasStore } from '@/stores/graphStore'
import { useNodeDefStore } from '@/stores/nodeDefStore'
import { useSettingStore } from '@/stores/settingStore'
import { useToastStore } from '@/stores/toastStore'
import { useWorkflowStore } from '@/stores/workflowStore'
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
import { useWorkspaceStore } from '@/stores/workspaceStore'

Expand Down Expand Up @@ -192,10 +194,10 @@ watch(
// Update the progress of the executing node
watch(
() =>
[executionStore.executingNodeId, executionStore.executingNodeProgress] as [
NodeId | null,
number | null
],
[
executionStore.executingNodeId,
executionStore.executingNodeProgress
] satisfies [NodeId | null, number | null],
([executingNodeId, executingNodeProgress]) => {
for (const node of comfyApp.graph.nodes) {
if (node.id == executingNodeId) {
Expand Down Expand Up @@ -334,6 +336,16 @@ onMounted(async () => {
}
)

whenever(
() => useCanvasStore().canvas,
(canvas) => {
useEventListener(canvas.canvas, 'litegraph:set-graph', () => {
useWorkflowStore().updateActiveGraph()
})
},
{ immediate: true }
)

emit('ready')
})
</script>
2 changes: 2 additions & 0 deletions src/components/graph/SelectionToolbox.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
<BypassButton />
<PinButton />
<MaskEditorButton />
<ConvertToSubgraphButton />
<DeleteButton />
<RefreshButton />
<ExtensionCommandButton
Expand All @@ -28,6 +29,7 @@ import { computed } from 'vue'

import BypassButton from '@/components/graph/selectionToolbox/BypassButton.vue'
import ColorPickerButton from '@/components/graph/selectionToolbox/ColorPickerButton.vue'
import ConvertToSubgraphButton from '@/components/graph/selectionToolbox/ConvertToSubgraphButton.vue'
import DeleteButton from '@/components/graph/selectionToolbox/DeleteButton.vue'
import ExecuteButton from '@/components/graph/selectionToolbox/ExecuteButton.vue'
import ExtensionCommandButton from '@/components/graph/selectionToolbox/ExtensionCommandButton.vue'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<template>
<Button
v-show="isVisible"
v-tooltip.top="{
value: t('commands.Comfy_Graph_ConvertToSubgraph.label'),
showDelay: 1000
}"
severity="secondary"
text
icon="pi pi-box"
@click="() => commandStore.execute('Comfy.Graph.ConvertToSubgraph')"
/>
</template>

<script setup lang="ts">
import Button from 'primevue/button'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'

import { useCommandStore } from '@/stores/commandStore'
import { useCanvasStore } from '@/stores/graphStore'

const { t } = useI18n()
const commandStore = useCommandStore()
const canvasStore = useCanvasStore()

const isVisible = computed(() => {
return (
canvasStore.groupSelected ||
canvasStore.rerouteSelected ||
canvasStore.nodeSelected
)
})
</script>
8 changes: 8 additions & 0 deletions src/components/graph/selectionToolbox/DeleteButton.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<template>
<Button
v-show="isDeletable"
v-tooltip.top="{
value: t('commands.Comfy_Canvas_DeleteSelectedItems.label'),
showDelay: 1000
Expand All @@ -13,10 +14,17 @@

<script setup lang="ts">
import Button from 'primevue/button'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'

import { useCommandStore } from '@/stores/commandStore'
import { useCanvasStore } from '@/stores/graphStore'

const { t } = useI18n()
const commandStore = useCommandStore()
const canvasStore = useCanvasStore()

const isDeletable = computed(() =>
canvasStore.selectedItems.some((x) => x.removable !== false)
)
</script>
5 changes: 3 additions & 2 deletions src/components/graph/selectionToolbox/MaskEditorButton.vue
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,9 @@ const commandStore = useCommandStore()
const canvasStore = useCanvasStore()

const isSingleImageNode = computed(() => {
const nodes = canvasStore.selectedItems.filter(isLGraphNode)
return nodes.length === 1 && nodes.some(isImageNode)
const { selectedItems } = canvasStore
const item = selectedItems[0]
return selectedItems.length === 1 && isLGraphNode(item) && isImageNode(item)
})

const openMaskEditor = () => {
Expand Down
Loading