Why
record_execution cannot be set from apps/portal at all — every route ships
with it off and no way to turn it on outside SQL or a raw API call. And once
#254/#255 land there is still nothing that displays a recording.
The natural home is the canvas playground. A user turns recording on, fires a
request from the playground they already have open, and watches the run appear —
without leaving the canvas the run is about to be drawn on.
This is the viewer half of #208 and the recording half of #207.
What
A tab strip in the playground modal header: API Playground | Track Execution. PlaygroundModal is already a size="cover" shell whose body is a
single slot, so the tabs go in the header next to the heading.
Track Execution tab.
- A record button with a live blip while recording is on, wired to
PATCH /v1/:projectId/routes/:id. Toggling it triggers a recompile, so the
button must show that it is applying rather than flipping instantly.
- Recording on with no runs yet → tell the user to switch to the playground tab
and fire a request. The empty state is an instruction, not a shrug.
- Poll the run list for the route while the tab is open — new runs appear
without a manual refresh — and stop polling when it closes.
- Run list, newest first. A run that dropped spans or was truncated is labelled
incomplete.
- Clicking a run opens a draggable split pane, 30/70: the route graph on one
side, span detail on the other.
Graph overlay. Nodes tint green or red from each span's outcome. Blocks
that never ran stay neutral — "did not run" and "ran and passed" are different
states and must not look alike. Condition blocks record which edge they took
(branch), so the taken edge is highlighted. Selecting a node shows that span's
input, output, duration and error.
Custom blocks. Spans recorded inside a custom block carry the nested
graph's node ids, not the route's — that is what nullable customBlockId marks.
Overlaid on the route canvas they highlight nothing, so they are excluded from
the tint and surfaced grouped under their invoking block instead.
Async runs. A run forked by an async custom block outlives the request and is
its own root, linked back by parentRunId/parentSeq. Link to it from the
invoking span rather than trying to draw it inside the parent.
Notes
apps/portal only. apps/web is legacy — leave it.
- No hardcoded colors. Green/red come from the
success/danger tokens, not
literals, or the overlay breaks in light theme. See AGENT.md.
- The canvas already has a readonly mode that keeps elements selectable so panels
can be opened without editing — reuse it rather than adding a second one.
routeVersion is on the run header. Route versioning is not here yet, so the
viewer draws the current graph; a span whose blockId is no longer on the
canvas is skipped rather than treated as an error.
- The project-wide
/:projectId/executions page is still a ComingSoon stub. It
is a reasonable second entry point into the same run detail later, but it is
not needed for this issue.
- This is demoable before real traffic produces spans. Publishing synthetic
runs with scripts/publishTraceRun.ts for a real route id lights up the whole
UI. Recording from actual playground requests needs the producer, which is
separate work on the compiled runtime.
Depends on #254 and #255.
Why
record_executioncannot be set fromapps/portalat all — every route shipswith it off and no way to turn it on outside SQL or a raw API call. And once
#254/#255 land there is still nothing that displays a recording.
The natural home is the canvas playground. A user turns recording on, fires a
request from the playground they already have open, and watches the run appear —
without leaving the canvas the run is about to be drawn on.
This is the viewer half of #208 and the recording half of #207.
What
A tab strip in the playground modal header:
API Playground | Track Execution.PlaygroundModalis already asize="cover"shell whose body is asingle slot, so the tabs go in the header next to the heading.
Track Execution tab.
PATCH /v1/:projectId/routes/:id. Toggling it triggers a recompile, so thebutton must show that it is applying rather than flipping instantly.
and fire a request. The empty state is an instruction, not a shrug.
without a manual refresh — and stop polling when it closes.
incomplete.
side, span detail on the other.
Graph overlay. Nodes tint green or red from each span's
outcome. Blocksthat never ran stay neutral — "did not run" and "ran and passed" are different
states and must not look alike. Condition blocks record which edge they took
(
branch), so the taken edge is highlighted. Selecting a node shows that span'sinput, output, duration and error.
Custom blocks. Spans recorded inside a custom block carry the nested
graph's node ids, not the route's — that is what nullable
customBlockIdmarks.Overlaid on the route canvas they highlight nothing, so they are excluded from
the tint and surfaced grouped under their invoking block instead.
Async runs. A run forked by an async custom block outlives the request and is
its own root, linked back by
parentRunId/parentSeq. Link to it from theinvoking span rather than trying to draw it inside the parent.
Notes
apps/portalonly.apps/webis legacy — leave it.success/dangertokens, notliterals, or the overlay breaks in light theme. See AGENT.md.
can be opened without editing — reuse it rather than adding a second one.
routeVersionis on the run header. Route versioning is not here yet, so theviewer draws the current graph; a span whose
blockIdis no longer on thecanvas is skipped rather than treated as an error.
/:projectId/executionspage is still aComingSoonstub. Itis a reasonable second entry point into the same run detail later, but it is
not needed for this issue.
runs with
scripts/publishTraceRun.tsfor a real route id lights up the wholeUI. Recording from actual playground requests needs the producer, which is
separate work on the compiled runtime.
Depends on #254 and #255.