Skip to content

Flow designer: render loop / parallel / try_catch as nested regions on the canvas (ADR-0031 follow-up) #2670

Description

@os-zhuang

Context

ADR-0031 (framework) introduced structured control-flow containersloop, 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.tsxloopRepeat, parallelGitFork, try_catchShieldAlert), but nothing reads config.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_catch unsupported and runs loop as 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/validateControlFlow in 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 (loop w/ config.body, parallel w/ config.branches, try_catch w/ config.try/catch) in the node-layer map; render a container variant of NodeCard with a header + collapse/expand toggle. When expanded, recursively computeLayout each 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 = stacked Try / Catch). Nested nodes are not yet editable (regions still JSON-edited). This requires flow-canvas-layout.ts to support variable container heights feeding back into the parent layer (today NODE_H is 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); route FlowNodeInspector lookup 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 in config.<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 buildFlowProblems to descend into regions and mirror the spec's analyzeRegion (single-entry/single-exit/acyclic, parallel ≥2 branches, try required) 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 nested ui positions live — config.<region>.nodes[i].ui?
  • Variable card heightsNODE_H is 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.
  • onPatch contract — shallow top-level merge, so every region edit rebuilds the full nodes array.
  • Back-compat — a loop with no config.body is a legacy flat-graph loop and MUST keep rendering as a plain card (constructs are additive); parallel needs ≥2 branches; try_catch.try required, catch optional.
  • Type source — the designer re-declares minimal FlowNode/FlowEdge locally; consider importing FlowRegion/ParallelBranch from @objectstack/spec to avoid drift.
  • Note: packages/plugin-designer/ProcessDesigner.tsx is 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions