Files
fusion/packages/dashboard/app/components/nodes/node-help.ts
gsxdsm be7c23693c feat(FN-6880): author optional-group container in the node editor (U4)
Render and author optional-group as a third group-container kind beside
foreach/loop: register the node type (OptionalGroupNode) so it renders with a
header + defaultOn badge and parentId template children, treat it as a group
everywhere in workflow-flow-mapping (irToFlow children, flowToIr template
reassembly, cascade-delete, condition-editable), add a palette entry, and an
inspector defaultOn toggle. Includes a node-help entry and round-trip / toggle /
cascade-delete tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 18:29:58 -07:00

307 lines
17 KiB
TypeScript

import type { WorkflowEditorNodeKind, WorkflowFlowNodeData } from "./WorkflowNodeTypes";
/*
FNXC:WorkflowEditor 2026-06-21-10:00:
The node detail pane must teach, not just edit. Every workflow node — including the engine-managed graph-only policy nodes (merge gate, branch-group member integration / promotion, PR nodes, recovery/retry) — needs an in-editor Help section describing what it does, how to configure it, and its inputs/outputs/edges. This was prompted by a user unable to tell what "branch-group-member-integration", "branch-group-promotion", and "merge gate" meant in the editor.
Help is keyed by the node's EFFECTIVE kind: the preserved original IR kind (`data.irKind`) when present, else the editor kind (`data.kind`). Graph-only IR kinds collapse to merge/gate/hold editor shapes via GRAPH_ONLY_EDITOR_KIND, so without the preserved kind the branch-group/PR/merge nodes would all read as a generic "merge"/"gate".
Per-node body text is English reference documentation (analogous to node-summary's raw, untranslated config values); only the repeated structural section labels are routed through i18n by the inspector. Keep this content in sync when node config fields or edge routing change.
*/
/** A node's effective kind for help lookup: the preserved original IR kind when
* the editor collapsed a graph-only policy node onto a generic shape, else the
* editor kind. Mirrors workflow-flow-mapping's `preservedIrKind`. */
export function effectiveNodeKind(data: WorkflowFlowNodeData): string {
return typeof data.irKind === "string" ? data.irKind : data.kind;
}
export interface NodeHelp {
/** Human title for the node kind (the inspector heading reuses this). */
title: string;
/** One- to two-sentence description of what the node does. */
summary: string;
/** How to configure it. Omitted for structural nodes with no config. */
configure?: string;
/** What arrives at the node (incoming edges / available context). */
inputs: string;
/** What the node produces / passes downstream. */
outputs: string;
/** Outgoing edges and the conditions/outcomes that route them. */
edges: string;
/** Engine-managed policy node: surfaced read-only, not hand-authored. The
* inspector shows an "Engine-managed" badge for these. */
graphOnly?: boolean;
}
/** Help content keyed by effective node kind. Covers every editor kind plus the
* graph-only IR kinds (merge lifecycle, branch groups, PR mode, recovery). */
const NODE_HELP: Record<string, NodeHelp> = {
// ── Editor (user-authored) kinds ──────────────────────────────────────────
start: {
title: "Start",
summary: "Marks where a task enters the workflow. Every workflow has exactly one start node.",
configure:
"Set the Entry column to choose which board column a task lands in when it enters (v2 workflows). Leave on Auto to use the first column.",
inputs: "None — this is the entry point.",
outputs: "Hands the task to the first downstream node.",
edges: "One outgoing edge (success). No incoming edges.",
},
end: {
title: "End",
summary: "A terminal state. A task that reaches an end node is finished on that path.",
inputs: "One or more incoming edges.",
outputs: "None — the task stops here.",
edges: "Incoming edges only; no outgoing edges.",
},
prompt: {
title: "Prompt (agent step)",
summary:
"Runs a unit of work against the task — an AI model, a named agent, a skill, or a CLI command. The workhorse node for executing, planning, and reviewing.",
configure:
"Write the Prompt, then pick an Executor (model, agent, skill, CLI, or CLI-agent) and its options (model, agent, skill, or command). Optionally set Gate mode (advisory vs blocking), Max retries, Auto-approve, or Wait for user input.",
inputs: "The task plus any prior step output and context.",
outputs: "The step's result, passed downstream; may record a gate verdict.",
edges: "success / failure outgoing edges. As a blocking gate it can stop the task on failure.",
},
script: {
title: "Script",
summary: "Runs a named project script (defined in project settings) as a workflow step.",
configure:
"Set Script name to a script from project settings. Set Gate mode to choose whether a non-zero exit blocks the task. The node prompt is passed to the script via FUSION_NODE_PROMPT.",
inputs: "The task; the node prompt via FUSION_NODE_PROMPT.",
outputs: "The script's exit status and output.",
edges: "success / failure.",
},
gate: {
title: "Gate",
summary:
"A decision checkpoint that evaluates a prompt and routes the task by its verdict, optionally blocking progress.",
configure:
"Write the gate Prompt. Set Gate mode to Advisory (records a verdict but never blocks) or Gate (blocks the task on failure).",
inputs: "The task plus prior context.",
outputs: "A pass/fail (or outcome) verdict.",
edges: "success / failure; a blocking gate holds the task on failure.",
},
merge: {
title: "Merge boundary",
summary:
"A marker separating pre-merge from post-merge steps. Steps before it run before the branch merges; steps after run after.",
configure: "No fields to set — placement is what matters. Position it where the merge happens in your pipeline.",
inputs: "The task after upstream steps complete.",
outputs: "Passes the task to post-merge steps.",
edges: "One outgoing edge (success).",
},
hold: {
title: "Hold",
summary:
"Pauses the task until a release condition is met — a manual promote, a timer, downstream capacity, a dependency, or an external event.",
configure:
"Pick a Release condition: Manual promote, Timer, Downstream capacity, Dependency complete, or External event.",
inputs: "The task arriving from upstream.",
outputs: "Releases the task downstream once the condition is satisfied.",
edges: "One outgoing edge (success), taken once released.",
},
split: {
title: "Split (parallel branch)",
summary:
"Fans the task out into multiple branches that run concurrently. Pair with a Join downstream to recombine them.",
configure: "No fields to set — connect multiple outgoing edges; each becomes a parallel branch.",
inputs: "A single task path.",
outputs: "Multiple concurrent branches.",
edges: "Multiple outgoing edges, one per branch. Recombine with a Join.",
},
join: {
title: "Join",
summary: "Waits for parallel branches (from a Split) and recombines them according to a join policy.",
configure:
"Set Join mode: All branches, Any branch, or Quorum (n) with a count. Set On branch failure to Collect (wait for all) or Fail-fast (cancel siblings).",
inputs: "Multiple parallel branches.",
outputs: "A single resumed path once the join policy is satisfied.",
edges: "One outgoing edge (success), taken when the join condition is met.",
},
foreach: {
title: "For-each",
summary:
"Runs a template of steps once per item (e.g. per parsed step), sequentially or in parallel. Renders as a group you drop step nodes into.",
configure:
"Set Mode (sequential/parallel), Isolation (shared or per-step worktree), Concurrency (parallel only), and Max rework cycles (the bound on rework loop-backs). Drop a step-execute node inside.",
inputs: "A collection of items (e.g. parsed steps) plus the task.",
outputs: "Aggregated per-item results.",
edges:
"success once all iterations finish. Internal rework edges loop back within a step instance, bounded by Max rework cycles.",
},
loop: {
title: "Loop",
summary:
"Repeats a template of steps until an exit condition is met or a cap is hit. Renders as a group you drop loop steps into.",
configure:
"Set the Exit condition (output contains / output matches regex) and its value or pattern, an optional Watch node id, Max iterations, and Timeout (ms).",
inputs: "The task plus the loop body steps.",
outputs: "The final iteration's result.",
edges: "One outgoing edge (success) on exit. Exits on condition match, max iterations, or timeout.",
},
// FNXC:WorkflowOptionalGroup 2026-06-21-11:30: An optional-group is a container whose body runs once when the task enables it and is skipped otherwise. Enable state is the per-task `enabledWorkflowSteps` facet, seeded from the group's `defaultOn`.
"optional-group": {
title: "Optional group",
summary:
"Holds a group of steps that run only when the task has this group enabled. Enabled tasks run the group's steps once at this position; disabled tasks pass straight through. Renders as a group you drop step nodes into.",
configure:
"Set the group Name and whether it is Enabled by default for new tasks (defaultOn). A task can override the default per-task. Drop the optional steps inside the region.",
inputs: "The task arriving from upstream, plus prior context.",
outputs: "The group's result when enabled; an unchanged pass-through when disabled.",
edges:
"success once the group finishes (or is skipped). A template failure inside an enabled group routes the group's failure edge.",
},
"step-review": {
title: "Step review",
summary:
"An AI review gate that emits a verdict (approve / revise / rethink / unavailable) used to route the task — typically back for rework or forward on approval.",
configure:
"Set Review type (plan or code) and an optional Review model. Route each outgoing edge by verdict; mark a loop-back edge as Rework.",
inputs: "The artifact or step output to review.",
outputs: "A verdict: approve, revise, rethink, or unavailable.",
edges:
"Verdict edges (outcome:approve / revise / rethink / unavailable). A rework edge loops back, bounded by Max rework cycles.",
},
"parse-steps": {
title: "Parse steps",
summary:
"Parses a task artifact (e.g. PROMPT.md) into discrete steps a downstream for-each can iterate over.",
configure: "Pick the Artifact to parse (e.g. PROMPT.md) and the Parser (e.g. step-headings, plus any plugin parsers).",
inputs: "A task artifact or document.",
outputs: "A list of parsed steps for a downstream for-each.",
edges: "success / failure.",
},
code: {
title: "Code",
summary:
"Runs a sandboxed TypeScript snippet as a workflow step — for lightweight transforms, routing, or computed values.",
configure: "Write the TypeScript Source and an optional Timeout (ms). Syntax is validated at save.",
inputs: "Task context available to the snippet.",
outputs: "The snippet's return value.",
edges: "success / failure.",
},
notify: {
title: "Notify",
summary:
"Emits a notification event (and optional title/message) without changing the task's path — for pings on state changes.",
configure:
"Pick an Event type (or a Custom event) and optional Title/Message. Templates may use {{taskTitle}}, {{taskId}}, {{workflowName}}, and {{context:key}}.",
inputs: "The task at this point in the flow.",
outputs: "A notification event; the task continues unchanged.",
edges: "One outgoing edge (success); the node is pass-through.",
},
// ── Graph-only (engine-managed) IR kinds ──────────────────────────────────
"merge-gate": {
title: "Auto-merge gate",
summary:
"Checks whether the task is ready to auto-merge: a live PR/merge entity exists, auto-merge is opted in, and the entity is merge-ready (approved, checks green, mergeable clean).",
configure: "Engine-managed checkpoint — not hand-edited. Governed by the project and task auto-merge settings.",
inputs: "An approved task with its PR/merge entity.",
outputs: "An auto-on / auto-off decision.",
edges:
"outcome:auto-on → branch-group member integration; auto-off → parks at the manual merge hold for a human.",
graphOnly: true,
},
"merge-attempt": {
title: "Merge attempt",
summary:
"Performs the actual merge of the task's branch toward the integration/default branch (squash by project default), with conflict and post-merge audit handling.",
configure: "Engine-managed — not hand-edited. Follows the project's merge strategy and audit settings.",
inputs: "A promotion-ready branch.",
outputs: "A merged branch, or a conflict requiring manual resolution.",
edges: "success → end; conflict/failure → manual merge hold.",
graphOnly: true,
},
"manual-merge-hold": {
title: "Manual merge hold",
summary:
"Parks the task in review for a human to merge when auto-merge is off or a step needs manual resolution. While auto-merge is off, in-review is terminal until a person merges.",
configure: "Engine-managed park state — not hand-edited.",
inputs: "A task blocked from auto-merge, or one with a merge conflict.",
outputs: "A human-resolved merge that resumes the flow.",
edges: "On manual resolution, loops back into integration/merge (rework).",
graphOnly: true,
},
"retry-backoff": {
title: "Retry backoff",
summary: "Waits a backoff interval before retrying a failed step, bounded by a retry budget.",
configure: "Engine-managed — not hand-edited.",
inputs: "A failed step eligible for retry.",
outputs: "A delayed retry of the step.",
edges: "Loops back to the step until the retry budget is exhausted.",
graphOnly: true,
},
"recovery-router": {
title: "Recovery router",
summary:
"A self-healing decision point that routes a stuck or interrupted task onto the right recovery path (retry, rebound, or escalate).",
configure: "Engine-managed — not hand-edited.",
inputs: "A task in an anomalous or interrupted state.",
outputs: "A recovery-route decision.",
edges: "Branches to retry, rebound, or manual paths by recovery outcome.",
graphOnly: true,
},
"branch-group-member-integration": {
title: "Branch group · member integration",
summary:
"For a task in a shared branch group, integrates this member's work onto the group's shared branch. A soft pre-integration step that runs even when global auto-merge is off (it only assembles the group branch).",
configure: "Engine-managed — not hand-edited. Active only for shared-branch-group members.",
inputs: "An approved group-member task and the group's shared branch.",
outputs: "The member's work landed on the shared branch.",
edges: "success → branch group promotion; manual-required → manual merge hold.",
graphOnly: true,
},
"branch-group-promotion": {
title: "Branch group · promotion",
summary:
"Once all members have landed on the shared branch, carries the complete group forward — merging the group branch toward the integration branch and creating-or-reusing the group's single managed PR. Idempotent: re-running never creates a second PR. Gated by group/global auto-merge.",
configure: "Engine-managed — not hand-edited. Runs once the group is complete and auto-merge is eligible.",
inputs: "A complete shared branch group (all members landed).",
outputs: "The group promoted toward the integration branch, plus its single managed PR.",
edges: "success → merge attempt; manual-required → manual merge hold.",
graphOnly: true,
},
"pr-create": {
title: "PR create",
summary: "Creates (or reuses) the pull request for the task in pull-request merge mode.",
configure: "Engine-managed — not hand-edited. Active in pull-request merge mode.",
inputs: "A task branch ready for review.",
outputs: "An open PR entity (created or reused).",
edges: "success → the PR review/merge path.",
graphOnly: true,
},
"pr-respond": {
title: "PR respond",
summary:
"Responds to PR review feedback — addressing comments and pushing follow-up commits — during the PR review cycle.",
configure: "Engine-managed — not hand-edited.",
inputs: "PR review comments and threads.",
outputs: "Replies and follow-up commits on the PR.",
edges: "Loops within the PR review cycle until feedback is resolved.",
graphOnly: true,
},
"pr-merge": {
title: "PR merge",
summary: "Merges the pull request once it is approved and all checks pass, in pull-request mode.",
configure: "Engine-managed — not hand-edited. Governed by auto-merge readiness.",
inputs: "An approved, green PR.",
outputs: "A merged PR.",
edges: "success → end; blocked → manual merge hold.",
graphOnly: true,
},
};
/** Resolve help for a node by its effective kind, or null when none is known
* (callers skip rendering the Help section). */
export function nodeHelpFor(kind: WorkflowEditorNodeKind | string): NodeHelp | null {
return NODE_HELP[kind] ?? null;
}
/** Resolve help for a flow node, honoring the preserved IR kind. */
export function nodeHelpForData(data: WorkflowFlowNodeData): NodeHelp | null {
return nodeHelpFor(effectiveNodeKind(data));
}