# Factory run view — a readable transcript, not a JSON dump

audience: AI coding agents first. Contract-level: seams and behavior are pinned; write the
bodies by reading the repo.

**Repo root:** `/home/user/Projects/overdeck/.worktrees/factory-ui`. All paths relative to it.

## Goal

A factory run reads like a chat transcript you scroll through. Each progress line is
clickable and opens a drawer showing the real artifact — the diff the agent wrote, the bash
command and its output, the file it created, the prompt it was given, the gate that failed.
Today the same data renders as tables of raw JSON, which is why it is unusable.

Nothing new is instrumented. The trace database already holds every byte this view needs.

## Decisions already made by the owner — do not relitigate

1. **Astryx supplies the components.** `@astryxdesign/core` + `@astryxdesign/lab` (Meta, MIT,
   React 19). It ships the exact surface this view needs: `ChatReasoning` (collapsible step
   with label/duration/streaming state), `LogStream` (`variant: 'terminal'`, follow mode),
   `CodeBlock` (tokenized), `Drawer`, `Markdown`, `Collapsible`, `Timestamp`, `Stepper`.
   A reference clone to read APIs from:
   `/home/user/.cache/agent-tmp/claude-1000/-home-user-Projects-overdeck/c0c245b9-d222-46a3-b6e9-d16f2626c5f9/scratchpad/astryx`
2. **Scope is `/factory` only.** This is a trial of Astryx, not a deck migration. Every other
   page keeps `@platform-modules/ui-primitives` and `@overdeck/deck-ui` untouched. Do NOT
   convert other pages, and do NOT remove `DataTable` anywhere.
3. **React 18 → latest 19 upgrade is in scope**, because Astryx requires it. Owner's
   instruction: upgrade to the LATEST React possible — `react` and `react-dom` `19.2.8` at
   time of writing (`@astrojs/react` 6 accepts `^19`); take whatever is newest at install
   time, and upgrade `@types/react`/`@types/react-dom` to match. Verified safe to attempt:
   `@platform-modules/ui-primitives` declares `react >=18`, its Radix deps accept `^19`, and
   the repo contains zero `defaultProps`, `propTypes`, `ReactDOM.render`, `findDOMNode`, or
   string refs. If something nonetheless breaks under 19, STOP and report — never paper over
   it by pinning a component back or suppressing a type error.

## Architecture

### Route

New page `apps/web/src/pages/factory/[adwId].astro` — a thin routing shell, per the repo's
page convention. `/factory` keeps its existing run list; each `FactoryRunCard` gains a link
into the run view. The list page is otherwise unchanged.

Data source is unchanged: the collector's `/api/collector/state` payload, panel
`factory-runs`, consumed through `useCollectorState`. The run view selects its run by
`adwId` client-side. Do NOT add a per-run collector route — the payload already carries
phases, events, attempts, gates, diffs (with `diffText`), processes, decisions and tool calls.
An unknown `adwId` renders an explicit not-found state naming the id, never a blank page.

### Layout

Two regions, mirroring how the owner reads runs elsewhere:

- **Transcript (main column)** — vertical scrollback, oldest at top.
- **Activity rail (right)** — the live agent stream while a run is in flight; collapsible,
  and hidden entirely for a finished run.

On narrow viewports the rail collapses below the transcript. Responsive via existing
layout conventions in `apps/web/src/components/**`; no fixed pixel page widths.

### Transcript composition

Ordered by real timestamps, never by array position:

1. **Request block** — the run's `request`, rendered through Astryx `Markdown`, collapsed past
   a fixed height with a show-more affordance.
2. **Step rows** — one per phase; agent attempts nest under their phase. Each row is a
   `ChatReasoning` entry: an icon keyed to kind, a one-line summary, a duration, and
   `isStreaming` while the underlying phase or attempt has no `endedAt`.
3. **Terminal block** — final status, totals (tokens, cost, wall time), and the run's
   verdict. A failed run shows its failure reason here, not only inside a drawer.

**Honest summaries — the load-bearing rule.** A step's summary line is COMPOSED FROM REAL
FIELDS ONLY: phase name, kind, owner/agent, model, file counts and insertion/deletion counts
from `phase_diffs`, tool-call count, duration, gate pass/fail. NEVER generate narrative prose
describing what an agent "did" — the trace has no such field, and inventing one fabricates
data. Where the factory does record human text (`phases.description`, a gate's failure
reason, an agent's `error`), show that text verbatim.

### Step drawer

Clicking a step opens an Astryx `Drawer` over the transcript. Sections, in order, each
omitted when the underlying data is absent (never rendered empty):

- **Identity** — agent, model, account, host, started/ended, duration, return code, session id.
- **Prompts** — system and user prompt via `Markdown`, each independently collapsible.
- **Tool calls** — ordered by `seq`, rendered BY KIND, which is the whole point of this view:
  - `bash` → `LogStream` in `terminal` variant: the command line, then its output; non-zero
    exit rendered at `error` level.
  - `write` / `edit` → `DiffView` (below) when the recorded arguments contain a before/after
    or a patch; otherwise `CodeBlock` of the written content.
  - `read` / `ls` / `grep` / `find` → one collapsed line naming the path or pattern, expanding
    to a `CodeBlock` of the result excerpt.
  - any other tool → `CodeBlock` of arguments and result excerpt.
  Truncation is already applied upstream (`TOOL_RESULT_EXCERPT_CHARS`). Where a value was
  truncated, SAY SO in the UI — a silently clipped result reads as a complete one.
- **Files changed** — per-file rows from `phase_diffs.files` (path, status, +/−), each
  expanding to `DiffView` of that file's hunks parsed out of `diffText`. When the stored diff
  is `truncated`, label it.
- **Gates** — per check: the argv actually run, return code, pass/fail, and the check's output
  through `LogStream`.

### The one new component

`DiffView` — Astryx ships no diff renderer, so this is built and lives in
`packages/deck-ui/src/DiffView.tsx` with a colocated test, exported from the barrel and
registered in the `/design-system` gallery (the barrel⊆gallery test enforces this).

```
DiffView({ diff: string, path?: string, defaultExpanded?: boolean }): JSX
```

- Parses unified-diff text into files → hunks → lines typed `add | del | context | meta`.
- Renders old/new line numbers, a +/− gutter, and per-file collapse.
- Input is untrusted text from a database: render as text only, never as markup.
- A string that does not parse as a unified diff renders verbatim in a monospace block with a
  visible notice — never a silent blank, never a thrown error that takes the page down.

Everything else composes Astryx. Do NOT hand-roll a chat scrollback, a terminal log viewer,
or syntax highlighting — those exist and are tested upstream.

### Theming

Astryx theming is CSS custom properties. Map its theme variables onto the deck's existing
tokens from `@platform-modules/ui-tokens` in one place, so /factory matches the rest of the
deck in BOTH light and dark. No raw colour literals in component source. `LogStream`'s
terminal variant carries its own dark palette by design — that is acceptable inside a
terminal block, and must not leak to the surrounding page.

### What must not be lost

The current tables are the only place some signals appear. Keep every one of them reachable:
retain the existing `FactoryTraceTables` content under a collapsed **Raw trace** disclosure at
the bottom of the run view. Rendering prettier must not remove a field the owner can see today.

## Error handling

- Collector unreachable or the panel missing → the existing `CollectorQueryBoundary` /
  unavailable states, unchanged in wording.
- A run with zero phases → an explicit "no steps recorded" state naming the run id.
- Any per-step render failure is contained to that step, showing the raw record, and never
  blanks the whole transcript.

## Testing

- `DiffView`: parse of multi-file and multi-hunk diffs; add/delete/context classification;
  a non-diff string falls back visibly; markup in diff text is escaped.
- Transcript: steps ordered by timestamp; a running phase marked streaming; a finished run
  renders no activity rail; summaries contain only values present in the fixture (assert no
  invented prose).
- Drawer: bash tool call renders command AND output; a `write` call renders a diff; a
  truncated result is labelled; absent sections are omitted rather than rendered empty.
- Route: unknown `adwId` renders the not-found state.
- Regression: the `/factory` list page and every other page still render after the React 19
  bump.

## Acceptance — run these and report the real output

1. `pnpm --filter @overdeck/deck-ui test` and `pnpm --filter @overdeck/deck-ui typecheck` — green.
2. `pnpm --filter web typecheck` and `pnpm --filter web build` — green.
3. The barrel⊆gallery test and slopgate — green. Never suppress or baseline a violation.
4. `/factory/<adwId>` for a real completed run in the trace db renders: the request, step
   rows, and a drawer containing at least one bash command with its output and one diff.
   Report the run id used and what you actually saw.

## Hard prohibitions

- Work only inside the repo root above.
- Do NOT convert other pages to Astryx or remove `DataTable` anywhere.
- Do NOT invent narrative summaries for steps.
- Do NOT drop any signal currently visible in the trace tables.
- Browser-driven checks go through `~/.claude/bin/e2e-remote`; never launch a local headless
  browser, and never set `E2E_REMOTE_OK=1` to bypass the guard.
