Repository navigation
Flow designer: render loop / parallel / try_catch as nested regions on the canvas (ADR-0031 follow-up) #2670
Description
Context
ADR-0031 (framework) introduced structured control-flow containers — loop, parallel, try_catch — whose bodies are nested regions carried in config (representation B): config.body for loop, config.branches[] for parallel, config.try + config.catch for try_catch. The engine executes them and folds their per-iteration/per-branch/handler step logs into the run log, and the run-observability Runs panel now nests those steps (#2667). ADR-0031 explicitly defers the remaining piece: "Designer rendering of the constructs (in ../objectui) is tracked separately." This issue is that piece.
Problem
The flow designer canvas renders loop / parallel / try_catch as flat single node cards. Their nested regions are not visualized at all, and are editable only as raw JSON:
- Icons/tones/palette entries exist (
flow-canvas-parts.tsx—loop→Repeat,parallel→GitFork,try_catch→ShieldAlert), but nothing readsconfig.body/branches/try/catch. - The inspector explicitly punts (
inspectors/flow-node-config.ts:558-568): "Their bodies are nested regions … sub-graphs the flat field kinds can't model; authors edit them in the JSON source editor." Region config falls through to the "Advanced (JSON)" textarea (inspectors/FlowNodeInspector.tsx:147-149). - The debug simulator marks
parallel/try_catchunsupported and runsloopas a single pass (previews/simulator/flow-simulator.ts:43,360-373). - Client-side validation (
previews/flow-problems.ts) never descends into regions — single-entry/single-exit/acyclic is enforced server-side only (analyzeRegion/validateControlFlowin the spec).
So an author composing a loop body, parallel branches, or a try/catch handler is hand-editing JSON — exactly the error-prone authoring ADR-0031 set out to remove.
Current state (grounded)
The automation flow canvas is hand-rolled and dependency-free (no reactflow/@xyflow/dagre/elkjs), under packages/app-shell/src/views/metadata-admin/previews/:
| File | Role |
|---|---|
FlowCanvas.tsx |
The canvas — absolutely-positioned cards over one global SVG edge layer; drag/pan/zoom; add/insert/delete. Flat nodes.map(...) node layer (:801-827). |
flow-canvas-parts.tsx |
NodeCard (fixed NODE_W×NODE_H), NODE_PALETTE, nodeIcon/nodeTone/nodeCategory (type→visual switches). |
flow-canvas-layout.ts |
computeLayout — layered longest-path, top-to-bottom; constant NODE_H; manual node.ui.{x,y} overrides; edge-path math. |
FlowPreview.tsx |
Host — owns draft/onPatch/flat selection. |
onPatch is a shallow top-level merge (StudioDesignSurface.tsx:1356-1357), so any region edit rebuilds the whole nodes array with the container's mutated config.
Prior art: FlowRunsPanel.buildStepTree (objectui#2667) already reconstructs region nesting and names it (Iteration N / Branch N / Try / Catch, regionLabel) — but it nests via indentation in a read-only log list from runtime step logs, not spatially from static config. Reuse its labeling model; the canvas problem (spatial nesting of an editable graph) is different.
Spec shapes (framework automation/control-flow.zod.ts, consumed as @objectstack/spec): FlowRegion = { nodes[≥1], edges } (single-entry/single-exit/acyclic); loop.body? (optional → legacy flat loops stay valid), parallel.branches[≥2], try_catch.try (required) + catch?.
Proposed design
Render each container as an expandable card that spatially contains a nested region sub-canvas, laid out recursively with the same computeLayout, with the container sized to bound its region(s). Deliver in phases so value lands early and the hard interactions are de-risked:
- Phase 1 — read-only nested rendering (highest value, bounded). Detect container nodes (
loopw/config.body,parallelw/config.branches,try_catchw/config.try/catch) in the node-layer map; render a container variant ofNodeCardwith a header + collapse/expand toggle. When expanded, recursivelycomputeLayouteach region into a region-local coordinate space, draw its nodes + edges (per-region SVG group / offset transform), and size the container to bound them (parallel= N side-by-side branch columns with names;try_catch= stackedTry/Catch). Nested nodes are not yet editable (regions still JSON-edited). This requiresflow-canvas-layout.tsto support variable container heights feeding back into the parent layer (todayNODE_His constant) and nested-edge routing. Collapsed containers render exactly as today. - Phase 2 — nested selection + structured inspector. Path-based selection (
containerId → regionKey → nodeId) — flat{kind:'node', id}selection collides across regions (FlowPreview.tsx:83); routeFlowNodeInspectorlookup through the path. Replace the raw-JSON region block with structured editing of the selected nested node. - Phase 3 — nested editing. Region-scoped
addNode/insertOnEdge/delete/drag (the current handlers mutate top-level arrays only,FlowCanvas.tsx:193-376); palette "+" affordances target a specific region; store nested positions inconfig.<region>.nodes[i].ui. Reparenting a node across a region boundary is the hardest interaction — can be last. - Phase 4 — client-side region validation. Extend
buildFlowProblemsto descend into regions and mirror the spec'sanalyzeRegion(single-entry/single-exit/acyclic,parallel≥2 branches,tryrequired) so region errors surface as canvas badges instead of only failing at server registration.
Files likely touched
FlowCanvas.tsx (node-layer + edge-layer + handlers), flow-canvas-layout.ts (recursive region layout, variable heights, nested edges), flow-canvas-parts.tsx (container NodeCard variant + collapse/expand), FlowPreview.tsx (region context + path selection + nested onPatch), inspectors/flow-node-config.ts + FlowNodeInspector.tsx (structured region editing), previews/flow-problems.ts (region validation), optionally simulator/flow-simulator.ts (region expansion), plus FlowCanvas/layout tests.
Tricky bits / open questions
- Coordinate systems — one flat frame +
node.ui.{x,y}today; nested nodes need a region-local space. Where do nesteduipositions live —config.<region>.nodes[i].ui? - Variable card heights —
NODE_His constant; a container grows to bound its region → structural change to the layered layout +diagramSize. - Nested edges in one SVG — the edge layer is a single global
<svg>keyed on top-level ids; region-local edges need per-region groups/offsets, plus edges that enter/leave a container. - Collapse/expand default (collapsed for density vs expanded for authoring) and re-flow on toggle.
- Selection identity — path-based, ripples into the inspector.
onPatchcontract — shallow top-level merge, so every region edit rebuilds the fullnodesarray.- Back-compat — a
loopwith noconfig.bodyis a legacy flat-graph loop and MUST keep rendering as a plain card (constructs are additive);parallelneeds ≥2 branches;try_catch.tryrequired,catchoptional. - Type source — the designer re-declares minimal
FlowNode/FlowEdgelocally; consider importingFlowRegion/ParallelBranchfrom@objectstack/specto avoid drift. - Note:
packages/plugin-designer/ProcessDesigner.tsxis a separate BPMN swimlane designer and is not this surface — out of scope.
Acceptance (Phase 1)
Opening a flow with a loop/parallel/try_catch that has a populated region shows the region's body/branches/handlers nested inside the container card (expandable), with correct labels — no more opaque single card, and no JSON required to see the structure. Legacy flat loops and all non-container nodes render unchanged.
Scoped from an investigation of the current designer canvas; filed as the ADR-0031 "designer rendering" follow-up to objectui#2667 (Runs-panel nesting) and framework#3240 (history compaction). Suggested as a phased feature — Phase 1 is independently shippable.
Activity
feat(studio): visualize loop/parallel/try_catch nested regions on the…
feat(studio): inline push-down expansion of loop/parallel/try_catch r…
feat(flow-designer): #2670 Phase 3 — nested container node selection …
feat(flow-designer): schema-driven keyValue + numberList mapping (#3304…