Mosaic one model, many lenses

ADR 0034 — Interactive workflow canvas

This ADR closes the "partial" row in the sync/ee.md feature map for graph/diagram editors: EE's ee-flow is a React-Flow–parity canvas+DOM hybrid (drag, zoom, undo/redo, minimap, auto-layout, context menus, collab transport), and Mosaic's U4 answer was a read-only workflow DAG — static longest-path layering at codegen rendered as SVG. This ADR makes that graph an interactive canvas and, where Mosaic's compile-time model lets it do more for free, does: a live run tracer bound to the L7 streamed run endpoint and the L5 run history.

Context

EE workflows are runtime artifacts: the WorkflowGraphEditor island (blocks/workflow/ee-workflow-ui) wraps ee_flow::FlowCanvas to author workflows visually — node palette, connections, a property panel, and save/load against the Workflow aggregate. The canvas edits the model.

Mosaic workflows are declared in the tessera (tessera.yaml, the single source of truth, ADR 0008) and compiled into the app at build time. A visual DSL editor would be a parallel authoring path — a second source of truth for workflows — against this repo's core doctrine. What the declared model does have that EE's authoring canvas does not bind to: real runtime data. Executions stream node_start / node_output / done over POST /api/workflows/{slug}/run?stream=true (L7) and every top-level run is kept in the run history (GET /api/workflows/{slug}/runs[/{id}], L5) with a full per-node trace.

So the Mosaic canvas is the interactive viewer/tracer for the declared model: every interactive affordance of ee-flow that makes sense for a fixed graph (pan/zoom, node drag, undo/redo, auto-layout, minimap, inspection) plus live execution binding on top.

Decision

Every workflow page (/workflows/{slug}) now emits its DAG as before — the codegen-time layout is still the static SVG, so the page is fully readable with no JS — but wrapped in a canvas container:

  • .wf-canvas container — carries the declared graph as a data-wf payload (nodes with their full detail for the inspector — title, about, code, input/output fields, params, exec block, free-form props — edges with conditions, and the codegen layout as initial positions). The container is enhanced in place by web/static/wf_canvas.js, a single static script (served exactly like dispatch.js/layout.js) that upgrades every .wf-canvas in the document and a MutationObserver that catches ones rendered later (CSR route navigation). The page loads the script idempotently through a generated mount_wf_canvas_js() (wasm-only, no-op in SSR).
  • Pan / zoom — drag the background to pan, wheel to zoom at the cursor (clamped 0.2×–4×), toolbar +/− and fit-to-view (0), double-click to fit. A dot-grid background pans and zooms with the world (SVG pattern — one DOM tree, no separate canvas layer).
  • Node drag — nodes reposition freely; positions persist per workflow in localStorage (mosaic-wf-<slug>) and survive reloads. Edges re-route live (same bezier + arrow as the static render).
  • Undo / redo — an operation stack over layout actions (drag, auto-layout, reset), with Ctrl+Z / Ctrl+Shift+Z / Ctrl+Y and toolbar buttons.
  • Auto-layout — the same longest-path layering the codegen computes (layers as columns, declaration order within a layer, cycle guard), ported to the client as a one-click re-arrange; "reset" returns to the build-time layout.
  • Minimap — corner overview with a viewport rectangle; click/drag to recenter.
  • Inspector — click a node for its id, type, about, run state, last output, code, inputs, outputs, params, props and exec config; click an edge for its endpoints and condition; Esc or a background click closes it.
  • Live run tracing (the "more and better" part) — a run panel with one input per start-node variable (typed: numeric/bool coercion, defaults, required) or a raw JSON body editor when the workflow takes no declared inputs. "run" POSTs the streamed run endpoint and parses the SSE frames from the fetch body reader (POST + SSE; EventSource cannot POST). While running, each node gets a state ring — amber pulse while running, blue when done, red on failure (the last started-but-unresolved node) — and per-node outputs land in the inspector. The panel also lists the ten most recent runs from the run history (✓/✗, age, duration); clicking one replays its trace onto the canvas.
  • Keyboard — +/-/0 zoom/fit, undo/redo, Esc; ignored while an input has focus.

No DSL key: like the static DAG before it, the canvas is part of the built-in workflow surface and is emitted whenever the app declares workflows. The tessera stays the only workflow definition; nothing the canvas does writes back to the model. The WSS voice and run APIs are unchanged — the canvas is a client over existing endpoints.

Files

  • crates/mosaic-render/src/wf_canvas.rs (new) — wf_canvas_js() (the static enhancer) and canvas_payload() (the per-workflow data-wf JSON from WfPlan + the codegen layout).
  • crates/mosaic-render/src/web.rs — workflow-page codegen (canvas container, toolbar, inspector, minimap, run panel, the data-wf payload const, mount_wf_canvas_js), the canvas CSS block, and the workflow_page_is_an_interactive_canvas render test.
  • Re-pinned goldens: full-stack web/src/pages.rs + new web/static/ wf_canvas.js; styles.css in the four web lenses.

Consequences

  • The workflow page goes from a static diagram to an interactive canvas with zero backend changes: every feature (pan/zoom/drag/undo/minimap/inspector) works offline against the declared model, and the run panel works against the existing L5/L7 endpoints.
  • The no-JS path is preserved: the SSR HTML is still the complete read-only SVG (longest-path layout, tooltips, conditions on hover).
  • Layout edits are per-browser (localStorage) and session-visual — they are never a definition change, so no second source of truth for workflows is introduced. A future "persist layout app-wide" would be a one-endpoint extension, not a model change.
  • The generated page grows a ~15 KB static JS file (one per app, emitted only when workflows exist) plus per-workflow payload consts.
  • The canvas deliberately does not port ee-flow's authoring features (node/edge creation, property editing, save-to-aggregate) or its collab transport: authoring the tessera is the tessera's job, and Mosaic's run data replaces the value a second authoring surface would add.