Compare commits

...

8 Commits

Author SHA1 Message Date
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
9 changed files with 157 additions and 29 deletions

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

@@ -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(() => {