Canvas System
The system view β how a Python node spec becomes a rendered, navigable node
A node is defined once, in Python, and appears on the canvas with no per-node TypeScript. The canvas system is the machinery that makes that true: one registry that discovers nodes, one endpoint that publishes their specs, and one generic renderer that draws any node from its spec. This page is the system view β discovery, schema, rendering, and the navigation around composites. The node class you implement is Base Canvas Node; the graph artifact it builds (schema, storage, composition, the run handoff) is Graph System.
1. What the canvas system is
One unified editor (UnifiedGraphEditor on CanvasPage), one node registry, one catalog. Every element β built-in nodes, nodeset tools, composites, state containers β is a BaseCanvasNode served through the same pipeline, so there is no special-case rendering path per node family. The job of this system is purely presentation and authoring: it discovers what nodes exist, hands their specs to the frontend, draws them, and lets you compose and navigate them. Execution and storage live elsewhere (Graph System / Graph Executor).
2. The node-spec β rendered-node pipeline
This is the spine of the whole system. A node's appearance flows from its Python ui_config to the rendered handle in four hops, with no node-specific code on the frontend:
Β§4 covers discovery and the schema endpoint; Β§5 covers the renderer and layouts. The point to hold onto: add a node in Python, it appears on the canvas β the frontend never learns its name.
3. The catalog at a glance
The catalog is everything you can drop on the canvas, grouped by category. The 14 built-ins (Graph System Β§3 lists them) sit alongside the tools of every loaded nodeset (${nodeset}__${node}) and any saved graph nodes. A handle's colour is its wire type (Wire Types Β§9); a node frame's tint is its category β don't conflate the two. The categories: environment, llm, prompt, policy, perception, processing, decision, tool, skill, agent, control, boundary, server, output, example, custom.
4. Registry & the schema endpoint
WorkspaceComponentRegistry (components/registry.py) scans workspace/ at startup, discovering nodesets and nodes; built-ins are registered into NODE_HANDLERS by builtin_nodes.py, and workspace nodes by register_node() (last-write-wins on node_type). Whatever ends up in NODE_HANDLERS is published by one endpoint β GET /api/components/node-schemas β which emits, per node, {type, display_name, description, category, icon, kind, ports_mode, config_schema, default_config, ui_config, input_ports[], output_ports[]} (the exact shape is Base Canvas Node Β§9). Sibling endpoints list and load nodesets (NodeSets Β§10). That JSON is the catalog feed; nothing about a node is hard-coded on the client.
5. Frontend rendering
The frontend registers exactly two custom node components; everything else is generic:
// unifiedNodeTypes.ts β only TWO node types need a custom component. export const unifiedNodeTypes = { compositeNode: CompositeCanvasNode, // nested-graph shell stateContainer: StateContainerNode, // shared-memory box _generic: GenericBlockRenderer, // everything else, driven by ui_config }; // every other node_type falls through this Proxy to GenericBlockRenderer: export const proxiedNodeTypes = new Proxy(unifiedNodeTypes, { get: (target, prop) => target[prop] ?? GenericBlockRenderer, });
GenericBlockRenderer reads a node's ui_config.layout and dispatches to one of five layouts: BlockLayout (standard rectangle), StripLayout (narrow vertical gate β iterIn/iterOut), ViewerLayout (read-only runtime widgets), ImageGridViewerLayout (the configurable RGB/depth grid), and NoteLayout (the markdown note annotation). Ports are placed by portResolution.ts (resolveInstancePorts), which mirrors the backend's _resolve_ports + ports_mode so dynamic-port nodes render the same instance ports the executor will see; a LIST[T] handle gets a doubled outer ring. Inline ConfigField widgets and read-only DisplayField widgets come straight from ui_config (Base Canvas Node Β§7).
6. Composite navigation
A composite renders as a compositeNode whose input/output handles are derived from the graphIn/graphOut nodes inside its subgraph (CompositeNodeView). βEdit subgraphβ drills in. Navigation is tab-aware: useFlowStore gives each tab its own canvasStack, and pushCanvas/popCanvas/goToDepth move up and down the breadcrumb as you enter and leave nested graphs. The ExplorerPanel sidebar is the catalog of saved graphs and graph nodes β double-click opens a graph in a tab, drag a graph node onto the canvas to drop a composite. The storage behind all this (the two kinds, save/load, flatten) is Graph System Β§6; this page covers only the navigation UX.
7. Canvas chrome & toggles
Around the graph sit a few controls. The ExecutionToolbar carries the run controls plus two render-only filters β a State toggle (hide/show state-container nodes and their dashed access-grant lines) and a Viewer toggle (hide edges into viewer sinks) β neither touches graph.edges. The EnvPanel renders the active env nodeset's control surface (Env Panels). Access grants attach to every node's bottom-center __state__ handle and render as dashed violet lines, distinct from data edges (State Containers Β§2).
The resizable bottom panel (OutputDrawer) tabs Properties / State / Logs / Source / Report. The Source tab (SourcePanel, lazy-loaded so CodeMirror stays an on-demand chunk) edits the selected node's slice of its NodeSet source β module-level globals, the functions its class transitively references, and the class itself β one stacked editor per segment. Save splices the segments back by line range (PUT /api/components/nodesets/{name}/source/scoped, syntax-checked server-side); the nodeset watcher then hot-reloads local NodeSets and the tab flips βSavedβ β βReloaded ββ on the components_changed broadcast, while server-mode NodeSets show a stale banner with an explicit restart. Built-in nodes (no __ in the type) are not editable.
8. Known tensions
- Catalog scale β as nodeset count grows, the flat category grouping will need search/filter beyond what the sidebar offers today.
- Multi-agent layouts β the auto-layout (
POST /api/graphs/layout) is tuned for a single agent loop; deeply nested or multi-scope graphs can still need manual nudging. - Execution visualisation β live state is shown per-node via
viewer_data; a consolidated run timeline across nodes is not yet a first-class view.