Compare commits

...

9 Commits

Author SHA1 Message Date
AustinMroz
91a1071583 Disable control widgets on link to parent (#8112)
When a link is made to a widget with control (like seed) , the control
widget can no longer be used to update it's state. To better communicate
this, the control widgets are now given the disabled property when their
parent widget is linked.

| Before | After |
| ------ | ----- |
| <img width="360" alt="before"
src="https://github.com/user-attachments/assets/9b6c6c02-2481-486a-bb07-c19d00abe36d"
/> | <img width="360" alt="after"
src="https://github.com/user-attachments/assets/837000ac-8a12-4d51-879b-a58e0577ff10"
/>|

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8112-Disable-control-widgets-on-link-to-parent-2ea6d73d365081afad77db6c5f56e085)
by [Unito](https://www.unito.io)

---------

Co-authored-by: github-actions <github-actions@github.com>
2026-01-19 21:51:52 +00:00
Comfy Org PR Bot
4bf6a19600 1.37.11 (#8161)
Patch version increment to 1.37.11

**Base branch:** `core/1.37`

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8161-1-37-11-2ed6d73d3650818c886beb12ee781f8b)
by [Unito](https://www.unito.io)

Co-authored-by: comfy-pr-bot <172744619+comfy-pr-bot@users.noreply.github.com>
2026-01-19 12:39:10 -08:00
Comfy Org PR Bot
ed73ae7a61 [backport core/1.37] feat: make subgraphs blueprints appear higher in node library sidebar (#8141)
Backport of #8140 to `core/1.37`

Automatically created by backport workflow.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8141-backport-core-1-37-feat-make-subgraphs-blueprints-appear-higher-in-node-library-sideba-2ec6d73d365081afaa3fd9566b0d84cb)
by [Unito](https://www.unito.io)

Co-authored-by: Christian Byrne <cbyrne@comfy.org>
2026-01-17 21:27:44 -07:00
Comfy Org PR Bot
4f059c993d [backport core/1.37] Fix copypasted primitives inside subgraphs (#8095)
Backport of #8094 to `core/1.37`

Automatically created by backport workflow.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8095-backport-core-1-37-Fix-copypasted-primitives-inside-subgraphs-2ea6d73d365081148237fee347907dbf)
by [Unito](https://www.unito.io)

Co-authored-by: AustinMroz <austin@comfy.org>
2026-01-15 21:43:40 -08:00
Comfy Org PR Bot
64be98873b [backport core/1.37] fix: prevent Record Audio waveform from overflowing node bounds (#8081)
Backport of #8070 to `core/1.37`

Automatically created by backport workflow.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8081-backport-core-1-37-fix-prevent-Record-Audio-waveform-from-overflowing-node-bounds-2e96d73d365081fc85eddf301c4624e1)
by [Unito](https://www.unito.io)

Co-authored-by: Terry Jia <terryjia88@gmail.com>
2026-01-15 14:02:18 -08:00
Comfy Org PR Bot
a963d587cf [backport core/1.37] Fix: Update for Image Widget test (#8061)
Backport of #8031 to `core/1.37`

Automatically created by backport workflow.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8061-backport-core-1-37-Fix-Update-for-Image-Widget-test-2e96d73d365081908903d331043cac80)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Alexander Brown <drjkl@comfy.org>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Alexander Brown <448862+DrJKL@users.noreply.github.com>
2026-01-14 21:03:36 -08:00
Comfy Org PR Bot
8ce1f6ccd3 [backport core/1.37] feat(price-badges): add ByteDance SeeDance 1.5 prices (#8058)
Backport of #8046 to `core/1.37`

Automatically created by backport workflow.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8058-backport-core-1-37-feat-price-badges-add-ByteDance-SeeDance-1-5-prices-2e96d73d36508162aac4c2f410f04284)
by [Unito](https://www.unito.io)

Co-authored-by: Alexander Piskun <13381981+bigcat88@users.noreply.github.com>
2026-01-14 16:39:22 -08:00
Comfy Org PR Bot
af50d48dd8 [backport core/1.37] [API Nodes] add price badges for Meshy 3D nodes (#8048)
Backport of #7966 to `core/1.37`

Automatically created by backport workflow.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8048-backport-core-1-37-API-Nodes-add-price-badges-for-Meshy-3D-nodes-2e86d73d3650812782defb4875111446)
by [Unito](https://www.unito.io)

Co-authored-by: Alexander Piskun <13381981+bigcat88@users.noreply.github.com>
2026-01-14 12:43:58 -08:00
Comfy Org PR Bot
c6dfaefbd3 [backport core/1.37] fix: version mismatch warning appearing in Playwright tests despite DisableWarnings setting (#8038)
Backport of #8036 to `core/1.37`

Automatically created by backport workflow.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8038-backport-core-1-37-fix-version-mismatch-warning-appearing-in-Playwright-tests-despite--2e86d73d3650818c81bacf39eeb54e13)
by [Unito](https://www.unito.io)

Co-authored-by: Christian Byrne <cbyrne@comfy.org>
2026-01-13 20:43:23 -07:00
12 changed files with 163 additions and 29 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 58 KiB

View File

@@ -194,7 +194,10 @@ test.describe('Image widget', () => {
const comboEntry = comfyPage.page.getByRole('menuitem', {
name: 'image32x32.webp'
})
await comboEntry.click({ noWaitAfter: true })
await comboEntry.click()
// Stabilization for the image swap
await comfyPage.nextFrame()
// Expect the image preview to change automatically
await expect(comfyPage.canvas).toHaveScreenshot(

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 46 KiB

View File

@@ -1,7 +1,7 @@
{
"name": "@comfyorg/comfyui-frontend",
"private": true,
"version": "1.37.10",
"version": "1.37.11",
"type": "module",
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
"homepage": "https://comfy.org",

View File

@@ -2,6 +2,17 @@ import { formatCreditsFromUsd } from '@/base/credits/comfyCredits'
import type { INodeInputSlot, LGraphNode } from '@/lib/litegraph/src/litegraph'
import type { IComboWidget } from '@/lib/litegraph/src/types/widgets'
/**
* Meshy credit pricing constant.
* 1 Meshy credit = $0.04 USD
* Change this value to update all Meshy node prices.
*/
const MESHY_CREDIT_PRICE_USD = 0.04
/** Convert Meshy credits to USD */
const meshyCreditsToUsd = (credits: number): number =>
credits * MESHY_CREDIT_PRICE_USD
const DEFAULT_NUMBER_OPTIONS: Intl.NumberFormatOptions = {
minimumFractionDigits: 0,
maximumFractionDigits: 0
@@ -209,13 +220,24 @@ const byteDanceVideoPricingCalculator = (node: LGraphNode): string => {
const resolutionWidget = node.widgets?.find(
(w) => w.name === 'resolution'
) as IComboWidget
const generateAudioWidget = node.widgets?.find(
(w) => w.name === 'generate_audio'
) as IComboWidget | undefined
if (!modelWidget || !durationWidget || !resolutionWidget) return 'Token-based'
const model = String(modelWidget.value).toLowerCase()
const resolution = String(resolutionWidget.value).toLowerCase()
const seconds = parseFloat(String(durationWidget.value))
const generateAudio =
generateAudioWidget &&
String(generateAudioWidget.value).toLowerCase() === 'true'
const priceByModel: Record<string, Record<string, [number, number]>> = {
'seedance-1-5-pro': {
'480p': [0.12, 0.12],
'720p': [0.26, 0.26],
'1080p': [0.58, 0.59]
},
'seedance-1-0-pro': {
'480p': [0.23, 0.24],
'720p': [0.51, 0.56],
@@ -233,13 +255,15 @@ const byteDanceVideoPricingCalculator = (node: LGraphNode): string => {
}
}
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 modelKey = model.includes('seedance-1-5-pro')
? 'seedance-1-5-pro'
: 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'
@@ -255,8 +279,10 @@ const byteDanceVideoPricingCalculator = (node: LGraphNode): string => {
const [min10s, max10s] = baseRange
const scale = seconds / 10
const minCost = min10s * scale
const maxCost = max10s * scale
const audioMultiplier =
modelKey === 'seedance-1-5-pro' && generateAudio ? 2 : 1
const minCost = min10s * scale * audioMultiplier
const maxCost = max10s * scale * audioMultiplier
if (minCost === maxCost) return formatCreditsLabel(minCost)
return formatCreditsRangeLabel(minCost, maxCost)
@@ -525,6 +551,54 @@ const calculateTripo3DGenerationPrice = (
return formatCreditsLabel(dollars)
}
/**
* Meshy Image to 3D pricing calculator.
* Pricing based on should_texture widget:
* - Without texture: 20 credits
* - With texture: 30 credits
*/
const calculateMeshyImageToModelPrice = (node: LGraphNode): string => {
const shouldTextureWidget = node.widgets?.find(
(w) => w.name === 'should_texture'
) as IComboWidget
if (!shouldTextureWidget) {
return formatCreditsRangeLabel(
meshyCreditsToUsd(20),
meshyCreditsToUsd(30),
{ note: '(varies with texture)' }
)
}
const shouldTexture = String(shouldTextureWidget.value).toLowerCase()
const credits = shouldTexture === 'true' ? 30 : 20
return formatCreditsLabel(meshyCreditsToUsd(credits))
}
/**
* Meshy Multi-Image to 3D pricing calculator.
* Pricing based on should_texture widget:
* - Without texture: 5 credits
* - With texture: 15 credits
*/
const calculateMeshyMultiImageToModelPrice = (node: LGraphNode): string => {
const shouldTextureWidget = node.widgets?.find(
(w) => w.name === 'should_texture'
) as IComboWidget
if (!shouldTextureWidget) {
return formatCreditsRangeLabel(
meshyCreditsToUsd(5),
meshyCreditsToUsd(15),
{ note: '(varies with texture)' }
)
}
const shouldTexture = String(shouldTextureWidget.value).toLowerCase()
const credits = shouldTexture === 'true' ? 15 : 5
return formatCreditsLabel(meshyCreditsToUsd(credits))
}
/**
* Static pricing data for API nodes, now supporting both strings and functions
*/
@@ -1812,6 +1886,27 @@ const apiNodeCosts: Record<string, { displayPrice: string | PricingFunction }> =
TripoRefineNode: {
displayPrice: formatCreditsLabel(0.3)
},
MeshyTextToModelNode: {
displayPrice: formatCreditsLabel(meshyCreditsToUsd(20))
},
MeshyRefineNode: {
displayPrice: formatCreditsLabel(meshyCreditsToUsd(10))
},
MeshyImageToModelNode: {
displayPrice: calculateMeshyImageToModelPrice
},
MeshyMultiImageToModelNode: {
displayPrice: calculateMeshyMultiImageToModelPrice
},
MeshyRigModelNode: {
displayPrice: formatCreditsLabel(meshyCreditsToUsd(5))
},
MeshyAnimateModelNode: {
displayPrice: formatCreditsLabel(meshyCreditsToUsd(3))
},
MeshyTextureNode: {
displayPrice: formatCreditsLabel(meshyCreditsToUsd(10))
},
// Google/Gemini nodes
GeminiNode: {
displayPrice: (node: LGraphNode): string => {
@@ -2527,6 +2622,9 @@ export const useNodePricing = () => {
'animate_in_place'
],
TripoTextureNode: ['texture_quality'],
// Meshy nodes
MeshyImageToModelNode: ['should_texture'],
MeshyMultiImageToModelNode: ['should_texture'],
// Google/Gemini nodes
GeminiNode: ['model'],
GeminiImage2Node: ['resolution'],
@@ -2540,9 +2638,24 @@ export const useNodePricing = () => {
'sequential_image_generation',
'max_images'
],
ByteDanceTextToVideoNode: ['model', 'duration', 'resolution'],
ByteDanceImageToVideoNode: ['model', 'duration', 'resolution'],
ByteDanceFirstLastFrameNode: ['model', 'duration', 'resolution'],
ByteDanceTextToVideoNode: [
'model',
'duration',
'resolution',
'generate_audio'
],
ByteDanceImageToVideoNode: [
'model',
'duration',
'resolution',
'generate_audio'
],
ByteDanceFirstLastFrameNode: [
'model',
'duration',
'resolution',
'generate_audio'
],
ByteDanceImageReferenceNode: ['model', 'duration', 'resolution'],
WanTextToVideoApi: ['duration', 'size'],
WanImageToVideoApi: ['duration', 'resolution'],

View File

@@ -8,6 +8,7 @@ import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMuta
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
import { LayoutSource } from '@/renderer/core/layout/types'
import { removeNodeTitleHeight } from '@/renderer/core/layout/utils/nodeSizeUtil'
import { forEachNode } from '@/utils/graphTraversalUtil'
import { CanvasPointer } from './CanvasPointer'
import type { ContextMenu } from './ContextMenu'
@@ -4041,6 +4042,8 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
layoutStore.batchUpdateNodeBounds(newPositions)
this.selectItems(created)
forEachNode(graph, (n) => n.onGraphConfigured?.())
forEachNode(graph, (n) => n.onAfterGraphConfigured?.())
graph.afterChange()
this.emitAfterChange()

View File

@@ -1,5 +1,5 @@
import { whenever } from '@vueuse/core'
import { computed, onMounted } from 'vue'
import { computed, nextTick, onMounted } from 'vue'
import { useI18n } from 'vue-i18n'
import { useToastStore } from './toastStore'
@@ -65,9 +65,12 @@ export function useFrontendVersionMismatchWarning(
versionCompatibilityStore.dismissWarning()
}
onMounted(() => {
onMounted(async () => {
// Only set up the watcher if immediate is true
if (immediate) {
// Wait for next tick to ensure reactive updates from settings load have propagated
await nextTick()
whenever(
() => versionCompatibilityStore.shouldShowWarning,
() => {

View File

@@ -88,11 +88,16 @@ export const useVersionCompatibilityStore = defineStore(
return Date.now() < dismissedUntil
})
const warningsDisabled = computed(() =>
settingStore.get('Comfy.VersionCompatibility.DisableWarnings')
)
const shouldShowWarning = computed(() => {
const warningsDisabled = settingStore.get(
'Comfy.VersionCompatibility.DisableWarnings'
return (
hasVersionMismatch.value &&
!isDismissed.value &&
!warningsDisabled.value
)
return hasVersionMismatch.value && !isDismissed.value && !warningsDisabled
})
const warningMessage = computed(() => {

View File

@@ -12,11 +12,11 @@
</div>
<div
v-if="isRecording || isPlaying || recordedURL"
class="flex h-14 w-full items-center gap-4 rounded-lg px-4 bg-node-component-surface text-text-secondary"
class="flex h-14 w-full min-w-0 items-center gap-2 rounded-lg px-3 bg-node-component-surface text-text-secondary"
>
<!-- Recording Status -->
<div class="flex min-w-30 items-center gap-2">
<span class="min-w-20 text-xs">
<div class="flex shrink-0 items-center gap-1">
<span class="text-xs">
{{
isRecording
? t('g.listening', 'Listening...')
@@ -27,11 +27,11 @@
: ''
}}
</span>
<span class="min-w-10 text-sm">{{ formatTime(timer) }}</span>
<span class="text-sm">{{ formatTime(timer) }}</span>
</div>
<!-- Waveform Visualization -->
<div class="flex h-8 flex-1 items-center gap-2 overflow-x-clip">
<div class="flex h-8 min-w-0 flex-1 items-center gap-2 overflow-hidden">
<div
v-for="(bar, index) in waveformBars"
:key="index"
@@ -45,7 +45,7 @@
<button
v-if="isRecording"
:title="t('g.stopRecording', 'Stop Recording')"
class="flex size-8 animate-pulse items-center justify-center rounded-full border-0 bg-smoke-500/33 transition-colors"
class="flex shrink-0 size-8 animate-pulse items-center justify-center rounded-full border-0 bg-smoke-500/33 transition-colors"
@click="handleStopRecording"
>
<div class="size-2.5 rounded-sm bg-danger-100" />
@@ -54,7 +54,7 @@
<button
v-else-if="!isRecording && recordedURL && !isPlaying"
:title="t('g.playRecording') || 'Play Recording'"
class="flex size-8 items-center justify-center rounded-full border-0 bg-smoke-500/33 transition-colors"
class="flex shrink-0 size-8 items-center justify-center rounded-full border-0 bg-smoke-500/33 transition-colors"
@click="handlePlayRecording"
>
<i class="text-text-secondary icon-[lucide--play] size-4" />
@@ -63,7 +63,7 @@
<button
v-else-if="isPlaying"
:title="t('g.stopPlayback') || 'Stop Playback'"
class="flex size-8 items-center justify-center rounded-full border-0 bg-smoke-500/33 transition-colors"
class="flex shrink-0 size-8 items-center justify-center rounded-full border-0 bg-smoke-500/33 transition-colors"
@click="handleStopPlayback"
>
<i class="text-text-secondary icon-[lucide--square] size-4" />

View File

@@ -139,6 +139,9 @@ export function addValueControlWidgets(
'Allows the linked widget to be changed automatically, for example randomizing the noise seed.'
valueControl[IS_CONTROL_WIDGET] = true
updateControlWidgetLabel(valueControl)
Object.defineProperty(valueControl, 'disabled', {
get: () => targetWidget.computedDisabled
})
const widgets: [IComboWidget, ...IStringWidget[]] = [valueControl]
const isCombo = isComboWidget(targetWidget)
@@ -160,6 +163,9 @@ export function addValueControlWidgets(
updateControlWidgetLabel(comboFilter)
comboFilter.tooltip =
"Allows for filtering the list of values when changing the value via the control generate mode. Allows for RegEx matches in the format /abc/ to only filter to values containing 'abc'."
Object.defineProperty(comboFilter, 'disabled', {
get: () => targetWidget.computedDisabled
})
widgets.push(comboFilter)
}

View File

@@ -299,9 +299,10 @@ export const useNodeDefStore = defineStore('nodeDef', () => {
const nodeDefs = computed(() => {
const subgraphStore = useSubgraphStore()
// Blueprints first for discoverability in the node library sidebar
return [
...Object.values(nodeDefsByName.value),
...subgraphStore.subgraphBlueprints
...subgraphStore.subgraphBlueprints,
...Object.values(nodeDefsByName.value)
]
})
const nodeDataTypes = computed(() => {