Document centralized node presentation exceptions

This commit is contained in:
BlackVortexAI
2026-08-28 18:29:57 +02:00
parent b7a7d8a500
commit c18017e6d3
11 changed files with 139 additions and 0 deletions
+57
View File
@@ -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.
+2
View File
@@ -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;
+2
View File
@@ -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";
+2
View File
@@ -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";
+2
View File
@@ -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";
+2
View File
@@ -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 {
+2
View File
@@ -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([
@@ -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";
+2
View File
@@ -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";
+64
View File
@@ -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;
+2
View File
@@ -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);}}