AgentCanvas / Pages / Developer Guide / Core / Decisions / Components / ADR-components-004
2026-03-30
Date
2026-03-30
Status
accepted
Field
components
Old ID
ADR-013

Context

Adding a new canvas node required 4 file touches: a Python handler class, a custom .tsx React component, an import in unifiedNodeTypes.ts, and a catalog entry in unifiedCatalog.ts. Nodeset tools (Habitat, SAM) already bypassed this — GenericBlockRenderer auto-rendered them from backend port schemas. But 13 built-in nodes still had dedicated .tsx files for what amounted to simple port+label rendering. The framework had an asymmetry: nodeset tools = 1 Python file, built-in nodes = 4 files.

Decision

(1) Add NodeUIConfig and ConfigField dataclasses to BaseCanvasNode in bases.py. Every node inherits ui_config with sensible defaults. (2) NodeUIConfig declares: color (Tailwind colour key), layout ("block" or "strip"), width/min_height/rounding (strip geometry), and config_fields (inline UI controls). (3) ConfigField supports 6 widget types: label, slider, text, select, toggle, textarea. (4) Serialize ui_config in the /inner-nodes API response. (5) Rewrite GenericBlockRenderer.tsx to support block layout (standard rectangle) and strip layout (narrow vertical gate like IterIn/IterOut). Block layout renders colour overrides and config fields. Strip layout renders vertical text with handles. (6) Enrich static catalog entries with _schema from backend so built-in nodes get ui_config at drag time. (7) Reduce unifiedNodeTypes.ts from 18 explicit mappings to 7 (6 custom UI + 1 fallback). (8) Delete 13 custom .tsx node files — the proxy fallback routes them to GenericBlockRenderer.

Alternatives

(a) Keep custom .tsx for all built-in nodes — rejected, maintenance burden with no benefit over auto-rendering. (b) Use a JSON config file instead of Python ClassVars — rejected, would separate UI config from the node class, violating the "one file per node" principle. (c) Generate .tsx files from Python at build time — rejected, code generation adds complexity; runtime schema reading is simpler.

Rationale

One Python class = one canvas node, everywhere. Built-ins, nodeset tools, and custom nodes all follow the same pattern. The 6 remaining custom .tsx files (PromptTemplate, Composite, 4 output viewers) are the irreducible minimum — they render truly complex UI (template editors with variable chips, live WebSocket image/log/metrics viewers) that cannot be expressed as ports + config fields.

Affected docs

base-canvas-node.md, roadmap.md