Skip to content
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
This repository was archived by the owner on Nov 4, 2026. It is now read-only.

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 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 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 heights — NODE_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

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions