Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
113 changes: 113 additions & 0 deletions src/extensions/core/customCombo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { shallowReactive } from 'vue'

import { useChainCallback } from '@/composables/functional/useChainCallback'
import type { CanvasPointerEvent } from '@/lib/litegraph/src/types/events'
import { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
import { LLink } from '@/lib/litegraph/src/litegraph'
import { app } from '@/scripts/app'

function applyToGraph(this: LGraphNode, extraLinks: LLink[] = []) {
if (!this.outputs[0].links?.length || !this.graph) return

const links = [
...this.outputs[0].links.map((l) => this.graph!.links[l]),
...extraLinks
]
let v = this.widgets?.[0].value
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

Add defensive check for widgets array.

The code assumes this.widgets[0] exists without validation. While the node definition likely ensures this, adding a defensive check improves robustness.

-  let v = this.widgets?.[0].value
+  const widget = this.widgets?.[0]
+  if (!widget) return
+  let v = widget.value
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let v = this.widgets?.[0].value
const widget = this.widgets?.[0]
if (!widget) return
let v = widget.value
🤖 Prompt for AI Agents
In src/extensions/core/customCombo.ts around line 14, the code accesses
this.widgets[0].value without validating this.widgets or this.widgets[0]; add a
defensive check to ensure this.widgets is an array and has at least one element
before reading .value (e.g., guard with if (!Array.isArray(this.widgets) ||
this.widgets.length === 0) return or set v = undefined), and use the safe v
afterwards so you don't access .value on undefined.

// For each output link copy our value over the original widget value
for (const linkInfo of links) {
const node = this.graph?.getNodeById(linkInfo.target_id)
const input = node?.inputs[linkInfo.target_slot]
if (!input) {
console.warn('Unable to resolve node or input for link', linkInfo)
continue
}

const widgetName = input.widget?.name
if (!widgetName) {
console.warn('Invalid widget or widget name', input.widget)
continue
}

const widget = node.widgets?.find((w) => w.name === widgetName)
if (!widget) {
console.warn(`Unable to find widget "${widgetName}" on node [${node.id}]`)
continue
}

widget.value = v
widget.callback?.(
widget.value,
app.canvas,
node,
app.canvas.graph_mouse,
{} as CanvasPointerEvent
)
}
}
Comment on lines +9 to +47
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

Extract shared logic to reduce duplication.

This function duplicates significant logic from widgetInputs.ts (lines 42-86). The core link-traversal and widget-update pattern is identical.

Consider extracting the shared logic into a common utility function that both implementations can use, perhaps accepting a value-transformer function to handle the text-replacement difference.

Example approach:

// In a shared utility file
function propagateWidgetValue(
  node: LGraphNode,
  outputIndex: number,
  value: any,
  extraLinks: LLink[] = []
) {
  if (!node.outputs[outputIndex].links?.length || !node.graph) return
  
  const links = [
    ...node.outputs[outputIndex].links.map((l) => node.graph!.links[l]),
    ...extraLinks
  ]
  
  for (const linkInfo of links) {
    const targetNode = node.graph?.getNodeById(linkInfo.target_id)
    const input = targetNode?.inputs[linkInfo.target_slot]
    // ... rest of logic
  }
}
🤖 Prompt for AI Agents
In src/extensions/core/customCombo.ts around lines 7 to 45 the applyToGraph
implementation duplicates the link-traversal and widget-update logic found in
widgetInputs.ts; extract that shared behavior into a common utility (e.g.,
propagateWidgetValue) placed in a shared utils file, which accepts the node
(this), output index (0), the value to propagate, optional extraLinks, and an
optional value-transformer callback; move the common code that resolves graph
links, finds target node/input/widget, sets widget.value and invokes
widget.callback into that utility, keep all existing null-checks and warning
messages, and replace applyToGraph with a call to the new utility (passing a
transformer that returns v unchanged) and update widgetInputs.ts to call the
same utility with its own transformer for text replacement so both
implementations reuse the single function and preserve typings and behavior.


function onNodeCreated(this: LGraphNode) {
this.applyToGraph = useChainCallback(this.applyToGraph, applyToGraph)

const comboWidget = this.widgets![0]
const values = shallowReactive<string[]>([])
comboWidget.options.values = values

const updateCombo = () => {
values.splice(
0,
values.length,
...this.widgets!.filter(
(w) => w.name.startsWith('option') && w.value
).map((w) => `${w.value}`)
)
if (app.configuringGraph) return
if (values.includes(`${comboWidget.value}`)) return
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

Clarify string coercion intent.

The comparison uses string template coercion ${comboWidget.value} rather than direct comparison. This might be intentional to handle type flexibility, but it's unclear.

If intentional, consider adding a comment:

-    if (values.includes(`${comboWidget.value}`)) return
+    // Coerce to string to handle mixed types
+    if (values.includes(`${comboWidget.value}`)) return

Or use explicit conversion for clarity:

-    if (values.includes(`${comboWidget.value}`)) return
+    if (values.includes(String(comboWidget.value))) return
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (values.includes(`${comboWidget.value}`)) return
if (values.includes(String(comboWidget.value))) return
🤖 Prompt for AI Agents
In src/extensions/core/customCombo.ts around line 63, the check uses template
string coercion if (values.includes(`${comboWidget.value}`)) which is unclear;
replace the template string with an explicit conversion (e.g.,
String(comboWidget.value)) so intent is obvious, or if the template coercion was
intentional add a one-line comment explaining why coercion via string is
required (type(s) expected and why) so future readers understand the comparison.

comboWidget.value = values[0] ?? ''
comboWidget.callback?.(comboWidget.value)
}
comboWidget.callback = useChainCallback(comboWidget.callback, () =>
this.applyToGraph!()
)

function addOption(node: LGraphNode) {
if (!node.widgets) return
const newCount = node.widgets.length - 1
node.addWidget('string', `option${newCount}`, '', () => {})
const widget = node.widgets.at(-1)
if (!widget) return

let value = ''
Object.defineProperty(widget, 'value', {
get() {
return value
},
set(v) {
value = v
updateCombo()
if (!node.widgets) return
const lastWidget = node.widgets.at(-1)
if (lastWidget === this) {
if (v) addOption(node)
return
}
if (v || node.widgets.at(-2) !== this || lastWidget?.value) return
node.widgets.pop()
node.computeSize(node.size)
this.callback(v)
}
})
}
addOption(this)
}

app.registerExtension({
name: 'Comfy.CustomCombo',
beforeRegisterNodeDef(nodeType, nodeData) {
if (nodeData?.name !== 'CustomCombo') return
nodeType.prototype.onNodeCreated = useChainCallback(
nodeType.prototype.onNodeCreated,
onNodeCreated
)
}
})
1 change: 1 addition & 0 deletions src/extensions/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { isCloud } from '@/platform/distribution/types'

import './clipspace'
import './contextMenuFilter'
import './customCombo'
import './dynamicPrompts'
import './editAttention'
import './electronAdapter'
Expand Down
16 changes: 16 additions & 0 deletions src/stores/nodeDefStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,22 @@ export const SYSTEM_NODE_DEFS: Record<string, ComfyNodeDefV1> = {
python_module: 'nodes',
description:
'Node that add notes to your project. Reformats text as markdown.'
},
CustomCombo: {
name: 'CustomCombo',
display_name: 'Custom Combo',
category: 'utils',
input: {
required: { choice: [[], {}], option0: ['STRING', {}] },
optional: {}
},
output: ['string'],
output_name: ['output'],
output_is_list: [],
output_node: false,
python_module: 'nodes',
experimental: true,
description: 'Outputs the chosen string from a user provided list.'
}
}

Expand Down