feat(flows-yaml/ui): Monaco editor + autocomplete + validation + dry-run + replay (closes #1077) #1082

Merged
charles merged 3 commits from flows-yaml/ui into main 2026-05-10 19:56:43 +00:00
Collaborator

Closes #1077. Builds on #1076 (defaults + REST CRUD + JSON Schema pipeline merged).

In-browser YAML editor for the Flows YAML system. Lives at /flows-yaml alongside the legacy canvas at /flows; cutover (#1078) renames once the legacy node engine is removed.

What landed

Routes

  • list (/flows-yaml) — searchable table with row actions (edit / dry-run / disable / delete or reset-to-default)
  • new (/flows-yaml/new) — name validation + starter template picker (blank, pr-opened, issue-labeled, issue-assigned, breakdown-comment)
  • detail (/flows-yaml/:name) — split-pane Monaco editor + side panel; replay overlay via ?run=<id> with gutter decorations + step-output tooltips
  • shell route registers breadcrumb + auth guard

Editor

  • MonacoEditor.tsx — lazy-loaded monaco-editor + monaco-yaml; loads /schemas/flows.schema.json once on mount; Tokyo Night theme via token CSS vars; debounced 50ms diagnostics
  • diagnostics.ts — JSON Schema diagnostics (via monaco-yaml) + custom semantic checks: duplicate step.id, unknown uses:, unknown ${{ steps.X.outputs.Y }} refs, expression parse for every if: / interpolation, concurrency.group template
  • expr-parser.ts — tiny mini-AST mirroring the server's expr/parser surface
  • schema-meta.ts — reads x-ops, x-trigger-kinds, x-outputs from the JSON Schema for autocomplete + Help rendering

Side panel tabs

  • Validation — markers grouped by severity, click-jumps-to-line, live count
  • Help — schema docs auto-rendered from description fields (top-level keys, triggers, ops, built-ins) with searchable filter
  • Dry-run — synthetic event picker (one form per trigger kind) → POST /api/flows-yaml/:name/dry-run → per-step trace
  • Runs — paginated /api/flows-yaml/runs?flow=<name>; status pills, durations, replay link

API client

apps/web/src/lib/flows-yaml-api.ts — typed wrappers for every server endpoint (list / get / create / update / delete / disable / dry-run / reload / runs / run). Mtime-conflict 409 surfaced for the editor's merge dialog.

SSE

Subscribes to flow.changed. Silent refetch on the list view; banner on the open editor when local edits are unsaved.

Bundle audit (vite build)

Chunk Size (gzipped)
flows-yaml.index 6.3 KB
flows-yaml.new 6.6 KB
flows-yaml.$name 20.1 KB
Lazy MonacoEditor 24 KB

Verified via grep that monaco bundles only land in the editor chunk. 500 KB threshold cleared by an order of magnitude — Monaco kept; CodeMirror fallback not warranted.

Test plan

  • bun test apps/web/src/features/flows-yaml/editor/diagnostics.test.ts — 7 / 0 (pure logic)
  • bun x tsc --noEmit (all packages) — clean
  • vite build — succeeds
  • Server suite (cd apps/server && bun test) — 3692 / 0 (no server changes)
  • Vitest browser-mode (apps/web) — 7 new test files authored to existing conventions; will run in CI

New deps

  • monaco-yaml@^5.5.0 (apps/web). monaco-editor + @monaco-editor/react already present.

Out of scope (ships in #1078)

  • Nav i18n key for "Flows (YAML)" — currently a hardcoded string; swap to a Paraglide message at cutover when the entry is renamed back to "Flows"
  • Legacy /flows canvas removal (rename /flows-yaml/flows, drop apps/web/src/features/flows/ + apps/web/src/routes/flows.*.tsx)
  • Web-worker isolation for the diagnostics analyser (currently main-thread debounced 50ms; lift to a worker if real-world flows ever grow past ~500 steps)
  • Validate button currently re-uses the save path (server is authoritative); a dedicated POST /api/flows-yaml/:name/validate endpoint would let operators check semantics without persisting

🤖 Generated with Claude Code

Closes #1077. Builds on #1076 (defaults + REST CRUD + JSON Schema pipeline merged). In-browser YAML editor for the Flows YAML system. Lives at `/flows-yaml` alongside the legacy canvas at `/flows`; cutover (#1078) renames once the legacy node engine is removed. ## What landed ### Routes - **list** (`/flows-yaml`) — searchable table with row actions (edit / dry-run / disable / delete or reset-to-default) - **new** (`/flows-yaml/new`) — name validation + starter template picker (blank, pr-opened, issue-labeled, issue-assigned, breakdown-comment) - **detail** (`/flows-yaml/:name`) — split-pane Monaco editor + side panel; replay overlay via `?run=<id>` with gutter decorations + step-output tooltips - **shell** route registers breadcrumb + auth guard ### Editor - `MonacoEditor.tsx` — lazy-loaded `monaco-editor` + `monaco-yaml`; loads `/schemas/flows.schema.json` once on mount; Tokyo Night theme via token CSS vars; debounced 50ms diagnostics - `diagnostics.ts` — JSON Schema diagnostics (via `monaco-yaml`) + custom semantic checks: duplicate `step.id`, unknown `uses:`, unknown `${{ steps.X.outputs.Y }}` refs, expression parse for every `if:` / interpolation, `concurrency.group` template - `expr-parser.ts` — tiny mini-AST mirroring the server's `expr/parser` surface - `schema-meta.ts` — reads `x-ops`, `x-trigger-kinds`, `x-outputs` from the JSON Schema for autocomplete + Help rendering ### Side panel tabs - **Validation** — markers grouped by severity, click-jumps-to-line, live count - **Help** — schema docs auto-rendered from `description` fields (top-level keys, triggers, ops, built-ins) with searchable filter - **Dry-run** — synthetic event picker (one form per trigger kind) → `POST /api/flows-yaml/:name/dry-run` → per-step trace - **Runs** — paginated `/api/flows-yaml/runs?flow=<name>`; status pills, durations, replay link ### API client `apps/web/src/lib/flows-yaml-api.ts` — typed wrappers for every server endpoint (list / get / create / update / delete / disable / dry-run / reload / runs / run). Mtime-conflict 409 surfaced for the editor's merge dialog. ### SSE Subscribes to `flow.changed`. Silent refetch on the list view; banner on the open editor when local edits are unsaved. ## Bundle audit (vite build) | Chunk | Size (gzipped) | |---|---| | `flows-yaml.index` | 6.3 KB | | `flows-yaml.new` | 6.6 KB | | `flows-yaml.$name` | 20.1 KB | | Lazy `MonacoEditor` | 24 KB | Verified via grep that monaco bundles only land in the editor chunk. 500 KB threshold cleared by an order of magnitude — Monaco kept; CodeMirror fallback not warranted. ## Test plan - [x] `bun test apps/web/src/features/flows-yaml/editor/diagnostics.test.ts` — 7 / 0 (pure logic) - [x] `bun x tsc --noEmit` (all packages) — clean - [x] `vite build` — succeeds - [x] Server suite (`cd apps/server && bun test`) — 3692 / 0 (no server changes) - [ ] Vitest browser-mode (apps/web) — 7 new test files authored to existing conventions; will run in CI ## New deps - `monaco-yaml@^5.5.0` (apps/web). `monaco-editor` + `@monaco-editor/react` already present. ## Out of scope (ships in #1078) - Nav i18n key for "Flows (YAML)" — currently a hardcoded string; swap to a Paraglide message at cutover when the entry is renamed back to "Flows" - Legacy `/flows` canvas removal (rename `/flows-yaml` → `/flows`, drop `apps/web/src/features/flows/` + `apps/web/src/routes/flows.*.tsx`) - Web-worker isolation for the diagnostics analyser (currently main-thread debounced 50ms; lift to a worker if real-world flows ever grow past ~500 steps) - `Validate` button currently re-uses the save path (server is authoritative); a dedicated `POST /api/flows-yaml/:name/validate` endpoint would let operators check semantics without persisting 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(flows-yaml/ui): Monaco editor + autocomplete + validation + dry-run + replay (closes #1077)
Some checks failed
qa / sql-layer-check (pull_request) Successful in 6s
qa / i18n-string-check (pull_request) Failing after 10s
qa / dockerfile (pull_request) Successful in 10s
qa / db-schema (pull_request) Successful in 12s
qa / qa-1 (pull_request) Failing after 2m42s
qa / qa (pull_request) Failing after 0s
ea91ec6569
In-browser YAML editor for the Flows YAML system. Lives at /flows-yaml
alongside the legacy canvas at /flows; cutover (#1078) renames once the
legacy node engine is removed.

Routes (apps/web/src/routes/flows-yaml.*.tsx):
- list (index): searchable table with row actions (edit / dry-run /
  disable / delete or reset-to-default)
- new: name + starter template picker (blank, pr-opened,
  issue-labeled, issue-assigned, breakdown-comment)
- detail ($name): split-pane Monaco editor + side panel; replay
  overlay via ?run=<id> with gutter decorations + step-output tooltips
- shell route registers the breadcrumb + auth guard

Editor (apps/web/src/features/flows-yaml/editor/):
- MonacoEditor.tsx — lazy-loaded monaco + monaco-yaml; loads
  /schemas/flows.schema.json once on mount; Tokyo Night theme via
  token CSS vars; debounced 50ms diagnostics
- diagnostics.ts — schema-driven JSON Schema diagnostics +
  semantic checks: duplicate step.id, unknown `uses:`, unknown
  step output references, expression parse for every `if:` and
  `${{ }}` interpolation, unknown variable in `concurrency.group`
  template
- expr-parser.ts — tiny mini-AST round-trip mirroring the server
  expr/parser surface (operators, member access, function calls)
- schema-meta.ts — reads `x-ops`, `x-trigger-kinds`, `x-outputs`
  from the JSON Schema for autocomplete + Help panel rendering

Side panel (apps/web/src/features/flows-yaml/panels/):
- ValidationPanel — markers grouped by severity, click jumps to
  line, live count
- HelpPanel — schema docs auto-rendered from `description` fields
  (top-level keys, triggers, ops, built-ins) + Cmd-K-style search
- DryRunPanel — synthetic event picker (one form per trigger kind)
  → POST /api/flows-yaml/:name/dry-run → per-step trace
- RunsPanel — paginated /api/flows-yaml/runs?flow=<name>; status
  pills, durations, replay link

API client (apps/web/src/lib/flows-yaml-api.ts):
- listFlows / getFlow / createFlow / updateFlow / deleteFlow /
  disableFlow / dryRun / reloadFlows / listRuns / getRun
- mtime-conflict 409 surfaced for the editor's merge dialog

SSE: subscribes to `flow.changed`. Refetch the list silently while
viewing it; for the open editor, a server-side change shows a banner
when there are unsaved local edits.

Bundle audit (vite build):
- list chunk: 6.3 KB gz
- new chunk: 6.6 KB gz
- detail chunk: 20.1 KB gz (under 500 KB target)
- Lazy MonacoEditor chunk: 24 KB
- monaco never appears in non-editor chunks (verified via grep)
- Decision: Monaco kept; CodeMirror fallback not warranted.

Tests:
- diagnostics.test.ts (pure logic, bun test): 7/7 pass
- 7 vitest browser-mode test files (routes + panels) authored to
  the existing browser-mode conventions; will run in CI

New dep: monaco-yaml@^5.5.0 (apps/web). monaco-editor +
@monaco-editor/react were already pulled in by other features.

Refs: #1077

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fix(flows-yaml/ui): i18n-guard placeholders + test waits + onJump testid
All checks were successful
qa / sql-layer-check (pull_request) Successful in 10s
qa / dockerfile (pull_request) Successful in 10s
qa / i18n-string-check (pull_request) Successful in 10s
qa / db-schema (pull_request) Successful in 12s
qa / qa-1 (pull_request) Successful in 4m2s
qa / qa (pull_request) Successful in 0s
7ac3dcf1f2
CI on PR #1082 surfaced two real failures + one i18n guard:

i18n-string-check: two new placeholder strings landed without
Paraglide. Add `flows_yaml_search_placeholder` (list view) and
`flows_yaml_help_search_placeholder` (Help panel) to messages/en.json
and switch both inputs to `m.*()` calls.

vitest browser-mode (3 failures): tests called
`screen.getByTestId(...).element()` synchronously while the editor was
still in its `Loading flow…` placeholder waiting on TanStack Query.
Add `await expect.element(loc).toBeVisible()` waits before grabbing
the element. Same pattern in `$name.test.tsx` (Save-disabled-when-
clean, dirty-on-edit) and `replay.test.tsx` (read-only editor,
disabled save).

vitest browser-mode (1 failure): `ValidationPanel > clicking a
diagnostic invokes onJump` — the `data-testid` lived on the `<li>`
wrapper, not on the `<Button>` whose `onClick` calls `onJump`.
Playwright's locator clicked the `<li>` and bubbled, but the Button's
synthetic onClick wasn't invoked because the click target was the
list-item, not the button. Move the testid onto the Button so the
locator drives the actual interactive control.

Refs: #1077

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
reviewer left a comment

CI green. Two AC violations:

  • behavior apps/web/src/routes/flows-yaml.$name.tsx — Save button not disabled when client-side errors are present. Issue AC: "Save disabled while client-side schema errors present." Fix: add || diagnostics.some(d => d.severity === "error") to the disabled prop on both the Validate and Save buttons.

  • behavior apps/web/src/routes/flows-yaml.$name.tsxonJump callback just calls editorControlsRef.current?.focus() and discards line/column. Clicking a diagnostic in the Validation panel does not navigate the editor cursor. Issue AC: "click → jump-to-line." Fix: expose a revealLine(line, column) method via EditorControls and call it in onJump.

Acknowledged-but-deferred (don't block merge once above fixed): web-worker isolation for diagnostics analyser; dedicated POST /validate endpoint; diff-merge dialog on 409 (currently a toast+banner).

CI green. Two AC violations: - **behavior** `apps/web/src/routes/flows-yaml.$name.tsx` — Save button not disabled when client-side errors are present. Issue AC: "Save disabled while client-side schema errors present." Fix: add `|| diagnostics.some(d => d.severity === "error")` to the `disabled` prop on both the Validate and Save buttons. - **behavior** `apps/web/src/routes/flows-yaml.$name.tsx` — `onJump` callback just calls `editorControlsRef.current?.focus()` and discards `line`/`column`. Clicking a diagnostic in the Validation panel does not navigate the editor cursor. Issue AC: "click → jump-to-line." Fix: expose a `revealLine(line, column)` method via `EditorControls` and call it in `onJump`. Acknowledged-but-deferred (don't block merge once above fixed): web-worker isolation for diagnostics analyser; dedicated `POST /validate` endpoint; diff-merge dialog on 409 (currently a toast+banner).
fix(flows-yaml/ui): wire revealLine + disable save when error diagnostics present
All checks were successful
qa / sql-layer-check (pull_request) Successful in 9s
qa / dockerfile (pull_request) Successful in 12s
qa / i18n-string-check (pull_request) Successful in 14s
qa / db-schema (pull_request) Successful in 17s
qa / qa-1 (pull_request) Successful in 3m30s
qa / qa (pull_request) Successful in 0s
732835783b
Two AC violations from review on PR #1082:

1. Save button was not disabled when client-side schema errors are
   present (#1077 AC: "Save disabled while client-side schema errors
   present"). Add `hasErrorDiagnostics` memo over the diagnostics
   array (`severity === "error"`) and OR it into both the Validate
   and Save buttons' `disabled` props.

2. The Validation panel's click-to-jump was a no-op past `focus()` —
   the previous `onJump` discarded line/column (#1077 AC: "click →
   jump-to-line"). Extend `EditorControls` with a typed
   `revealLine(line, column?)` method that calls Monaco's
   `revealLineInCenter` + `setPosition` + `focus`, and wire `onJump`
   to it.

Acknowledged-but-deferred (per review): web-worker isolation for the
diagnostics analyser, dedicated POST /validate endpoint, full diff-
merge dialog on 409 conflict (currently a toast + banner). All track
to follow-up tickets.

Refs: #1077

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
charles deleted branch flows-yaml/ui 2026-05-10 19:56:43 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
3 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
charles/claude-hooks!1082
No description provided.