feat: add WidgetValueStore for centralized widget value management (#8594)

## Summary

Implements Phase 1 of the **Vue-owns-truth** pattern for widget values.
Widget values are now canonical in a Pinia store; `widget.value`
delegates to the store while preserving full backward compatibility.

## Changes

- **New store**: `src/stores/widgetValueStore.ts` - centralized widget
value storage with `get/set/remove/removeNode` API
- **BaseWidget integration**: `widget.value` getter/setter now delegates
to store when widget is associated with a node
- **LGraphNode wiring**: `addCustomWidget()` automatically calls
`widget.setNodeId(this.id)` to wire widgets to their nodes
- **Test fixes**: Added Pinia setup to test files that use widgets

## Why

This foundation enables:
- Vue components to reactively bind to widget values via `computed(() =>
store.get(...))`
- Future Yjs/CRDT backing for real-time collaboration
- Cleaner separation between Vue state and LiteGraph rendering

## Backward Compatibility

| Extension Pattern | Status |
|-------------------|--------|
| `widget.value = x` |  Works unchanged |
| `node.widgets[i].value` |  Works unchanged |
| `widget.callback` |  Still fires |
| `node.onWidgetChanged` |  Still fires |

## Testing

-  4252 unit tests pass
-  Build succeeds

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8594-feat-add-WidgetValueStore-for-centralized-widget-value-management-2fc6d73d36508160886fcb9f3ebd941e)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: GitHub Action <action@github.com>
This commit is contained in:
Alexander Brown
2026-02-10 19:37:17 -08:00
committed by GitHub
parent d044bed9b2
commit a7c2115166
36 changed files with 814 additions and 186 deletions

View File

@@ -1412,7 +1412,7 @@ export class GroupNodeHandler {
handlerGroupData.oldToNewWidgetMap[Number(n)]?.[w]
const widget = this.widgets.find((wg) => wg.name === widgetName)
if (widget) {
widget.type = 'hidden'
widget.hidden = true
widget.computeSize = () => [0, -4]
}
}

View File

@@ -19,14 +19,14 @@ useExtensionService().registerExtension({
const showValueWidget = ComfyWidgets['MARKDOWN'](
this,
'preview',
'preview_markdown',
['MARKDOWN', {}],
app
).widget as DOMWidget<HTMLTextAreaElement, string>
const showValueWidgetPlain = ComfyWidgets['STRING'](
this,
'preview',
'preview_text',
['STRING', { multiline: true }],
app
).widget as DOMWidget<HTMLTextAreaElement, string>
@@ -48,6 +48,7 @@ useExtensionService().registerExtension({
showValueWidgetPlain.options.hidden = value
}
showValueWidget.label = 'Preview'
showValueWidget.hidden = true
showValueWidget.options.hidden = true
showValueWidget.options.read_only = true
@@ -55,6 +56,7 @@ useExtensionService().registerExtension({
showValueWidget.element.disabled = true
showValueWidget.serialize = false
showValueWidgetPlain.label = 'Preview'
showValueWidgetPlain.hidden = false
showValueWidgetPlain.options.hidden = false
showValueWidgetPlain.options.read_only = true
@@ -71,7 +73,7 @@ useExtensionService().registerExtension({
: onExecuted.apply(this, [message])
const previewWidgets =
this.widgets?.filter((w) => w.name === 'preview') ?? []
this.widgets?.filter((w) => w.name.startsWith('preview_')) ?? []
for (const previewWidget of previewWidgets) {
const text = message.text ?? ''

View File

@@ -23,6 +23,7 @@ import { getNodeByLocatorId } from '@/utils/graphTraversalUtil'
import { api } from '../../scripts/api'
import { app } from '../../scripts/app'
import { useWidgetValueStore } from '@/stores/widgetValueStore'
function updateUIWidget(
audioUIWidget: DOMWidget<HTMLAudioElement, string>,
@@ -137,9 +138,16 @@ app.registerExtension({
}
}
let value = ''
audioUIWidget.options.getValue = () => value
audioUIWidget.options.setValue = (v) => (value = v)
audioUIWidget.options.getValue = () =>
(useWidgetValueStore().getWidget(node.id, inputName)
?.value as string) ?? ''
audioUIWidget.options.setValue = (v) => {
const widgetState = useWidgetValueStore().getWidget(
node.id,
inputName
)
if (widgetState) widgetState.value = v
}
return { widget: audioUIWidget }
}