feat(flow-designer): #2670 Phase 3 — select & edit nested container nodes via the schema-driven inspector#2699
Merged
Conversation
…gating Structured-inspector Phase 3 (#2670), part B. The flow designer's config forms are (per ADR-0018) driven server-first from a node type's published `configSchema` (jsonSchemaToFlowFields), falling back to the hardcoded client form. Two gaps meant CEL vs `interpolate()` template semantics were lost: 1. The schema adapter had no way to tell an expression apart from a template — every string became plain `text`, dropping the mono editor, the data picker's brace mode, and validation. 2. The inline brace-trap (ADR-0032) and the canvas expression badges fired on ANY `expression`-kind field. A loop / map `collection`, whose value is a single-brace `{leadList}` template (not bare CEL), was flagged as a malformed condition — the online (schema→text) and offline (hardcoded→ expression, mis-validated) forms had already diverged now that the engine publishes a loop configSchema, so this is also a bug fix. Changes: • json-schema-to-fields: read a string property's `xExpression: 'expression' | 'template'` marker (Zod `.meta()` channel, same as `xRef` / `xEnumDeprecated`). 'expression' → a CEL `expression` field; 'template' → `expression` + `refMode: 'template'` (mono + picker, `{var}` mode, no predicate check); multiline variants map to `textarea` in the matching refMode; object-list columns map expression → CEL column, template → text (columns carry no refMode). Precedence: xRef > enum > xExpression > scalar. Unknown values degrade to plain text. • FlowNodeConfigField: gate the ADR-0032 brace-trap on the EFFECTIVE mode — an `expression` field in `refMode: 'template'` is a legal `{var}` template, so the CEL check is suppressed and the scope-aware note switches to template (`{…}`-hole) semantics. • flow-expr-problems: skip `refMode: 'template'` expression fields when scanning the draft, so canvas badges match the inline check. • flow-node-config: flag loop.collection / map.collection `refMode: 'template'` (their `{leadList}` / `{items}` placeholders are templates). All other expression fields are genuine predicates — audited, left as-is. Tests: xExpression → four field mappings + illegal-value degrade + precedence + columns; loop/map collection refMode + predicate fields unchanged; the loop collection template no longer reported by flow-expr-problems while a real CEL predicate still is; FlowNodeConfigField renders no brace error on a `{leadList}` template, a template note on an out-of-scope hole, and still errors on a `{record.x}` predicate mistake. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VuvxWgoadqryqBcjs7TpVi
Structured-inspector Phase 3 (#2670), part C1. Phase 2 (#2680) rendered a container's regions (loop.body / parallel.branches / try_catch.try/catch) inline on the canvas, but the nested nodes were read-only — editing their config still meant hand-writing JSON. This makes a nested node selectable so the next step (C2) can route it to the schema-driven inspector. • flow-nested-selection.ts (new): the selection contract for a nested node. A `MetadataSelection` is flat (`{ kind, id }`), so a nested node is encoded as `containerId::regionKey::nodeId` under a distinct `NESTED_NODE_KIND`. Parsing anchors the middle segment to the CLOSED set of real region keys (`body` / `try` / `catch` / `branch-N`), so an ambiguous id still splits deterministically — and because the write-back (C2) matches every segment exactly against the draft, a mis-parse can only ever resolve to "not found", never a wrong write. `regionConfigPathOf` / `regionLabelOf` mirror extractRegions so the codec and the canvas layout never disagree on the key set (locked by an anti-drift test). • flow-region-view: a RegionNode becomes a selectable control (button role, keyboard, selection ring) when given an `onSelect`; read-only and byte-identical to Phase 2 otherwise. The `data-region-node-id` hook is always emitted for targeting. • flow-canvas-parts / FlowCanvas: thread the selected nested node (matched by container id → selection ring) and an `onSelectNested` emitter through NodeCard. Nested selection is gated on design mode, exactly like top-level selection, so read-only trays stay read-only. Collapsing a container clears a now-hidden nested selection (computed outside the state updater so a canvas remount can't wrongly clear a valid selection). • FlowPreview: owns the codec end — decodes a `NESTED_NODE_KIND` selection to the structured path (the canvas only ever sees the path) and encodes back on select. A nested selection naturally zeroes `selectedId` (kind !== 'node'), so top-level keyboard delete / palette wiring disable themselves with no defensive code. Tests: codec round-trip (incl. branch-7, `::`-bearing node ids) + rejects illegal tokens; region path/label mapping + anti-drift vs extractRegions; FlowCanvas — a body-node click emits the structured path and never bubbles to the container, the selection ring lands on the matching node, collapse clears the selection, a parallel branch node is tagged `branch-1`, and the tray stays read-only (no role, no routing) outside design mode. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VuvxWgoadqryqBcjs7TpVi
…es (path write-back) Structured-inspector Phase 3 (#2670), part C2 — the payoff. A node inside a container region (loop.body / parallel.branches[] / try_catch.try/catch) now opens in the SAME schema-driven inspector as a top-level node and writes back into `config.<region>.nodes[i]`. • flow-nested-selection: adds `locateFlowNode(draft, selection)` → a NodeLocation with the resolved node, whether it is nested, its scope anchor (the container id — a region runs in its container's outer scope, ADR-0031), and a `write(next)` that produces the draft patch. The nested write rebuilds the container with EXPLICIT SPREADS + spliceArray, never a path walk — a generic setAtPath through the region path would objectify the `config.branches` array and mis-prune; this keeps arrays arrays and preserves each region's `edges` / a branch's `name`. A stale location (deleted node, moved draft) returns null so the caller no-ops. • FlowNodeInspector: resolves through locateFlowNode and routes every edit (patchNode / setField / commitAdvanced / remove) through loc.write. A nested node gets a `container › region › node` breadcrumb; its scope injects the container's loop iteratorVariable (excluded from the graph walk at the container's own id). Nested nodes are edit-only this phase: id is read-only (with a hint — rename in the container's Advanced JSON), delete is hidden, and a nested decision drops the virtual Target column AND skips top-level edge mirroring — a region's internal routing is not managed here, and mirroring would otherwise forge phantom top-level edges to nested ids. FlowInspector needs no routing change: a non-edge selection already lands on FlowNodeInspector, and a nested selection zeroes `selectedId` so top-level keyboard delete / palette wiring self-disable. • useFlowScope: takes an optional `extraRefs`, merged before de-dup / grouping. • preview-samples: the console flow sample gains a permanent loop container with a body region, so the preview gallery can drive nested selection + edit. Tests: locateFlowNode top-level + nested write-back (body edges / sibling config preserved, no top-level edges emitted, `Array.isArray(branches)` holds, stale → null); FlowNodeInspector nested read (breadcrumb + schema fields), nested label write into body.nodes[i] / branch-1.nodes[0], id locked + delete hidden + hint, nested decision drops Target and never mirrors edges (top-level keeps it), stale path → empty state naming the node id; useFlowScope extraRefs merge / de-dup / "Loop item" grouping. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VuvxWgoadqryqBcjs7TpVi
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
Contributor
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
…ntainer types Extend the preview-gallery flow sample with a `parallel` (two named branches) and a `try_catch` (try + catch) container alongside the existing `loop`, using the same node shapes as the framework showcase flows (Batch Reminders / Fan-out Notify / Resilient Sync). The gallery now demonstrates and verifies nested-node selection + schema-driven editing across every container region kind (body / branch-N / try / catch) — browser-checked end to end: expand → select nested node (ring + `container › region › node` breadcrumb) → edit → live canvas update, for each container. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VuvxWgoadqryqBcjs7TpVi
os-zhuang
marked this pull request as ready for review
July 19, 2026 17:13
Contributor
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
This was referenced Jul 20, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of #2670 (Phase 3). Builds on Phase 1 (#2675, read-only region popover) and Phase 2 (#2680, inline region expansion). This phase makes a node inside a container region (
loop.body/parallel.branches[]/try_catch.try/catch) a first-class, editable selection through the same schema-driven inspector as a top-level node.Three self-contained commits, each independently green.
B —
xExpressionmarker + expression-mode validation gatingThe config forms are (ADR-0018) generated server-first from a node type's published
configSchema, and the adapter had no way to tell a bare-CEL expression from aninterpolate(){var}template — so every string became plaintext, and the ADR-0032 brace-trap fired on anyexpression-kind field. Now the engine publishes aloopconfigSchema, the online (schema→text) and offline (hardcoded→expression, mis-validated) forms had already diverged, so this is also a bug fix.json-schema-to-fields: read a string property'sxExpression: 'expression' | 'template'marker.'expression'→ CEL field;'template'→expression+refMode: 'template'(mono + picker,{var}, no predicate check); multiline variants →textareain the matching mode; object-list columns map expression→CEL, template→text. PrecedencexRef > enum > xExpression > scalar; unknown → text.expressionfield inrefMode: 'template'(e.g. a loop/mapcollectionauthored{leadList}) is no longer flagged as a malformed condition.loop.collection/map.collectionrefMode: 'template'; all other expression fields are genuine predicates (audited, unchanged).C1 — nested-node selection on the inline canvas
flow-nested-selection.ts: a nested node is encoded into the flatMetadataSelectionascontainerId::regionKey::nodeIdunder a distinctNESTED_NODE_KIND. Parsing anchors the middle segment to the closed set of real region keys, so an ambiguous id still splits deterministically — and since the write-back matches every segment exactly against the draft, a mis-parse can only ever resolve to "not found", never a wrong write.regionConfigPathOf/regionLabelOfmirrorextractRegions(locked by an anti-drift test).FlowPreviewowns the codec end; the canvas only ever sees the structured path.C2 — schema-driven inspector for nested nodes (path write-back)
locateFlowNode(draft, selection)→ aNodeLocationwith the resolved node, whether it's nested, its scope anchor (the container id — a region runs in its container's outer scope, ADR-0031), and awrite(next)that produces the draft patch.FlowNodeInspectorroutes every edit through it, so it never branches on where the node lives;FlowInspectorneeds no routing change.container › region › nodebreadcrumb and injects the container's loopiteratorVariableinto scope. It is edit-only this phase: id read-only (with a hint), delete hidden, and a nested decision drops the virtual Target column.Two traps this design avoids (with regression tests)
setAtPaththrough the region path would turnconfig.branches([…]) into{0:…}and mis-prune. The nested write rebuilds the container with explicit spreads +spliceArray, keeping arrays arrays and preservingregion.edges/ a branch'sname. Test assertsArray.isArray(branches)survives.draft.edges; mirroring there would forge top-level edges pointing at nested ids. Edge mirroring (and the Target column) is gated!nested; the commit patch carries noedges.Non-goals (follow-ups)
Nested structural editing (add/remove/drag/reparent), recursive container expansion,
problemstraversal into regions (this inspector is the first validation surface for a nested node), nested id rename, parallel horizontal layout. A nested decision row's staletargetkey is stripped byrowsToListon first commit (same terminal state as top level).Companion
Descriptor-side counterpart (emit
xExpressionfrom the executor Zod, fill the ~11 node types with noconfigSchema): objectstack-ai/objectstack#3304.Verification
json-schema-to-fields,flow-node-config,flow-expr-problems,FlowNodeConfigField,flow-nested-selection(codec + locator + D5 array regression),FlowCanvas(nested selection),FlowNodeInspector(nested read/write, id-lock, delete-hidden, decision Target strip, stale→empty),useFlowScope(extraRefs). Full metadata-admin suite: 98 files / 865 tests pass.turbo type-check(app-shell + console) clean; eslint clean (no new warnings).?only=flow): expand the loop → select the nested node (ring on) → inspector shows the breadcrumb + schema fields + read-only id → rename → canvas node updates live → top-level select/delete unaffected → collapse clears the selection. All checkpoints pass.🤖 Generated with Claude Code
https://claude.ai/code/session_01VuvxWgoadqryqBcjs7TpVi
Generated by Claude Code