From 236e89989f20f909354e0d337439395d91cfdecb Mon Sep 17 00:00:00 2001 From: mrhid6 Date: Mon, 20 Jul 2026 14:30:55 +0100 Subject: [PATCH] docs: add workflow builder v2 spec --- .../2026-07-20-workflow-builder-v2-design.md | 150 ++++++++++++++++++ 1 file changed, 150 insertions(+) create mode 100644 docs/superpowers/specs/2026-07-20-workflow-builder-v2-design.md diff --git a/docs/superpowers/specs/2026-07-20-workflow-builder-v2-design.md b/docs/superpowers/specs/2026-07-20-workflow-builder-v2-design.md new file mode 100644 index 0000000..1427f49 --- /dev/null +++ b/docs/superpowers/specs/2026-07-20-workflow-builder-v2-design.md @@ -0,0 +1,150 @@ +# Workflow Builder v2 — Design + +**Date:** 2026-07-20 +**Status:** Approved (design) — ready for implementation planning +**Scope:** Overhaul the workflow builder UI to match the approved mockup, add drag-and-drop (library→canvas + reorder), base-step editing/deletion with cascade, step input parameters, an Edit-Workflow modal, runs navigation, and fix the save crash. Enhancement to the merged Server Workflows feature. Independent of the in-flight log-streaming work. + +--- + +## 1. Summary + +The shipped builder diverges from the approved mockup and is missing interactions. This iteration: + +1. **Restyle** the builder (`/workflows/[id]`) to the approved mockup: dotted-grid canvas, 340px node cards with index badge + shell badge + status, wire connectors with dashed-amber "passes" env chips, a kicker/field inspector, and a library of grabbable step cards with descriptions. +2. **Drag-and-drop**: drag a library step onto the canvas to add it; drag nodes to reorder. Remove the up/down/remove buttons. +3. **Base-step editing**: an "Edit base step" modal edits the shared library step (name/interpreter/script/outputs/inputs/secret refs) and **saves**; deleting a shared step **cascades** — it is pulled from every workflow that references it. +4. **Input parameters**: a base step can declare inputs (`name` + `default` + `description`); when placed, each placement sets values; the runner injects them into the step's environment. +5. **Env visibility**: show the output variables passed between steps as chips on the wires (already partially present — align to the mockup). +6. **Edit-Workflow modal**: edit name, target servers, delete the workflow, and other workflow settings. +7. **Runs navigation**: a runs list page per workflow, linked from the builder and the workflows list. +8. **Bug fix**: `updateWorkflow` returns `{updated:true}`, which the builder stores as the workflow and then crashes on `[...wf.steps]` ("d.steps is not iterable"). Fix the endpoint to return the updated workflow and harden the client. + +--- + +## 2. Locked decisions + +| Topic | Decision | +|-------|----------| +| Visual target | The approved mockup (artifact `61ab5256`). Adopt its layout + **amber (`#f5a524`) as the builder signal/focus color**, plus bash-green (`#3fb950`) / pwsh-blue (`#5b9bff`) badge colors. Keep the app's existing `surface`/`border`/`text-*` tokens for panels so it integrates with the dark theme. | +| Drag-and-drop | Native HTML5 DnD. Library cards are `draggable`; the canvas has drop targets (between nodes + end zone) to insert; nodes are `draggable` to reorder. Clicking a library card still appends (keyboard/fallback). | +| Step removal | No per-node buttons. Remove a placed step from the **inspector** ("Remove from workflow"). | +| Input params | `WorkflowStep.declared_inputs: [{name, default, description}]`. `WorkflowStepRef.inputs: map[name]value`. Runner resolves `value = ref.inputs[name] ?? default` and injects as env vars. | +| Edit scope | Inspector script edit = **per-placement override** (existing `overrides`, "forks a local copy"). A separate **Edit base step** modal updates the shared library step for all workflows. | +| Cascade delete | Deleting a library step pulls its `step_id` from every `workflow.steps` and re-sequences remaining `order`s. | +| Env chips | Names passed between steps = union of prior steps' `declared_outputs`. Shown on the wire between nodes. | +| Edit workflow | Modal launched from the topbar: name, target-servers multiselect, delete workflow. | +| Runs nav | New page `/workflows/[id]/runs` (list); links from the builder topbar and the workflows list page. | +| Save fix | `PUT /api/workflows/:id` returns the full updated `Workflow`. Client also guards against non-workflow responses. | +| Proto | **No proto change** — input params travel through the existing `RunStepCmd.Env`. | + +--- + +## 3. Data model (`server/internal/models/workflow.go`) + +Add an input-parameter type and fields: + +```go +type InputParam struct { + Name string `bson:"name" json:"name"` + Default string `bson:"default" json:"default"` + Description string `bson:"description" json:"description"` +} +``` + +- `WorkflowStep` gains: `DeclaredInputs []InputParam `bson:"declared_inputs" json:"declared_inputs"``. +- `WorkflowStepRef` gains: `Inputs map[string]string `bson:"inputs,omitempty" json:"inputs,omitempty"`` (per-placement values). +- `ResolvedStep` gains: `Inputs map[string]string `bson:"inputs" json:"inputs"`` (frozen resolved input env for the run). + +`declared_inputs` defaults to `[]` on create (like `declared_outputs`). + +--- + +## 4. Services + +### 4.1 Step CRUD (`server/internal/services/workflows.go`) + +- `CreateStep`: default `DeclaredInputs` to `[]InputParam{}` when nil; persist it. +- `UpdateStep`: add `declared_inputs` to the `$set`. +- `DeleteStep` → **cascade**. New behavior: within the delete, also update every workflow that references the step: + 1. `DeleteOne` on `workflow_steps` by `step_id` (as today). + 2. Load all workflows containing the step (`workflows` where `steps.step_id == stepID`); for each, remove the matching `WorkflowStepRef`(s), re-sequence remaining `order` values to `0..n-1`, and `UpdateWorkflow`. + Keep it a single service call `DeleteStep(stepID)` so the handler is unchanged. Audit both the step deletion and each affected workflow via `LogEvent`. + +### 4.2 Workflow update returns the workflow (`server/internal/services/workflows.go` + handler) + +- `UpdateWorkflow(id, w)` stays, but the **handler** `updateWorkflow` re-fetches and returns the full workflow: after `services.UpdateWorkflow`, call `services.GetWorkflow(id)` and return it (200 with the `Workflow` JSON) instead of `{"updated": true}`. This is the crash fix's server half. + +### 4.3 Runner input injection (`server/internal/services/workflow_runner.go`) + +- `resolveSteps`: when freezing each `ResolvedStep`, compute `Inputs`: for each `InputParam` on the library step, `value = ref.Inputs[name]` if present else `param.Default`; store the resulting `map[string]string` on `ResolvedStep.Inputs`. +- `runServer`: when building `cmdEnv`, merge `step.Inputs` **first** (base layer), then `runEnv` (upstream outputs), then `secretVals` (highest precedence). Input values are not secrets and are not masked (they are user-provided config, not secret material) — unless a value coincidentally equals a secret literal, existing masking still catches it in logs. + +--- + +## 5. REST API + +No new endpoints. Changes: +- `updateWorkflow` handler returns the `Workflow` (see §4.2). +- `deleteStep` handler unchanged (cascade lives in the service). +- Existing `GET /api/workflows/:id/runs`, `GET /api/runs/:runId` power the runs pages. +- `createStep`/`updateStep` accept `declared_inputs` via the existing `ShouldBindJSON(&models.WorkflowStep)` (no handler change once the model has the field). + +--- + +## 6. Frontend + +### 6.1 Tokens + primitives + +- `tailwind.config.ts`: add `bash: "#3fb950"`, `pwsh: "#5b9bff"`. Amber signal reuses a new `signal: "#f5a524"` token (add it) for the builder's focus ring / env chips / run button; `signal-ink: "#241800"` for text on amber chips. +- Add a `Modal` primitive to `components/ui` (overlay + centered panel, `onClose`, ESC + backdrop click, `title`, children, exported from `index.ts`). Used by the Edit-base-step and Edit-workflow modals. + +### 6.2 API client (`web/lib/api.ts`) + +- `WorkflowStep`: add `declared_inputs: InputParam[]`. +- New `InputParam { name: string; default: string; description: string }`. +- `WorkflowStepRef`: add `inputs?: Record`. +- `updateWorkflow` return type stays `Workflow` (now actually returns one). Add a client guard: if a mutation is expected to return a workflow but the body lacks `steps`, treat it as an error / refetch. +- No other method changes; `deleteStep`, `updateStep`, `createStep`, `listRuns`, `getRun` already exist. + +### 6.3 Builder restyle + DnD (`web/app/workflows/[id]/page.tsx`) + +Rebuild the three-pane builder to the mockup: + +- **Topbar:** brand dot + `Workflows /` crumb + workflow name + `· draft · edited …`; right side a **Targets** chip (read-only summary, e.g. "3 servers"), a **Runs** link (→ `/workflows/[id]/runs`), an **Edit** button (opens Edit-workflow modal), **Save**, **Run workflow** (amber). Remove the inline name input and the in-canvas Target-servers card (both move into the Edit-workflow modal). +- **Library (left):** section header with `+` (opens Edit-base-step modal in "new" mode), a search box that filters by name, grouped `Shared · Bash` / `Shared · PowerShell`, each a `.step-card` with shell badge, name, description, and a grip glyph; `draggable`. Clicking still appends. Each card has an edit affordance (pencil / context) that opens the Edit-base-step modal for that step. +- **Canvas (center):** dotted-grid background. Render placed steps as 340px node cards: index badge, title, shell badge, (build-time) no status pill — status is a run concern; in the builder show the interpreter badge only. Node body shows a syntax-lite script preview. Between nodes render a wire + a dashed-amber `passes` chip row listing the union of prior `declared_outputs` (names only in the builder). Nodes are `draggable` to reorder; drop targets sit on the wires and at the end ("+ Drop a step here"). Selecting a node opens it in the inspector. +- **Inspector (right):** kicker "Step N · Inspector", title with shell badge + name. Fields: **Step name** (edits the placement label? — placements don't have a name; show the library name read-only, edit happens in the base-step modal), **Command** (script `