diff --git a/docs/design/node-presentation.md b/docs/design/node-presentation.md new file mode 100644 index 0000000..c7d9f83 --- /dev/null +++ b/docs/design/node-presentation.md @@ -0,0 +1,57 @@ +# BV node presentation architecture + +This document is the authoritative contract for BV node and widget presentation. +The machine-readable exception registry is `PRESENTATION_EXCEPTIONS` in +`ui/src/regional/nodePresentation.ts`. + +## Non-negotiable rules + +1. Every new BV UI surface uses the shared BV UI pack and its tokens/components. +2. Every manipulation of a ComfyUI node, port or widget is implemented in the + central presentation/manipulation modules. +3. Feature modules may own domain state and structural graph behavior. They must + delegate hiding, projection, ordering, measurement, compaction and resizing. +4. Classic, Node Library Ghost and Nodes 2.0 consume the same semantic policy. +5. A connected Legacy port stays visible even when Legacy Debug is disabled. +6. Internal state and provider plumbing remain serialized and executable while + their visual representation is hidden. +7. User-controlled dimensions are never overwritten by an automatic refresh. +8. Direct `converted-widget`, `hidden`, `computeSize`, `setSize`, DOM projection + or slot-visibility code outside the central modules requires a registered + exception before it is merged. + +## Central modules + +| Module | Responsibility | +| --- | --- | +| `ui/src/regional/nodePresentation.ts` | Semantic roles, node policies and exception registry | +| `ui/src/regional/classicNodePresentation.ts` | Classic adapter and measured layout | +| `ui/src/regional/nodes2NodePresentation.ts` | Nodes 2.0 DOM adapter | +| `ui/src/regional/nodePreviewProjection.ts` | Node Library Ghost adapter | +| `ui/src/regional/portProjection.ts` | Runtime provider-port projection and connection-aware layout | + +Node-specific code may request these operations. It must not reimplement them. + +## Registered exceptions + +The current exceptions, their exact implementation paths, rationale and intended +centralization path live in `PRESENTATION_EXCEPTIONS`. This keeps the list next +to the policy used at runtime and prevents documentation from silently diverging. + +An exception is legitimate only when it adapts a genuinely different ComfyUI +lifecycle seam, such as promoted Subgraph proxy widgets or runtime-generated +graph slots. A node being visually unusual is not sufficient justification. + +## Adding or changing UI behavior + +Before implementation: + +1. Add or extend the semantic role/policy centrally. +2. Extend the applicable Classic, Ghost and Nodes 2.0 adapter tests. +3. Use BV UI pack components and tokens for rendered controls. +4. If a central interface cannot represent the requirement, deepen that module. +5. Only if the lifecycle seam genuinely differs, register an exception with its + file path, reason and concrete future centralization path. + +No presentation task is complete until implementation, automated coverage, +visual acceptance and this architecture contract agree. diff --git a/js/bv_pipe_shared.js b/js/bv_pipe_shared.js index c8d9f5a..155b4ee 100644 --- a/js/bv_pipe_shared.js +++ b/js/bv_pipe_shared.js @@ -1,4 +1,6 @@ // bv_pipe_shared.js +// Registered presentation exception: dynamic-pipe-slot-structure. +// See PRESENTATION_EXCEPTIONS in ui/src/regional/nodePresentation.ts. import { app } from "../../scripts/app.js"; export const MAX_SLOTS = 100; diff --git a/js/bv_prompt_debug.js b/js/bv_prompt_debug.js index 36a4fa7..1b34820 100644 --- a/js/bv_prompt_debug.js +++ b/js/bv_prompt_debug.js @@ -1,3 +1,5 @@ +// Registered presentation exception: execution-result-preview-widgets. +// See PRESENTATION_EXCEPTIONS in ui/src/regional/nodePresentation.ts. import { app } from "../../scripts/app.js"; import { ComfyWidgets } from "../../scripts/widgets.js"; diff --git a/js/bv_seed.js b/js/bv_seed.js index 21ab9e3..be67f59 100644 --- a/js/bv_seed.js +++ b/js/bv_seed.js @@ -1,3 +1,5 @@ +// Registered presentation exception: seed-action-projection. +// See PRESENTATION_EXCEPTIONS in ui/src/regional/nodePresentation.ts. import { app } from "../../scripts/app.js"; import { applySeedAction, materializeSeedControl } from "./bv_seed_model.js"; diff --git a/js/bv_smart_pipe.js b/js/bv_smart_pipe.js index bc5fa64..a9ca999 100644 --- a/js/bv_smart_pipe.js +++ b/js/bv_smart_pipe.js @@ -1,3 +1,5 @@ +// Registered presentation exception: dynamic-pipe-slot-structure. +// See PRESENTATION_EXCEPTIONS in ui/src/regional/nodePresentation.ts. import { app } from "../../scripts/app.js"; import { moveMarkedPortToEnd, nextFreeOrdinal, promoteConnectedInheritedSlots, promoteInheritedSlot, resolveLocalSlotNameCollisions, retainedMissingSlots, reusableSmartPipePortIndex, smartPipeSlotName, uniqueSmartPipeSlotName, updateSmartPipePort } from "./bv_smart_pipe_slots.js"; import { mergePipeSchemas, SMART_PIPE_DEFAULT_TITLE } from "./bv_smart_pipe_merge_model.js"; diff --git a/js/bv_smart_pipe_merge.js b/js/bv_smart_pipe_merge.js index 05c6b17..cc622a0 100644 --- a/js/bv_smart_pipe_merge.js +++ b/js/bv_smart_pipe_merge.js @@ -1,3 +1,5 @@ +// Registered presentation exception: dynamic-pipe-slot-structure. +// See PRESENTATION_EXCEPTIONS in ui/src/regional/nodePresentation.ts. import { app } from "../../scripts/app.js"; import { activeDescriptorFor, collectExpandedPipeAddresses, propagate } from "./bv_smart_pipe.js"; import { diff --git a/js/bv_subgraph_ui_projection.js b/js/bv_subgraph_ui_projection.js index 3100f92..e8f6de1 100644 --- a/js/bv_subgraph_ui_projection.js +++ b/js/bv_subgraph_ui_projection.js @@ -1,3 +1,5 @@ +// Registered presentation exception: subgraph-promoted-widget-proxies. +// See PRESENTATION_EXCEPTIONS in ui/src/regional/nodePresentation.ts. const PRESENTATION_TYPES = new Set(["BV_HEADING", "BV_SPACER", "BV_DIVIDER"]); import { normalizeDynamicComboSelection } from "./bv_dynamic_combo_model.js"; const PRESENTATION_NODE_TYPES = new Map([ diff --git a/ui/src/components/control/bv_control_center.ts b/ui/src/components/control/bv_control_center.ts index e3900ed..306de52 100644 --- a/ui/src/components/control/bv_control_center.ts +++ b/ui/src/components/control/bv_control_center.ts @@ -1,3 +1,5 @@ +// Registered presentation exception: control-center-dynamic-widgets. +// See PRESENTATION_EXCEPTIONS in regional/nodePresentation.ts for rationale and exit path. import { getApp } from "../../appHelper.js"; import { collectAllGroups, collectNodesByType, nodeMatchesType } from "../../util/control/collector"; import { BVControlConfig, CONFIG_CHANGED_EVENT, readConfig, writeConfig } from "../../util/control/configHandler"; diff --git a/ui/src/regional/m0ResourceSpike.tsx b/ui/src/regional/m0ResourceSpike.tsx index 3639a17..0e62915 100644 --- a/ui/src/regional/m0ResourceSpike.tsx +++ b/ui/src/regional/m0ResourceSpike.tsx @@ -1,3 +1,5 @@ +// Registered presentation exception: m0-runtime-resource-projection. +// See PRESENTATION_EXCEPTIONS in nodePresentation.ts for rationale and exit path. import React from "react"; import ReactDOM from "react-dom/client"; import { getApi, getApp } from "../appHelper.js"; diff --git a/ui/src/regional/nodePresentation.ts b/ui/src/regional/nodePresentation.ts index f5d4bd2..0079ab2 100644 --- a/ui/src/regional/nodePresentation.ts +++ b/ui/src/regional/nodePresentation.ts @@ -1,6 +1,70 @@ export type PresentationSurface="classic"|"ghost"|"nodes2"; export type PresentationRole="public"|"legacy"|"internalState"|"provider"|"dynamicReserve"; +/** + * Central contract for every BV node/widget presentation mutation. + * + * New UI must use the shared BV UI pack. New code must not hide, resize, + * replace, reorder or project ComfyUI nodes/widgets directly outside the + * central presentation modules. The entries below document the pre-existing + * adapters that still have to touch a ComfyUI-specific lifecycle seam. They + * are exceptions to be retired or absorbed here, not examples to copy. + * + * Full invariants, ownership rules and adapter rationale: + * docs/design/node-presentation.md + */ +export type PresentationException=Readonly<{ + id:string; + implementation:readonly string[]; + reason:string; + centralizationPath:string; +}>; + +export const PRESENTATION_EXCEPTIONS:readonly PresentationException[]=[ + { + id:"regional-prompt-bootstrap-measurement", + implementation:["ui/src/regional/portProjection.ts#installRegionalPromptCreationLayout"], + reason:"ComfyUI measures the backend definition before the Regional Prompt instance has its projected provider markers and action widgets.", + centralizationPath:"Keep this lifecycle adapter until the central presentation inventory can run before the first ComfyUI measurement.", + }, + { + id:"m0-runtime-resource-projection", + implementation:["ui/src/regional/m0ResourceSpike.tsx"], + reason:"The M0 collector and consumers create graph-bound provider slots and DOM pickers dynamically, before the normal policy inventory is complete.", + centralizationPath:"Move its local widget hiding and slot flags behind central widget/port projection helpers before extending M0.", + }, + { + id:"control-center-dynamic-widgets", + implementation:["ui/src/components/control/bv_control_center.ts"], + reason:"Control toggles and the conflict-status row are generated from runtime configuration and must be reconciled in a stable order.", + centralizationPath:"Keep dynamic widget creation local; move all hiding, sizing and ordering primitives into the central manipulation library.", + }, + { + id:"subgraph-promoted-widget-proxies", + implementation:["js/bv_subgraph_ui_projection.js","js/bv_subgraph_nodes2_projection.js","js/bv_subgraph_layout.js"], + reason:"Promoted Subgraph widgets are ComfyUI-owned proxy objects that are recreated asynchronously and differ between Classic and Nodes 2.0.", + centralizationPath:"Keep the lifecycle adapter separate, but route shared visibility and sizing decisions through this module and the presentation bridge.", + }, + { + id:"dynamic-pipe-slot-structure", + implementation:["js/bv_pipe_shared.js","js/bv_smart_pipe.js","js/bv_smart_pipe_merge.js"], + reason:"Pipe nodes add, remove and relabel structural graph slots from their runtime schema; this is domain behavior, not merely visual projection.", + centralizationPath:"Structural slot ownership remains local; every visual hide, compact or resize operation must use the central presentation bridge.", + }, + { + id:"seed-action-projection", + implementation:["js/bv_seed.js"], + reason:"Seed actions are semantic controls that also project through promoted Subgraph widgets and therefore require a source/host adapter.", + centralizationPath:"Keep seed state transitions local and route widget construction, sizing and host projection through shared presentation primitives.", + }, + { + id:"execution-result-preview-widgets", + implementation:["js/bv_prompt_debug.js"], + reason:"Prompt AST Debug and Text Log Writer expose read-only multiline content populated only by execution callbacks.", + centralizationPath:"Keep execution data handling local; create and style preview widgets exclusively through the shared BV UI/presentation helpers.", + }, +] as const; + export type PresentationPort=Readonly<{ direction:"input"|"output"; name:string; diff --git a/ui/src/regional/portProjection.ts b/ui/src/regional/portProjection.ts index b2d3a8a..a457bd8 100644 --- a/ui/src/regional/portProjection.ts +++ b/ui/src/regional/portProjection.ts @@ -16,6 +16,8 @@ export function markProjectedProvider(slot:any){if(!slot)return;slot.hidden=true export function suppressInitialProjectedProviderDefinitions(nodeData:any){const optional=nodeData?.input?.optional;if(!optional)return[];const removed:string[]=[];for(const [name,spec]of Object.entries(optional)){if(PROVIDERS.has(String((spec as any)?.[0]??""))){delete optional[name];removed.push(name)}}return removed} +// Registered presentation exception: regional-prompt-bootstrap-measurement. +// See PRESENTATION_EXCEPTIONS in nodePresentation.ts for rationale and exit path. export function installRegionalPromptCreationLayout(nodeType:any){const prototype=nodeType?.prototype,original=prototype?.computeSize;if(!prototype||typeof original!=="function"||prototype.__bvRegionalPromptCreationLayout)return;prototype.__bvRegionalPromptCreationLayout=true;prototype.computeSize=function(){if(this.__bvRegionalPromptUiReady)return original.apply(this,arguments);const view=measurementView(this,{inputs:(this.inputs??[]).filter((slot:any)=>!internal(slot)),outputs:(this.outputs??[]).filter((slot:any)=>slot?.name!=="lora_bindings"),widgets:[{type:"button",name:"open_regional_editor",computeSize:()=>[0,20]},{type:"button",name:"quick_edit_regional_prompts",computeSize:()=>[0,20]}]});return original.apply(view,arguments)}} export function retainNeededProjectedInputs(node:any,wantedNames:string[],matches:(slot:any)=>boolean){const wanted=new Set(wantedNames);for(const {slot,index} of (node.inputs??[]).map((slot:any,index:number)=>({slot,index})).filter(({slot}:any)=>matches(slot)&&!wanted.has(String(slot.name))&&slot.link==null).sort((a:any,b:any)=>b.index-a.index))node.removeInput?.(index);for(const name of wantedNames){const slot=node.inputs?.find((item:any)=>item.name===name);if(slot)markProjectedProvider(slot);}}