The Iterion editor is a React/Vite SPA embedded in the Go binary
(pkg/server/). It originally hosted a single concern: authoring .bot
workflows visually — drag-and-drop on a ReactFlow canvas, inspector
panels for declarations, validation against the IR compiler, save/load
through a REST file API.
When the run console UI was added (Phase 1 = pkg/runview backend +
REST endpoints; Phase 2 = WebSocket transport + frontend RunView), a
design choice surfaced:
Should the live execution view be integrated into the studio (one canvas, one page, with a
mode: edit | observetoggle), or should it live as a separate route/page with its own canvas, store, and transport layer?
The separation was retained. This ADR documents the reasoning so future contributors understand why fusing the two views — a tempting simplification — would be a step backwards.
useDocumentStore.EditorView) showed that mixing edit and observe semantics on a
single ReactFlow canvas leads to ambiguous interaction (does a click
select for editing, or for inspecting an artifact at sequence N?).The run console is implemented as a separate page in the same SPA:
/ → EditorView (author mode)/runs/new?file=<path> → LaunchView (launch form)/runs/:id → RunView (observer mode)/runs → RunListView (history)useDocumentStore — AST, IR, dirty flag, undo/redo (editor only)useRunStore — events, snapshots, execution state, WS connection
(run view only)useRunStore.reset() is
called when navigating away from RunView./api/files/open, /api/validate,
/api/files/save) — request/response, on-demand, no streaming./api/runs/{id}) plus a persistent
WebSocket (/api/ws/runs/{id}) that streams events with
seq-based replay and ring-buffered logs.Canvas
(studio/src/components/Canvas/Canvas.tsx)
with full ReactFlow interactivity.RunCanvasIR
(studio/src/components/Runs/RunCanvasIR.tsx),
a distinct read-only component that
paints execution state (running, succeeded, failed, paused) onto
the IR graph and supports time-travel scrubbing — but no mutations.setLocation('/runs/new?file=…'); after run creation,
the LaunchView navigates to /runs/{run_id}. Returning to the
editor uses setLocation('/'). Deep-links carry context
(?from={runId}, ?node={irNodeId}) so the user can move between
the two without losing thread.mode: edit | observe toggleOne canvas, one route, one store. A toggle (or implicit detection based on the URL) switches between editing the document and observing a run.
Rejected because:
The studio and the run console rendered in two panes of the same page, both alive at once.
Rejected because:
Open the run view as a slide-over panel above the studio.
Rejected because:
RunView must, since
operators frequently bookmark /runs/{id} to follow long runs.Editing structure (AST mutations, validation, save/load) and visualizing execution state (event streams, scrubbing, logs) are genuinely different domains. They share no business logic, they have no overlapping invariants, and they evolve independently.
| Aspect | Editor | RunView |
|---|---|---|
| Source of truth | AST + IR | Sequenced event stream |
| Refresh trigger | User action (validate/save) | WebSocket push (~1–10 ms) |
| Persistence model | File save via REST | Append-only events.jsonl (read-only) |
| Mutation surface | Full (CRUD on nodes/edges) | None |
| Memory pressure | Bounded by file size | Bounded by ring buffer + virtualization |
A unified state container would have to special-case nearly every operation. Two stores stay simple.
Because useDocumentStore and useRunStore are physically separate
Zustand instances, no editor action can ever mutate run state and
vice-versa. This is a load-bearing guarantee: a run is pinned to a
specific workflow hash, and accidental edits during observation would
break that contract. Separation enforces it at the type level.
In EditorView, clicking a node means “select for inspection/edit”.
In RunView, clicking a node means “show me this node’s artifacts at
the current sequence”. Sharing a canvas would force every handler to
branch on mode. Two canvases keep each interaction model crisp.
Navigation friction is minimal because the studio and the run view exchange contextual deep-links:
/runs/new?file=…/?file=…&node=…&from={runId}?from={runId} query param triggers a banner in the studio
(“Coming from run X — see in console”) so the user keeps thread.RunView ships dedicated optimizations — virtualized event log, ring
buffer for logs, throttled WebSocket batching — without bloating the
editor bundle or complicating its render path. The studio remains lean
and focused.
A user cannot tweak a .bot file while observing a running execution
of it. This is intentional: the run is pinned to a specific
workflow hash via the store, and editing the source mid-run has no
clear semantics (does the change apply? to future iterations only? to
already-scheduled branches?). The current model — edit, save, launch a
new run — is unambiguous and matches the engine’s actual behavior.
If a user wants to watch a run while editing a different workflow, they need two browser tabs. This is a real but minor cost. If demand grows, a parent split layout can compose the two existing views without altering them.
Canvas (editor) and RunCanvasIR (run view) duplicate some
boilerplate (ReactFlowProvider setup, layout helpers). The duplication
is acceptable: the requirements diverge enough (editable vs. read-only,
domain-specific overlays) that a shared component would accumulate
configuration flags faster than it would save lines.
Each view ships its own header, breadcrumb, and toolbar. Minimal cost; shared atoms (buttons, icons) are factored where appropriate.