Compare commits

...

19 Commits

Author SHA1 Message Date
Austin Mroz
2c523ba2c8 Move label loading into autogrow code 2026-03-28 13:47:52 -07:00
Austin Mroz
cd33b16061 Simplify rename logic 2026-03-28 13:36:46 -07:00
Arthur R Longbottom
dbd2767225 fix: use live getter for combo values, works in both LG and Vue mode
Replace onDrawForeground polling with Object.defineProperty getter on
comboWidget.options.values. Every read gets fresh labels from connected
inputs, ensuring the dropdown stays in sync after renames in Vue mode.
2026-03-27 19:48:12 -05:00
Arthur R Longbottom
2e944a6be4 fix: use local label override for immediate Vue reactivity on rename 2026-03-27 19:43:19 -05:00
Arthur R Longbottom
09a481588d fix: prevent double finishRename from enter+blur race 2026-03-27 19:37:14 -05:00
Arthur R Longbottom
1855d4cc03 fix: move tooltip to connection dot only to prevent hover CLS 2026-03-27 19:34:40 -05:00
Arthur R Longbottom
b5007573b1 feat: add slot-text-highlight semantic color for hover feedback
Add --node-component-slot-text-highlight token: black on light theme,
white on dark theme. Use it for input slot label hover instead of
hardcoded text-white.
2026-03-27 19:32:58 -05:00
Arthur R Longbottom
2a065c5093 fix: use white hover color, remove transition to prevent CLS 2026-03-27 19:30:55 -05:00
Arthur R Longbottom
58b795b419 fix: remove cursor-text from slot label, keep crosshair 2026-03-27 19:29:28 -05:00
Arthur R Longbottom
8448821ee1 revert: remove cursor-default, keep parent crosshair 2026-03-27 19:28:23 -05:00
Arthur R Longbottom
081a55007a fix: use default cursor on slot label area 2026-03-27 19:26:15 -05:00
Arthur R Longbottom
3f684bd95c fix: Vue slot rename — match font size, prevent error sound, fix reactivity
- Add .prevent on enter/escape keydown to suppress error beep
- Use text-[length:inherit] leading-[inherit] to match span font size
- Add right-click on connection dot to start rename
- Defer isRenaming=false to nextTick so label propagates before re-render
2026-03-27 19:22:20 -05:00
Arthur R Longbottom
8b32d696f0 fix: restore renamed slot labels after configure
Autogrow recreates inputs fresh during configure, losing custom labels.
Hook onConfigure to re-apply labels from serialized workflow data.
2026-03-27 19:15:00 -05:00
Arthur R Longbottom
08799e8163 fix: only show connected inputs in branch combo, fix serialization
- Filter combo values to only show inputs with active connections
- Serialize the relative index within connected inputs (not absolute
  input index) so the backend's autogrow dict lookup succeeds
- Update draw-frame check to track both connections and labels
2026-03-27 19:13:43 -05:00
Arthur R Longbottom
61760be9ff feat: double-click to rename input slots in Vue nodes mode
- Add inline rename on double-click with input field
- Hover effect transitions slot text to lighter gray
- Fires node:slot-label:changed trigger for reactivity
- Respects nameLocked flag
2026-03-27 19:09:46 -05:00
Arthur R Longbottom
9f70912899 fix: add getSlotMenuOptions to BranchNode for slot rename
The default slot context menu skips "Rename Slot" for inputs that have
a widget property set. Override getSlotMenuOptions on BranchNode to
always show the rename option for input slots.
2026-03-27 19:06:36 -05:00
Arthur R Longbottom
bd596997b8 fix: branch node combo reactivity and LG slot rename trigger
- Replace values() function with shallowReactive array for Vue tracking
- Add onDrawForeground check to detect label changes and refresh combo
- Fire node:slot-label:changed from LG "Rename Slot" context menu
  (previously mutated slot.label directly without triggering)
2026-03-27 18:56:38 -05:00
Austin Mroz
98d0cca188 Fix slice and serialization
lazy doesn't seem to track correctly on the backend. Needs further
investigation
2026-03-27 18:48:04 -05:00
Austin Mroz
a125261b8a POC branch node 2026-03-27 18:48:03 -05:00
5 changed files with 146 additions and 11 deletions

View File

@@ -249,6 +249,7 @@
--node-component-slot-dot-outline-opacity: 5%;
--node-component-slot-dot-outline: var(--color-black);
--node-component-slot-text: var(--color-ash-800);
--node-component-slot-text-highlight: var(--color-black);
--node-component-surface-highlight: var(--color-ash-500);
--node-component-surface-hovered: var(--color-smoke-200);
--node-component-surface-selected: var(--color-charcoal-200);
@@ -391,6 +392,7 @@
--node-component-slot-dot-outline-opacity: 10%;
--node-component-slot-dot-outline: var(--color-white);
--node-component-slot-text: var(--color-slate-200);
--node-component-slot-text-highlight: var(--color-white);
--node-component-surface-highlight: var(--color-slate-100);
--node-component-surface-hovered: var(--color-charcoal-600);
--node-component-surface-selected: var(--color-charcoal-200);
@@ -537,6 +539,9 @@
)
);
--color-node-component-slot-text: var(--node-component-slot-text);
--color-node-component-slot-text-highlight: var(
--node-component-slot-text-highlight
);
--color-node-component-surface-highlight: var(
--node-component-surface-highlight
);

View File

@@ -572,6 +572,20 @@ function withComfyAutogrow(node: LGraphNode): asserts node is AutogrowNode {
}
}
)
// Restore renamed labels after configure (autogrow recreates inputs fresh)
node.onConfigure = useChainCallback(
node.onConfigure,
(data: { inputs?: Array<{ label?: string; name: string }> }) => {
if (!data?.inputs) return
for (const serializedInput of data.inputs) {
if (!serializedInput.label) continue
const match = node.inputs.find(
(inp) => inp.name === serializedInput.name
)
if (match) match.label = serializedInput.label
}
}
)
}
function applyAutogrow(node: LGraphNode, inputSpecV2: InputSpecV2) {
withComfyAutogrow(node)

View File

@@ -1,4 +1,4 @@
import { shallowReactive } from 'vue'
import { computed, ref, shallowReactive, watch } from 'vue'
import { useChainCallback } from '@/composables/functional/useChainCallback'
import type { CanvasPointerEvent } from '@/lib/litegraph/src/types/events'
@@ -124,6 +124,42 @@ function onCustomComboCreated(this: LGraphNode) {
addOption(this)
}
const renameTrigger = ref(0)
function onBranchSelectorCreated(this: LGraphNode) {
this.applyToGraph = applyToGraph
//FIXME: watchers probably leak since we're not in a vue component
const connectionsTrigger = ref(0)
this.widgets?.pop()
const labels = computed(() => {
void renameTrigger.value
void connectionsTrigger.value
return this.inputs.slice(0, -2).map((inp) => inp.label)
})
const values = () => labels.value
const node = this
const comboWidget = this.addWidget('combo', 'branch', '', () => {}, {
values
})
watch([renameTrigger, connectionsTrigger], () => {
if (app.configuringGraph || labels.value.includes(`${comboWidget.value}`))
return
comboWidget.value = labels.value[0] ?? ''
comboWidget.callback?.(comboWidget.value)
})
comboWidget.serializeValue = () =>
node.inputs.slice(0, -1).findIndex((inp) => inp.label === comboWidget.value)
// Refresh on connection changes (add/remove inputs)
this.onConnectionsChange = useChainCallback(
this.onConnectionsChange,
() => connectionsTrigger.value++
)
}
function onCustomIntCreated(this: LGraphNode) {
const valueWidget = this.widgets?.[0]
if (!valueWidget) return
@@ -227,6 +263,11 @@ app.registerExtension({
nodeType.prototype.onNodeCreated,
onCustomComboCreated
)
else if (nodeData?.name === 'BranchNode')
nodeType.prototype.onNodeCreated = useChainCallback(
nodeType.prototype.onNodeCreated,
onBranchSelectorCreated
)
else if (nodeData?.name === 'PrimitiveInt')
nodeType.prototype.onNodeCreated = useChainCallback(
nodeType.prototype.onNodeCreated,
@@ -237,5 +278,11 @@ app.registerExtension({
nodeType.prototype.onNodeCreated,
onCustomFloatCreated
)
},
init() {
app.graph.onTrigger = useChainCallback(app.graph.onTrigger, (e) => {
if (e.type !== 'node:slot-label:changed') return
renameTrigger.value++
})
}
})

View File

@@ -97,6 +97,7 @@ import {
LinkDirection,
LinkMarkerShape,
LinkRenderType,
NodeSlotType,
RenderShape,
TitleMode
} from './types/globalEnums'
@@ -8803,6 +8804,10 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
if (input?.value) {
if (slot_info) {
slot_info.label = input.value
node.graph?.trigger('node:slot-label:changed', {
nodeId: node.id,
slotType: info.input ? NodeSlotType.INPUT : NodeSlotType.OUTPUT
})
}
setDirty()
}

View File

@@ -2,10 +2,9 @@
<div v-if="renderError" class="node-error p-1 text-xs text-red-500"></div>
<div
v-else
v-tooltip.left="tooltipConfig"
:class="
cn(
'lg-slot lg-slot--input group m-0 flex items-center rounded-r-lg',
'lg-slot lg-slot--input group/slot m-0 flex items-center rounded-r-lg',
'cursor-crosshair',
dotOnly ? 'lg-slot--dot-only' : 'pr-6',
{
@@ -20,6 +19,7 @@
<!-- Connection Dot -->
<SlotConnectionDot
ref="connectionDotRef"
v-tooltip.left="tooltipConfig"
:class="
cn(
'w-3 -translate-x-1/2',
@@ -31,40 +31,51 @@
@click="onClick"
@dblclick="onDoubleClick"
@pointerdown="onPointerDown"
@contextmenu.stop.prevent="startRename"
/>
<!-- Slot Name -->
<div class="flex h-full min-w-0 items-center">
<input
v-if="isRenaming"
ref="renameInputRef"
v-model="renameValue"
class="m-0 w-full truncate border-none bg-transparent p-0 text-[length:inherit] leading-[inherit] text-node-component-slot-text outline-none"
@blur="finishRename"
@keydown.enter.prevent="finishRename"
@keydown.escape.prevent="cancelRename"
@click.stop
@pointerdown.stop
/>
<span
v-if="!props.dotOnly && !hasNoLabel"
v-else-if="!props.dotOnly && !hasNoLabel"
:class="
cn(
'truncate text-node-component-slot-text',
'truncate text-node-component-slot-text hover:text-node-component-slot-text-highlight',
hasError && 'font-medium text-error'
)
"
@dblclick.stop="startRename"
>
{{
slotData.label ||
slotData.localized_name ||
(slotData.name ?? `Input ${index}`)
}}
{{ displayLabel }}
</span>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, onErrorCaptured, ref, watchEffect } from 'vue'
import { computed, nextTick, onErrorCaptured, ref, watchEffect } from 'vue'
import type { ComponentPublicInstance } from 'vue'
import { useErrorHandling } from '@/composables/useErrorHandling'
import type { INodeSlot } from '@/lib/litegraph/src/litegraph'
import { NodeSlotType } from '@/lib/litegraph/src/types/globalEnums'
import { useSlotLinkDragUIState } from '@/renderer/core/canvas/links/slotLinkDragUIState'
import { getSlotKey } from '@/renderer/core/layout/slots/slotIdentifier'
import { useNodeTooltips } from '@/renderer/extensions/vueNodes/composables/useNodeTooltips'
import { useSlotElementTracking } from '@/renderer/extensions/vueNodes/composables/useSlotElementTracking'
import { useSlotLinkInteraction } from '@/renderer/extensions/vueNodes/composables/useSlotLinkInteraction'
import { app } from '@/scripts/app'
import { cn } from '@/utils/tailwindUtil'
import SlotConnectionDot from './SlotConnectionDot.vue'
@@ -83,6 +94,15 @@ interface InputSlotProps {
const props = defineProps<InputSlotProps>()
const labelOverride = ref<string | null>(null)
const displayLabel = computed(
() =>
labelOverride.value ||
props.slotData.label ||
props.slotData.localized_name ||
(props.slotData.name ?? `Input ${props.index}`)
)
const hasNoLabel = computed(
() =>
!props.slotData.label &&
@@ -142,4 +162,48 @@ const { onClick, onDoubleClick, onPointerDown } = useSlotLinkInteraction({
index: props.index,
type: 'input'
})
// ── Inline rename ─────────────────────────────────────────────
const isRenaming = ref(false)
const renameValue = ref('')
const renameInputRef = ref<HTMLInputElement | null>(null)
function startRename() {
if (props.slotData.nameLocked) return
renameValue.value = displayLabel.value
isRenaming.value = true
nextTick(() => {
renameInputRef.value?.select()
})
}
let renameCommitted = false
function finishRename() {
if (!isRenaming.value || renameCommitted) return
renameCommitted = true
const newLabel = renameValue.value.trim()
const node = app.canvas?.graph?.getNodeById(props.nodeId ?? '')
const slot = node?.inputs?.[props.index]
if (newLabel && newLabel !== displayLabel.value && slot) {
slot.label = newLabel
labelOverride.value = newLabel
node?.graph?.trigger('node:slot-label:changed', {
nodeId: node.id,
slotType: NodeSlotType.INPUT
})
app.canvas?.setDirty(true, true)
}
nextTick(() => {
isRenaming.value = false
renameCommitted = false
})
}
function cancelRename() {
isRenaming.value = false
}
</script>