9Visual Canvas Editor
A React Flow editor where you wire up an agent graph, drop nodes from a live backend catalog, and watch every port update during execution โ no separate "design" and "monitor" tools.
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:
- Edit topology โ drag/drop nodes, draw typed wires, validate connections before submit.
- Configure nodes โ every node ships its own settings (model name, temperature, image grid size); these can't be hand-coded panels per node type.
- Run and watch โ show port-by-port output while the executor is running, not just a final summary.
- Drill in / out โ open a composite, edit its inner graph, save back, without losing the outer canvas context.
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
- React Flow (
@xyflow/react) renders nodes and edges; it owns geometry (positions, viewport, drag state). - Zustand store (
useFlowStore) owns graph semantics (typed wires, port resolution, composite drill-stack, per-node live output). Tab-aware: each open tab is an independent slice with its own nodes/edges/nodeOutputs; top-level selectors are projections of the active tab so older components keep working. - WSManager (
ws.ts) is one singleton WebSocket with auto-reconnect (exponential backoff 1s โ 30s), a typed handler map, and a wildcard channel. Every panel that needs live data subscribes by frametypeโ no per-feature socket.
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:
| Mode | State source | Save semantics |
|---|---|---|
root | Reads/writes useFlowStore directly | Edits are immediately live; no save button |
subgraph | Local React state seeded from innerGraph | Explicit 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:
- Built-in nodes from the bundled
buildBuiltinEntriestable (iterIn/Out, graphIn/Out, viewers, state containers, composite, graphNode) โ theirdefault_configmirrors backendBaseCanvasNode.default_configso a freshly dropped node validates immediately. - Loaded nodeset tools fetched from
GET /api/components/loadedafter any load/unload โ each tool'snode_type,input_ports,output_ports, andconfigSchemadrive both the palette entry and the dropped node's initial state. - Composite graphs registered as nodeset
graph_nodesโ they appear as a single drop-tile and expand into a subgraph editor when opened.
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:
text/textarea/select/slider/checkboxโ primitive fieldsport_listโ used byiterIn/iterOut/stateContainer/imageViewerto add/remove typed ports; edits feed back intoportResolutionso the node's handle row updates livellmRefโ picker bound to the loaded profile list (GET /api/profiles)
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:
| Frame | Emitter | Frontend handler |
|---|---|---|
nav_status | GraphExecutor + LoopRunner | Updates the run-state banner (idle / running / paused / error) and toolbar |
nav_step | GraphExecutor._broadcast_step | Consolidated per-step payload from graphOut sinks โ feeds the agent node's nodeOutputs entry (RGB / depth / action / position / containers / metrics) |
viewer_data | textViewer / textScroll / imageViewer | Per-node payload ({node_id, step, fields}) โ routed by node_id straight into that node's nodeOutputs.fields slice |
error_event | ErrorBus (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:
| Panel | Role |
|---|---|
NodeLibrary | Drag source โ built-ins + loaded nodesets + graph_nodes |
PropertiesPanel | Selected-node config form (auto-rendered from ui_config) |
ExecutionToolbar | Run / pause / cancel / restore-checkpoint / step-delay slider |
StatePanel | Live state-container values + graph_state preview |
EnvPanel | Env-panel methods exposed by loaded env nodesets (split / episode / reset) |
ObservationPanel | RGB / depth / overhead view from nav_step |
LogPanel / StepLogPanel | Per-node inner_log timeline (ADR-observability-003) |
OutputDrawer | Tabbed dump of every node's last output, typed by wire |
ReportPanel | ErrorBus envelopes โ clickable, jumps to the offending node on the canvas |
ExplorerPanel | Tree view of nodes by composite / category โ useful in large graphs |
TemplatePicker | Starter graphs from TEMPLATES (defaultGraph.ts) |
SaveGraphDialog | Save current canvas as a named graph via POST /api/graphs |
8. Run Flow
Pressing Run in the toolbar fires runPipeline(graph):
- Convert the live ReactFlow nodes/edges back to a
GraphDefinitionviafromFlowNodes+fromFlowEdges. - Client-side pre-flight: for
eval_graph: true(default), require โฅ1graphOutnode. Surfaces a clearer error than waiting for the backend'svalidate_graph_connectivity. - Build the
LoopDefinitionwire payload (nodes, edges, containers, access_grants, step_budget, terminationCondition, presetId, hooks). - POST to
/api/navigate/run(ADR-platform-002 โ single endpoint) with an optionalexecution_idandstep_delay_ms. - The backend acknowledges with
{ execution_id }and starts emittingnav_status/nav_step/viewer_data/error_eventframes 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.
9. Related Pages and Tools
The visual editor is one of several React surfaces backed by the same store and WS:
- NodeSet Manager (
pages/NodeSetManager.tsx) โ load/unload nodesets, pick local vs server mode, view manifest. Updates to loaded nodesets reflow the editor's catalog. - Eval page (
eval/EvalPage.tsx) โ submit / monitor / cancel batch runs (see capability 8); reuses the same WSManager for per-episodenav_step. - Replay page (
replay/ReplayPage.tsx) โ scrub through stored per-episode logs (ADR-eval-004); renders the same node shells with frozen data. - Log viewer (
logs/LogViewerPage.tsx) โ historic execution logs with the canvas re-rendered from the saved graph; per-node inner_logs are filtered, paginated, and clickable.
10. Key Files
| File | Role |
|---|---|
agentcanvas/frontend/src/canvas/UnifiedGraphEditor.tsx | Root + subgraph editor; ReactFlow host, drag-drop, save/reset |
agentcanvas/frontend/src/canvas/useFlowStore.ts | Zustand store: tab-aware nodes/edges/nodeOutputs/canvasStack; integrates ReactFlow change handlers |
agentcanvas/frontend/src/canvas/unifiedCatalog.ts | Catalog merge: built-ins + loaded nodesets + composite graph_nodes |
agentcanvas/frontend/src/canvas/unifiedNodeTypes.ts | ReactFlow nodeTypes registry โ wraps every node type with the shared shell |
agentcanvas/frontend/src/canvas/nodes/shared/NodeShell.tsx | Universal node frame: handle rows, status dots, error badge, composite-drill button |
agentcanvas/frontend/src/canvas/nodes/shared/HandleDot.tsx | Per-handle status dot driven by nodeOutputs |
agentcanvas/frontend/src/canvas/nodes/composite/CompositeNodeView.tsx | Composite tile + drill button โ subgraph modal |
agentcanvas/frontend/src/canvas/panels/PropertiesPanel.tsx | Auto-rendered config form from ui_config.config_fields |
agentcanvas/frontend/src/canvas/panels/NodeLibrary.tsx | Drag-source list with categories |
agentcanvas/frontend/src/canvas/panels/ExecutionToolbar.tsx | Run/pause/cancel/step-delay controls; observes nav_status |
agentcanvas/frontend/src/canvas/runPipeline.ts | Editor โ LoopDefinition โ POST /api/navigate/run |
agentcanvas/frontend/src/canvas/graphConversion.ts | GraphDefinition โ ReactFlow Node[]/Edge[] (incl. containers, access grants) |
agentcanvas/frontend/src/canvas/portResolution.ts | Resolve instance ports (iterIn / stateContainer / imageViewer) from live config |
agentcanvas/frontend/src/ws.ts | WSManager singleton โ auto-reconnect, typed handler map, wildcard channel |
agentcanvas/frontend/src/store.ts | WS subscriptions: routes nav_step / viewer_data / nav_status / error_event into the appropriate store slices |
Status
| Item | Status | Notes |
|---|---|---|
| Unified root/subgraph editor | Done | UnifiedGraphEditor handles both modes with one component |
| Typed-wire client-side validation | Done | isCompatibleWireConnection in useFlowStore.onConnect |
| Auto-rendered properties panel | Done | ConfigFieldRenderer covers text / textarea / select / slider / checkbox / port_list / llmRef |
| Catalog merge (builtin + loaded + composite) | Done | unifiedCatalog refreshes on nodeset load/unload |
| Single multiplexed WebSocket | Done | One wsManager with typed channels + wildcard |
| Per-handle live status | Done | NodeShell + HandleDot driven by nodeOutputs |
| Consolidated nav_step from graphOut sinks | Done | Recognised port names (rgb/depth/action/state/done/metrics/response) fed into agent node slice |
| Per-node viewer_data routing | Done | Sink nodes emit their own viewer_data frames during forward() |
| Tab-aware canvas slices | Done | Each tab owns nodes/edges/nodeOutputs/canvasStack |
| Composite drill-down with save/reset | Done | Modal subgraph editor; parent canvas preserved underneath |
| Pre-flight: eval_graph requires graphOut | Done | Caught client-side before /api/navigate/run round-trip |
| Step-delay slider for human-readable runs | Done | step_delay_ms passed on every /api/navigate/run call |
| Restore-checkpoint UI | Done | Toolbar reads get_checkpoints(); per-scope (ADR-executor-003) |
| Error-bus badges on offending node | Done | Report panel click jumps to node on canvas |
| Mobile / touch UX | Planned | ReactFlow supports it but no UI tuning done; right panels do not collapse on narrow viewports |
| Multi-selection composite extraction | Planned | "Select N nodes โ wrap into composite" โ group-selection scaffolding exists in groupSelection.ts; UI command not wired |