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

@@ -10,7 +10,11 @@ import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMuta
import { LayoutSource } from '@/renderer/core/layout/types'
import { adjustColor } from '@/utils/colorUtil'
import type { ColorAdjustOptions } from '@/utils/colorUtil'
import { commonType, toClass } from '@/lib/litegraph/src/utils/type'
import {
commonType,
isNodeBindable,
toClass
} from '@/lib/litegraph/src/utils/type'
import { SUBGRAPH_OUTPUT_ID } from '@/lib/litegraph/src/constants'
import type { DragAndScale } from './DragAndScale'
@@ -1957,6 +1961,14 @@ export class LGraphNode
this.widgets ||= []
const widget = toConcreteWidget(custom_widget, this, false) ?? custom_widget
this.widgets.push(widget)
// Only register with store if node has a valid ID (is already in a graph).
// If the node isn't in a graph yet (id === -1), registration happens
// when the node is added via LGraph.add() -> node.onAdded.
if (this.id !== -1 && isNodeBindable(widget)) {
widget.setNodeId(this.id)
}
return widget
}
@@ -3499,7 +3511,7 @@ export class LGraphNode
* Toggles advanced mode of the node, showing advanced widgets
*/
toggleAdvanced() {
if (!this.widgets?.some((w) => w.advanced)) return
if (!this.hasAdvancedWidgets()) return
if (!this.graph) throw new NullGraphError()
this.graph._version++
this.showAdvanced = !this.showAdvanced
@@ -3877,6 +3889,21 @@ export class LGraphNode
return !isHidden
}
/**
* Returns all widgets that should participate in layout calculations.
* Filters out hidden widgets only (not collapsed/advanced).
*/
getLayoutWidgets(): IBaseWidget[] {
return this.widgets?.filter((w) => !w.hidden) ?? []
}
/**
* Returns `true` if the node has any advanced widgets.
*/
hasAdvancedWidgets(): boolean {
return this.widgets?.some((w) => w.advanced) ?? false
}
updateComputedDisabled() {
if (!this.widgets) return
for (const widget of this.widgets)
@@ -4087,7 +4114,7 @@ export class LGraphNode
w: IBaseWidget
}[] = []
const visibleWidgets = this.widgets.filter((w) => !w.hidden)
const visibleWidgets = this.getLayoutWidgets()
for (const w of visibleWidgets) {
if (w.computeSize) {