Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
3dd3e26
[backport cloud/1.33] feat: open template via URL in linear mode (#6968)
comfy-pr-bot Nov 27, 2025
31d8422
[backport cloud/1.33] fix: don't use registry when only checking for …
christian-byrne Nov 27, 2025
334404a
[backport cloud/1.33] fix: remove LOD from vue nodes (#6983)
christian-byrne Nov 27, 2025
0cd0218
[backport cloud/1.33] fix: Vue Node <-> Litegraph node height offset …
christian-byrne Nov 27, 2025
896867b
[backport cloud/1.33] fix: add filter for combo widgets (#7003)
comfy-pr-bot Nov 27, 2025
550ca0c
[backport cloud/1.33] Remove app.graph usage from widgetInput code (#…
comfy-pr-bot Nov 28, 2025
637c199
[backport cloud/1.33] [fix] Re-encode cloud-subscription video to VP9…
comfy-pr-bot Nov 28, 2025
22aea29
[backport cloud/1.33] [feat] Show "Finished in" duration for complete…
comfy-pr-bot Nov 28, 2025
72a2581
[backport cloud/1.33] feat(api-nodes-pricing): add prices for ByteDan…
comfy-pr-bot Nov 29, 2025
7b589b5
[backport cloud/1.33] mark vue nodes menu toggle with beta tag (#7052)
comfy-pr-bot Nov 30, 2025
3856e0d
[backport cloud/1.33] fix: loader node widget value shows placeholder…
comfy-pr-bot Nov 30, 2025
ffa55cb
[backport cloud/1.33] Simplify Vue node resize to bottom-right corner…
christian-byrne Dec 1, 2025
3920413
[backport cloud/1.33] [fix] Prevent drag activation during Vue node r…
comfy-pr-bot Dec 1, 2025
c8a1df3
[backport cloud/1.33] feat(api-nodes-pricing): add prices for Kling O…
comfy-pr-bot Dec 1, 2025
b663244
[backport cloud/1.33] fix: normalize path separators in comfyAPIPlugi…
comfy-pr-bot Dec 3, 2025
6c34085
[backport cloud/1.33] cloud: increase feature flag polling interval t…
comfy-pr-bot Dec 3, 2025
d314172
[feat] Add remote config support for model upload and asset update fe…
luke-mino-altherr Dec 4, 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
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.
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.
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.
54 changes: 54 additions & 0 deletions browser_tests/tests/vueNodes/interactions/node/resize.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import {
comfyExpect as expect,
comfyPageFixture as test
} from '../../../../fixtures/ComfyPage'

test.describe('Vue Node Resizing', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.VueNodes.Enabled', true)
await comfyPage.vueNodes.waitForNodes()
})

test('should resize node without position drift after selecting', async ({
comfyPage
}) => {
// Get a Vue node fixture
const node = await comfyPage.vueNodes.getFixtureByTitle('Load Checkpoint')
const initialBox = await node.boundingBox()
if (!initialBox) throw new Error('Node bounding box not found')

// Select the node first (this was causing the bug)
await node.header.click()
await comfyPage.page.waitForTimeout(100) // Brief pause after selection

// Get position after selection
const selectedBox = await node.boundingBox()
if (!selectedBox)
throw new Error('Node bounding box not found after select')

// Verify position unchanged after selection
expect(selectedBox.x).toBeCloseTo(initialBox.x, 1)
expect(selectedBox.y).toBeCloseTo(initialBox.y, 1)

// Now resize from bottom-right corner
const resizeStartX = selectedBox.x + selectedBox.width - 5
const resizeStartY = selectedBox.y + selectedBox.height - 5

await comfyPage.page.mouse.move(resizeStartX, resizeStartY)
await comfyPage.page.mouse.down()
await comfyPage.page.mouse.move(resizeStartX + 50, resizeStartY + 30)
await comfyPage.page.mouse.up()

// Get final position and size
const finalBox = await node.boundingBox()
if (!finalBox) throw new Error('Node bounding box not found after resize')

// Position should NOT have changed (the bug was position drift)
expect(finalBox.x).toBeCloseTo(initialBox.x, 1)
expect(finalBox.y).toBeCloseTo(initialBox.y, 1)

// Size should have increased
expect(finalBox.width).toBeGreaterThan(initialBox.width)
expect(finalBox.height).toBeGreaterThan(initialBox.height)
})
})
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.
49 changes: 0 additions & 49 deletions browser_tests/tests/vueNodes/nodeStates/lod.spec.ts

This file was deleted.

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.
6 changes: 4 additions & 2 deletions build/plugins/comfyAPIPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,12 +88,14 @@ export function comfyAPIPlugin(isDev: boolean): Plugin {

if (result.exports.length > 0) {
const projectRoot = process.cwd()
const relativePath = path.relative(path.join(projectRoot, 'src'), id)
const relativePath = path
.relative(path.join(projectRoot, 'src'), id)
.replace(/\\/g, '/')
const shimFileName = relativePath.replace(/\.ts$/, '.js')

let shimContent = `// Shim for ${relativePath}\n`

const fileKey = relativePath.replace(/\.ts$/, '').replace(/\\/g, '/')
const fileKey = relativePath.replace(/\.ts$/, '')
const warningMessage = getWarningMessage(fileKey, shimFileName)

if (warningMessage) {
Expand Down
24 changes: 24 additions & 0 deletions cloud-loader-dropdown.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
Fixes loader dropdown placeholder
===============================

Cloud loader dropdowns hydrate via `useAssetWidgetData(nodeType)`, so `dropdownItems` stays empty until the Asset API returns friendly filenames. Meanwhile `modelValue` already holds the saved asset and the watcher at [WidgetSelectDropdown.vue#L215-L227](https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/src/renderer/extensions/vueNodes/widgets/components/WidgetSelectDropdown.vue#L215-L227) only tracks `modelValue`. It runs before assets load, fails to find a match, clears `selectedSet`, and the placeholder persists.

```ts
watch(
modelValue,
(currentValue) => {
if (currentValue === undefined) {
selectedSet.value.clear()
return
}
const item = dropdownItems.value.find((item) => item.name === currentValue)
if (item) {
selectedSet.value.clear()
selectedSet.value.add(item.id)
}
},
{ immediate: true }
)
```

Once the API resolves, `dropdownItems` recomputes but nothing resyncs because the watcher never sees that change. Desktop doesn’t hit this because it still reads from `widget.options.values` immediately.
51 changes: 0 additions & 51 deletions packages/design-system/src/css/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -1329,57 +1329,6 @@ audio.comfy-audio.empty-audio-widget {
will-change: transform;
}

/* START LOD specific styles */
/* LOD styles - Custom CSS avoids 100+ Tailwind selectors that would slow style recalculation when .isLOD toggles */

.isLOD .lg-node {
box-shadow: none;
filter: none;
backdrop-filter: none;
text-shadow: none;
mask-image: none;
clip-path: none;
background-image: none;
text-rendering: optimizeSpeed;
border-radius: 0;
contain: layout style;
transition: none;
}

.isLOD .lg-node-header {
border-radius: 0;
pointer-events: none;
}

.isLOD .lg-node-widgets {
pointer-events: none;
}

.lod-toggle {
visibility: visible;
}

.isLOD .lod-toggle {
visibility: hidden;
}

.lod-fallback {
display: none;
}

.isLOD .lod-fallback {
display: block;
}

.isLOD .image-preview img {
image-rendering: pixelated;
}

.isLOD .slot-dot {
border-radius: 0;
}
/* END LOD specific styles */

/* ===================== Mask Editor Styles ===================== */
/* To be migrated to Tailwind later */
#maskEditor_brush {
Expand Down
Binary file modified public/assets/images/cloud-subscription.webm
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -44,17 +44,22 @@ import { useI18n } from 'vue-i18n'

import { isCloud } from '@/platform/distribution/types'
import { useTelemetry } from '@/platform/telemetry'
import { app } from '@/scripts/app'
import { useCommandStore } from '@/stores/commandStore'
import { useNodeDefStore } from '@/stores/nodeDefStore'
import { useQueueSettingsStore } from '@/stores/queueStore'
import { useWorkspaceStore } from '@/stores/workspaceStore'
import { useMissingNodes } from '@/workbench/extensions/manager/composables/nodePack/useMissingNodes'
import { graphHasMissingNodes } from '@/workbench/extensions/manager/utils/graphHasMissingNodes'

import BatchCountEdit from '../BatchCountEdit.vue'

const workspaceStore = useWorkspaceStore()
const { mode: queueMode, batchCount } = storeToRefs(useQueueSettingsStore())

const { hasMissingNodes } = useMissingNodes()
const nodeDefStore = useNodeDefStore()
const hasMissingNodes = computed(() =>
graphHasMissingNodes(app.graph, nodeDefStore.nodeDefsByName)
)

const { t } = useI18n()
const queueModeMenuItemLookup = computed(() => {
Expand Down
9 changes: 7 additions & 2 deletions src/components/breadcrumb/SubgraphBreadcrumbItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,13 @@ import {
ComfyWorkflow,
useWorkflowStore
} from '@/platform/workflow/management/stores/workflowStore'
import { app } from '@/scripts/app'
import { useDialogService } from '@/services/dialogService'
import { useCommandStore } from '@/stores/commandStore'
import { useNodeDefStore } from '@/stores/nodeDefStore'
import { useSubgraphNavigationStore } from '@/stores/subgraphNavigationStore'
import { appendJsonExt } from '@/utils/formatUtil'
import { useMissingNodes } from '@/workbench/extensions/manager/composables/nodePack/useMissingNodes'
import { graphHasMissingNodes } from '@/workbench/extensions/manager/utils/graphHasMissingNodes'

interface Props {
item: MenuItem
Expand All @@ -79,7 +81,10 @@ const props = withDefaults(defineProps<Props>(), {
isActive: false
})

const { hasMissingNodes } = useMissingNodes()
const nodeDefStore = useNodeDefStore()
const hasMissingNodes = computed(() =>
graphHasMissingNodes(app.graph, nodeDefStore.nodeDefsByName)
)

const { t } = useI18n()
const menu = ref<InstanceType<typeof Menu> & MenuState>()
Expand Down
2 changes: 2 additions & 0 deletions src/components/sidebar/ComfyMenuButton.vue
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
@click.stop="handleNodes2ToggleClick"
>
<span class="p-menubar-item-label text-nowrap">{{ item.label }}</span>
<Tag severity="info" class="ml-2 text-xs">{{ $t('g.beta') }}</Tag>
<ToggleSwitch
v-model="nodes2Enabled"
class="ml-4"
Expand Down Expand Up @@ -101,6 +102,7 @@

<script setup lang="ts">
import type { MenuItem } from 'primevue/menuitem'
import Tag from 'primevue/tag'
import TieredMenu from 'primevue/tieredmenu'
import type { TieredMenuMethods, TieredMenuState } from 'primevue/tieredmenu'
import ToggleSwitch from 'primevue/toggleswitch'
Expand Down
12 changes: 5 additions & 7 deletions src/composables/graph/useVueNodeLifecycle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@ import { shallowRef, watch } from 'vue'

import { useGraphNodeManager } from '@/composables/graph/useGraphNodeManager'
import type { GraphNodeManager } from '@/composables/graph/useGraphNodeManager'
import { useRenderModeSetting } from '@/composables/settings/useRenderModeSetting'
import { useVueFeatureFlags } from '@/composables/useVueFeatureFlags'
import { useVueNodesMigrationDismissed } from '@/composables/useVueNodesMigrationDismissed'
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations'
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
import { useLayoutSync } from '@/renderer/core/layout/sync/useLayoutSync'
import { removeNodeTitleHeight } from '@/renderer/core/layout/utils/nodeSizeUtil'
import { ensureCorrectLayoutScale } from '@/renderer/extensions/vueNodes/layout/ensureCorrectLayoutScale'
import { app as comfyApp } from '@/scripts/app'
import { useToastStore } from '@/platform/updates/common/toastStore'
Expand All @@ -26,11 +26,6 @@ function useVueNodeLifecycleIndividual() {

let hasShownMigrationToast = false

useRenderModeSetting(
{ setting: 'LiteGraph.Canvas.MinFontSizeForLOD', vue: 0, litegraph: 8 },
shouldRenderVueNodes
)

const initializeNodeManager = () => {
// Use canvas graph if available (handles subgraph contexts), fallback to app graph
const activeGraph = comfyApp.canvas?.graph
Expand All @@ -44,7 +39,10 @@ function useVueNodeLifecycleIndividual() {
const nodes = activeGraph._nodes.map((node: LGraphNode) => ({
id: node.id.toString(),
pos: [node.pos[0], node.pos[1]] as [number, number],
size: [node.size[0], node.size[1]] as [number, number]
size: [node.size[0], removeNodeTitleHeight(node.size[1])] as [
number,
number
]
}))
layoutStore.initializeFromLiteGraph(nodes)

Expand Down
51 changes: 46 additions & 5 deletions src/composables/node/useNodePricing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,21 @@ const calculateRunwayDurationPrice = (node: LGraphNode): string => {
return `$${cost}/Run`
}

const makeOmniProDurationCalculator =
(pricePerSecond: number): PricingFunction =>
(node: LGraphNode): string => {
const durationWidget = node.widgets?.find(
(w) => w.name === 'duration'
) as IComboWidget
if (!durationWidget) return `$${pricePerSecond.toFixed(3)}/second`

const seconds = parseFloat(String(durationWidget.value))
if (!Number.isFinite(seconds)) return `$${pricePerSecond.toFixed(3)}/second`

const cost = pricePerSecond * seconds
return `$${cost.toFixed(2)}/Run`
}

const pixversePricingCalculator = (node: LGraphNode): string => {
const durationWidget = node.widgets?.find(
(w) => w.name === 'duration_seconds'
Expand Down Expand Up @@ -131,18 +146,25 @@ const byteDanceVideoPricingCalculator = (node: LGraphNode): string => {
'720p': [0.51, 0.56],
'1080p': [1.18, 1.22]
},
'seedance-1-0-pro-fast': {
'480p': [0.09, 0.1],
'720p': [0.21, 0.23],
'1080p': [0.47, 0.49]
},
'seedance-1-0-lite': {
'480p': [0.17, 0.18],
'720p': [0.37, 0.41],
'1080p': [0.85, 0.88]
}
}

const modelKey = model.includes('seedance-1-0-pro')
? 'seedance-1-0-pro'
: model.includes('seedance-1-0-lite')
? 'seedance-1-0-lite'
: ''
const modelKey = model.includes('seedance-1-0-pro-fast')
? 'seedance-1-0-pro-fast'
: model.includes('seedance-1-0-pro')
? 'seedance-1-0-pro'
: model.includes('seedance-1-0-lite')
? 'seedance-1-0-lite'
: ''

const resKey = resolution.includes('1080')
? '1080p'
Expand Down Expand Up @@ -699,6 +721,21 @@ const apiNodeCosts: Record<string, { displayPrice: string | PricingFunction }> =
KlingVirtualTryOnNode: {
displayPrice: '$0.07/Run'
},
KlingOmniProTextToVideoNode: {
displayPrice: makeOmniProDurationCalculator(0.112)
},
KlingOmniProFirstLastFrameNode: {
displayPrice: makeOmniProDurationCalculator(0.112)
},
KlingOmniProImageToVideoNode: {
displayPrice: makeOmniProDurationCalculator(0.112)
},
KlingOmniProVideoToVideoNode: {
displayPrice: makeOmniProDurationCalculator(0.168)
},
KlingOmniProEditVideoNode: {
displayPrice: '$0.168/second'
},
LumaImageToVideoNode: {
displayPrice: (node: LGraphNode): string => {
// Same pricing as LumaVideoNode per CSV
Expand Down Expand Up @@ -1873,6 +1910,10 @@ export const useNodePricing = () => {
KlingDualCharacterVideoEffectNode: ['mode', 'model_name', 'duration'],
KlingSingleImageVideoEffectNode: ['effect_scene'],
KlingStartEndFrameNode: ['mode', 'model_name', 'duration'],
KlingOmniProTextToVideoNode: ['duration'],
KlingOmniProFirstLastFrameNode: ['duration'],
KlingOmniProImageToVideoNode: ['duration'],
KlingOmniProVideoToVideoNode: ['duration'],
MinimaxHailuoVideoNode: ['resolution', 'duration'],
OpenAIDalle3: ['size', 'quality'],
OpenAIDalle2: ['size', 'n'],
Expand Down
Loading
Loading