From 1f4a4af079a556a5dbcbe2c574dd18c3aa88d816 Mon Sep 17 00:00:00 2001 From: Alexander Brown Date: Thu, 7 May 2026 19:51:18 -0700 Subject: [PATCH 001/142] docs: add subgraph promoted widgets ADR (#11997) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Adds an ADR documenting the canonical subgraph promoted-widget model and legacy proxy-widget ratchet. ## Changes - **What**: Defines linked `SubgraphInput` promoted widgets, host-owned sparse value overlays, proxy-widget repair/quarantine behavior, primitive-node repair, and separate display-only preview exposures. - **Breaking**: None; documentation only. - **Dependencies**: None. ## Review Focus - Whether the ADR cleanly separates value-owning promoted widgets from display-only preview exposures. - Whether the legacy ratchet, quarantine, primitive repair, and UI identity decisions are clear enough for implementation review. ## Screenshots (if applicable) N/A @Coderabbitai why would a docs update need an end-to-end test? ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-11997-docs-add-subgraph-promoted-widgets-ADR-3576d73d36508133bf1ee8d49282cac1) by [Unito](https://www.unito.io) --------- Co-authored-by: GitHub Action --- docs/adr/0008-entity-component-system.md | 1 + ...raph-promoted-widgets-use-linked-inputs.md | 328 ++++++++++++++++++ .../before-after-flows.md | 210 +++++++++++ .../disambiguating-source-node-id.md | 147 ++++++++ .../system-comparison.md | 37 ++ docs/architecture/ecs-migration-plan.md | 9 + docs/architecture/proto-ecs-stores.md | 7 + .../subgraph-boundaries-and-promotion.md | 58 ++-- 8 files changed, 765 insertions(+), 32 deletions(-) create mode 100644 docs/adr/0009-subgraph-promoted-widgets-use-linked-inputs.md create mode 100644 docs/adr/0009-subgraph-promoted-widgets-use-linked-inputs/before-after-flows.md create mode 100644 docs/adr/0009-subgraph-promoted-widgets-use-linked-inputs/disambiguating-source-node-id.md create mode 100644 docs/adr/0009-subgraph-promoted-widgets-use-linked-inputs/system-comparison.md diff --git a/docs/adr/0008-entity-component-system.md b/docs/adr/0008-entity-component-system.md index 42b427f0f6..36233903a0 100644 --- a/docs/adr/0008-entity-component-system.md +++ b/docs/adr/0008-entity-component-system.md @@ -249,6 +249,7 @@ Companion architecture documents that expand on the design in this ADR: | [ECS Lifecycle Scenarios](../architecture/ecs-lifecycle-scenarios.md) | Before/after walkthroughs of lifecycle operations (node removal, link creation, etc.) | | [World API and Command Layer](../architecture/ecs-world-command-api.md) | How each lifecycle scenario maps to a command in the World API | | [Subgraph Boundaries and Widget Promotion](../architecture/subgraph-boundaries-and-promotion.md) | Design rationale for modeling subgraphs as node components, not separate entities | +| [ADR 0009: Subgraph promoted widgets](0009-subgraph-promoted-widgets-use-linked-inputs.md) | Follow-up decision for promoted widget identity and value ownership at subgraph boundaries | | [Appendix: Critical Analysis](../architecture/appendix-critical-analysis.md) | Independent verification of the accuracy of the architecture documents | | [Change Tracker](../architecture/change-tracker.md) | Documents the current undo/redo system that ECS cross-cutting concerns will replace | diff --git a/docs/adr/0009-subgraph-promoted-widgets-use-linked-inputs.md b/docs/adr/0009-subgraph-promoted-widgets-use-linked-inputs.md new file mode 100644 index 0000000000..daec5aacfb --- /dev/null +++ b/docs/adr/0009-subgraph-promoted-widgets-use-linked-inputs.md @@ -0,0 +1,328 @@ +# 9. Subgraph promoted widgets use linked inputs + +Date: 2026-05-05 + +Appendices: + +- [Before/after flow diagrams](./0009-subgraph-promoted-widgets-use-linked-inputs/before-after-flows.md) +- [System comparison](./0009-subgraph-promoted-widgets-use-linked-inputs/system-comparison.md) +- [Removing `disambiguatingSourceNodeId`](./0009-subgraph-promoted-widgets-use-linked-inputs/disambiguating-source-node-id.md) + +## Status + +Proposed + +## Context + +Subgraph widget promotion historically had two overlapping representations: + +1. `properties.proxyWidgets`, a serialized list of source node/widget tuples; +2. linked subgraph inputs, where an interior widget-bearing input is exposed + through the subgraph boundary. + +This created ambiguous ownership. Runtime value reads could collapse to an +interior source widget, while host `widgets_values` could also carry an +exterior value. Multiple host instances of the same subgraph could therefore +stomp one another, and serialization could mutate interior widgets as a +persistence carrier for exterior values. + +The ECS widget migration makes that ambiguity more expensive: widgets are +becoming entities with component state keyed by stable entity identity, and +subgraphs are modeled as graph boundary structure rather than a separate +promotion-specific entity kind. + +## Decision + +Promoted widgets are represented only as standard linked `SubgraphInput` +widgets. A promoted widget is a host-scoped widget entity owned by a subgraph +input on a host `SubgraphNode`. The interior source widget supplies schema, +type, options, tooltip, and default metadata, but it is not the owner of the +host value. + +Display-only preview surfacing, such as `$$canvas-image-preview`, is not a +promoted widget. It is a separate preview-exposure system because it has no +host-owned widget value, does not feed prompt serialization, and often points at +virtual `serialize: false` pseudo-widgets that may not exist on the source node. + +`properties.proxyWidgets` becomes a legacy load-time input only. Successful +repair consumes entries from `proxyWidgets`; canonical saves do not re-emit +those entries. The standard serialized representation is the existing subgraph +interface/input form plus host-node `widgets_values`. + +Display-only preview exposures use their own host-node-scoped serialized entry, +`properties.previewExposures`, instead of `properties.proxyWidgets` and instead +of linked `SubgraphInput` widgets. Canonical preview-exposure JSON uses preview +language, not widget language: + +```ts +type PreviewExposure = { + name: string + sourceNodeId: string + sourcePreviewName: string +} +``` + +Host-node scope preserves current behavior where different instances of the +same subgraph can choose different exposed previews. + +The entry intentionally stores only host preview identity and source locator +identity. `name` is the host-scoped stable identity for this preview exposure, +analogous to `SubgraphInput.name`; it is not a display label. It is generated +with existing collision behavior, such as `nextUniqueName(...)`, when an +exposure is created. Media type, display labels, titles, image/video/audio URLs, +and other runtime preview details are derived from the current graph and output +state. Array order is the canonical display order. Preview exposures do not get +a separate persisted `label` in this slice; if a future rename UX needs one, it +should follow the same rule as subgraph inputs: `name` is identity and `label` +is display-only. + +Preview exposures are persisted user choices after creation. Packing nodes into +a subgraph may auto-add recommended preview exposures for supported output +nodes, and users may explicitly add or remove additional preview exposures +afterward. Normal load/save does not re-derive previews from node type alone, +because that would make old workflows change when support for new preview node +types is added. Unresolved preview exposures remain persisted and inert; +automatic cleanup does not prune them. They are removed only by explicit user +action or by destruction/unpacking of the owning host. + +Preview exposures compose through nested subgraph hosts by chaining immediate +boundaries. If an outer subgraph wants to show a preview exposed by an inner +subgraph host, the outer `previewExposures` entry points at the immediate inner +`SubgraphNode`, and `sourcePreviewName` names the inner host's preview-exposure +identity, not the deepest interior preview name. Runtime preview resolution may +then follow the inner host's own preview exposures to find media. Canonical JSON +does not persist flattened deep paths, because deep paths would couple host UI +state to private nested graph internals. + +## Identity and value ownership + +- UI/value identity is host-scoped: host node locator plus + `SubgraphInput.name`. +- Host-scoped identity means the host `SubgraphNode` instance within its + containing `graphScope`; the interior source node is not the state or + persistence owner. +- `SubgraphInput.name` is the stable internal identity. +- `SubgraphInput.label` / `localized_name` are display-only. +- `SubgraphInput.id` may be used for slot-instance reconciliation, not as the + persisted widget value key. +- Source node/widget identity remains metadata for diagnostics, missing-model + lookup, schema projection, and migration only. +- The host/exterior value wins over the interior/source value during repair, + persistence, and prompt serialization. + +This follows the existing widget/slot convention: `name` is identity, `label` +is display. + +Promoted-widget value state is a host-scoped sparse overlay over source-widget +metadata and defaults. The source widget remains the schema/default provider; +host value state is materialized only when the exterior value differs from the +effective source default or when restored from persisted host state. Canonical +save/load must not eagerly mirror source defaults or use interior widgets as +persistence carriers. + +## Forward migration + +Loading a workflow with legacy `proxyWidgets` runs a one-way repair: + +1. Parse `properties.proxyWidgets` with the existing Zod-inferred tuple type. +2. Invalid raw `proxyWidgets` data logs `console.error`, does not throw, and is + not quarantined. +3. Build a multi-pass association map before mutation: + - normalized legacy proxy entry; + - projected legacy promoted-widget order; + - host `widgets_values` value, preserving sparse holes; + - repair strategy or failure reason; + - whether the entry is a value widget or display-only preview exposure. +4. Defer mutations until node IDs/entity IDs are stable and the subgraph graph + is configured. +5. On flush, re-resolve against current graph state, because clone/paste/load + flows may have remapped or created nodes and links. +6. If already represented by a linked `SubgraphInput`, consider the legacy + entry resolved and consume it. +7. Otherwise repair through existing subgraph input/link systems. +8. If the entry is display-only preview surfacing, migrate it into the separate + preview-exposure representation instead of creating a linked `SubgraphInput`. +9. If value-widget repair fails, write inert quarantine metadata and warn. + +The repair is idempotent. Pending plans store tuple/value data and re-check the +current graph before applying mutations. + +Legacy entries are classified as preview exposures when either: + +- the legacy source name starts with `$$`; or +- the source node resolves to a matching pseudo-preview widget, such as a + `serialize: false` preview/video/audio UI widget. + +Everything else is treated as a value-widget promotion candidate. An unresolved +preview-shaped entry remains inert at runtime and is still persisted, because +preview-capable pseudo-widgets and output media can be removed and re-added +dynamically. It is not quarantined because it has no user value to preserve. A +non-`$$` entry that cannot resolve to a source widget is a value-widget repair +failure and follows the quarantine path unless it can resolve to a +pseudo-preview widget. + +## Proxy widget error quarantine + +Valid legacy entries that cannot be repaired are persisted in +`properties.proxyWidgetErrorQuarantine`. Quarantined entries are inert: they do +not hydrate runtime promoted widgets, do not participate in execution, and are +not used for app-mode/favorites identity. + +Quarantine entries preserve enough information to avoid data loss and support +future tooling: + +```ts +type ProxyWidgetErrorQuarantineEntry = { + originalEntry: ProxyWidgetTuple + reason: + | 'missingSourceNode' + | 'missingSourceWidget' + | 'missingSubgraphInput' + | 'ambiguousSubgraphInput' + | 'unlinkedSourceWidget' + | 'primitiveBypassFailed' + hostValue?: TWidgetValue + attemptedAtVersion: 1 +} +``` + +Unresolved legacy UI selections/favorites are dropped with `console.warn`. +Workflow-level promotion/value intent is preserved by +`proxyWidgetErrorQuarantine`, not by a second UI quarantine format. + +## Primitive-node repair + +Legacy `proxyWidgets` may point at `PrimitiveNode` outputs. Primitive nodes +serve nearly the same purpose as subgraph inputs: they provide a widget value to +one or more target widget inputs. The migration repairs this expected legacy +shape in the first migration rather than quarantining it by default. + +Primitive repair: + +- coalesces exact duplicate legacy entries during planning; +- uses the primitive node's user title as the base input name when the node was + renamed, otherwise the primitive output widget name; +- applies existing naming behavior and `nextUniqueName(...)` for collisions; +- uses the existing primitive merge/config compatibility logic; +- creates one `SubgraphInput` for the primitive fanout; +- reconnects every former primitive output target to that input in target + order, using standard connect/disconnect APIs; +- applies the host value when one exists, otherwise seeds from the source + primitive value; +- leaves the primitive node and its widget value in place, but disconnected and + inert. + +Primitive repair is all-or-quarantine. If any target cannot be validated or +reconnected, the migration does not leave a partial rewrite; it quarantines the +entry with `hostValue` and logs the reason. + +## Serialization + +After repair/quarantine: + +- `properties.proxyWidgets` is omitted for repaired entries; +- display-only preview entries are omitted from `properties.proxyWidgets` and + emitted through `properties.previewExposures`; +- `properties.proxyWidgetErrorQuarantine` carries unrepaired valid entries; +- preview exposures do not carry quarantine values because they do not own user + values; unresolved preview exposures remain inert in `previewExposures`; +- host `widgets_values` contains host-owned values only for canonical host + widgets, not source-owned defaults or interior persistence copies; +- quarantined legacy values live in `proxyWidgetErrorQuarantine.hostValue`; +- array-form `widgets_values` remains for now. + +Preview exposures are display-only UI metadata. They drive host canvas/app-mode +preview rendering, but they do not create prompt inputs, do not create +`widgets_values`, do not alter node execution order, do not become executable +graph edges, and do not participate in prompt serialization. Runtime mapping +from backend `display_node`/output messages to a host preview exposure is a UI +projection only. + +The old `SubgraphNode.serialize()` behavior that copied exterior promoted +values into connected interior widgets is removed. A temporary TODO should mark +that removal point until the migration is proven stable. Host values are +serialized through standard subgraph-input widgets instead. + +Longer term, `widgets_values` should move from array order to an object/map +keyed by stable widget name, but that migration is out of scope for this +decision. + +## App mode, builder, and favorites + +The runtime migration and UI identity migration ship in the same slice. The UI +must not persist promoted selections by source node/widget identity after this +change. + +Canonical UI identity is: + +```ts +type PromotedWidgetUiIdentity = { + hostNodeLocator: string + subgraphInputName: string +} +``` + +Legacy source-identity selections are migrated when they resolve through the +standard input created or confirmed by the migration. Unresolved selections are +dropped with a warning. + +Preview exposure output selections are also host-scoped and must not persist +interior source node identity. Canonical preview/output identity is: + +```ts +type PreviewExposureUiIdentity = { + hostNodeLocator: string + previewName: string +} +``` + +The UI references the explicit preview exposure itself. This keeps subgraphs +opaque: consumers select the host boundary contract, not the interior node that +currently supplies media. Legacy output selections that refer to interior +preview source nodes may migrate if they resolve to a preview-exposure chain; +otherwise they are dropped with `console.warn`. There is no separate preview UI +quarantine. + +## PromotionStore + +`PromotionStore` becomes vestigial. It may remain temporarily as a derived +runtime compatibility/index layer for existing consumers, but it is not +serialized authority, must not create promotions without linked +`SubgraphInput`s, and should be removed once consumers query the standard graph +interface directly. + +## Considered options + +### Keep `proxyWidgets` as canonical serialized topology + +Rejected. This preserves two representations for the same concept and keeps +source-widget identity in the value-ownership path. + +### Preserve bare promoted widgets as degraded runtime state + +Rejected. This would avoid some migration complexity, but it perpetuates the +ambiguity that caused host/source value bugs and makes ECS identity less clear. + +### Quarantine primitive-node promotions by default + +Rejected. Primitive-node proxy promotions are expected legacy workflows, and +quarantining them would break users unnecessarily. They are repaired by bypassing +the primitive node when the repair can be validated all-or-nothing. + +### Migrate `widgets_values` to object/map form now + +Rejected for this slice. Name-keyed object form is the desired long-term +direction, but combining it with the promotion migration increases blast radius +for existing workflow consumers that still assume array order. + +## Consequences + +- Promoted widget values become host-instance-owned and ECS-compatible. +- Source widgets remain metadata/default providers, not persistence carriers. +- Legacy workflows are repaired toward one standard representation. +- Quarantine preserves unrepaired valid legacy data without reintroducing bare + runtime promotion. +- Primitive fanout repair is more complex, but avoids breaking common existing + workflows. +- UI code must migrate with the runtime migration to avoid mixed identity states. +- `PromotionStore` has a clear removal path. diff --git a/docs/adr/0009-subgraph-promoted-widgets-use-linked-inputs/before-after-flows.md b/docs/adr/0009-subgraph-promoted-widgets-use-linked-inputs/before-after-flows.md new file mode 100644 index 0000000000..2f8dcd7d08 --- /dev/null +++ b/docs/adr/0009-subgraph-promoted-widgets-use-linked-inputs/before-after-flows.md @@ -0,0 +1,210 @@ +# Appendix: Before and after flows + +This appendix visualizes the ownership and migration flows described in +[ADR 0009](../0009-subgraph-promoted-widgets-use-linked-inputs.md). + +## Before: proxy widgets and linked inputs overlap + +Historically, promoted widgets could be represented both as serialized +`properties.proxyWidgets` entries and as linked subgraph inputs. Runtime value +reads could collapse back to the interior source widget, while host +`widgets_values` could also carry an exterior value for the same promoted UI. + +```mermaid +flowchart TD + workflow[Workflow JSON] --> proxyWidgets[properties.proxyWidgets] + workflow --> hostValues[host widgets_values] + proxyWidgets --> promotionStore[PromotionStore / promotion runtime] + promotionStore --> sourceWidget[Interior source widget] + linkedInput[Linked SubgraphInput] --> hostWidget[Host promoted widget] + sourceWidget --> hostWidget + hostValues --> hostWidget + hostWidget --> prompt[Prompt serialization] + hostWidget -. may copy value back .-> sourceWidget + sourceWidget -. shared by host instances .-> otherHost[Another host instance] + + classDef legacy fill:#fff3cd,stroke:#a66f00,color:#332200 + classDef ambiguous fill:#f8d7da,stroke:#842029,color:#330000 + classDef canonical fill:#d1e7dd,stroke:#0f5132,color:#052e16 + + class proxyWidgets,promotionStore legacy + class sourceWidget,hostValues ambiguous + class linkedInput,hostWidget canonical +``` + +Key problems in the old flow: + +- `properties.proxyWidgets` and linked `SubgraphInput` widgets could describe + the same promotion. +- Interior source widgets supplied both schema metadata and, in some flows, + persisted host values. +- Multiple host instances of the same subgraph could stomp one another through + the shared interior widget value. +- Display-only previews were mixed into widget-promotion language even though + they do not own values or feed prompt serialization. + +## After: linked inputs are the promoted-widget boundary + +Promoted value widgets are now represented only as standard linked +`SubgraphInput` widgets. The source widget remains the schema/default provider, +but the host `SubgraphNode` owns the promoted value. + +```mermaid +flowchart TD + workflow[Workflow JSON] --> subgraphInterface[Subgraph interface / inputs] + workflow --> hostValues[host widgets_values] + subgraphInterface --> subgraphInput[SubgraphInput.name] + subgraphInput --> hostWidget[Host-scoped widget entity] + hostValues --> hostWidget + sourceWidget[Interior source widget] --> schema[Schema, type, options, tooltip, default] + schema --> hostWidget + hostWidget --> prompt[Prompt serialization] + + hostIdentity[Host node locator + SubgraphInput.name] --> hostWidget + sourceWidget -. metadata only .-> diagnostics[Diagnostics / lookup / migration] + sourceWidget -. no host value ownership .-> schema + + classDef owner fill:#d1e7dd,stroke:#0f5132,color:#052e16 + classDef metadata fill:#cff4fc,stroke:#055160,color:#032830 + classDef persisted fill:#e2e3e5,stroke:#41464b,color:#212529 + + class subgraphInterface,subgraphInput,hostWidget,hostIdentity owner + class sourceWidget,schema,diagnostics metadata + class workflow,hostValues persisted +``` + +Canonical ownership after the migration: + +- UI/value identity is host-scoped: host node locator plus + `SubgraphInput.name`. +- `SubgraphInput.name` is stable identity; labels and localized names are + display-only. +- Host values win during repair, persistence, and prompt serialization. +- Source widgets provide metadata and defaults only. +- Canonical saves omit repaired `properties.proxyWidgets` entries. + +## Legacy load migration + +Loading a workflow with legacy `proxyWidgets` performs an idempotent repair. The +repair builds a plan before mutating graph state, then re-resolves against the +current graph when node IDs and links are stable. + +```mermaid +flowchart TD + start[Load workflow] --> parse{Parse properties.proxyWidgets} + parse -->|invalid raw data| invalid[console.error and ignore] + parse -->|valid tuples| plan[Build repair plan] + plan --> classify{Classify entry} + + classify -->|value widget| valueRepair{Already linked SubgraphInput?} + valueRepair -->|yes| consume[Consume legacy proxy entry] + valueRepair -->|no| repair[Repair through subgraph input/link systems] + repair --> repairResult{Repair succeeded?} + repairResult -->|yes| consume + repairResult -->|no| quarantine[Persist proxyWidgetErrorQuarantine] + + classify -->|primitive fanout| primitive[Validate all primitive targets] + primitive --> primitiveResult{All targets reconnectable?} + primitiveResult -->|yes| primitiveRepair[Create one SubgraphInput and reconnect fanout] + primitiveRepair --> consume + primitiveResult -->|no| quarantine + + classify -->|display-only preview| preview[Create / keep previewExposures entry] + preview --> consume + + consume --> save[Canonical save] + quarantine --> save + save --> omit[Omit repaired entries from proxyWidgets] + save --> keepQuarantine[Persist unrepaired value intent in quarantine] + save --> keepPreview[Persist previews in previewExposures] + + classDef ok fill:#d1e7dd,stroke:#0f5132,color:#052e16 + classDef warn fill:#fff3cd,stroke:#a66f00,color:#332200 + classDef error fill:#f8d7da,stroke:#842029,color:#330000 + classDef neutral fill:#e2e3e5,stroke:#41464b,color:#212529 + + class consume,repair,primitiveRepair,preview,save,omit,keepPreview ok + class plan,classify,valueRepair,primitive,primitiveResult,repairResult neutral + class quarantine,keepQuarantine warn + class invalid error +``` + +## Preview exposures are separate from value widgets + +Display-only previews, such as `$$canvas-image-preview`, are not promoted +widgets. They have host-scoped serialized identity, but they do not create +prompt inputs, do not create `widgets_values`, and do not own user values. + +```mermaid +flowchart TD + hostNode[Host SubgraphNode] --> previewExposures[properties.previewExposures] + previewExposures --> exposure[PreviewExposure.name] + exposure --> sourceLocator[sourceNodeId + sourcePreviewName] + sourceLocator --> runtimePreview[Runtime preview/output state] + runtimePreview --> hostCanvas[Host canvas / app-mode preview] + + exposure --> uiIdentity[hostNodeLocator + previewName] + runtimePreview -. UI projection only .-> hostCanvas + previewExposures -. no prompt input .-> noPrompt[No prompt serialization] + previewExposures -. no value widget .-> noValue[No widgets_values entry] + previewExposures -. no graph edge .-> noEdge[No executable graph edge] + + classDef preview fill:#cff4fc,stroke:#055160,color:#032830 + classDef noValue fill:#f8d7da,stroke:#842029,color:#330000 + classDef persisted fill:#e2e3e5,stroke:#41464b,color:#212529 + + class previewExposures,exposure,sourceLocator,runtimePreview,hostCanvas,uiIdentity preview + class noPrompt,noValue,noEdge noValue + class hostNode persisted +``` + +For nested subgraphs, preview exposures chain across immediate host boundaries +instead of persisting flattened deep paths. + +```mermaid +flowchart LR + outerHost[Outer SubgraphNode] --> outerExposure[Outer previewExposures entry] + outerExposure --> innerHost[Immediate inner SubgraphNode] + innerHost --> innerExposure[Inner previewExposures entry] + innerExposure --> deepestPreview[Interior preview source] + deepestPreview --> media[Resolved media] + + outerExposure -. sourcePreviewName names inner preview identity .-> innerExposure + outerExposure -. does not persist deep private path .-> opaque[Subgraph internals remain opaque] + + classDef boundary fill:#d1e7dd,stroke:#0f5132,color:#052e16 + classDef preview fill:#cff4fc,stroke:#055160,color:#032830 + classDef note fill:#fff3cd,stroke:#a66f00,color:#332200 + + class outerHost,innerHost boundary + class outerExposure,innerExposure,deepestPreview,media preview + class opaque note +``` + +## Serialization summary + +```mermaid +flowchart TD + canonical[Canonical serialized SubgraphNode] --> inputs[Subgraph interface / inputs] + canonical --> values[widgets_values for host-owned values] + canonical --> previews[properties.previewExposures] + canonical --> quarantine[properties.proxyWidgetErrorQuarantine] + canonical -. omits repaired entries .-> noProxy[No canonical proxyWidgets] + + inputs --> valueWidgets[Promoted value widgets] + values --> valueWidgets + previews --> previewUi[Display-only preview UI] + quarantine --> futureTooling[Future recovery tooling] + + valueWidgets --> prompt[Prompt serialization] + previewUi -. not serialized into prompt .-> prompt + quarantine -. inert .-> prompt + + classDef canonical fill:#d1e7dd,stroke:#0f5132,color:#052e16 + classDef inert fill:#fff3cd,stroke:#a66f00,color:#332200 + classDef removed fill:#f8d7da,stroke:#842029,color:#330000 + + class inputs,values,valueWidgets,prompt,canonical canonical + class previews,previewUi,quarantine,futureTooling inert + class noProxy removed +``` diff --git a/docs/adr/0009-subgraph-promoted-widgets-use-linked-inputs/disambiguating-source-node-id.md b/docs/adr/0009-subgraph-promoted-widgets-use-linked-inputs/disambiguating-source-node-id.md new file mode 100644 index 0000000000..f9cb51b1f4 --- /dev/null +++ b/docs/adr/0009-subgraph-promoted-widgets-use-linked-inputs/disambiguating-source-node-id.md @@ -0,0 +1,147 @@ +# Appendix: Removing `disambiguatingSourceNodeId` + +This appendix explains where the existing promotion system needs +`disambiguatingSourceNodeId`, why that need appears, and how the canonical form +chosen by [ADR 0009](../0009-subgraph-promoted-widgets-use-linked-inputs.md) +removes the pattern from promoted-widget identity. + +## Why the disambiguator exists + +The legacy promotion model identifies a promoted widget by source location: + +```ts +type PromotedWidgetSource = { + sourceNodeId: string + sourceWidgetName: string + disambiguatingSourceNodeId?: string +} +``` + +`sourceNodeId` is the immediate interior node visible from the host subgraph. +That is not always the original widget owner. When promotions pass through +nested subgraphs, two promoted widgets can have the same immediate +`sourceNodeId` and `sourceWidgetName` while pointing at different leaf widgets. +`disambiguatingSourceNodeId` carries the deepest source node ID so the runtime +can choose the right promoted view. + +```mermaid +flowchart TD + outerHost[Outer host SubgraphNode] --> middleNode[Interior middle SubgraphNode] + middleNode --> middleWidgetA[Promoted widget view: text] + middleNode --> middleWidgetB[Promoted widget view: text] + middleWidgetA --> leafA[Leaf source node 17 / widget text] + middleWidgetB --> leafB[Leaf source node 42 / widget text] + + oldKeyA[Old key: middleNodeId + text + disambiguatingSourceNodeId 17] + oldKeyB[Old key: middleNodeId + text + disambiguatingSourceNodeId 42] + middleWidgetA -. requires .-> oldKeyA + middleWidgetB -. requires .-> oldKeyB + + classDef host fill:#d1e7dd,stroke:#0f5132,color:#052e16 + classDef ambiguous fill:#fff3cd,stroke:#a66f00,color:#332200 + classDef leaf fill:#cff4fc,stroke:#055160,color:#032830 + + class outerHost host + class middleNode,middleWidgetA,middleWidgetB,oldKeyA,oldKeyB ambiguous + class leafA,leafB leaf +``` + +The disambiguator is therefore not a domain concept. It is compensating for an +identity model that asks host UI state to identify private nested internals. + +## Existing places that need it + +| Area | Current use of `disambiguatingSourceNodeId` | Ambiguity being patched | +| ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- | +| Promotion source types | `PromotedWidgetSource` and `PromotedWidgetView` carry the optional field. | Source identity needs more than immediate node ID plus widget name for nested promoted views. | +| Concrete widget resolution | `findWidgetByIdentity(...)` matches promoted views by `(disambiguatingSourceNodeId ?? sourceNodeId)` when a source node ID is supplied. | Multiple promoted views under the same intermediate node can share a widget name. | +| Legacy proxy normalization | Prefixed legacy names such as `123:widget_name` are converted into structured source identity and tested with candidate disambiguators. | Old serialized names encode leaf identity inside the widget name string. | +| Promotion store keys | `makePromotionEntryKey(...)`, `isPromoted(...)`, and `demote(...)` include the field in equality. | Store-level uniqueness would collapse distinct nested promotions without the leaf ID. | +| Linked promotion propagation | `SubgraphNode._resolveLinkedPromotionBySubgraphInput(...)` preserves the leaf ID when a linked input targets an inner subgraph promoted view. | The outer host otherwise sees only the immediate inner `SubgraphNode` and the promoted widget name. | +| Subgraph editor UI | The editor uses the field when resolving active widgets and when writing reordered/toggled promotions back to the store. | UI list operations must not merge same-name promoted views from different leaves. | + +## New promoted-widget identity + +ADR 0009 moves promoted value identity to the host boundary: + +```ts +type PromotedWidgetUiIdentity = { + hostNodeLocator: string + subgraphInputName: string +} +``` + +The canonical widget is owned by a `SubgraphInput` on the host +`SubgraphNode`. The host widget no longer needs to identify the deepest source +node to preserve value identity. The source widget is consulted for schema, +defaults, diagnostics, and migration, but it is not the value owner. + +```mermaid +flowchart TD + host[Host SubgraphNode] --> inputA[SubgraphInput.name: prompt] + host --> inputB[SubgraphInput.name: negative_prompt] + inputA --> hostWidgetA[Host-owned widget entity] + inputB --> hostWidgetB[Host-owned widget entity] + + hostWidgetA -. schema/default metadata .-> sourceA[Interior source widget text] + hostWidgetB -. schema/default metadata .-> sourceB[Interior source widget text] + + identityA[Identity: hostNodeLocator + prompt] --> hostWidgetA + identityB[Identity: hostNodeLocator + negative_prompt] --> hostWidgetB + sourceA -. not part of host value key .-> identityA + sourceB -. not part of host value key .-> identityB + + classDef owner fill:#d1e7dd,stroke:#0f5132,color:#052e16 + classDef metadata fill:#cff4fc,stroke:#055160,color:#032830 + classDef removed fill:#f8d7da,stroke:#842029,color:#330000 + + class host,inputA,inputB,hostWidgetA,hostWidgetB,identityA,identityB owner + class sourceA,sourceB metadata +``` + +This is the same rule the subgraph interface already uses: `name` is stable +identity, and `label` / `localized_name` are display-only. + +## How the new form removes each need + +| Previous disambiguation site | New canonical replacement | +| ------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | +| `PromotedWidgetSource.disambiguatingSourceNodeId` | Host value identity is `hostNodeLocator + SubgraphInput.name`; source locator fields become migration/diagnostic metadata only. | +| `PromotedWidgetView.disambiguatingSourceNodeId` | Host-scoped widget entities are derived from subgraph inputs, not from promoted views chained through nested source widgets. | +| `findWidgetByIdentity(...)` leaf matching | Runtime value lookup starts from the host input identity; source traversal is metadata resolution, not value identity resolution. | +| Legacy prefixed widget-name normalization | Load migration consumes legacy source-shaped entries and writes standard subgraph input state or quarantine metadata. | +| PromotionStore source-key equality | `PromotionStore` becomes a temporary derived index; canonical consumers query subgraph inputs directly. | +| Linked promotion propagation across nested hosts | Nested value composition is represented boundary-by-boundary by linked subgraph inputs with stable names. | +| Subgraph editor active widget matching | Editor state can operate on host boundary entries instead of matching leaf source widgets through same-name promoted views. | + +## Boundary-by-boundary nested flow + +The new form avoids flattened deep source paths. Each host boundary exposes its +own named input, and the next outer host links to that immediate boundary +contract. + +```mermaid +flowchart LR + leaf[Leaf node widget] --> innerInput[Inner SubgraphInput.name: text] + innerInput --> innerHostWidget[Inner host-owned widget] + innerHostWidget --> outerInput[Outer SubgraphInput.name: prompt] + outerInput --> outerHostWidget[Outer host-owned widget] + + innerIdentity[Inner value key: innerHost + text] --> innerHostWidget + outerIdentity[Outer value key: outerHost + prompt] --> outerHostWidget + leaf -. schema/default source .-> innerHostWidget + leaf -. not persisted as outer value key .-> outerIdentity + + classDef boundary fill:#d1e7dd,stroke:#0f5132,color:#052e16 + classDef source fill:#cff4fc,stroke:#055160,color:#032830 + classDef note fill:#fff3cd,stroke:#a66f00,color:#332200 + + class innerInput,innerHostWidget,outerInput,outerHostWidget,innerIdentity,outerIdentity boundary + class leaf source +``` + +Because each layer has its own stable `SubgraphInput.name`, two same-name leaf +widgets no longer require a persisted leaf-node disambiguator at the outer host. +If the user exposes both, the collision is resolved when the host inputs are +created by assigning distinct input names with the existing unique-name +behavior. diff --git a/docs/adr/0009-subgraph-promoted-widgets-use-linked-inputs/system-comparison.md b/docs/adr/0009-subgraph-promoted-widgets-use-linked-inputs/system-comparison.md new file mode 100644 index 0000000000..82265820e5 --- /dev/null +++ b/docs/adr/0009-subgraph-promoted-widgets-use-linked-inputs/system-comparison.md @@ -0,0 +1,37 @@ +# Appendix: System comparison + +This appendix compares the legacy promoted-widget systems with the canonical +linked-input model chosen by +[ADR 0009](../0009-subgraph-promoted-widgets-use-linked-inputs.md). + +| Concern | Legacy `properties.proxyWidgets` promotions | Linked `SubgraphInput` promotions before migration | New canonical linked-input system | +| -------------------------- | -------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------- | +| Serialized authority | `properties.proxyWidgets` stores source node/widget tuples as promotion topology. | Subgraph interface/input links can also represent the same exposed widget. | Subgraph interface/input links are the only canonical topology for promoted value widgets. | +| Load-time role | Hydrates promoted widgets directly from legacy tuples. | May already describe the promoted widget, creating overlap with `proxyWidgets`. | Existing linked inputs are accepted as resolved; legacy tuples are consumed by repair or quarantined. | +| Save-time role | Could be re-emitted as promotion state. | Serialized as normal subgraph interface data. | Repaired `proxyWidgets` entries are omitted; standard subgraph inputs plus host `widgets_values` are saved. | +| Value owner | Ambiguous: host `widgets_values` and the interior source widget could both carry the value. | Closer to the desired boundary model, but still coexisted with source/proxy ownership paths. | Host `SubgraphNode` owns value state through host-scoped widget identity. | +| Schema/default provider | Interior source widget provides schema and may also become persistence carrier. | Interior source widget provides source metadata through the link. | Interior source widget provides schema, type, options, tooltip, and defaults only. | +| UI identity | Often persisted by source node/widget identity. | Can use subgraph input identity, but mixed states still exist while proxy identity remains. | Host node locator plus `SubgraphInput.name`. | +| Display label handling | Source widget identity and display concerns can blur. | Uses existing subgraph input naming conventions. | `SubgraphInput.name` is stable identity; `label` / `localized_name` are display-only. | +| Multiple host instances | Risk of host instances stomping one another through shared interior values. | Better host boundary shape, but overlap with proxy/source value paths can reintroduce ambiguity. | Host-instance-owned sparse overlay prevents shared interior widget value stomping. | +| Prompt serialization | May read values through promoted runtime state that can collapse to source widgets. | Can serialize through standard subgraph input widgets when used consistently. | Promoted values serialize only through standard host-owned subgraph-input widgets. | +| Interior mutation on save | Existing `SubgraphNode.serialize()` behavior could copy exterior values into connected interior widgets. | Could still be affected by legacy copy-back behavior. | Copy-back is removed; source widgets are not persistence carriers. | +| Primitive-node promotions | Legacy tuples may point at `PrimitiveNode` outputs. | Not the canonical primitive fanout representation by itself. | Repaired all-or-nothing into one `SubgraphInput` that reconnects validated fanout targets. | +| Invalid or unresolved data | Invalid data could sit in legacy promotion state or fail repair paths. | Missing linked inputs can be ambiguous when proxy data exists. | Invalid raw data logs and is ignored; unrepaired valid value entries go to `proxyWidgetErrorQuarantine`. | +| Display-only previews | Often mixed into `proxyWidgets` despite not being value widgets. | Linked inputs are inappropriate because previews do not own values or prompt inputs. | Separate host-scoped `properties.previewExposures` entries model preview UI only. | +| Preview persistence | Preview selections can depend on source preview/widget-like identity. | No clean distinction from promoted widget inputs. | Preview identity is host node locator plus `previewName`; unresolved previews stay inert and persisted. | +| Nested preview behavior | Deep source identity can leak through host UI state. | Linked value inputs do not model display-only preview composition. | Preview exposures chain across immediate subgraph host boundaries; deep private paths are not persisted. | +| ECS compatibility | Weak: value identity can depend on source widget tuples and mutable interior widgets. | Partial: linked inputs fit boundary modeling, but duplicate authority remains. | Strong: host-scoped widget entity identity maps cleanly to ECS component state. | +| Long-term status | Legacy load-time input only. | Becomes the standard representation once overlap is removed. | Canonical system; `PromotionStore` becomes a temporary derived compatibility/index layer. | + +## Practical migration summary + +| Legacy shape | New result | +| -------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | +| Valid `proxyWidgets` entry already represented by a linked `SubgraphInput` | Entry is consumed; the existing linked input remains canonical. | +| Valid value-widget `proxyWidgets` entry without a linked input | Repair creates or reconnects standard subgraph input/link state. | +| Valid primitive fanout entry | Repair creates one `SubgraphInput`, reconnects all validated targets, and leaves the primitive node inert. | +| Valid value-widget entry that cannot be repaired | Entry is persisted in `properties.proxyWidgetErrorQuarantine` with the host value when available. | +| Preview-shaped legacy entry | Entry is migrated into `properties.previewExposures`, not a linked input. | +| Unresolved preview exposure | Entry remains inert in `previewExposures`; it is not quarantined because it owns no user value. | +| Invalid raw `proxyWidgets` data | Logs `console.error`, does not throw, and is not quarantined. | diff --git a/docs/architecture/ecs-migration-plan.md b/docs/architecture/ecs-migration-plan.md index a3ba8fbe53..e6a1d1d61a 100644 --- a/docs/architecture/ecs-migration-plan.md +++ b/docs/architecture/ecs-migration-plan.md @@ -231,6 +231,11 @@ assigning synthetic widget IDs (via `lastWidgetId` counter on LGraphState). the ID mapping — widgets currently lack independent IDs, so the bridge must maintain a `(nodeId, widgetName) -> WidgetEntityId` lookup. +**Promoted-widget caveat:** ADR 0009 assigns promoted value widgets a +host-boundary identity (`host node locator + SubgraphInput.name`). Interior +source node/widget identity is preserved only as migration and diagnostic +metadata. + ### 2c. Read-only bridge for Node metadata Populate `NodeType`, `NodeVisual`, `Properties`, `Execution` components by @@ -663,6 +668,10 @@ The 6 proto-ECS stores use 6 different keying strategies: | NodeOutputStore | `"${subgraphId}:${nodeId}"` | | SubgraphNavigationStore | subgraphId or `'root'` | +ADR 0009 refines the promoted-widget target: promoted value widgets should use +host boundary identity (`host node locator + SubgraphInput.name`), not interior +source node/widget identity. + The World unifies these under branded entity IDs. But stores that use composite keys (e.g., `nodeId:widgetName`) reflect a genuine structural reality — a widget is identified by its relationship to a node. Synthetic diff --git a/docs/architecture/proto-ecs-stores.md b/docs/architecture/proto-ecs-stores.md index 0831f0535d..f5f09ccb50 100644 --- a/docs/architecture/proto-ecs-stores.md +++ b/docs/architecture/proto-ecs-stores.md @@ -17,6 +17,10 @@ Six stores extract entity state out of class instances into centralized, queryab | NodeOutputStore | Execution results | `nodeLocatorId` | `"${subgraphId}:${nodeId}"` | Output data, preview URLs | | SubgraphNavigationStore | Canvas viewport | `subgraphId` | `subgraphId` or `'root'` | LRU viewport cache | +ADR 0009 refines promoted-widget identity: promoted value widgets are keyed by +the host boundary (`host node locator + SubgraphInput.name`), while interior +source node/widget identity is migration and diagnostic metadata only. + ## 2. WidgetValueStore **File:** `src/stores/widgetValueStore.ts` @@ -254,6 +258,9 @@ Each store invents its own identity scheme: | NodeOutputStore | `"${subgraphId}:${nodeId}"` | Composite string | No | In the ECS target, all of these would use branded entity IDs (`WidgetEntityId`, `NodeEntityId`, etc.) with compile-time cross-kind protection. +For promoted value widgets, ADR 0009 narrows the target key to host boundary +identity (`host node locator + SubgraphInput.name`) instead of interior source +identity. ## 6. Extraction Map diff --git a/docs/architecture/subgraph-boundaries-and-promotion.md b/docs/architecture/subgraph-boundaries-and-promotion.md index 51d7b8b941..0426a8afe1 100644 --- a/docs/architecture/subgraph-boundaries-and-promotion.md +++ b/docs/architecture/subgraph-boundaries-and-promotion.md @@ -404,26 +404,21 @@ Whichever candidate is chosen: instance-specific state beyond inputs — must remain reachable. This is a constraint, not a current requirement. -### Recommendation and decision criteria +### Decision -**Lean toward A.** It eliminates an entire subsystem by recognizing a structural -truth: promotion is adding a typed input to a function signature. The type -system already handles widget creation for typed inputs. Building a parallel -mechanism for "promoted widgets" is building a second, narrower version of -something the system already does. +[ADR 0009](../adr/0009-subgraph-promoted-widgets-use-linked-inputs.md) +chooses Candidate A for promoted value widgets. It eliminates an entire +subsystem by recognizing a structural truth: promotion is adding a typed input +to a function signature. The type system already handles widget creation for +typed inputs. Building a parallel mechanism for "promoted widgets" is building +a second, narrower version of something the system already does. The cost of A is a migration path for existing `proxyWidgets` serialization. On -load, the `SerializationSystem` converts `proxyWidgets` entries into interface -inputs and boundary links. This is a one-time ratchet conversion — once -loaded and re-saved, the workflow uses the new format. - -**Choose B if** the team determines that promoted widgets must remain -visually or behaviorally distinct from normal input widgets in ways the type → -widget mapping cannot express, or if the `proxyWidgets` migration burden exceeds -the current release cycle's capacity. - -**Decision needed before** Phase 3 of the ECS migration, when systems are -introduced and the widget/connectivity architecture solidifies. +load, the `SerializationSystem` converts value-widget `proxyWidgets` entries +into interface inputs and boundary links. Once loaded and re-saved, the workflow +uses the new format. ADR 0009 separates display-only preview exposures from +promoted value widgets; those previews use their own host-scoped serialized +representation instead of linked `SubgraphInput` widgets. --- @@ -471,14 +466,14 @@ and produces the recursive `ExportedSubgraph` structure, matching the current format exactly. Existing workflows, the ComfyUI backend, and third-party tools see no change. -| Direction | Format | Notes | -| --------------- | ------------------------------- | ---------------------------------------- | -| **Save/export** | Nested (current shape) | SerializationSystem walks scope tree | -| **Load/import** | Nested (current) or future flat | Ratchet: normalize to flat World on load | +| Direction | Format | Notes | +| --------------- | ------------------------------- | ------------------------------------------ | +| **Save/export** | Nested (current shape) | SerializationSystem walks scope tree | +| **Load/import** | Nested (current) or future flat | Migration: normalize to flat World on load | -The "ratchet conversion" pattern: load any supported format, normalize to the -internal model. The system accepts old formats indefinitely but produces the -current format on save. +The migration pattern: load any supported format and normalize to the internal +model. The system accepts old formats indefinitely but produces the current +format on save. ### Widget identity at the boundary @@ -511,13 +506,12 @@ SubgraphIO { } ``` -If Candidate A (connections-only promotion) is chosen: promoted widgets become -interface inputs, serialized as additional `SubgraphIO` entries. On load, legacy -`proxyWidgets` data is converted to interface inputs and boundary links (ratchet -migration). On save, `proxyWidgets` is no longer written. - -If Candidate B (simplified promotion) is chosen: `proxyWidgets` continues to be -serialized in its current format. +ADR 0009 chooses Candidate A (connections-only promotion) for promoted value +widgets: they become interface inputs, serialized as additional `SubgraphIO` +entries. On load, legacy value-widget `proxyWidgets` data is converted to +interface inputs and boundary links. On save, repaired `proxyWidgets` entries +are no longer written. Display-only preview exposures use separate +host-scoped `previewExposures` serialization. ### Backward-compatible loading contract @@ -555,7 +549,7 @@ This document proposes or surfaces the following changes to | World structure | Implied per-graph containment | Flat World with `graphScope` tags; one World per workflow | | Acyclicity | Not addressed | DAG invariant on `SubgraphStructure.graphId` references, enforced on mutation | | Boundary model | Deferred | Typed interface contracts on `SubgraphStructure`; no virtual nodes or magic IDs | -| Widget promotion | Treated as a given feature to migrate | Open decision: Candidate A (connections-only) vs B (simplified component) | +| Widget promotion | Treated as a given feature to migrate | ADR 0009 chooses Candidate A: promoted value widgets are linked inputs | | Serialization | Not explicitly separated from internal model | Internal model ≠ wire format; `SerializationSystem` is the membrane | | Backward compat | Implicit | Explicit contract: load any prior format, indefinitely | From 88866fc5648ca8716e3f86a9c790ffe59d996e22 Mon Sep 17 00:00:00 2001 From: Dante Date: Fri, 8 May 2026 12:06:03 +0900 Subject: [PATCH 002/142] fix: restore nightly publish_types build (#12073) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary The nightly `Release Draft Create` -> `publish_types / Build types` job has been failing on every run since 1.45.1 ([failed run](https://github.com/Comfy-Org/ComfyUI_frontend/actions/runs/25531376650)). Reproduced locally on `main` (`c8c0e5386`). ## Root cause `pnpm build:types` ends with `FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory`. The `vite-plugin-dts` rolled-up type generation now exceeds Node's default ~4GB heap on the GH runner. The `TS2742`/`TS4082` warnings printed earlier are non-fatal diagnostics from the plugin pre-pass — the api-extractor rollup itself completes once the heap is large enough. ## Fix Set `NODE_OPTIONS='--max-old-space-size=8192'` in `build:types`, matching the existing pattern already used by `build` and `build:cloud` (only one-line change). ## Verification - `pnpm build:types` exits 0 locally with the change (built in ~40s). - `dist/index.d.ts` (1.9MB) emitted with the public types intact: `ComfyExtension`, `ComfyApi`, `ComfyApp`, `ComfyNodeDef`, `InputSpec`, `DOMWidget`, etc. - `dist/package.json` correctly produced by `scripts/prepare-types.js`. - Reverting the change reproduces the OOM crash. ## Test plan - [ ] Trigger `Release NPM Types` workflow manually (or wait for next nightly) and confirm `Build types` step succeeds. ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-12073-fix-restore-nightly-publish_types-build-35a6d73d3650819e95aecfbd8a66847c) by [Unito](https://www.unito.io) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 453f19ad7d..eff283dc2d 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "build:cloud": "cross-env DISTRIBUTION=cloud NODE_OPTIONS='--max-old-space-size=8192' nx build", "build:desktop": "nx build @comfyorg/desktop-ui", "build-storybook": "storybook build", - "build:types": "nx build --config vite.types.config.mts && node scripts/prepare-types.js", + "build:types": "cross-env NODE_OPTIONS='--max-old-space-size=8192' nx build --config vite.types.config.mts && node scripts/prepare-types.js", "build:analyze": "cross-env ANALYZE_BUNDLE=true pnpm build", "build": "cross-env NODE_OPTIONS='--max-old-space-size=8192' pnpm typecheck && nx build", "size:collect": "node scripts/size-collect.js", From 0ef98de8ebb52e4685c25bbbe6c8fbb136a09834 Mon Sep 17 00:00:00 2001 From: Dante Date: Fri, 8 May 2026 20:56:35 +0900 Subject: [PATCH 003/142] fix: make credits help icon a tooltip button in cloud user popover (FE-617) (#12072) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary The help icon (lucide circle-help) next to the credits balance in the cloud user popover was a bare `` with `v-tooltip` and `cursor-help`. PrimeVue tooltip on a bare `` did not fire reliably and the icon had no focus/keyboard semantics, so users saw "no hover action and not clickable". Wrap the icon in `
diff --git a/src/platform/secrets/components/SecretFormDialog.test.ts b/src/platform/secrets/components/SecretFormDialog.test.ts new file mode 100644 index 0000000000..04bfaae87b --- /dev/null +++ b/src/platform/secrets/components/SecretFormDialog.test.ts @@ -0,0 +1,98 @@ +import { render } from '@testing-library/vue' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { defineComponent } from 'vue' +import { createI18n } from 'vue-i18n' + +import SecretFormDialog from './SecretFormDialog.vue' + +vi.mock('../composables/useSecretForm', () => ({ + useSecretForm: () => ({ + form: { provider: '', name: '', secretValue: '' }, + errors: {}, + loading: false, + apiError: '', + providerOptions: [], + handleSubmit: vi.fn() + }) +})) + +vi.mock('primevue/inputtext', () => ({ + default: { name: 'InputText', template: '' } +})) +vi.mock('primevue/password', () => ({ + default: { name: 'Password', template: '' } +})) + +let capturedPointerDownOutside: ((event: Event) => void) | null = null + +vi.mock('@/components/ui/button/Button.vue', () => ({ + default: { name: 'Button', template: '' } +})) + +vi.mock('@/components/ui/select/Select.vue', () => ({ + default: { name: 'Select', template: '
' } +})) +vi.mock('@/components/ui/select/SelectContent.vue', () => ({ + default: { name: 'SelectContent', template: '
' } +})) +vi.mock('@/components/ui/select/SelectItem.vue', () => ({ + default: { name: 'SelectItem', template: '
' } +})) +vi.mock('@/components/ui/select/SelectTrigger.vue', () => ({ + default: { name: 'SelectTrigger', template: '
' } +})) +vi.mock('@/components/ui/select/SelectValue.vue', () => ({ + default: { name: 'SelectValue', template: '' } +})) + +vi.mock('@/components/ui/dialog/Dialog.vue', () => ({ + default: { name: 'Dialog', template: '
' } +})) +vi.mock('@/components/ui/dialog/DialogPortal.vue', () => ({ + default: { name: 'DialogPortal', template: '
' } +})) +vi.mock('@/components/ui/dialog/DialogOverlay.vue', () => ({ + default: { name: 'DialogOverlay', template: '
' } +})) +vi.mock('@/components/ui/dialog/DialogContent.vue', () => ({ + default: defineComponent({ + name: 'DialogContent', + inheritAttrs: false, + setup(_, { attrs }) { + const onPointerDownOutside = (attrs as Record)[ + 'onPointerDownOutside' + ] as ((event: Event) => void) | undefined + capturedPointerDownOutside = onPointerDownOutside ?? null + }, + template: '
' + }) +})) +vi.mock('@/components/ui/dialog/DialogHeader.vue', () => ({ + default: { name: 'DialogHeader', template: '
' } +})) +vi.mock('@/components/ui/dialog/DialogTitle.vue', () => ({ + default: { name: 'DialogTitle', template: '
' } +})) +vi.mock('@/components/ui/dialog/DialogClose.vue', () => ({ + default: { name: 'DialogClose', template: '