Skip to content

feat(react): runs page — list, filters, timeline, detail drawer#402

Draft
aryasaatvik wants to merge 6 commits intoRhysSullivan:mainfrom
aryasaatvik:feat/react-runs-page
Draft

feat(react): runs page — list, filters, timeline, detail drawer#402
aryasaatvik wants to merge 6 commits intoRhysSullivan:mainfrom
aryasaatvik:feat/react-runs-page

Conversation

@aryasaatvik
Copy link
Copy Markdown
Contributor

Stack

Depends on #401#400#399#398#396 — merge those first. Cross-fork GitHub PRs can't use a branch on a contributor fork as a base, so these PRs display as independent in the UI. The real dependency chain is the commit graph.

# PR Purpose
1 #396 `feat(sdk)`: ExecutionStore backed by DBAdapter
2 #398 `feat(execution)`: persist engine runs + tool calls
3 #399 `feat(execution)`: trigger propagation (CLI/HTTP/MCP)
4 #400 `feat(apps)`: execution tables in drizzle schemas
5 #401 `feat(api)`: /executions list/get/tool-calls endpoints
6 #this ← you are here `feat(react)`: runs page UI

Until the earlier five land, this diff includes their commits. Tip: filter the Files tab to `apps//src/routes/`, `packages/react/src/**`, and `apps//src/web/shell.tsx` to see just this PR's surface.


Summary

Lands the full `/runs` observability UI on top of the `/executions` HTTP API from #401. Route wired in both `apps/cloud` and `apps/local`; sidebar gets a "Runs" entry.

What ships

React package (`packages/react/`)

  • `api/executions.tsx` — client hitting `GET /executions`, `GET /executions/:id`, `GET /executions/:id/tool-calls`. Flattens the server's nested `{ execution, pendingInteraction }` into a single row type so components can read `row.id` / `row.createdAt` directly. Wire types kept as epoch-ms numbers — server serializes `Date` at the edge so the UI never touches Schema-decoded Dates.
  • `pages/runs.tsx` — infinite-scrolling list, filter rail, timeline chart, detail drawer, keyboard-driven navigation (`j` live, `r` refresh, `/` filter command, `?` help, `↑/↓` row nav, `b` collapse rail). URL params carry filter state so runs links are shareable.
  • `components/runs/*` — 16 components: shell, row, detail drawer, facet rail, CLI-style filter command + parser, timeline chart with drag-to-zoom, live-mode controls, keyboard-help overlay.
  • `hooks/use-live-mode.ts` — captures the cutoff timestamp at activation; `useEffectEvent` isolates live-toggle from stale closures.
  • `hooks/use-local-storage.ts` — SSR-safe persistence helper for field-visibility prefs.
  • `api/provider.tsx` — wraps `RegistryProvider` in a shared `QueryClientProvider`.
  • New deps: `@tanstack/react-query`, `date-fns`, `@date-fns/utc`, `react-hotkeys-hook`.
  • `styles/globals.css` — `--color-success|warning|error|info` tokens used by status dots.

Apps

  • `routes/runs.tsx` (both apps) — file route bound to `` with Effect-Schema validation on each query param.
  • `routeTree.gen.ts` (both apps) — regenerated manually to register `/runs` so `tsc` passes. The tanstack-router vite plugin overwrites this on next dev start.
  • `web/shell.tsx` (both apps) — sidebar NavItem for "Runs".

Test plan

  • `bun x tsc --noEmit` in `@executor/react`, `apps/local`, `apps/cloud` — all clean.
  • No new runtime tests — the list/detail flow goes through the API layer which the SDK's `executions.test.ts` exercises end-to-end via the in-memory adapter. The UI's shape matches the HTTP client; any misalignment would surface as a typecheck error.
  • Dev-server smoke (reviewer): start the daemon, visit `/runs`, exercise filters + live mode.

Notes

  • The filter command's DSL (`status:failed trigger:mcp tool:github.*` …) is kept from the execution-history reference branch unchanged; the parser (`filter-command-parser.ts`) has the grammar.
  • Timeline chart uses Recharts with drag-to-zoom. Bucket size adapts by window (30s / 2m / 30m / 3h / 1d).
  • Detail drawer uses Shiki for code syntax highlighting (Vitesse themes) and supports `↑/↓` for prev/next navigation through the filtered list.

Next in the stack: PR7 (independent) — OTLP HTTP observability swap.

Adds execution history persistence to the core SDK surface, wiring
three new tables (`execution`, `execution_interaction`,
`execution_tool_call`) into `coreSchema` and exposing an
`ExecutionStore` service on `executor.executions`.

Changes:
- `core-schema.ts`: three new tables with `scope_id` / `execution_id`
  / `tool_path` / `trigger_kind` / `created_at` indexes for the runs
  UI's faceting + timeline queries.
- `ids.ts`: branded `ExecutionId`, `ExecutionInteractionId`,
  `ExecutionToolCallId`.
- `executions.ts`: `Execution`, `ExecutionInteraction`,
  `ExecutionToolCall` Schema classes, status enums,
  create/update/filter/sort/meta input types, and the
  `ExecutionStore` Context.Tag.
- `execution-store.ts`: `makeExecutionStore(core)` — an
  adapter-backed `ExecutionStoreService` implementation. Wraps
  `typedAdapter<CoreSchema>` for CRUD, handles cursor-based
  pagination, filter predicates (status, trigger, tool-path glob,
  time range, code substring, hadElicitation), and builds list meta
  with facets + chart buckets.
- `cursor.ts`: base64url `{ createdAt, id }` pagination cursors.
- `executor.ts`: constructs the store once per executor, exposes via
  `executor.executions`.
- `executions.test.ts`: round-trip + lifecycle coverage against the
  in-memory adapter (no migrations needed).

Follow-up work (future PRs in the stack):
- wire the engine to record runs + tool calls through this store,
- add `/executions` API endpoints, and
- land the runs UI.
@aryasaatvik aryasaatvik force-pushed the feat/react-runs-page branch from fa4efb5 to 86bd68b Compare April 24, 2026 20:00
Wires `executor.executions` into the Effect-native engine so every
`execute()` / `executeWithPause()` / `resume()` call writes an
`execution` row and its associated tool-call + interaction rows to
whichever `DBAdapter` backs the SDK.

Engine additions:
- `ExecutionTrigger` type + new `trigger?` option on `execute` and
  `executeWithPause`. Callers attribute runs ("cli", "http", "mcp",
  …); the kind + optional meta blob are persisted on the row.
- A stable `crypto.randomUUID()` execution id is minted at entry and
  reused as `PausedExecution.id`, so callers and the DB share the
  same identifier and counts line up across pause/resume.
- `makeRecordingInvoker` wraps the `SandboxToolInvoker` passed to the
  code executor; each `invoke` writes a tool-call row (running →
  completed|failed with duration). Storage errors are ignored so
  bookkeeping failures can never fail the tool call itself.
- `persistTerminalState` runs once on fiber success or failure and
  writes final status, result/error, logs, toolCallCount, completedAt.
- Pausable path: on elicitation, the execution transitions to
  `waiting_for_interaction` and a pending interaction row is created;
  `resume` resolves it (or cancels it if action === "cancel") before
  unblocking the fiber. A `toolCallCounters` map keeps the same Ref
  across pause/resume so the final count is accurate.
- Inline path: wraps the caller-supplied `onElicitation` so every
  inline elicitation gets the same pending → resolved bookkeeping.

Tests (`engine-persistence.test.ts`, 5 cases) cover:
- completed run + tool call rows
- error result → status=failed, errorText captured
- toolCallCount rolls up correctly
- trigger kind + meta persist on the row
- failed tool call records status=failed with errorText
Flows the trigger: { kind, meta } option the engine added in the
previous PR end-to-end so the runs UI can facet by attribution
surface. Also promotes recording writes from Effect.ignore to a
defect-absorbing variant so a misconfigured storage backend can't
take down an execution.

Surfaces:
- HTTP API (packages/core/api): /executions POST now declares an
  x-executor-trigger optional header. Handler reads it (defaulting to
  "http") and passes it as the engine's trigger option.
- MCP host (packages/hosts/mcp): explicit trigger: { kind: "mcp" } on
  engine.execute (inline elicitation path) and engine.executeWithPause
  (paused flow).
- CLI: stamps every /executions call from executeCode with
  x-executor-trigger: cli. Covers call, search, describe, sources —
  every subcommand that runs code goes through this helper.

Engine robustness:
- Introduced silent helper (Effect.catchAllCause(() => Effect.void))
  and swapped every bookkeeping .pipe(Effect.ignore) over to it.
  Effect.ignore only catches typed failures; a synchronous throw
  inside an adapter (e.g. storage-drizzle when the schema is missing
  the execution model) becomes a defect and was bypassing ignore.
  With silent, misconfigured storage just means no row — the
  execution itself succeeds.

Verified by the MCP stdio integration test which previously leaked
the [storage-drizzle] unknown model error into the MCP tool result
text. Now returns the expected code result.
@aryasaatvik aryasaatvik force-pushed the feat/react-runs-page branch from 86bd68b to 2e08a3f Compare April 24, 2026 20:06
Adds the three `execution*` tables to both app drizzle schemas
(sqlite + postgres) so `executor.executions` writes actually land on
disk in real deployments. Until this PR, persistence silently
no-op'd because the `storage-drizzle` adapter throws on unknown
models (absorbed by the engine's `silent` wrapper but no row gets
written).

- `apps/local/src/server/executor-schema.ts`: three sqlite tables
  matching the DBSchema shape from `@executor/sdk` (scope_id PK on
  execution, standalone PK on child rows, matching indexes for
  scope / status / trigger_kind / created_at / tool_path /
  namespace).
- `apps/cloud/src/services/executor-schema.ts`: mirror in pg-core
  with `bigint` for epoch-ms columns and `timestamp` for Date
  columns.
- Fresh `drizzle-kit generate` output on each app's `drizzle/` dir
  (local `0004_fancy_red_wolf.sql`, cloud
  `0006_panoramic_mother_askani.sql`).

No test changes — the MCP stdio integration test already exercises
this path end-to-end (runs `return 2+2` through the daemon, which
now successfully records + returns "4" as expected).
Extends the existing `/executions` group with the three read
endpoints the runs UI needs. Handlers delegate to
`executor.executions.*` (added in RhysSullivan#396 / RhysSullivan#398) and scope each read
to the innermost executor scope — same rule the engine applies
when writing.

**Endpoints:**
- `GET /executions` — list with filter + cursor + optional meta.
  Query params: `limit`, `cursor`, `status` (CSV), `trigger` (CSV),
  `tool` (CSV of paths/globs), `from`/`to` (epoch ms), `after`,
  `code` (substring), `sort` (`<field>,<dir>`), `elicitation`
  (`"true"` / `"false"`). Meta bundles facets + timeline buckets;
  handler only asks for it when the request isn't paginated
  (no `cursor` / `after`), so cheap "first page, full facets" is
  the default call shape.
- `GET /executions/:id` — single execution detail +
  `pendingInteraction`. 404 on unknown id via
  `ExecutionNotFoundError` (already declared on the group).
- `GET /executions/:id/tool-calls` — tool-call timeline. 404 on
  unknown execution (guard rail so empty arrays don't mask typos).

**Response shape:** every `Date` is serialized to epoch ms at the
handler edge (`.getTime()`) so the wire format stays numeric. The
schemas in `api.ts` mirror the SDK's row projections one-to-one
modulo that transform.

**CSV + enum handling:** `splitCsv`, `parseSortParam`,
`parseElicitationParam` live in the handler file because they're
edge concerns — the SDK takes typed arrays and enums. Invalid sort
fields / directions drop back to defaults (no 400).

No new tests — the handlers are thin wrappers over the SDK store,
which already has round-trip + filter + meta coverage in
`packages/core/sdk/src/executions.test.ts`. The CSV/enum parsers
are small enough to validate by inspection.
… mode

Lands the full \`/runs\` observability UI on top of the \`/executions\`
HTTP API shipped in RhysSullivan#401. Route wired in both \`apps/cloud\` and
\`apps/local\`; sidebar gets a "Runs" entry.

React package:
- \`api/executions.tsx\` — client hitting \`GET /executions\`,
  \`GET /executions/:id\`, \`GET /executions/:id/tool-calls\`.
  Flattens the server's nested \`{ execution, pendingInteraction }\`
  into a single row type so components can read \`row.id\` /
  \`row.createdAt\` directly. Wire types are kept as epoch-ms numbers
  (server serializes \`Date\` at the edge) so the UI never has to
  know about Schema-decoded Dates.
- \`pages/runs.tsx\` — infinite-scrolling list, filter rail, timeline
  chart, detail drawer, keyboard-driven navigation (\`j\` live, \`r\`
  refresh, \`/\` filter command, \`?\` help, \`↑/↓\` row nav, \`b\`
  collapse rail). URL params carry the filter state so runs links
  are shareable.
- \`components/runs/*\` — 16 components covering the shell, rows,
  drawer, facet rail, CLI-style filter command + parser, timeline
  chart with drag-to-zoom, live-mode controls, and the keyboard-help
  overlay.
- \`hooks/use-live-mode.ts\` — captures the cutoff timestamp at
  activation so new rows are rendered above a divider without the
  list jumping. \`useEffectEvent\` isolates live-toggle from stale
  closures.
- \`hooks/use-local-storage.ts\` — SSR-safe persistence helper,
  used for field visibility preferences.
- \`api/provider.tsx\` — wraps \`RegistryProvider\` in a shared
  \`QueryClientProvider\` (React Query for the list + detail fetches).
- Deps added: \`@tanstack/react-query\`, \`date-fns\`, \`@date-fns/utc\`,
  \`react-hotkeys-hook\`.
- \`styles/globals.css\` — \`--color-success|warning|error|info\`
  tokens used by the status dots.

Apps:
- \`routes/runs.tsx\` (cloud + local) — file route bound to
  \`<RunsPage search={Route.useSearch()} />\` with Effect-Schema
  validation on each query param.
- \`routeTree.gen.ts\` (cloud + local) — regenerated to register
  \`/runs\`. The tanstack-router vite plugin will overwrite on next
  dev start; kept manual for now so \`bun x tsc --noEmit\` passes in
  CI.
- \`web/shell.tsx\` (cloud + local) — sidebar NavItem for "Runs".

## Test plan

- [x] \`bun x tsc --noEmit\` in \`@executor/react\`, \`apps/local\`,
  \`apps/cloud\` — all clean.
- [x] No new runtime tests (the list/detail flow goes through the
  API layer which the SDK's executions.test.ts exercises end-to-end
  via the in-memory adapter).
- [ ] Dev-server smoke: start the daemon, visit \`/runs\`, exercise
  filters + live mode — to be verified by reviewer or in CI once
  the full stack is merged.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant