Node Editor Redesign Spec

Date: 2026-05-13 Status: Draft, pending review Author: Claude (with Matti)

1. Goal

Modernize the NodeTool editor UI — content-forward node bodies, cleaner port/edge styling, a curated left panel, an evolved inspector, and bespoke editing-node bodies — while preserving NodeTool’s existing graph engine, execution model, and Python bridge. The redesign reorganizes how nodes present content and how the user configures them — it does not change the workflow runtime, the WebSocket protocol, or the node-author API.

This is the new editor. No opt-in mode, no preview flag. Each PR replaces a piece of the existing UI directly.

2. Non-goals

3. Tracks

Track Scope summary
A Port + edge polish
B ContentCardBody (parameterized by output type)
C Left-panel quick-access categories
D Inspector evolution + show-as-input toggle
E 9 bespoke editing-node bodies
F Group node label redesign

4. Architecture

4.1 Node body routing

NodeContent.tsx already chooses a body component by node type. Extend the registry:

resolveBody(nodeType):
  if nodeType in BESPOKE_BODY_REGISTRY:        // Track E
      return BESPOKE_BODY_REGISTRY[nodeType]
  if nodeType in CONTENT_CARD_REGISTRY:        // Track B
      return ContentCardBody
  return GenericBody (basic_fields + advanced toggle)

The two registries are permanent — they’re how nodes opt into a specific body type, not transitional gates. Utility nodes (Constant…, If, Loop, Map, control-flow) intentionally stay on the generic body forever.

4.2 What stays unchanged

5. Track A — Port + edge polish

A1. Node-hover port labels

Today: each property field shows a permanent label next to its handle. After this track: labels are hidden by default and become visible while the cursor is over the node card (anywhere on the node, not just the handle). Handle shape and styling stay as they are today — only the label visibility changes.

A2. Edge endpoint labels

The static label tags at edge endpoints (“Input*”, “Result”, “Prompt”, etc.) — already present in NodeTool to a degree near connection points; needs polish to render as small floating chips at edge tips.

A3. Typed port endpoint chips

Each handle gets a small inline pill containing its type icon (a “T” pill for text, image icon for image, etc.).

A4. Typed edge coloring

Audit the edge renderer; ensure every edge’s stroke color equals colorForType(sourceHandle.type). Today, ConnectionLine.tsx (drag-in-progress) uses colorForType but the static edges may not.

A5. Collapsed-state polish

NodeTool already supports data.collapsed. Fan-out workflows benefit from small header-only nodes as routing hubs (e.g., a single text prompt fanning out to many image generators). Tighten the collapsed visual:

6. Track B — ContentCardBody

6.1 Component shape

web/src/components/node_types/ContentCardBody.tsx

interface ContentCardBodyProps {
  nodeId: string;
  nodeMetadata: NodeMetadata;
  data: NodeData;
  primaryOutput: OutputSlot;   // determines body variant
  basicFields: string[];       // sub-set of properties rendered inline below preview
}

Renders a fixed-size card with three regions:

  1. Header — already provided by NodeHeader (provider icon + title + “…” menu)
  2. Preview area — variant based on primaryOutput.type
  3. Footer strip<DynamicInputButton /> (bottom-left) + primary action <RunModelButton /> (bottom-right) — optional, present only when relevant

6.2 Preview variants by output type

Output type Empty state Populated state
image / tensor[H,W,C] <CheckerDropzone /> with subtle “Run Model” hint Full-bleed image, object-fit: contain, falls back to cover when card width forces it
image_mask / alpha Black background with white “no mask yet” glyph Mask rendered against checker bg (alpha) or against source image if a compositing context exists
video Checker dropzone with play-icon overlay <VideoPlayer /> primitive (see §11)
str / text Empty multiline area, “Run Model” prompt subtle Readonly multiline TextField, auto-resizes to card; scroll if overflow
model_3d Checker dropzone with cube icon Static thumbnail; full 3D viewer not in scope
audio Reuse existing AudioPlayer empty state AudioPlayer (already exists)

6.3 Default card sizes

Output type Default width × height (px)
image (square) 280 × 280
image (variable aspect) 280 × 320
video 320 × 220 (16:9 + player)
str / text 320 × 200
model_3d 280 × 280
audio 320 × 120

These are applied at node creation time (via nodeFactory or equivalent on drop). User resize remains available. The defaults exist only to make freshly dropped grids feel uniform.

6.4 CONTENT_CARD_REGISTRY initial members

The registry starts with explicit node types so we don’t accidentally affect utility nodes. Initial set:

Registry lives in web/src/components/node_types/contentCardRegistry.ts. Authors of new nodes opt in by adding their node type.

6.5 Dynamic input button

web/src/components/ui_primitives/DynamicInputButton.tsx

6.6 Run Model button

web/src/components/ui_primitives/RunModelButton.tsx

6.7 Provider icon coverage

Audit IconForType / brand icon registry. Ensure every provider/category has a crisp SVG mark at 20×20 and 14×14:

Missing icons added under web/src/components/icons/providers/.

7. Track C — Left-panel quick-access

7.1 Current state

Today: PanelLeft.tsx is the workflow/asset panel; node picking happens via NodeMenu.tsx (full registry browser, modal or always-open). The full registry is good for finding, but the left panel needs to surface curated quick-access instead.

7.2 Target

The left edge gets a two-column structure:

Each tile shows:

7.3 Categories (initial)

Sidebar item Content
Search Global node search (current NodeMenu collapsed into this column)
History Recently-used nodes (already implemented as RecentNodesTiles.tsx)
Workflows Saved workflows + favorites
Image Models All Track-B image generators
Video Models All Track-B video generators
3D Models All Track-B 3D generators
Quick access Prompt, Import, Export, Preview, Import Model, Import LoRA — useful primitives
Tools Editing nodes (Levels, Crop, Channels, Blur, Compositor, Painter, etc.)

7.4 Component layout

PanelLeft.tsx (resizable, existing)
├── Sidebar (new: QuickAccessSidebar.tsx) — icon stack
└── Main column (selected category)
    ├── CategorySearchBar.tsx
    └── QuickAccessGrid.tsx — 2-col tile grid
        └── QuickAccessTile.tsx — single node card

7.5 Full registry browser

Stays available as a secondary panel or modal triggered from the Search sidebar item. Reuses existing NodeMenu.tsx rendering. The full menu does not disappear — it becomes the deep-search entry point.

7.6 Configuration

Categories defined in web/src/config/quickAccessCategories.ts as a static array of { id, label, icon, filter }. Tags-based filter against node metadata (namespace, category, tags).

8. Track D — Inspector evolution

8.1 Current state

Inspector.tsx exists and renders the selected node’s full property list with Property* field renderers. This track shifts the split between what renders on the node body vs. inspector.

8.2 Property visibility model

For each property on the selected node:

Today’s “show all advanced” toggle on the node is removed — the inspector becomes the canonical place to find advanced props.

8.3 Inspector layout

Inspector.tsx
├── Header: provider icon · node title · "..." menu
├── PropertyList (basic first, then advanced, grouped)
│   └── For each property:
│       ├── Label · (i) info tooltip · [⇢ show as input] toggle
│       └── PropertyField (existing renderers)
└── (footer reserved; intentionally empty — no cost/run section)

Info tooltip (i) reads from existing property.description.

8.4 “Show as input handle” toggle

New per-property toggle in the inspector, available on advanced properties only:

Implementation:

8.5 Inspector files

9. Track E — Bespoke editing-node bodies

Nine nodes get bespoke bodies under web/src/components/node_types/editing/. Each is a self-contained component receiving the standard node props.

E1. Levels (LevelsBody.tsx)

E2. Crop (CropBody.tsx)

E3. Channels (ChannelsBody.tsx)

E4. Blur (BlurBody.tsx)

E5. Compositor (CompositorBody.tsx)

E6. Masks Extractor (MasksExtractorBody.tsx)

E7. Rotate-and-Flip (RotateAndFlipBody.tsx)

E8. Resize (ResizeBody.tsx)

E9. Painter (PainterBody.tsx)

E10. Bespoke registry

web/src/components/node_types/editing/bespokeRegistry.ts maps node type → body component. Ships one node at a time; partial rollout is fine because un-registered nodes fall through to ContentCardBody then to the generic body.

E11. Backend coverage check (pre-implementation)

Before each bespoke body lands, confirm the backend node exists with the right outputs:

Body Backend node Status
Levels Curves (rename or alias OK) Exists
Crop CropNode Exists
Channels extract-channel Verify
Blur Blur Verify in lib-image-filter.ts
Compositor New Compositor Need to add
Masks Extractor SAM / Bria Verify per provider
Rotate-and-Flip RotateNode + FlipNode Combine FE-side
Resize ResizeNode Exists
Painter New Painter node Need to add

Two confirmed backend additions (Compositor, Painter). Both small (image-in, image/mask-out), no model dependency.

10. Track F — Group node redesign

web/src/components/node/GroupNode.tsx already exists with title input + ColorPicker + RunGroupButton + BypassGroupButton.

Changes:

Reuses existing ColorPicker, RunGroupButton, BypassGroupButton unchanged.

11. Shared primitives

New primitives in web/src/components/ui_primitives/:

Primitive Purpose Used by
VideoPlayer.tsx Embeddable video player with custom controls (timestamp, seek, play/pause, speed) ContentCardBody (video variant)
CheckerDropzone.tsx Empty-state preview with optional drop handling ContentCardBody (empty states)
DynamicInputButton.tsx + Add another X bottom-left button ContentCardBody, editing-node bodies
RunModelButton.tsx Bottom-right primary action button ContentCardBody, some editing-node bodies
TypedPortChip.tsx Small type-icon pill at handle endpoints PropertyField, edge endpoints
EdgeEndpointLabel.tsx Floating chip near edge endpoints showing property name Edge renderer (if not coverable via existing label slot)

All primitives follow STRATEGY.md rules: no raw MUI components outside ui_primitives/ or editor_ui/, theme tokens only, default size constants live next to the primitive.

12. PR sequence

Each PR replaces a piece of the existing UI directly. No flag, no parallel paths — each one ships as the new default.

Sequence keeps risk low: small isolated tracks first; biggest visual changes once the primitives are in place; bespoke editor nodes last (one PR each).

  1. PR 1 — Track A (ports + edges) — A1, A2, A3, A4, A5 in one PR (all small, all related)
  2. PR 2 — Track F (Group node) — Pill header + tinted bg
  3. PR 3 — Shared primitives — VideoPlayer, CheckerDropzone, DynamicInputButton, RunModelButton, TypedPortChip (no behavior change yet; just the primitives)
  4. PR 4 — ContentCardBody scaffold — Component + image variant; registry starts with ~3 generator nodes to validate
  5. PR 5 — ContentCardBody variants — Add video, text, 3D variants
  6. PR 6 — CONTENT_CARD_REGISTRY full rollout — Add all generator + text-content nodes
  7. PR 7 — Track C (left panel) — QuickAccessSidebar + categories; full registry remains via Search
  8. PR 8 — Track D (inspector) — Property visibility model + show-as-input toggle + exposedInputs data field
  9. PR 9..17 — Track E (one node per PR) — Resize, Rotate-and-Flip, Crop, Blur, Channels, Masks Extractor, Levels, Compositor, Painter (cheapest first, Painter last)

Backend additions (Compositor and Painter nodes) ship in their respective Track-E PRs as paired backend + frontend changes.

If a PR introduces a visible regression, it gets reverted on its own — each PR is independently revertable because tracks are narrow.

13. Risks & open questions

13.1 Risks

13.2 Open questions

14. Files affected (summary)

New

Modified

Unchanged (explicitly)