Comfy-Org / Comfy-Org/ComfyUI_frontend

ECS branch: Nodes 2.0 dispatches on the stored widget type, so runtime widget.type swaps stop working

Open
#15,579 5 comments 0 reactions 1 assignee Claimed by @christian-byrne View on GitHub
area:widgets claimed Potential Bug
Dominant language
TypeScript
Stars
2k
Forks
704
Avg merge
1d 8h
Merged PRs (30d)
512

Description

Nodes 2.0 decides how to render a widget from `widgetValueStore`, not from the live widget, but `BaseWidget.type` is never written back to the store. An extension that swaps `widget.type` after the node is in the graph gets no effect in Nodes 2.0. On `main` it worked.

## Where

On `feature/ecs-migration` (PR #14246), `useProcessedWidgets.ts`:

```ts
if (!shouldRenderAsVue({ type: widgetState.type, options })) return null
...
vueComponent: getComponent(widgetState.type) || (renderState?.isDOMWidget ? WidgetDOM : WidgetLegacy)
```

Both decisions read `widgetState.type` from the store.

On `main`, `safeWidgetMapper` in `useGraphNodeManager.ts` rebuilt `SafeWidgetData` on every recompute with `type: widget.type`, so `shouldRenderAsVue(widget)` and `getComponent(widget.type)` both saw the live string.

## Why it does not update

`BaseWidget.type` is a plain instance field, assigned once in the constructor. `_state.type` is written only in the constructor and by `setNodeId` -> `useWidgetValueStore().registerWidget()`. There is no `type` setter, no proxy, and no writeback. `registerWidget` also early-returns an existing state when the type matches, so it is not a refresh path either.

The pre-registration case is handled and tested: `setNodeId` passes `type: this.type` rather than spreading the constructor `_state`, covered by `BaseWidget.test.ts` "registers the live widget type". The post-registration case is not.

## Why it matters now

The ECS Compatibility Pre-Release Report (2026-08-21) clears `comfyui-easy-use` (198,516 installers, top-5 by install weight) by code review only, on the reasoning that "neither renderer dispatches on the type string". That is not true of `main`'s Vue renderer, which dispatches on the live string in two places. The branch makes it true, so the clearance is reasoning about the post-change state and concluding the pack is safe from a property the change itself introduces.

Type repurposing at runtime is a common hide-a-widget idiom in the pack ecosystem (`widget.type = 'converted-widget'` / `'hidden'`, with the original stashed on `origType`).

## Fix options

1. Mirror `type` into the store on assignment: turn `BaseWidget.type` into an accessor over `_state.type`, matching how `label` and `disabled` already work.
2. Keep the field but fall back to the live widget in the renderer: `liveWidgets.get(id)?.type ?? widgetState.type`. `liveWidgets` is already built in `computeProcessedWidgets`.

Option 1 is the one consistent with the store being the source of truth. It also fixes `SimplifiedWidget.type` and `renderKey`, which have the same staleness.

## Verification this needs

A seeded control, not a code read: install a pack that swaps `widget.type` at runtime, or fake one, and confirm the widget hides in Nodes 2.0 on `main` and does not on the branch. This is the same class of gap as the compat battery having no seeded-defect control.

Found while reviewing PR #14246. Posted as https://github.com/Comfy-Org/ComfyUI_frontend/pull/14246#discussion_r3833412271

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.