AgentCanvas / Pages / Developer Guide / Capabilities / 9Visual Canvas Editor
2026-06-17

Every other capability page in this section describes a backend mechanism. This one is about the surface researchers actually touch: the in-browser canvas where graphs are authored, run, and observed. The editor is a single React app that talks to the backend over HTTP for state-changing actions and a single multiplexed WebSocket for live execution traffic. The same canvas is used to drill into composite subgraphs, configure nodes via auto-rendered forms, and replay finished episodes โ€” no mode switching.

Design docs: Canvas System ยท Graph System ยท Execution Logs


1. Problem

An agent-graph authoring tool needs four things that don't combine easily:

If any of these is split across a separate page, the round-trip cost dominates. The unified editor folds all four into one ReactFlow canvas backed by a single WebSocket stream.

2. Three-Layer Architecture

+-------------------+   HTTP /api/*       +--------------------------+
|  Browser (React)  | ------------------> |  FastAPI backend         |
|  - ReactFlow      |                     |  - WorkspaceComponentRegistry     |
|  - Zustand store  | <-- WS /ws -------- |  - GraphExecutor      |
|  - WSManager      |   nav_step,         |  - ErrorBus              |
+-------------------+   viewer_data,      +--------------------------+
                        nav_status,
                        error_event

State-changing actions (load nodeset, run graph, cancel run) are HTTP because they need a synchronous result. Live execution is WS because it's a high-volume one-way fan-out.

3. Graph Editor

UnifiedGraphEditor is the same component at the root canvas and inside a composite's inner graph:

ModeState sourceSave semantics
rootReads/writes useFlowStore directlyEdits are immediately live; no save button
subgraphLocal React state seeded from innerGraphExplicit Save / Reset; onSave writes back to the parent composite node's data.subgraph

Both modes share the drag-drop pipeline, typed-wire compatibility check (isCompatibleWireConnection), and the same proxiedNodeTypes registry, so a composite editor looks and behaves identically to the root โ€” only the persistence model differs.

3.1 Typed Wire Compatibility

Connections go through useFlowStore.onConnect โ†’ isCompatibleWireConnection. The check pulls each port's wire_type from portResolution (which walks _schema + live config.ports overrides for instance-derived ports such as iterIn, stateContainer, imageViewer) and rejects mismatches before the edge enters the store. This means the canvas can never reach the backend in a state that would only be caught by validate_graph_connectivity.

3.2 Node Library and Catalog

The left-side NodeLibrary reads from unifiedCatalog, which merges three sources:

4. Configuration Panel

Selecting a node opens PropertiesPanel, which renders fields straight from the node's ui_config.config_fields (or, for non-customised nodes, infers them from configSchema). One renderer (ConfigFieldRenderer) covers the common widgets:

There is no per-node React form. Adding a new node type means adding its ui_config and configSchema on the Python side; the UI is automatic.

5. Live Execution View

While the executor runs, the editor consumes four WS frame types from state.broadcast:

FrameEmitterFrontend handler
nav_statusGraphExecutor + LoopRunnerUpdates the run-state banner (idle / running / paused / error) and toolbar
nav_stepGraphExecutor._broadcast_stepConsolidated per-step payload from graphOut sinks โ€” feeds the agent node's nodeOutputs entry (RGB / depth / action / position / containers / metrics)
viewer_datatextViewer / textScroll / imageViewerPer-node payload ({node_id, step, fields}) โ€” routed by node_id straight into that node's nodeOutputs.fields slice
error_eventErrorBus (ADR-observability-004)Pushed to errorStore + Report panel + per-node error badge on the canvas

Each node's shell (NodeShell + HandleDot) renders a per-handle status dot driven by nodeOutputs[node_id]; receiving a value flips the handle to "filled", emitting flips the output handle to "fresh". Composite nodes overlay their inner-graph progress as a small badge so you can see iteration depth without opening them.

6. Composite Drill-Down

Double-clicking a composite node opens its inner graph in a modal UnifiedGraphEditor mode="subgraph". The subgraph editor is seeded from the composite's NodeDef.subgraph, edits live in local React state, and on Save writes back via onSave โ€” the parent canvas stays mounted underneath, so positions, selection, and run state are preserved. The drill stack is recorded on the store's canvasStack so multi-level dives keep their crumb trail.

Live execution is rendered through the drill: when the executor enters a composite's expanded child scope, the inner editor's nodes light up with the same per-handle status dots as the root.

7. Panels

The canvas is wrapped by a tab-aware shell with one resizable bottom drawer and a stack of right-side tabs. Every panel is a thin Zustand consumer:

PanelRole
NodeLibraryDrag source โ€” built-ins + loaded nodesets + graph_nodes
PropertiesPanelSelected-node config form (auto-rendered from ui_config)
ExecutionToolbarRun / pause / cancel / restore-checkpoint / step-delay slider
StatePanelLive state-container values + graph_state preview
EnvPanelEnv-panel methods exposed by loaded env nodesets (split / episode / reset)
ObservationPanelRGB / depth / overhead view from nav_step
LogPanel / StepLogPanelPer-node inner_log timeline (ADR-observability-003)
OutputDrawerTabbed dump of every node's last output, typed by wire
ReportPanelErrorBus envelopes โ€” clickable, jumps to the offending node on the canvas
ExplorerPanelTree view of nodes by composite / category โ€” useful in large graphs
TemplatePickerStarter graphs from TEMPLATES (defaultGraph.ts)
SaveGraphDialogSave current canvas as a named graph via POST /api/graphs

8. Run Flow

Pressing Run in the toolbar fires runPipeline(graph):

  1. Convert the live ReactFlow nodes/edges back to a GraphDefinition via fromFlowNodes + fromFlowEdges.
  2. Client-side pre-flight: for eval_graph: true (default), require โ‰ฅ1 graphOut node. Surfaces a clearer error than waiting for the backend's validate_graph_connectivity.
  3. Build the LoopDefinition wire payload (nodes, edges, containers, access_grants, step_budget, terminationCondition, presetId, hooks).
  4. POST to /api/navigate/run (ADR-platform-002 โ€” single endpoint) with an optional execution_id and step_delay_ms.
  5. The backend acknowledges with { execution_id } and starts emitting nav_status / nav_step / viewer_data / error_event frames over the already-open WS โ€” no new connection.

Cancel goes through POST /api/navigate/run/stop; the toolbar disables itself based on nav_status.status.

The visual editor is one of several React surfaces backed by the same store and WS:

10. Key Files

FileRole
agentcanvas/frontend/src/canvas/UnifiedGraphEditor.tsxRoot + subgraph editor; ReactFlow host, drag-drop, save/reset
agentcanvas/frontend/src/canvas/useFlowStore.tsZustand store: tab-aware nodes/edges/nodeOutputs/canvasStack; integrates ReactFlow change handlers
agentcanvas/frontend/src/canvas/unifiedCatalog.tsCatalog merge: built-ins + loaded nodesets + composite graph_nodes
agentcanvas/frontend/src/canvas/unifiedNodeTypes.tsReactFlow nodeTypes registry โ€” wraps every node type with the shared shell
agentcanvas/frontend/src/canvas/nodes/shared/NodeShell.tsxUniversal node frame: handle rows, status dots, error badge, composite-drill button
agentcanvas/frontend/src/canvas/nodes/shared/HandleDot.tsxPer-handle status dot driven by nodeOutputs
agentcanvas/frontend/src/canvas/nodes/composite/CompositeNodeView.tsxComposite tile + drill button โ†’ subgraph modal
agentcanvas/frontend/src/canvas/panels/PropertiesPanel.tsxAuto-rendered config form from ui_config.config_fields
agentcanvas/frontend/src/canvas/panels/NodeLibrary.tsxDrag-source list with categories
agentcanvas/frontend/src/canvas/panels/ExecutionToolbar.tsxRun/pause/cancel/step-delay controls; observes nav_status
agentcanvas/frontend/src/canvas/runPipeline.tsEditor โ†’ LoopDefinition โ†’ POST /api/navigate/run
agentcanvas/frontend/src/canvas/graphConversion.tsGraphDefinition โ†” ReactFlow Node[]/Edge[] (incl. containers, access grants)
agentcanvas/frontend/src/canvas/portResolution.tsResolve instance ports (iterIn / stateContainer / imageViewer) from live config
agentcanvas/frontend/src/ws.tsWSManager singleton โ€” auto-reconnect, typed handler map, wildcard channel
agentcanvas/frontend/src/store.tsWS subscriptions: routes nav_step / viewer_data / nav_status / error_event into the appropriate store slices

Status

ItemStatusNotes
Unified root/subgraph editorDoneUnifiedGraphEditor handles both modes with one component
Typed-wire client-side validationDoneisCompatibleWireConnection in useFlowStore.onConnect
Auto-rendered properties panelDoneConfigFieldRenderer covers text / textarea / select / slider / checkbox / port_list / llmRef
Catalog merge (builtin + loaded + composite)DoneunifiedCatalog refreshes on nodeset load/unload
Single multiplexed WebSocketDoneOne wsManager with typed channels + wildcard
Per-handle live statusDoneNodeShell + HandleDot driven by nodeOutputs
Consolidated nav_step from graphOut sinksDoneRecognised port names (rgb/depth/action/state/done/metrics/response) fed into agent node slice
Per-node viewer_data routingDoneSink nodes emit their own viewer_data frames during forward()
Tab-aware canvas slicesDoneEach tab owns nodes/edges/nodeOutputs/canvasStack
Composite drill-down with save/resetDoneModal subgraph editor; parent canvas preserved underneath
Pre-flight: eval_graph requires graphOutDoneCaught client-side before /api/navigate/run round-trip
Step-delay slider for human-readable runsDonestep_delay_ms passed on every /api/navigate/run call
Restore-checkpoint UIDoneToolbar reads get_checkpoints(); per-scope (ADR-executor-003)
Error-bus badges on offending nodeDoneReport panel click jumps to node on canvas
Mobile / touch UXPlannedReactFlow supports it but no UI tuning done; right panels do not collapse on narrow viewports
Multi-selection composite extractionPlanned"Select N nodes โ†’ wrap into composite" โ€” group-selection scaffolding exists in groupSelection.ts; UI command not wired
AgentCanvas docs