The bypassed interior loader's only error-bearing widget was promoted
(linked from the subgraph input), which makes the widget inert — its
missing-model error is not the interior node's to restore. The test
passed before the input.link mirror deletion only because serialized
promotion links never populated the mirror, so the interior scan ran
against an inert widget by accident.
Add a second interior CheckpointLoaderSimple whose ckpt_name widget is
unlinked, so un-bypassing restores its own error through the plain
interior scan regardless of promoted-binding health.
Survivor selection consulted the link store, whose load-time registration
is first-wins by link map order — when a stale duplicate preceded the
referenced link, the referenced link was purged instead. Combined with
input slot drift, realignInputLinkSlots then had no link to realign and
the surviving registration stayed keyed to the stale slot, reading the
input as unconnected (issue #10291 regressed).
The serialized node data is now the survivor authority, with store
registration as the fallback for data that carries no input reference.
registerReroute silently overwrote existing chain entries, and the
identity-guarded delete meant a displaced owner could never vacate its
entry. Copying a subgraph node configured a throwaway live clone whose
identically-numbered reroutes shadowed the live chains, and pasting one
skipped reroute-id dedup entirely, conflating chains across graphs and
serializing corrupted parentId links. Serialize clipboard data from a
plain snapshot instead of a live clone, dedup reroute ids on paste with
the same machinery configure uses, and make registerReroute warn and
refuse foreign overwrites.
multiClone keeps node ids, so configuring the new subgraph while the
originals were still registered made every interior clone link lose
first-wins registration, hijacked the originals' reroute chains, and let
the later removal loop delete the badge keys the clones were piggybacking
on — converted nodes read disconnected and badge-less until save+reload.
Vacate the originals first: disconnect boundary links, remove interior
links and reroutes before the subgraph configures, and re-register the
subgraph's nodes in the badge store after the originals are removed.
Removing the last instance of a subgraph recursed unconditionally into
nested definitions: a nested definition still instanced elsewhere had its
inner nodes' badge registrations stripped and removal lifecycle fired
while live, while a definition referenced only inside the removed subtree
leaked its link topologies, reroute chains, and registry entry forever.
Compute the releasable set by excluding the removed instance from the
live-reference walk, then release each definition exactly once: lifecycle
first, then badge/link/reroute unregistration and registry removal.
isWidgetValue rejected null while the widget value type includes it, so
every promoted-widget hop coerced a stored null to undefined: serialize
dropped the value (deleting widgets_values entirely when all values were
null, so reload restored interior defaults), projection getters read null
back as undefined, reorder skipped rewriting it, and a null quarantined
host value fell through to stale widgets_values. Accept null in the
guard, use an explicit has-check for quarantine lookup, and compare
Reset-to-default enablement against the store state instead of
null-coalescing to the live widget value.
insertWorkflow configured a throwaway LGraph that adopted the file's
serialized graph id, registering scratch nodes into the root-scoped
stores. Inserting a workflow whose id matched the open one bound scratch
widgets first-wins to the live WidgetState objects and reverted unsaved
widget edits to file values; differing ids leaked the scratch bucket for
the session. Give the scratch graph a synthetic UUID and clear it in a
finally block so all store buckets are disposed even if copying throws.
LGraph.remove and the PrimitiveNode proxy-widget migration read
output.links through the warn-once deprecation getter, consuming the
telemetry budget before any third-party read could be observed. Migrate
both to store-backed queries, migrate two test assertions off the input
mirror, and pin node removal in the zero-deprecation-warnings regression
test.
The linkStore is the single source for input-slot connectivity.
Readers use node.isInputConnected / node.getInputLink or the slotLinks
input helpers; NodeInputSlot.link is a deprecation-telemetry accessor:
reads warn and return the store-derived id, writes warn naming
connect()/disconnectInput() and are ignored. Serialization derives
inputs[].link from the store; wire format unchanged.
Store re-keying is authoritative: moving a registered link evicts any
incumbent under the new key, so slot permutations cannot drop links;
first-wins placement remains only for load-time registration, which
duplicate-link dedup depends on. dynamicWidgets rebuilds capture one
at-rest slot-to-link snapshot and reconcile once at the end.
fixLinkInputSlots is replaced by realignInputLinkSlots inside
LGraph.configure, reading the effective (possibly deduplicated-clone)
node data. disconnectOutput's duplicated branches collapse into one
per-link loop that removes the store entry before onConnectionsChange
fires. 'link' in slot discriminators are deleted in favor of direction
threading; _setConcreteSlots writes wrappers back into node
inputs/outputs so identity-based lookups hold; addInput/addOutput drop
legacy link/links keys from extra_info. First-party code no longer
touches either deprecated mirror.
The linkStore is the single source for output-side connectivity. The
NodeOutputSlot.links field and all nine write sites are gone; a
read-only prototype getter derives the array from the store and fires
warnDeprecated as migration telemetry for extensions (no setter, so
writes throw). INodeOutputSlot keeps links as deprecated readonly so
'links in slot' discriminants still compile. Wire format is unchanged:
serialization derives outputs[].links from the store, and serialized
operators (linkFixer serialized branch, migrateReroute, unpackSubgraph
strip) are untouched; linkFixer now completes a missing input mirror
instead of deleting a store-registered link.
Also fixes createTestSubgraphNode minting duplicate node ids (the
fixture never reserved the id it assigned) and promoted-widget labels
not inheriting the source slot label - both previously masked by the
mirror or the id collision.
Extension migration: read via node.isOutputConnected(slot) /
node.getOutputNodes(slot); mutate via node.connect() /
node.disconnectOutput(). Design record: docs/architecture/
output-slot-connectivity.md Decision 6.
All ~30 internal output.links read sites now go through the slotLinks
helpers or slot.isConnected; the mirror is write-only pending deletion.
Serialization derives outputs[].links from the store, sorted ascending
by id (equal to push order for organically built graphs) and null when
empty. configure() fires its output callbacks from the serialized
argument instead of the mirror copy.
Deliberate bugfix: disconnectInput passed the mirror-array index as the
OUTPUT slot number to onConnectionsChange; it now passes
link_info.origin_slot.
Link queries now return fully-assigned links only: buildOutputIndex skips
floating topologies, matching the output.links mirror they replace. The
domain rule lives in isFloatingTopology, which also replaces the
hand-rolled sentinel checks in rerouteStore, dynamicWidgets, and
widgetValuePropagation. The slot-drag disconnect check keeps its floating
awareness via slotFloatingLinks, its pre-store behavior.
New node/slotLinks helpers (outputHasLinks/outputLinkIds/outputLinks)
back the remaining app readers: rerouteNode, widgetInputs, groupNode,
proxyWidgetMigration, and useNodeReplacement now read the store instead
of output.links. Behavior change, deliberate: PrimitiveNode's
onLastDisconnect fires on disconnect-all, where the stale mirror
previously suppressed it.
getOutputSlotLinks now returns ReadonlySet<LinkTopology> instead of link
ids: floating links draw ids from a separate counter, so a bare id cannot
be resolved safely through graph.links, and the migrated readers need
endpoints anyway. Migrated: minimap link extraction, slot-drag disconnect
check (one store query replaces a mirror read plus a slotFloatingLinks
scan), widget value propagation, and matchType link revalidation.
rerouteNode/widgetInputs/groupNode stay on the mirror deliberately - they
read inside connection callbacks whose behavior depends on mid-mutation
mirror staleness - as do the serialized-workflow repair tools. Design
record updated.
Adds a derived per-graph reverse index over link origins to linkStore,
exposing isOutputSlotConnected / getOutputSlotLinks as the output-side
mirror of the input queries. Target and origin slot keys are branded
separately so a key built for one index cannot be looked up in the other.
NodeSlots now passes connected to InputSlot and OutputSlot from the store;
the lg-slot--connected class reaches the dot via CSS, so SlotConnectionDot
needs no new prop. Design record in docs/architecture.
Default IBaseWidget TValue to the canonical WidgetValue so a widget value
may be null. The Vue update handler now assigns the same normalized value
to the store, live widget .value, and callback instead of coercing .value
to undefined. Coerce null/void to undefined only at the persisted
NodeProperty boundary.
Amp-Thread-ID: https://ampcode.com/threads/T-019f2a75-8d64-7605-980d-7aca51a5f19c
Co-authored-by: Amp <amp@ampcode.com>
The promoted-widget side panel only ever has one host SubgraphNode, so
collapse the parents: SubgraphNode[] prop to host?: SubgraphNode across
SectionWidgets/WidgetItem/WidgetActions and drop the [node] wrapper in
TabSubgraphInputs.
Amp-Thread-ID: https://ampcode.com/threads/T-019f2964-7a96-7579-8883-caeb3b2b07da
Co-authored-by: Amp <amp@ampcode.com>
isLinkedPromotion has no production callers outside promotionUtils.
Drop its dedicated direct-unit describe block and rewrite the demoteWidget
tests that used it as an assertion oracle to check observable interior
link state instead.
Amp-Thread-ID: https://ampcode.com/threads/T-019f2964-7a96-7579-8883-caeb3b2b07da
Co-authored-by: Amp <amp@ampcode.com>
- demoteRow now removes the linked input via the host input slot we
already have, instead of re-resolving the source and calling
demotePromotedInput
- un-export demotePromotedInput (only internal callers remain)
- drop dead parents param from renameWidget/promptRenameWidget
Amp-Thread-ID: https://ampcode.com/threads/T-019f2964-7a96-7579-8883-caeb3b2b07da
Co-authored-by: Amp <amp@ampcode.com>
isShownOnParents never changed observable behavior: for promoted rows
parents[0] === node (so favoriteNode was unchanged) and the toggle was
suppressed by isLinked; for interior rows it was always false. Its
Hide-toggle branch and parent-redirect were unreachable.
- drop isShownOnParents prop from WidgetItem and WidgetActions
- favorites key directly off node (was favoriteNode)
- WidgetActions toggle becomes a one-way Show Input action; remove the
dead handleHideInput + demoteWidget usage and the hideInput locale key
- remove SectionWidgets.isWidgetShownOnParents
Amp-Thread-ID: https://ampcode.com/threads/T-019f2964-7a96-7579-8883-caeb3b2b07da
Co-authored-by: Amp <amp@ampcode.com>
SectionWidgets and WidgetActions identified promoted subgraph-input widgets
by walking live links back to the interior source. Per ADR0009 a promoted
widget is host-scoped (widget.widgetId = graphId:hostNodeId:inputName, also
carried on the host input slot), so reuse that instead:
- isLinked: inputForWidget(node, widget)?.widgetId != null
- isWidgetShownOnParents: match parent inputs by widget.widgetId
- handleHideInput: drop unreachable source-resolution branch, keep demoteWidget
- remove now-unused widgetPromotedSource export
Favorites keep their current (nodeLocatorId, widgetName) key; added a
TODO(ADR0009) to move to a pure WidgetId key (deferred: needs a persisted
format migration).
Amp-Thread-ID: https://ampcode.com/threads/T-019f2964-7a96-7579-8883-caeb3b2b07da
Co-authored-by: Amp <amp@ampcode.com>
Remove claims that drifted from code: _version 'debug only', badge
computed 'tracks graph structure revision', the never-built
graphStructureRevision module, and the deleted badge store.
Reactive version bump before onNodeAdded scheduled the Vue flush ahead
of the hook's microtask-deferred missing-model scan; the errors tab was
evicted on paste before the pasted node's candidate existed.
Badge rows are derived presentation, not entity state; the store was a
materialized cache and its registration/watcher/scope machinery was
cache invalidation. nodeBadges(node) is now one lazy computed per node
instance in a WeakMap: entries die with their nodes, so registration,
teardown, and the stale-bucket bug class do not exist. A single
graphStructureRevision (bumped at add/remove/clear chokepoints and
structure events) replaces bucket membership, subgraphCreditsRevision,
and the live-graph-id seams. Litegraph consumes rows through a
one-function provider seam, keeping its import graph acyclic; the
identity-keyed draw cache is unchanged.
Extension badge kind deleted: node.badges is the raw extension surface,
sampled by the Vue partition on recompute. Store rows are replaced
wholesale per write so the draw cache keys on array identity instead of
JSON. wirePricingRecalculation and its onConnectionsChange patch were
redundant with the system's tracked sources. badgePosition returns as a
deprecated no-op accessor. BadgeData and pricing sources are
discriminated unions.
Subgraph instances share their definition's nodes, so mapAllNodes
revisits shared internals per instance. mapUniqueNodes visits each
definition once and terminates on cycles. Replaces the local walk in
badgeSystem and fixes duplicate PartnerNodesList entries (and v-for
keys) when a subgraph is instanced more than once.
- registeredNodeIds tracks bucket size only on get() miss, sparing
hit-path readers spurious invalidations from unrelated buckets
- replace mutable-accumulator walk with collectApiLeaves flatMap;
keeps definition-dedupe and cycle guard from legacy usePriceBadge
The system's watch tracked only the current graph id's bucket key; a
workflow load buckets nodes under a brand-new graph id the watcher had
never observed, so it never fired again and no badges were written
after any configure. registeredNodeIds now also reads the bucket map
size, so bucket creation and deletion wake readers regardless of which
id they last saw.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
LGraph.clear() and configure() reassign the root graph id, so the badge
system now resolves it on every recompute instead of capturing it at
start; a captured id stranded the system on a dead bucket after
Clear Workflow, dropping every badge on nodes added afterwards.
LGraph.add registers the node in the badge store only after
onNodeAdded: the store write wakes the system watcher and queues Vue's
flush, which must not overtake the deferred paste scan that surfaces
missing-model errors before selection-scoped tabs recalculate.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Wrapper aggregation counts inner api nodes (the old unconditional
per-api-node badge basis) rather than resolved credits rows, so a
pending async price still yields 'Partner Nodes x N'; the label is
localized via nodeBadge.partnerNodesCount. useNodePricing now caches
labels per signature: a leaf's own read and its wrapper's
promoted-override read previously shared one cache slot and evicted
each other, re-scheduling evaluations forever once both ran in eager
system effects.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Legacy drawBadges renders the store rows through a per-node memoized
LGraphBadge cache (core parts joined id/lifecycle/source and truncated,
icons resolved via a badge icon registry) followed by the raw
node.badges extension surface, whose thunks still evaluate per frame.
The Vue partition reads the same rows — trimming lifecycle brackets and
swapping a built-in node's source row for the Comfy logo chip — plus the
raw extension surface.
useNodeBadge now bootstraps the badge system per root graph and forwards
the subgraph structure events; dynamic-pricing recalculation wiring is
kept. The usePriceBadge closures, the VueNodeData.badges mirror, its
property trigger, and node.badgePosition (no ecosystem uses; single
writer/reader) are deleted. node.badges itself stays: three custom-node
packs actively push to it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
SubgraphNode wrappers project their credits row from the inner nodes'
store rows: several priced inner nodes collapse to 'Partner Nodes x N',
a single one shows its price with the wrapper's promoted widget values
overriding the inner node's own. The aggregation tracks inner rows, the
registered-node set, and an exported structure revision for the events
stores cannot observe.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Renderers own presentation: legacy joins id/lifecycle/source in its own
order with brackets intact, Vue trims and reorders. Rows carry the raw
projected text plus a part discriminator instead of pre-trimmed text.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
## Summary
Extracts node badges into a dedicated store per ADR 0008 — slice A of
`docs/architecture/node-badge-store.md`: store, plain `BadgeData` rows,
reactive badge system, and LGraph chokepoint registration. Stacked on
#13455.
## Changes
- **What**:
- `src/types/badgeData.ts` — plain `BadgeData { kind, text, fgColor?,
bgColor?, iconKey? }` rows (decision 1).
- `src/stores/nodeBadgeStore.ts` — root-scoped buckets keyed by
`NodeId`, kind-ordered reads, identity-checked row deletes,
`registerNode`/`unregisterNode`/`clearGraph` registration trio; row
writes refused for unregistered nodes (decision 2).
- `src/systems/badgeSystem.ts` — pure `computeBadges(sources)` +
effect-scope-per-node shell watching `registeredNodeIds`; writes core
and credits rows from
nodeDef/settings/palette/pricing/widget-value/link-connectivity sources
(decisions 3–4).
- `LGraph.add`/`remove`/`clear` + subgraph-definition GC maintain the
store registration (linkStore/rerouteStore convention).
- Design record updated with implementation notes.
- **Breaking**: none yet — `node.badges`/`useNodeBadge` closures
untouched; nothing starts the system in production until slice B's
consumer cutover.
## Review Focus
- Registration is bucket-key presence; the system discovers nodes by
watching `registeredNodeIds`, so litegraph never imports pricing/nodeDef
modules (no import cycle via the litegraph barrel).
- Core rows are fine-grained (lifecycle, id, source) under one
visibility rule — unification notes in the design record (Vue gains
HideBuiltIn on id badges; legacy join/truncation becomes slice-B
draw-cache presentation).
- Subgraph credits aggregation, the legacy `node.badges` surface, and
`badgePosition` are open decisions in the design record and deliberately
not in this PR.
- Slice A+B together would exceed the ~300-line guideline, hence the
split; store row APIs (`registerBadge`/`deleteBadge`) land now because
the slice-B shim consumes them.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
## Summary
Migrates the `inputs[].link` mirror-based reactivity tracking to
`linkStore` queries and deletes the slot-link reprojection machinery in
`useGraphNodeManager`.
## Changes
- **What**: `NodeSlots.linkedWidgetedInputs`, `usePartitionedBadges`'
pricing pokes, and `trackNodePrice` now query
`linkStore.isInputSlotConnected` (root graph id + unfiltered slot
index). Deletes the `node:slot-links:changed` handler and its
load-bearing comment, the emitter-less `node:slot-errors:changed`
handler, `refreshNodeInputs`, and the node-removal refresh-all loop.
Adds unified-mode NodeSlots tests (none existed) and a `trackNodePrice`
reactivity test. Drafts `docs/architecture/node-data-store.md` for the
next phase.
- **Not removed**: the `node:slot-links:changed` emitters and trigger
type entries, the `input.link` mirror itself, the computation-path
`inp.link` reads in `useNodePricing.buildJsonataContext` (migrated in
#13498 alongside the mirror deletion), and the `node:slot-label:changed`
handler — those belong to the Slot extraction / nodeDataStore phases.
## Review Focus
- The node-removal refresh-all loop deletion (own commit) — originally
shipped as a regression fix; gated locally on the vue-nodes legacy
widget e2e spec plus the full vueNodes suite (126 passed).
- Pricing badges previously only re-evaluated for widgeted inputs (the
trigger's gate); linkStore queries now also cover non-widgeted pricing
inputs — a correctness improvement, not parity.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
## Summary
Follow-up cleanups from the reroute chain store work (#13449): deletes
the `slot._floatingLinks` mirror in favor of endpoint derivation, fixes
two position/endpoint integrity gaps the mirror had masked, and
restructures the subgraph-unpack chain stitching.
Stacked on #13449.
## Changes
- **What**:
- `slot._floatingLinks` Sets deleted — a floating link's attachment is
fully encoded in its own endpoints, so `slotFloatingLinks(network, side,
nodeId, slot)` derives it; ~25 write sites across
`addFloatingLink`/`removeFloatingLink`, `setFloatingLinkOrigin`, and
every `FloatingRenderLink` connect method are gone
- `moveInputLink`/`moveOutputLink` gain the `node` param their
`dragNewFrom*` siblings already take
- `FloatingRenderLink.connectToSubgraphOutput` now writes the link's
**target** end (it wrote `origin_id = SUBGRAPH_OUTPUT_ID`,
clobbering/skewing endpoints — masked by the mirror)
- `removeInput`/`removeOutput` renumber floating-link slot indices
alongside real links (stale indices previously persisted into serialized
floating links)
- `Reroute.snapToGrid` mirrors into the layout store like `move()` does
(Vue hit-testing read a stale spatial index after snapped drags)
- Subgraph-unpack reroute chain stitching rewritten as
collect-then-stitch (two segment walks + one generic pointer pass,
replacing three interleaved walk-and-write loops)
## Review Focus
- Scoped out with written assessments: `input.link`/`output.links`
mirror removal (roadmap-scale `SlotConnection` extraction, ~530
accesses) and reroute position ownership inversion (layout store only
seeds the active graph, no writeback)
- Corrupt-data error paths in unpack stitching are normalized to
per-link skip (the old code broke the whole loop on some
internal-segment failures)
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
## Summary
Extracts reroute connectivity into a dedicated `rerouteStore` per ADR
0008: chain state (`parentId`, floating marker) is stored by reference,
and link membership (`linkIds`/`floatingLinkIds`) is derived from the
links' `parentId` chains instead of being hand-maintained at ~10 write
sites.
Stacked on #13436 (link topology store); design record in
`docs/architecture/reroute-chain-store.md`.
## Changes
- **What**:
- `LLink._state` now holds the link store's reactive proxy (BaseWidget
pattern), so `link.parentId` writes are tracked by Vue
- New `rerouteStore`: root-scoped buckets keyed by `RerouteId`,
`RerouteChain {id, parentId?, floating?}` registered by reference;
`Reroute` class reads/writes through the store proxy
- Membership derived via a per-graph computed reverse index over link
topologies + chain states; `Reroute.linkIds`/`floatingLinkIds` become
readonly derived accessors
- Deleted: all manual membership mutation sites,
`Reroute.validateLinks`, `Reroute.removeLink`, `Reroute.update`'s
linkIds param
- `LGraph._addReroute`/`_removeReroute` chokepoints mirror
`_addLink`/`_removeLink`
- Load-time reroute-id dedup across sibling subgraph definitions
(mirrors node-id dedup; reroute ids must now be per-root-unique)
- Serialization emits `linkIds` in ascending link-id order; goldens
added for byte-identical round-trip
- **Breaking**: `Reroute.linkIds`/`floatingLinkIds` are now
`ReadonlySet` accessors — external mutation of these sets no longer
affects membership (the chain is the single source of truth).
`Reroute.validateLinks`/`removeLink` removed.
## Review Focus
- `LLink.disconnect` now unregisters the link before pruning empty
reroutes, so derived counts exclude it
- `dropOnReroute` resolves the drop target's source output once before
connecting moved links — re-anchoring the first link's chain changes
derived membership immediately (`LinkConnector.ts`)
- Workflows whose stored `linkIds` contradict their chains are repaired
rather than preserved (design decision 5; no shim)
---------
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: github-actions <github-actions@github.com>
## Summary
Make a Pinia store (`useLinkStore`) the single source of truth for graph
link topology (ADR-0008 / ECS), mirroring `widgetValueStore`. `LLink`
topology fields become accessors over one `_state` object registered
into the store **by reference** — one copy, no mirror, no sync bridge.
## Changes
- **What**:
- New `useLinkStore` (`src/stores/linkStore.ts`) — **keyed by target
input slot**. At most one live link can target a given input slot
(litegraph disconnects the previous link before connecting a new one),
and every consumer queries by target, so the dominant query ("is this
input connected, and by what?") is one map lookup. Public surface is 6
actions (`registerLink`, `updateEndpoint`, `deleteLink`,
`isInputSlotConnected`, `getInputSlotLink`, `clearGraph`).
- Links without a unique target live in a per-graph side collection:
floating links (either endpoint unassigned; several can share an input
slot) and links into `SUBGRAPH_OUTPUT_ID` (a constant shared by every
subgraph in a root bucket). Neither is queried by target. Floating links
never answer `isInputSlotConnected`, matching `input.link` semantics.
- `LLink` topology backed by a single `_state: LinkTopology`; endpoint
setters delegate to `updateEndpoint` (displace → patch → re-place) when
registered. `updateEndpoint`/`deleteLink` take the topology by reference
and are identity-checked; `LLink._graphId` is set only while the link
actually holds a registration.
- `LGraph._addLink` / `_removeLink` chokepoints: every `_links` map
add/remove funnels through them (both `configure` loops, subgraph
reconnect, duplicate-link purge, `linkFixer`), keeping link-store and
layout entries in sync. Subgraph-definition removal and
non-root/unconfigured `clear()` unregister their links.
- Topology registered at every litegraph create site (`connectSlots`,
both `configure` loops, `addFloatingLink`, subgraph connect) and cleaned
up on `disconnect` / `removeFloatingLink` / `clear` / node-delete
cascade.
- Deleted the `layoutStore.ylinks` connectivity mirror +
`layoutMutations.createLink`/`deleteLink` +
`Create/DeleteLinkOperation`; node removal disconnects links before
`onNodeRemoved` fires, so layout cleanup needs no connectivity mirror or
layout→link cross-store call.
- Migrated the widget-disabled hot path (`WidgetItem.isLinked`,
`AppModeWidgetList`, `buildSlotMetadata`) to O(1) `isInputSlotConnected`
/ `getInputSlotLink`; removed the now-redundant `node:slot-links:changed
→ refreshNodeInputs` re-projection.
- Serialization round-trip goldens (plain / reroute-chain / floating /
subgraph). Because the store no longer keys by link id, subgraph link-id
collisions are irrelevant to it and **workflows load without any link-id
rewriting** (the node-id dedup for widget-store keys is pre-existing and
unchanged).
- **Breaking**: `LLink` topology fields (`origin_id`, `origin_slot`,
`target_id`, `target_slot`, `type`, `parentId`) are now accessors over
`_state`; `parentId` moves from own-property to prototype accessor
(`hasOwnProperty('parentId')` now returns `false`). `asSerialisable()`
output is byte-identical (guarded by goldens).
## Review Focus
- **Register-by-reference**: `LLink._state` is the exact object the
store holds — no second copy, no sync function. Verify no path writes
topology to two places.
- **Target-keying invariant**: one live link per input slot is enforced
by litegraph (`connectSlots` disconnects first); the store's side
collection covers exactly the links that violate or escape it (floating,
subgraph-output-node targets).
- Endpoint setters must delegate fully to `updateEndpoint` when
registered and mutate `_state` directly only when unregistered.
- Lifecycle completeness: every create path registers, every destroy
path unregisters — `_addLink`/`_removeLink` are the chokepoints.
- **Reroutes explicitly deferred.** Remaining `input.link` connectedness
*reads* (`nodeDataUtils`, `useNodePricing`, GLSL preview) are strangler
residue for a follow-up — not in scope here.
Stacked on #13322 — base is `drjkl/safe-widget-cleanup`.
## Migration note (extension authors)
`LLink` topology fields (`id`, `origin_id`, `origin_slot`, `target_id`,
`target_slot`, `type`, `parentId`) are now accessors over an internal
`_state` object, so they are non-enumerable on live instances.
`Object.keys(link)`, `JSON.stringify(link)`, `{...link}`, and
`structuredClone(link)` no longer expose them. To copy or serialize a
link, use `link.asSerialisable()` — direct field reads
(`link.origin_id`) are unchanged. Persisted workflow JSON is unaffected
(byte-identical, covered by goldens).
## Screenshots
Manual render-parity checklist (run against a ~200-node workflow): links
render; connect/disconnect updates live; undo/redo; subgraph nav. E2E
`nodeSelection.spec.ts` passes 13/13 including "a linked widget is
disabled". Note: 8 `linkInteraction.spec.ts` screenshot diffs (~0.01
ratio) observed — no commit in this branch touches link
geometry/coordinates/draw order, so these are
pre-existing/environmental; confirm against base before un-drafting.
---------
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: AustinMroz <austin@comfy.org>
## Summary
Move Vue widget render-only metadata out of the graph node manager and
into dedicated widget render state alongside widget values.
## Changes
- **What**: Adds `WidgetRenderState` in `widgetValueStore`, removes
`SafeWidgetData` from `useGraphNodeManager`, and has Vue widget
rendering compose from raw LiteGraph widgets plus store-backed
value/render state.
- **What**: Updates promoted widget, app mode, parameter panel, preview,
tooltip, and widget tests for the new render-state boundary.
- **What**: Hardens related browser tests against branded id typing and
brittle default workflow node ids.
## Review Focus
Please look closely at the ownership boundary between raw LiteGraph
widgets, `WidgetState`, and `WidgetRenderState`, especially promoted
widget metadata and linked-widget rendering.
## Validation
- `pnpm typecheck`
- `pnpm typecheck:browser`
- focused widget/node unit tests
- focused Playwright check for `Should display added widgets`
---------
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: AustinMroz <austin@comfy.org>
2026-07-09 10:42:30 -07:00
336 changed files with 11050 additions and 5274 deletions
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.