Skip to content
Open
Show file tree
Hide file tree
Changes from 34 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
23fdbe4
add control_after_generate ui
christian-byrne Oct 4, 2025
f853cb9
don't use forEach
christian-byrne Oct 5, 2025
57025df
seed widget
christian-byrne Oct 8, 2025
cfaeed1
seed widget2
christian-byrne Oct 12, 2025
04bd165
handle legacy step value
christian-byrne Oct 21, 2025
6acc7b0
feat: update NumberControlPopover with semantic design tokens
christian-byrne Nov 13, 2025
29af785
fix test
christian-byrne Nov 14, 2025
c18df9c
Fix display of controled widget values
AustinMroz Nov 25, 2025
abe600b
Revert useGraphNodeManager changes
AustinMroz Nov 25, 2025
fcf9a32
Merge origin/main
AustinMroz Nov 25, 2025
895a458
Swap NumberInputs to rekka ui to embed control
AustinMroz Nov 27, 2025
613fe12
Persist control widget value
AustinMroz Nov 27, 2025
94ade0a
[automated] Update test expectations
invalid-email-address Nov 27, 2025
0fb7466
Test fixes and nits
AustinMroz Nov 27, 2025
95b95ed
Merge branch 'main' into austin/vue-control-after-generate
christian-byrne Nov 30, 2025
69715b2
[automated] Update test expectations
invalid-email-address Nov 30, 2025
7dd2f52
Add max and mins from litegraph implementation
AustinMroz Dec 1, 2025
307771b
Merge 8e006bb8a306^
AustinMroz Dec 3, 2025
bbcb3b4
Merge main
AustinMroz Dec 3, 2025
ec07416
[automated] Update test expectations
invalid-email-address Dec 3, 2025
b5419f7
Empty commit to force tests to rerun
AustinMroz Dec 3, 2025
6d2f976
Fix number precision
AustinMroz Dec 3, 2025
9348995
[automated] Update test expectations
invalid-email-address Dec 3, 2025
9723e2b
Merge main
AustinMroz Dec 3, 2025
bb5c884
[automated] Update test expectations
invalid-email-address Dec 3, 2025
ff08660
Merge main
AustinMroz Dec 3, 2025
52daf4e
Revert to primevue, fix test
AustinMroz Dec 3, 2025
ad70768
Revert vitest changes, clear browser snapshots
AustinMroz Dec 3, 2025
4b4b90d
[automated] Update test expectations
invalid-email-address Dec 3, 2025
a84fd7a
Empty commit to trigger tests
AustinMroz Dec 4, 2025
0b5ded8
Merge main
AustinMroz Dec 5, 2025
a2391e6
[automated] Update test expectations
invalid-email-address Dec 5, 2025
5b6d835
Empty commit to force tests
AustinMroz Dec 5, 2025
939090f
nits
AustinMroz Dec 5, 2025
26e6aec
Further nits
AustinMroz Dec 6, 2025
1fa618c
Merge main
AustinMroz Dec 6, 2025
1ddf364
[automated] Update test expectations
invalid-email-address Dec 6, 2025
cfb579b
Remove global seed tests
AustinMroz Dec 6, 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
2 changes: 1 addition & 1 deletion browser_tests/fixtures/VueNodeHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ export class VueNodeHelpers {
return {
input: widget.locator('input'),
incrementButton: widget.locator('button').first(),
decrementButton: widget.locator('button').last()
decrementButton: widget.locator('button').nth(1)
}
}
}
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.
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.
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ test.describe('Vue Integer Widget', () => {
await comfyPage.loadWorkflow('vueNodes/linked-int-widget')
await comfyPage.vueNodes.waitForNodes()

const seedWidget = comfyPage.vueNodes.getWidgetByName('KSampler', 'seed')
const seedWidget = comfyPage.vueNodes
.getWidgetByName('KSampler', 'seed')
.first()
const controls = comfyPage.vueNodes.getInputNumberControls(seedWidget)
const initialValue = Number(await controls.input.inputValue())

Expand Down
18 changes: 16 additions & 2 deletions src/composables/graph/useGraphNodeManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ import type { NodeId } from '@/renderer/core/layout/types'
import type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
import { isDOMWidget } from '@/scripts/domWidget'
import { useNodeDefStore } from '@/stores/nodeDefStore'
import type { WidgetValue } from '@/types/simplifiedWidget'
import type { WidgetValue, SafeControlWidget } from '@/types/simplifiedWidget'
import { validateControlOption } from '@/types/simplifiedWidget'

import type {
LGraph,
Expand All @@ -47,6 +48,7 @@ export interface SafeWidgetData {
spec?: InputSpec
slotMetadata?: WidgetSlotMetadata
isDOMWidget?: boolean
controlWidget?: SafeControlWidget
}

export interface VueNodeData {
Expand Down Expand Up @@ -82,6 +84,17 @@ export interface GraphNodeManager {
cleanup(): void
}

function getControlWidget(widget: IBaseWidget): SafeControlWidget | undefined {
const cagWidget = widget.linkedWidgets?.find(
(w) => w.name == 'control_after_generate'
)
if (!cagWidget) return
return {
value: validateControlOption(cagWidget.value),
update: (value) => (cagWidget.value = validateControlOption(value))
}
}

export function safeWidgetMapper(
node: LGraphNode,
slotMetadata: Map<string, WidgetSlotMetadata>
Expand Down Expand Up @@ -114,7 +127,8 @@ export function safeWidgetMapper(
callback: widget.callback,
spec,
slotMetadata: slotInfo,
isDOMWidget: isDOMWidget(widget)
isDOMWidget: isDOMWidget(widget),
controlWidget: getControlWidget(widget)
}
} catch (error) {
return {
Expand Down
18 changes: 18 additions & 0 deletions src/locales/en/main.json
Original file line number Diff line number Diff line change
Expand Up @@ -1910,6 +1910,24 @@
"placeholderVideo": "Select video...",
"placeholderModel": "Select model...",
"placeholderUnknown": "Select media..."
},
"numberControl": {
"header" : {
"prefix": "Automatically update the value",
"after": "AFTER",
"before": "BEFORE",
"postfix": "running the workflow:"
},
"linkToGlobal": "Link to",
"linkToGlobalSeed": "Global Value",
"linkToGlobalDesc": "Unique value linked to the Global Value's control setting",
"randomize": "Randomize Value",
"randomizeDesc": "Shuffles the value randomly after each generation",
"increment": "Increment Value",
"incrementDesc": "Adds 1 to the value number",
"decrement": "Decrement Value",
"decrementDesc": "Subtracts 1 from the value number",
"editSettings": "Edit control settings"
}
Comment on lines +1914 to 1931
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Verify intentional ALL CAPS usage for mode labels.

Lines 1917-1918 use ALL CAPS for "AFTER" and "BEFORE" while surrounding text uses mixed case. This creates visual emphasis but is inconsistent with typical UI text conventions.

Please confirm this capitalization is intentional for design/UX reasons. If the goal is to emphasize the control mode, consider alternative approaches like bold styling or a separate UI treatment rather than ALL CAPS in the localization string.

Example alternative structure:

"header": {
  "prefix": "Automatically update the value",
  "afterMode": "after",
  "beforeMode": "before",
  "postfix": "running the workflow:"
}

Then apply emphasis via CSS in the component rather than hardcoding capitalization.

🤖 Prompt for AI Agents
In src/locales/en/main.json around lines 1914 to 1931, the header labels "AFTER"
and "BEFORE" are in ALL CAPS while surrounding text is mixed case; confirm
whether ALL CAPS is intentional for UX, and if not replace those values with
normal-cased alternatives (e.g., "after"/"before" or "After"/"Before") and
optionally rename the keys to semantic names like afterMode/beforeMode so
emphasis is applied in the component via styling/CSS instead of hardcoded
capitalization.

},
"widgetFileUpload": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,8 @@ const processedWidgets = computed((): ProcessedWidget[] => {
label: widget.label,
options: widgetOptions,
callback: widget.callback,
spec: widget.spec
spec: widget.spec,
controlWidget: widget.controlWidget
}

function updateHandler(value: WidgetValue) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
<script setup lang="ts">
import Button from 'primevue/button'
import Popover from 'primevue/popover'
import ToggleSwitch from 'primevue/toggleswitch'
Comment on lines +2 to +4
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider future migration away from PrimeVue components.

This component uses PrimeVue's Button, Popover, and ToggleSwitch. The coding guidelines state "Avoid new usage of PrimeVue components," and retrieved learnings mention replacing these with native alternatives. While this may be acceptable within the current architecture, plan to migrate away from PrimeVue in future refactoring.

Based on coding guidelines.

🤖 Prompt for AI Agents
In src/renderer/extensions/vueNodes/widgets/components/NumberControlPopover.vue
around lines 2 to 4, this file imports PrimeVue Button, Popover, and
ToggleSwitch which violates the "avoid new usage of PrimeVue components"
guideline; replace direct PrimeVue usage by creating/using local native or
framework-agnostic equivalents (e.g., a simple native <button> wrapper, a
lightweight popover component implemented with Portal/teleport and CSS, and a
toggle built from input[type="checkbox"] or an existing internal Toggle
component), or introduce small adapter wrappers (e.g., LocalButton,
LocalPopover, LocalToggle) and update the template and props to use those
wrappers so the component no longer depends on PrimeVue and is easier to migrate
later.

import { computed, ref } from 'vue'
import { useSettingStore } from '@/platform/settings/settingStore'
import { useDialogService } from '@/services/dialogService'
import { NumberControlMode } from '../composables/useStepperControl'
type ControlOption = {
description: string
icon?: string
mode: NumberControlMode
text?: string
title: string
}
const popover = ref()
const settingStore = useSettingStore()
const dialogService = useDialogService()
const toggle = (event: Event) => {
popover.value.toggle(event)
}
defineExpose({ toggle })
const ENABLE_LINK_TO_GLOBAL = false
const controlOptions: ControlOption[] = [
...(ENABLE_LINK_TO_GLOBAL
? ([
{
mode: NumberControlMode.LINK_TO_GLOBAL,
icon: 'pi pi-link',
title: 'linkToGlobal',
description: 'linkToGlobalDesc'
} satisfies ControlOption
] as ControlOption[])
: []),
{
mode: NumberControlMode.RANDOMIZE,
icon: 'icon-[lucide--shuffle]',
title: 'randomize',
description: 'randomizeDesc'
},
{
mode: NumberControlMode.INCREMENT,
text: '+1',
title: 'increment',
description: 'incrementDesc'
},
{
mode: NumberControlMode.DECREMENT,
text: '-1',
title: 'decrement',
description: 'decrementDesc'
}
]
const widgetControlMode = computed(() =>
settingStore.get('Comfy.WidgetControlMode')
)
const props = defineProps<{
controlMode: NumberControlMode
}>()
const emit = defineEmits<{
'update:controlMode': [mode: NumberControlMode]
}>()
const handleToggle = (mode: NumberControlMode) => {
if (props.controlMode === mode) return
emit('update:controlMode', mode)
}
const isActive = (mode: NumberControlMode) => {
return props.controlMode === mode
}
const handleEditSettings = () => {
popover.value.hide()
dialogService.showSettingsDialog()
}
</script>

<template>
<Popover
ref="popover"
class="bg-interface-panel-surface border border-interface-stroke rounded-lg"
>
<div class="w-113 max-w-md p-4 space-y-4">
<div class="text-sm text-muted-foreground leading-tight">
{{ $t('widgets.numberControl.header.prefix') }}
<span class="text-base-foreground font-medium">
{{
widgetControlMode === 'before'
? $t('widgets.numberControl.header.before')
: $t('widgets.numberControl.header.after')
}}
</span>
{{ $t('widgets.numberControl.header.postfix') }}
</div>

<div class="space-y-2">
<div
v-for="option in controlOptions"
:key="option.mode"
class="flex items-center justify-between py-2 gap-7"
>
<div class="flex items-center gap-2 flex-1 min-w-0">
<div
class="flex items-center justify-center w-8 h-8 rounded-lg flex-shrink-0 bg-secondary-background border border-border-subtle"
>
<i
v-if="option.icon"
:class="option.icon"
class="text-base text-base-foreground"
/>
<span
v-if="option.text"
class="text-xs font-normal text-base-foreground"
>
{{ option.text }}
</span>
</div>

<div class="flex flex-col gap-0.5 min-w-0 flex-1">
<div
class="text-sm font-normal text-base-foreground leading-tight"
>
<span v-if="option.mode === NumberControlMode.LINK_TO_GLOBAL">
{{ $t('widgets.numberControl.linkToGlobal') }}
<em>{{ $t('widgets.numberControl.linkToGlobalSeed') }}</em>
</span>
<span v-else>
{{ $t(`widgets.numberControl.${option.title}`) }}
</span>
</div>
<div
class="text-sm font-normal text-muted-foreground leading-tight"
>
{{ $t(`widgets.numberControl.${option.description}`) }}
</div>
</div>
</div>

<ToggleSwitch
:model-value="isActive(option.mode)"
class="flex-shrink-0"
@update:model-value="handleToggle(option.mode)"
/>
</div>
</div>
<div class="border-t border-border-subtle"></div>
<Button
class="w-full bg-secondary-background hover:bg-secondary-background-hover border-0 rounded-lg p-2 text-sm"
@click="handleEditSettings"
>
<div class="flex items-center justify-center gap-1">
<i class="pi pi-cog text-xs text-muted-foreground" />
<span class="font-normal text-base-foreground">{{
$t('widgets.numberControl.editSettings')
}}</span>
</div>
</Button>
</div>
</Popover>
</template>
Original file line number Diff line number Diff line change
@@ -1,22 +1,31 @@
<script setup lang="ts">
import { computed } from 'vue'

import type { SimplifiedWidget } from '@/types/simplifiedWidget'

import WidgetInputNumberInput from './WidgetInputNumberInput.vue'
import WidgetInputNumberSlider from './WidgetInputNumberSlider.vue'
import WidgetInputNumberWithControl from './WidgetInputNumberWithControl.vue'

defineProps<{
const props = defineProps<{
widget: SimplifiedWidget<number>
}>()

const modelValue = defineModel<number>({ default: 0 })

const hasControlAfterGenerate = computed(() => {
return !!props.widget.controlWidget
})
</script>

<template>
<component
:is="
widget.type === 'slider'
? WidgetInputNumberSlider
: WidgetInputNumberInput
hasControlAfterGenerate
? WidgetInputNumberWithControl
: widget.type === 'slider'
? WidgetInputNumberSlider
: WidgetInputNumberInput
"
v-model="modelValue"
:widget="widget"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script setup lang="ts">
import InputNumber from 'primevue/inputnumber'
import { computed } from 'vue'
import { computed, useSlots } from 'vue'

import type { SimplifiedWidget } from '@/types/simplifiedWidget'
import { cn } from '@/utils/tailwindUtil'
Expand Down Expand Up @@ -68,6 +68,8 @@ const buttonTooltip = computed(() => {
}
return null
})

const slots = useSlots()
</script>

<template>
Expand All @@ -89,8 +91,11 @@ const buttonTooltip = computed(() => {
:show-buttons="!buttonsDisabled"
:pt="{
root: {
class:
'[&>input]:bg-transparent [&>input]:border-0 [&>input]:truncate [&>input]:min-w-[4ch]'
class: cn(
'[&>input]:bg-transparent [&>input]:border-0',
'[&>input]:truncate [&>input]:min-w-[4ch]',
slots.default && '[&>input]:pr-7'
)
},
decrementButton: {
class: 'w-8 border-0'
Expand All @@ -107,6 +112,9 @@ const buttonTooltip = computed(() => {
<span class="pi pi-minus text-sm" />
</template>
</InputNumber>
<div class="absolute top-5 right-8 h-4 w-7 -translate-y-4/5">
<slot />
</div>
</WidgetLayoutField>
</template>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import {
filterWidgetProps
} from '@/utils/widgetPropFilter'

import { useNumberStepCalculation } from '../composables/useNumberStepCalculation'
import { useNumberWidgetButtonPt } from '../composables/useNumberWidgetButtonPt'
import { WidgetInputBaseClass } from './layout'
import WidgetLayoutField from './layout/WidgetLayoutField.vue'
Expand All @@ -56,7 +57,7 @@ const updateLocalValue = (newValue: number[] | undefined): void => {
}

const handleNumberInputUpdate = (newValue: number | undefined) => {
if (newValue) {
if (newValue !== undefined) {
updateLocalValue([newValue])
return
}
Expand All @@ -75,25 +76,7 @@ const precision = computed(() => {
})

// Calculate the step value based on precision or widget options
const stepValue = computed(() => {
// Use step2 (correct input spec value) instead of step (legacy 10x value)
if (widget.options?.step2 !== undefined) {
return widget.options.step2
}

// Otherwise, derive from precision
if (precision.value === undefined) {
return undefined
}

if (precision.value === 0) {
return 1
}

// For precision > 0, step = 1 / (10^precision)
// precision 1 → 0.1, precision 2 → 0.01, etc.
return 1 / Math.pow(10, precision.value)
})
const stepValue = useNumberStepCalculation(widget.options, precision, true)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this could be a utility function instead of a composable for now.


const sliderNumberPt = useNumberWidgetButtonPt({
roundedLeft: true,
Expand Down
Loading