Merge latest main (v1.29.2) into bl-selective-snapshot-update

This commit is contained in:
bymyself
2025-10-14 11:56:10 -07:00
27 changed files with 340 additions and 21 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 47 KiB

View File

@@ -60,7 +60,6 @@ async function getInputLinkDetails(
)
}
// Test helpers to reduce repetition across cases
function slotLocator(
page: Page,
nodeId: NodeId,
@@ -789,6 +788,45 @@ test.describe('Vue Node Link Interaction', () => {
})
})
test('should batch disconnect all links with ctrl+alt+click on slot', async ({
comfyPage
}) => {
const clipNode = (await comfyPage.getNodeRefsByType('CLIPTextEncode'))[0]
const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0]
expect(clipNode && samplerNode).toBeTruthy()
await connectSlots(
comfyPage.page,
{ nodeId: clipNode.id, index: 0 },
{ nodeId: samplerNode.id, index: 1 },
() => comfyPage.nextFrame()
)
await connectSlots(
comfyPage.page,
{ nodeId: clipNode.id, index: 0 },
{ nodeId: samplerNode.id, index: 2 },
() => comfyPage.nextFrame()
)
const clipOutput = await clipNode.getOutput(0)
expect(await clipOutput.getLinkCount()).toBe(2)
const clipOutputSlot = slotLocator(comfyPage.page, clipNode.id, 0, false)
await clipOutputSlot.dispatchEvent('pointerdown', {
button: 0,
buttons: 1,
ctrlKey: true,
altKey: true,
shiftKey: false,
bubbles: true,
cancelable: true
})
await comfyPage.nextFrame()
expect(await clipOutput.getLinkCount()).toBe(0)
})
test.describe('Release actions (Shift-drop)', () => {
test('Context menu opens and endpoint is pinned on Shift-drop', async ({
comfyPage,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 78 KiB

View File

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

View File

@@ -89,6 +89,21 @@
--color-node-hover-100: rgb(from var(--color-charcoal-100) r g b/ 0.15);
--color-node-hover-200: rgb(from var(--color-charcoal-100) r g b/ 0.1);
--color-modal-tag: rgb(from var(--color-gray-400) r g b/ 0.4);
--color-alpha-charcoal-600-30: color-mix(
in srgb,
var(--color-charcoal-600) 30%,
transparent
);
--color-alpha-stone-100-20: color-mix(
in srgb,
var(--color-stone-100) 20%,
transparent
);
--color-alpha-gray-500-50: color-mix(
in srgb,
var(--color-gray-500) 50%,
transparent
);
/* PrimeVue pulled colors */
--color-muted: var(--p-text-muted-color);
@@ -155,6 +170,8 @@
from var(--color-zinc-500) r g b / 10%
);
--node-component-widget-skeleton-surface: var(--color-zinc-300);
--node-component-disabled: var(--color-alpha-stone-100-20);
--node-icon-disabled: var(--color-alpha-gray-500-50);
--node-stroke: var(--color-gray-400);
--node-stroke-selected: var(--color-accent-primary);
--node-stroke-error: var(--color-error);
@@ -184,6 +201,8 @@
--node-component-tooltip-border: var(--color-slate-300);
--node-component-tooltip-surface: var(--color-charcoal-800);
--node-component-widget-skeleton-surface: var(--color-zinc-800);
--node-component-disabled: var(--color-alpha-charcoal-600-30);
--node-icon-disabled: var(--color-alpha-stone-100-20);
--node-stroke: var(--color-stone-200);
--node-stroke-selected: var(--color-pure-white);
--node-stroke-error: var(--color-error);
@@ -224,6 +243,8 @@
--color-node-component-widget-skeleton-surface: var(
--node-component-widget-skeleton-surface
);
--color-node-component-disabled: var(--node-component-disabled);
--color-node-icon-disabled: var(--node-icon-disabled);
--color-node-stroke: var(--node-stroke);
--color-node-stroke-selected: var(--node-stroke-selected);
--color-node-stroke-error: var(--node-stroke-error);

View File

@@ -178,8 +178,9 @@ app.registerExtension({
audioUIWidget.options.canvasOnly = true
const onAudioWidgetUpdate = () => {
if (typeof audioWidget.value !== 'string') return
audioUIWidget.element.src = api.apiURL(
getResourceURL(...splitFilePath(audioWidget.value as string))
getResourceURL(...splitFilePath(audioWidget.value))
)
}
// Initially load default audio file to audioUIWidget.

View File

@@ -832,6 +832,11 @@
"guidance": {
"name": "guidance"
}
},
"outputs": {
"0": {
"tooltip": null
}
}
},
"CLIPTextEncodeHiDream": {
@@ -871,6 +876,11 @@
"mt5xl": {
"name": "mt5xl"
}
},
"outputs": {
"0": {
"tooltip": null
}
}
},
"CLIPTextEncodeLumina2": {
@@ -937,6 +947,11 @@
"empty_padding": {
"name": "empty_padding"
}
},
"outputs": {
"0": {
"tooltip": null
}
}
},
"CLIPTextEncodeSDXL": {
@@ -1475,10 +1490,12 @@
},
"outputs": {
"0": {
"name": "positive"
"name": "positive",
"tooltip": null
},
"1": {
"name": "negative"
"name": "negative",
"tooltip": null
}
}
},
@@ -1974,6 +1991,11 @@
"batch_size": {
"name": "batch_size"
}
},
"outputs": {
"0": {
"tooltip": null
}
}
},
"EmptyHunyuanLatentVideo": {
@@ -1991,6 +2013,11 @@
"batch_size": {
"name": "batch_size"
}
},
"outputs": {
"0": {
"tooltip": null
}
}
},
"EmptyImage": {
@@ -2113,6 +2140,11 @@
"batch_size": {
"name": "batch_size"
}
},
"outputs": {
"0": {
"tooltip": null
}
}
},
"Epsilon Scaling": {
@@ -2200,6 +2232,11 @@
"conditioning": {
"name": "conditioning"
}
},
"outputs": {
"0": {
"tooltip": null
}
}
},
"FluxGuidance": {
@@ -2211,6 +2248,11 @@
"guidance": {
"name": "guidance"
}
},
"outputs": {
"0": {
"tooltip": null
}
}
},
"FluxKontextImageScale": {
@@ -2220,6 +2262,11 @@
"image": {
"name": "image"
}
},
"outputs": {
"0": {
"tooltip": null
}
}
},
"FluxKontextMaxImageNode": {
@@ -2272,6 +2319,11 @@
"reference_latents_method": {
"name": "reference_latents_method"
}
},
"outputs": {
"0": {
"tooltip": null
}
}
},
"FluxKontextProImageNode": {
@@ -2629,6 +2681,10 @@
"name": "files",
"tooltip": "Optional file(s) to use as context for the model. Accepts inputs from the Gemini Generate Content Input Files node."
},
"aspect_ratio": {
"name": "aspect_ratio",
"tooltip": "Defaults to matching the output image size to that of your input image, or otherwise generates 1:1 squares."
},
"control_after_generate": {
"name": "control after generate"
}
@@ -2870,10 +2926,12 @@
},
"outputs": {
"0": {
"name": "positive"
"name": "positive",
"tooltip": null
},
"1": {
"name": "latent"
"name": "latent",
"tooltip": null
}
}
},
@@ -2895,13 +2953,16 @@
},
"outputs": {
"0": {
"name": "positive"
"name": "positive",
"tooltip": null
},
"1": {
"name": "negative"
"name": "negative",
"tooltip": null
},
"2": {
"name": "latent"
"name": "latent",
"tooltip": null
}
}
},
@@ -3478,6 +3539,11 @@
"image": {
"name": "image"
}
},
"outputs": {
"0": {
"tooltip": null
}
}
},
"ImageYUVToRGB": {
@@ -3582,6 +3648,11 @@
"alpha": {
"name": "alpha"
}
},
"outputs": {
"0": {
"tooltip": null
}
}
},
"KarrasScheduler": {
@@ -4209,6 +4280,11 @@
"samples2": {
"name": "samples2"
}
},
"outputs": {
"0": {
"tooltip": null
}
}
},
"LatentApplyOperation": {
@@ -4220,6 +4296,11 @@
"operation": {
"name": "operation"
}
},
"outputs": {
"0": {
"tooltip": null
}
}
},
"LatentApplyOperationCFG": {
@@ -4231,6 +4312,11 @@
"operation": {
"name": "operation"
}
},
"outputs": {
"0": {
"tooltip": null
}
}
},
"LatentBatch": {
@@ -4242,6 +4328,11 @@
"samples2": {
"name": "samples2"
}
},
"outputs": {
"0": {
"tooltip": null
}
}
},
"LatentBatchSeedBehavior": {
@@ -4253,6 +4344,11 @@
"seed_behavior": {
"name": "seed_behavior"
}
},
"outputs": {
"0": {
"tooltip": null
}
}
},
"LatentBlend": {
@@ -4324,6 +4420,11 @@
"dim": {
"name": "dim"
}
},
"outputs": {
"0": {
"tooltip": null
}
}
},
"LatentCrop": {
@@ -4361,6 +4462,11 @@
"amount": {
"name": "amount"
}
},
"outputs": {
"0": {
"tooltip": null
}
}
},
"LatentFlip": {
@@ -4400,6 +4506,11 @@
"ratio": {
"name": "ratio"
}
},
"outputs": {
"0": {
"tooltip": null
}
}
},
"LatentMultiply": {
@@ -4411,6 +4522,11 @@
"multiplier": {
"name": "multiplier"
}
},
"outputs": {
"0": {
"tooltip": null
}
}
},
"LatentOperationSharpen": {
@@ -4425,6 +4541,11 @@
"alpha": {
"name": "alpha"
}
},
"outputs": {
"0": {
"tooltip": null
}
}
},
"LatentOperationTonemapReinhard": {
@@ -4433,6 +4554,11 @@
"multiplier": {
"name": "multiplier"
}
},
"outputs": {
"0": {
"tooltip": null
}
}
},
"LatentRotate": {
@@ -4455,6 +4581,11 @@
"samples2": {
"name": "samples2"
}
},
"outputs": {
"0": {
"tooltip": null
}
}
},
"LatentUpscale": {
@@ -7701,6 +7832,10 @@
"name": "video",
"tooltip": "The reference video used to generate the output video. Must be at least 5 seconds long. Videos longer than 5s will be automatically trimmed. Only MP4 format supported."
},
"steps": {
"name": "steps",
"tooltip": "Number of inference steps"
},
"control_type": {
"name": "control_type"
},
@@ -8106,6 +8241,11 @@
"upscale_method": {
"name": "upscale_method"
}
},
"outputs": {
"0": {
"tooltip": null
}
}
},
"PerpNeg": {
@@ -8407,7 +8547,7 @@
},
"mask": {
"name": "mask",
"tooltip": "Use the mask to define areas in the video to replace"
"tooltip": "Use the mask to define areas in the video to replace."
},
"prompt_text": {
"name": "prompt_text"
@@ -8418,6 +8558,10 @@
"seed": {
"name": "seed"
},
"region_to_modify": {
"name": "region_to_modify",
"tooltip": "Plaintext description of the object / region to modify."
},
"control_after_generate": {
"name": "control after generate"
}
@@ -8635,6 +8779,14 @@
"mode": {
"name": "mode"
}
},
"outputs": {
"0": {
"tooltip": null
},
"1": {
"tooltip": null
}
}
},
"Preview3D": {
@@ -10248,6 +10400,11 @@
"rescaling_scale": {
"name": "rescaling_scale"
}
},
"outputs": {
"0": {
"tooltip": null
}
}
},
"SkipLayerGuidanceDiTSimple": {
@@ -10269,6 +10426,11 @@
"end_percent": {
"name": "end_percent"
}
},
"outputs": {
"0": {
"tooltip": null
}
}
},
"SkipLayerGuidanceSD3": {
@@ -10290,6 +10452,11 @@
"end_percent": {
"name": "end_percent"
}
},
"outputs": {
"0": {
"tooltip": null
}
}
},
"SolidMask": {
@@ -10329,6 +10496,14 @@
"image": {
"name": "image"
}
},
"outputs": {
"0": {
"tooltip": null
},
"1": {
"tooltip": null
}
}
},
"SplitSigmas": {
@@ -11152,6 +11327,11 @@
"name": "image_interleave",
"tooltip": "How much the image influences things vs the text prompt. Higher number means more influence from the text prompt."
}
},
"outputs": {
"0": {
"tooltip": null
}
}
},
"TextEncodeQwenImageEdit": {
@@ -11379,6 +11559,11 @@
"clip_name3": {
"name": "clip_name3"
}
},
"outputs": {
"0": {
"tooltip": null
}
}
},
"TripoConversionNode": {
@@ -11760,6 +11945,11 @@
"model_name": {
"name": "model_name"
}
},
"outputs": {
"0": {
"tooltip": null
}
}
},
"USOStyleReference": {

View File

@@ -595,6 +595,13 @@ export function useSlotLinkInteraction({
event.altKey &&
!event.shiftKey
const shouldBatchDisconnectOutputLinks =
isOutputSlot &&
hasExistingOutputLink &&
ctrlOrMeta &&
event.altKey &&
!event.shiftKey
const existingInputLink =
isInputSlot && inputLinkId != null
? graph.getLink(inputLinkId)
@@ -604,6 +611,14 @@ export function useSlotLinkInteraction({
resolvedNode.disconnectInput(index, true)
}
if (shouldBatchDisconnectOutputLinks && resolvedNode) {
resolvedNode.disconnectOutput(index)
app.canvas?.setDirty(true, true)
event.preventDefault()
event.stopPropagation()
return
}
const baseDirection = isInputSlot
? inputSlot?.dir ?? LinkDirection.LEFT
: outputSlot?.dir ?? LinkDirection.RIGHT

View File

@@ -10,6 +10,7 @@ import {
filterWidgetProps
} from '@/utils/widgetPropFilter'
import { useNumberWidgetButtonPt } from '../composables/useNumberWidgetButtonPt'
import { WidgetInputBaseClass } from './layout'
import WidgetLayoutField from './layout/WidgetLayoutField.vue'
@@ -65,17 +66,24 @@ const useGrouping = computed(() => {
// Check if increment/decrement buttons should be disabled due to precision limits
const buttonsDisabled = computed(() => {
const currentValue = localValue.value || 0
return !Number.isSafeInteger(currentValue)
const currentValue = localValue.value ?? 0
return (
!Number.isFinite(currentValue) ||
Math.abs(currentValue) > Number.MAX_SAFE_INTEGER
)
})
// Tooltip message for disabled buttons
const buttonTooltip = computed(() => {
if (buttonsDisabled.value) {
return 'Increment/decrement disabled: value exceeds JavaScript precision limit (±2^53)'
}
return null
})
const inputNumberPt = useNumberWidgetButtonPt({
roundedLeft: true,
roundedRight: true
})
</script>
<template>
@@ -84,19 +92,14 @@ const buttonTooltip = computed(() => {
<InputNumber
v-model="localValue"
v-bind="filteredProps"
:show-buttons="!buttonsDisabled"
button-layout="horizontal"
size="small"
:step="stepValue"
:use-grouping="useGrouping"
:class="cn(WidgetInputBaseClass, 'w-full text-xs')"
:aria-label="widget.name"
:pt="{
incrementButton:
'!rounded-r-lg bg-transparent border-none hover:bg-zinc-500/30 active:bg-zinc-500/40',
decrementButton:
'!rounded-l-lg bg-transparent border-none hover:bg-zinc-500/30 active:bg-zinc-500/40'
}"
:show-buttons="!buttonsDisabled"
:pt="inputNumberPt"
@update:model-value="onChange"
>
<template #incrementicon>
@@ -120,4 +123,9 @@ const buttonTooltip = computed(() => {
margin: 1px 0;
box-shadow: none;
}
:deep(.p-inputnumber-button.p-disabled .pi),
:deep(.p-inputnumber-button.p-disabled .p-icon) {
color: var(--color-node-icon-disabled) !important;
}
</style>

View File

@@ -24,6 +24,8 @@
size="small"
pt:pc-input-text:root="min-w-full bg-transparent border-none text-center"
class="w-16"
:show-buttons="!buttonsDisabled"
:pt="sliderNumberPt"
@update:model-value="handleNumberInputUpdate"
/>
</div>
@@ -43,6 +45,7 @@ import {
filterWidgetProps
} from '@/utils/widgetPropFilter'
import { useNumberWidgetButtonPt } from '../composables/useNumberWidgetButtonPt'
import { WidgetInputBaseClass } from './layout'
import WidgetLayoutField from './layout/WidgetLayoutField.vue'
@@ -103,4 +106,24 @@ const stepValue = computed(() => {
// precision 1 → 0.1, precision 2 → 0.01, etc.
return 1 / Math.pow(10, precision.value)
})
const buttonsDisabled = computed(() => {
const currentValue = localValue.value ?? 0
return (
!Number.isFinite(currentValue) ||
Math.abs(currentValue) > Number.MAX_SAFE_INTEGER
)
})
const sliderNumberPt = useNumberWidgetButtonPt({
roundedLeft: true,
roundedRight: true
})
</script>
<style scoped>
:deep(.p-inputnumber-button.p-disabled .pi),
:deep(.p-inputnumber-button.p-disabled .p-icon) {
color: var(--color-node-icon-disabled) !important;
}
</style>

View File

@@ -0,0 +1,23 @@
const sharedButtonClasses =
'!inline-flex !items-center !justify-center !border-0 !bg-transparent text-inherit transition-colors duration-150 ease-in-out ' +
'hover:!bg-[var(--color-node-component-surface-hovered)] active:!bg-[var(--color-node-component-surface-selected)] ' +
'disabled:!bg-[var(--color-node-component-disabled)] disabled:!text-[var(--color-node-icon-disabled)] disabled:cursor-not-allowed'
export function useNumberWidgetButtonPt(options?: {
roundedLeft?: boolean
roundedRight?: boolean
}) {
const { roundedLeft = false, roundedRight = false } = options ?? {}
const increment = `${sharedButtonClasses}${roundedRight ? ' !rounded-r-lg' : ''}`
const decrement = `${sharedButtonClasses}${roundedLeft ? ' !rounded-l-lg' : ''}`
return {
incrementButton: {
class: increment.trim()
},
decrementButton: {
class: decrement.trim()
}
}
}