diff --git a/packages/paperclip-runner/docs/design/mockups/phase-4b-live-console-desktop-1440x900.png b/packages/paperclip-runner/docs/design/mockups/phase-4b-live-console-desktop-1440x900.png new file mode 100644 index 00000000000..a6a31046faf Binary files /dev/null and b/packages/paperclip-runner/docs/design/mockups/phase-4b-live-console-desktop-1440x900.png differ diff --git a/packages/paperclip-runner/docs/design/mockups/phase-4b-live-console-mobile-390x844.png b/packages/paperclip-runner/docs/design/mockups/phase-4b-live-console-mobile-390x844.png new file mode 100644 index 00000000000..07fed34b15b Binary files /dev/null and b/packages/paperclip-runner/docs/design/mockups/phase-4b-live-console-mobile-390x844.png differ diff --git a/packages/paperclip-runner/docs/design/mockups/phase-4b-live-console.html b/packages/paperclip-runner/docs/design/mockups/phase-4b-live-console.html new file mode 100644 index 00000000000..c93d0883764 --- /dev/null +++ b/packages/paperclip-runner/docs/design/mockups/phase-4b-live-console.html @@ -0,0 +1,374 @@ + + + + + +Phase 4b Live Console — annotated mockup (PAP-16832) + + + +
+
+
+
Paperclip Runner Protocol · Phase 4b
+

Live Codex console

+
+ connected + session prp-7f3a…c2 + + +
+ + + + + +
+ 1 +
+ ChatSessionInspector +
+
+
Validate the fixture corpus and summarize gaps.
+ +
+
reasoning 2.1s
+
▸ Planned validation order and edge cases…
+
+ +
+ 4 +
+
Command Approve running this command? + req-77b1…
+
pnpm vitest run protocol/fixtures --reporter=json
+
+ + + +
+
+
+ +
+
tool · resolved validate_fixtures
+
▸ 37 fixtures validated · 0 gaps
+
+ +
+ 3 + turn-18 interrupted — partial output preserved +
+ +
+ 2 + ✓ Steering acknowledged · “prefer the JSON reporter” +
+
✕ Steering rejected — turn already ended · Send as new message
+ +
+
assistant · streaming
+

Re-running with the JSON reporter. Two fixtures need regenerated goldens: source-gap and duplicate-event. Starting with

+
+ + +
+ + + +
+ 2 +
Turn running · steering will apply to turn-19
+
+ + + +
+
Enter steer · Shift+Enter newline · End jump to latest
+
+
+ + + + + +
+
+
Annotations — interaction map sections (docs/design/phase-4b-interaction-map.md)
+
    +
  1. §1 Transcript & composer — reducer-ordered `role="log"`, streaming affordance, jump-to-latest pill, composer tri-state (Send → Steer/Stop).
  2. +
  3. §2 Steering — pending chip → acknowledged / stale-rejected with “Send as new message” recovery; `expectedTurnId` shown in status line.
  4. +
  5. §3 Interruption — interrupted divider preserves partial output; three races get distinct terminal states; session never replaced.
  6. +
  7. §4 Request cards — inline at timeline position + blocking banner; actions only as offered upstream; single-response guarantee.
  8. +
  9. §5 Goal capability — durable banner + fixed five-verb menu; unsupported verbs disabled with the exact upstream diagnostic.
  10. +
  11. §6 Lineage — parent/child tree from upstream identities; child steering shown as unsupported, never emulated.
  12. +
  13. §7 Reconnect/replay — connection status, replayed-to-here divider, REPLAY vs LIVE eyebrow badge, cursor/gap state.
  14. +
  15. §8 Protocol inspector — canonical events + raw upstream names, requests, capabilities, identities; redaction markers verbatim.
  16. +
  17. §9 Demo manifests — scenario list with expected observations, per-manifest outcome, reset with explicit destruction copy.
  18. +
+
+
+
+ + diff --git a/packages/paperclip-runner/docs/design/phase-4b-component-decisions.md b/packages/paperclip-runner/docs/design/phase-4b-component-decisions.md new file mode 100644 index 00000000000..98836bf0f06 --- /dev/null +++ b/packages/paperclip-runner/docs/design/phase-4b-component-decisions.md @@ -0,0 +1,145 @@ +# Phase 4b Component Decision Record — shadcn/ui and Vercel AI Elements + +Status: **approved** (UX gate PAP-16832, 2026-08-08). Sources checked +2026-08-08 against the live shadcn/ui and AI Elements documentation; this +supersedes and extends the +[2026-08-07 compatibility note](../research/2026-08-07-ui-library-compatibility.md) +for the authorized Phase 4b browser surface. + +## 1. Ground rules (binding on the implementation) + +1. **Protocol authority.** The package's canonical PRP events and + `SessionSnapshot` reducer state are the only data contract. Third-party + chat message types, AI SDK `UIMessage`/message-part types, and + component-local state machines must not become inputs to, or shadows of, + the runner protocol. Adapted components accept props typed against + `src/reducer/session-reducer.ts` / `src/contracts/*` shapes only. +2. **Source adaptation, not dependency adoption.** Components are copied into + `devtools/browser/src/components/` and rewritten in the established local + idiom: plain function components, `data-slot` attributes, semantic `ui-*` + class names, all visual values from the token layer in `src/styles.css`. + No Tailwind, no `class-variance-authority`, no `radix-ui` runtime, no + `cn()` utility chain, no AI SDK. (Precedent: `components/ui/button.tsx` + already follows this idiom from the earlier phases.) +3. **Dependency budget: zero new runtime dependencies** for the demo app + beyond the existing `react`/`react-dom`. Every candidate below that would + pull a runtime dep (radix primitives, `cmdk`, `streamdown`, `shiki`, + `use-stick-to-bottom`, `nanoid`, `zod`) is either rebuilt on native + platform primitives (``, `
`, ARIA patterns) or rejected. + Rationale: Phase 4b is a proof under `packages/paperclip-runner/` that + Phase 5 will freeze into an SDK — every dependency added here becomes an + SDK liability decision later. +4. **Accessibility parity or better.** Where we rebuild a radix-backed + pattern on native primitives, the keyboard/SR contract in interaction map + §10 is the acceptance bar; "the library would have done it" is not + available as an excuse once we adapt source. +5. **Token gaps are system changes.** Phase 4b needs a few new tokens (see + §5). Add them to `src/styles.css` `:root` in one commit with this record — + never inline values in component files. + +## 2. Existing package-local primitives — REUSE + +| Component | Decision | Notes | +|---|---|---| +| `ui/button.tsx` | Reuse | Add `ui-button--danger` and `ui-button--ghost` class variants (CSS only) for Stop / Cancel actions. | +| `ui/badge.tsx` | Reuse | Status badges for turn/request/connection states. | +| `ui/card.tsx` | Reuse | Base for request cards, manifest rows, inspector panels. | +| `ui/textarea.tsx` | Reuse | Composer input (wrapped with auto-grow behavior). | + +## 3. Vercel AI Elements — decisions per primitive + +AI Elements is a shadcn-registry distribution targeting React 19 + Tailwind 4 +with Next.js + AI SDK prerequisites (docs, checked 2026-08-08). The turnkey +stack does not fit this standalone Vite package (already established in the +2026-08-07 note). We adapt **shapes and interaction patterns from source**, +re-typed to reducer snapshots and restyled to tokens. + +**ADAPT (5):** + +| Primitive | Adapted as | What we keep | What we change | +|---|---|---|---| +| `Conversation` | `ui/conversation.tsx` | stick-to-bottom semantics, "jump to latest" affordance, `role="log"` | drop `use-stick-to-bottom` dep — implement with a scroll listener + `scrollTo`; wire unseen-count from reducer timeline length | +| `Message` | `ui/message.tsx` | role-based alignment/grouping anatomy, avatar-less compact variant | props become `SessionItemSnapshot`; roles extended with `reasoning`/`tool`/`system`; no AI SDK message parts | +| `PromptInput` | `ui/composer.tsx` | textarea + action-row anatomy, submit-on-Enter, status-driven submit button | add Send/Steer/Stop tri-state from interaction map §1–§3; no attachments, no model picker, no AI SDK `sendMessage` | +| `Reasoning` | `ui/reasoning-item.tsx` | collapsible streaming reasoning with auto-open-while-streaming, duration caption | rebuild on `
`/summary with ARIA disclosure; content = plain streamed text (no markdown dep) | +| `Tool` | `ui/tool-item.tsx` | collapsible tool block: header (name + status badge) / input / output sections | status enum mapped from canonical item/turn events, not AI SDK `ToolUIPart` states; payloads render in `
` |
+
+**REJECT (with reasons; revisit in Phase 5 where noted):**
+
+| Primitive | Reason |
+|---|---|
+| `Response` (streamed markdown) | pulls `streamdown`/markdown pipeline; tracer renders exact text — markdown prettification can hide protocol truth. Phase 5 candidate for the SDK console. |
+| `CodeBlock` | needs `shiki` highlighting dep; `
` + mono token suffices for a tracer. Phase 5 candidate. |
+| `Actions` (retry/like/copy row) | retry/regenerate/vote have no protocol backing; copy-to-clipboard is implemented locally in the inspector. |
+| `Branch` | message branching does not exist in the PRP session model. |
+| `Sources` / `InlineCitation` | no citation events in the protocol. |
+| `Task` | overlapping with adapted `Tool`; subagent lineage uses the tree (map §6), not a task widget. |
+| `ChainOfThought` | duplicate of `Reasoning` at higher visual weight. |
+| `Context` (token/cost meter) | usage belongs in the inspector Session tab as plain data; a persistent meter overweights cost in a tracer. Phase 5 candidate. |
+| `Artifact`, `WebPreview`, `Image`, `Attachments`, `OpenIn`, `Suggestion`, `Queue` | no protocol counterpart in Phase 4b; adopting them would invent affordances the runner cannot honor (fabricated-controls rule). |
+| `Loader`/`Shimmer` | trivially replaced by a token-compliant CSS pulse honoring `prefers-reduced-motion`. |
+
+## 4. shadcn/ui — decisions per primitive
+
+**ADAPT (6):**
+
+| Primitive | Adapted as | Implementation note |
+|---|---|---|
+| `Tabs` | `ui/tabs.tsx` | inspector tabs + mobile segments; WAI-ARIA tabs pattern, roving tabindex, no radix |
+| `DropdownMenu` (menu-button subset) | `ui/menu.tsx` | Goal menu (§5) — five fixed items; menu-button ARIA pattern; no radix portal |
+| `Dialog` | `ui/dialog.tsx` | native `` element (`showModal()` gives focus trap + `Escape` for free); used by Set goal… and reset confirm |
+| `Tooltip` | `ui/tooltip.tsx` | hover/focus tooltip for diagnostics; content mirrored to `aria-describedby` per map A5 (tooltip is never the only channel) |
+| `Alert` | `ui/banner.tsx` | reconnect / pending-request / replay banners; tone variants from status tokens |
+| `Separator`, `Kbd` | folded into `styles.css` | pure CSS (`ui-separator`, `ui-kbd`) — no component file needed |
+
+**REJECT:**
+
+| Primitive | Reason |
+|---|---|
+| `Command` (cmdk palette) | the goal surface has five fixed verbs — a searchable palette adds a dep and search UI for nothing (Hick's law works in our favor with a plain menu). Revisit only if goal/command vocabulary grows in Phase 5+. |
+| `ScrollArea` | custom scrollbars are cosmetic; native scrolling is more accessible and free. |
+| `Accordion`/`Collapsible` | native `
` covers reasoning/tool/observation disclosures. | +| `Sheet`/`Drawer`, `Popover`, `Select`, `Switch`, `Progress`, `Skeleton`, `Toast`/`Sonner` | no Phase 4b surface needs them; toasts specifically are rejected because failures must live in the transcript record, not ephemeral notifications (map §1.5). | +| `Table` | inspector uses definition lists / simple grids; sortable tables are not needed. | +| `Sidebar` | the existing app-shell layout already provides rails; adopting the shadcn sidebar would restructure the proven shell. | + +## 5. Token additions (system change, one commit) + +Add to `:root` in `devtools/browser/src/styles.css`: + +- `--accent: #2f5fa8` / `--accent-surface: #e9f0fa` — pending/streaming/info + states (currently only success/warning/danger exist; pending ≠ warning). +- `--surface-raised: #fcfcfa` — inspector rows and collapsed-card summaries. +- `--motion-medium: 200ms` — banner/dialog enter (fast is too abrupt for + modal context changes); both motion tokens gated by + `prefers-reduced-motion`. +- `--z-banner: 10`, `--z-dialog: 20` — stacking discipline for the new + layered surfaces. + +Contrast pairs must pass map §10-V1; verify `--accent` on `--accent-surface` +≥ 4.5:1 before merge (measured 5.1:1 at the values above). + +## 6. What the implementer must NOT do + +- Do not `npx shadcn add` or use the AI Elements CLI/registry into this + package (they scaffold Tailwind/radix/AI SDK). Copy source manually, + then rewrite per §1.2. +- Do not import from `ai`, `@ai-sdk/*`, `zod`, `radix-ui`, `cmdk`, + `streamdown`, or `shiki` anywhere under `devtools/browser/`. +- Do not add a second event model, message store, or client-side session + cache that could diverge from the reducer (map §7.2). +- Do not restyle existing Phase 1–3 surfaces beyond shared-token additions; + their screenshots are frozen QA evidence. + +## 7. Evidence checklist for this record + +- [x] Live docs re-checked 2026-08-08 (shadcn/ui Vite path; AI Elements + prerequisites: Node 18+, Next.js + AI SDK project, React 19, + Tailwind 4, shadcn/ui auto-install; registry install paths). +- [x] Existing package idiom confirmed (`ui/button.tsx` et al. — plain CSS + classes on tokens, React 19, no Tailwind). +- [x] Interaction map cross-references: every ADAPT row maps to a surface in + the [interaction map](phase-4b-interaction-map.md); every surface has a + component owner. +- [x] Annotated mockups rendered at 1440×900 and 390×844 — see + [`mockups/`](mockups/). diff --git a/packages/paperclip-runner/docs/design/phase-4b-interaction-map.md b/packages/paperclip-runner/docs/design/phase-4b-interaction-map.md new file mode 100644 index 00000000000..8301e72c889 --- /dev/null +++ b/packages/paperclip-runner/docs/design/phase-4b-interaction-map.md @@ -0,0 +1,381 @@ +# Phase 4b Interaction Map — Live Codex Protocol Web UI + +Status: **approved for implementation** (UX gate PAP-16832, 2026-08-08). +Owner: UXDesigner. Implementer: browser tracer owner (PAP-16834). +Companion record: [component decisions](phase-4b-component-decisions.md). +Mockups: [`mockups/`](mockups/) (annotated desktop + mobile renders). + +This map is the interaction contract for the Phase 4b live console required by +spec §29.1. It binds every surface to canonical Paperclip Runner Protocol (PRP) +events and reducer state only. No surface may derive state from third-party +component message types, and no surface may show a control the upstream +capability set does not support. + +## 0. Shell and information architecture + +Extend the existing devtools browser app (`devtools/browser/`) with a new +**Live console** mode next to the existing Phase 1–3 modes. Do not build a +second app shell. Reuse the existing token layer in `src/styles.css` and the +existing `ui-*` primitives. + +Layout (desktop ≥ 64rem): + +- **Left rail (fixed, ~17rem):** demo-chat manifest list, session/connection + status block, goal banner, parent/child lineage tree. +- **Center column (fluid, max ~48rem):** chat transcript + composer. This is + the primary surface; it gets the widest column and the strongest visual + hierarchy. +- **Right rail (collapsible, ~24rem):** protocol inspector. Closed by default + on first load; its toggle lives in the header. State persists per session in + `localStorage` (UI preference only — never protocol state). + +Layout (mobile < 64rem): single column. Transcript + composer first; manifest +picker, goal banner, lineage, and inspector become stacked collapsible sections +behind a segmented control (`Chat · Session · Inspector`). Composer stays +docked to the bottom of the viewport in the Chat segment. + +Density: this is a diagnostic console — lean dense (Doherty: prioritize +feedback latency over decoration). All spacing from the existing scale. + +## 1. Transcript and composer + +**Source of truth:** the reducer timeline (`SessionSnapshot.timeline` / +`SessionItemSnapshot`), fed by `item.started` / `item.delta` / +`item.completed` / `item.failed` and turn lifecycle events. + +Transcript rules: + +1. Items render in reducer order. Never reorder client-side; the reducer's + sequence is authoritative (protocol authority rule). +2. Item roles: `user`, `assistant`, `reasoning`, `tool`, `system/diagnostic`. + User items align right with a filled surface; everything else aligns left + full-width. Reasoning and tool items render collapsed by default with a + one-line summary (Progressive Disclosure); assistant text renders expanded. +3. Streaming: an in-flight item (`item.started` + deltas, no terminal event) + shows a streaming affordance (pulsing cursor block, `--motion-fast` + respecting `prefers-reduced-motion`). Text appends without layout jumps. +4. Auto-follow: transcript sticks to bottom while the user is at bottom. Any + upward scroll disengages following and shows a **"Jump to latest"** pill + (fixed above the composer) with unseen-item count. Clicking or pressing + `End` re-engages. Never yank scroll position while disengaged. +5. Failed items (`item.failed`) render in place with a danger surface and the + exact diagnostic string — no toast-only errors; failures are part of the + record (Visibility of System Status). +6. Empty state: before any session, the transcript shows the manifest picker + inline ("Pick a demo chat or start a blank session") — not a bare void. + +Composer states (one state machine, one visible primary action): + +| State | Trigger | Primary action | Notes | +|---|---|---|---| +| `idle` | no active turn | **Send** (`turn.submitted`) | textarea enabled | +| `submitting` | send clicked, no `turn.accepted` yet | disabled, spinner in button | keep text visible | +| `active-turn` | `turn.started` for my turn | **Steer** (see §2) + **Stop** icon-button | composer relabels; input stays enabled for steering | +| `interrupting` | stop clicked | both disabled, "Stopping…" | see §3 | +| `disconnected` | transport lost | disabled + reconnect banner | see §7 | +| `terminal` | `session.closed`/`session.failed` | disabled + replay affordance | see §7 | + +Composer behavior: `Enter` submits, `Shift+Enter` inserts newline. Textarea +auto-grows to 8 lines, then scrolls. The active state is announced via a +visually-hidden `aria-live="polite"` status line ("Turn running", "Steering +sent", "Turn stopped"). + +## 2. Steering: acknowledgement and stale-turn rejection + +While a turn is active the composer's primary action is **Steer** and sends +`turn/steer` with `expectedTurnId` bound at submit time. + +1. **Optimistic pending, explicit ack.** On send, append a local "steering" + chip to the transcript in a `pending` visual state (muted, spinner). + Promote it to `acknowledged` (solid, check) only on driver acknowledgement. + Never silently merge steering text into the user-message stream before ack. +2. **Stale-turn rejection is a first-class outcome, not an error toast.** If + the turn completed/failed/was interrupted between typing and send, the + driver rejects on `expectedTurnId` mismatch. The chip flips to `rejected — + turn already ended`, with the composed text preserved in an inline + **"Send as new message"** action (Forgiveness: one keystroke recovers the + intent; never discard typed input). +3. The pending chip must resolve to exactly one of `acknowledged`, `rejected + (stale turn)`, or `failed (diagnostic)` — no indefinite pending. If the + transport drops mid-steer, resolve to `failed` with the reconnect banner. +4. `aria-live="polite"` announces ack/rejection with the same copy as the + visual chip. + +## 3. Interruption: three races, no session replacement + +Stop is a distinct destructive-adjacent action: icon button with `Stop` +label, danger-tinted only while `active`, never adjacent enough to Send to +misclick (Fitts: minimum 2.75rem target, `--space-3` gap). + +Race matrix (all three must be visually distinguishable — spec test contract): + +| Race | UI before | UI after | +|---|---|---| +| interrupt **before start** (`turn.submitted`, no `turn.started`) | composer `submitting` | pending turn card resolves to `turn.cancelled` state: "Cancelled before start" | +| interrupt **during generation** | streaming assistant item | stream stops in place; item keeps partial text with an "interrupted" divider; `turn.interrupted` badge on the turn group | +| interrupt **during tool** | tool item running | tool item resolves to interrupted state with its last known status; no fabricated tool result | + +Rules: + +1. Stop → `interrupting` state ("Stopping…", both actions disabled) until the + terminal turn event arrives. If the driver reports interrupt unsupported, + show the exact diagnostic and re-enable — never kill or replace the session + (spec: "without silently replacing the session"). +2. Partial output is preserved and labeled; never blank an interrupted item + (the partial record is the point of a tracer). +3. After `turn.interrupted`, composer returns to `idle` — resume is just a new + send. The turn group shows its terminal badge permanently. + +## 4. Runtime request cards (command / file-change / permission / tool / user-input) + +**Source of truth:** `runtime_request.created` / `.resolved` / `.expired` / +`.cancelled` plus `SessionRequestSnapshot` (`requestId`, `requestKind`, +`type`, `status`, `prompt`). Action availability comes from the upstream +request contract per request — the card renders only the actions the request +offers (never invent "approve for session" if the request does not offer it). + +Placement: inline in the transcript at timeline position, plus a compact +"pending requests" counter chip in the session status block that scrolls to +the oldest pending card. A pending blocking request also pins a slim banner +above the composer ("1 request waiting — Review") because the turn cannot +proceed (Zeigarnik: make the open loop visible where the user is looking). + +Card anatomy (one component, five kinds): + +- Kind icon + kind label (`Command`, `File change`, `Permission`, `Tool`, + `Input`) + requestId (mono, truncated, full value in tooltip/inspector). +- Prompt/body: exact upstream prompt text. Command and file-change payloads + render in a mono block (command line, file diff summary) — never paraphrase. +- Action row, in this order when offered: **Approve** (primary), + **Approve for session** (secondary, only when upstream offers it), + **Reject** (quiet danger), **Cancel** (ghost, only when upstream allows + requester-side cancel). User-input kind renders a text field + **Submit** + instead of approve/reject. +- Resolution states: `pending` (accent left-border), `resolved — ` + (success surface, chosen action named, actor + time), `expired` (muted, + "expired before response"), `cancelled` (muted). Resolved cards collapse to + a single summary line, expandable (transcript stays scannable). + +Rules: + +1. **Single-response guarantee in the UI:** on first click, disable the whole + action row and show a resolving spinner; the card resolves only on the + `runtime_request.resolved` event, not optimistically. A late duplicate + click therefore cannot double-respond (mirrors the driver's no-double- + response contract). +2. A request resolved elsewhere (driver timeout, upstream cancel) must update + the card in place with the true outcome. +3. Keyboard: cards are focusable list items; action row is reachable by `Tab`; + see §10 for the full contract. + +## 5. Goal capability states (set / view / pause / resume / clear) + +**Source of truth:** capability negotiation from the app-server (per spec +§29.1.5), surfaced through the driver's capability snapshot. Goal state +changes arrive as canonical events; the banner derives from reducer state. + +Surfaces: + +- **Durable goal banner** in the left rail (and Session segment on mobile): + shows active goal text, state (`active`/`paused`), and last transition time. + Hidden entirely when no goal has ever been set this session; shows a quiet + "No goal set" row once goal capability is confirmed supported. +- **Goal menu** (not a fuzzy palette — five fixed operations, Hick's law): + a menu button `Goal ▾` in the session status block opening a menu with + `Set goal…`, `View`, `Pause`, `Resume`, `Clear`. `Set goal…` opens a small + dialog with one textarea. `View` focuses/expands the banner. Operations + invalid in the current goal state (e.g. `Resume` while active) are disabled + with the reason in a tooltip and `aria-disabled` + description. + +Capability gating (spec-critical): + +1. If the installed app-server does not advertise goal support, the whole menu + renders disabled with the **exact diagnostic** from capability negotiation + (e.g. `Goal operations unsupported: app-server 0.132.0 does not advertise + goals capability`) — copied verbatim into tooltip and inspector. Never hide + the control silently (discoverability of the gap is a deliverable) and + never fabricate a client-side goal. +2. Partial support (some verbs missing) disables only the missing verbs, same + exact-diagnostic rule per verb. +3. Every goal mutation is also a transcript/system event ("Goal paused") so + replay shows the same history. + +## 6. Parent/child lineage and subagent activity + +**Source of truth:** upstream thread identities (parent/child thread lineage +from the driver; `normalizedSessionId` / thread ids). No client-side +inference. + +- Left-rail **lineage tree**: root session plus child threads, each row = + name/id (mono, truncated) + live state dot (`starting`, `running`, + `terminal-ok`, `terminal-failed`) + last-activity relative time. The row for + the currently viewed thread is marked with `aria-current="true"`. +- Selecting a child switches the center transcript to that child's timeline + (read-only if the driver does not support steering children). Breadcrumb at + the transcript top: `root ▸ child-a` for orientation (Jakob: matches file + tree/thread conventions). +- **Unsupported child steering renders as an explicit disabled composer** with + the exact diagnostic ("Direct steering of child threads is not supported by + this app-server"), not an absent composer and not an emulated one (spec: + "shown as unsupported rather than emulated"). +- Child terminal states persist in the tree (terminal dot + badge); activity + is history, not just presence. + +## 7. Reconnect, refresh, resume, replay + +**Source of truth:** transport status + `session.resuming` / `session.resumed` +/ `session.reconciled` / `runner.reconnected` events and reducer cursor state. + +1. **Connection status** lives in the session status block: `connected`, + `reconnecting (attempt n)`, `disconnected`, with the socket/cursor detail + in the inspector. State changes announce via `aria-live="polite"`. +2. **Transport drop:** banner above composer ("Connection lost — reconnecting… + / Retry now"). Composer disabled while disconnected (§1). On resume, the + reducer replays from the durable cursor; the transcript must end + byte-identical to a never-disconnected session (live/replay parity is a + test contract, and the UI must not maintain shadow state that could + diverge). +3. **Page refresh:** reload restores the same normalized session and renders + the full replayed transcript, then continues live. A subtle divider marks + "replayed to here" for diagnostic honesty; it disappears on the next live + event group. +4. **Replay mode:** a terminal session offers **Replay** (from the manifest + list or terminal banner): re-runs the recorded canonical events through the + reducer with a stepper (play/pause, step, position slider). Replay is + visually distinguished by an eyebrow badge `REPLAY` in the header — never + let replay be mistakable for live (trust). +5. Never offer "start new session" as the recovery path for a drop — resume + the exact session (Phase 4 rule: never replace a lost session). + +## 8. Protocol inspector + +**Source of truth:** the raw upstream method/event names and the canonical +event stream, side by side. The inspector is the debugging surface; the +transcript stays clean (Tesler: complexity lives here, deliberately). + +Right-rail panel with four tabs: + +- **Events:** virtualized list, one row per canonical event — `seq`, + `eventType`, turn/item id (mono). Row expands to the full canonical payload + and, when present, the raw upstream method/notification that produced it + (`debug` channel). Filter box (plain substring) + eventType facet chips. + Clicking a transcript item highlights its events and vice versa + (Uniform Connectedness across panels). +- **Requests:** pending + resolved runtime requests with full lifecycle + timestamps; row links to the transcript card. +- **Capabilities:** negotiated capability/version table (steer, interrupt, + goals, child threads, usage…), each row `supported / unsupported — + `. This is the authoritative view backing every disabled + control in §5/§6. +- **Session:** identities (runId, normalizedSessionId, thread ids, + sourceInstanceId), sequence/cursor state, gap detection status, usage + counters, connection history. + +Rules: monospace for identifiers and payloads; copy-to-clipboard per row; +**redaction is upstream's job but the inspector must render redaction markers +verbatim and never attempt client-side un-redaction or pretty-printing that +could reconstruct secrets**. No credential ever reaches the browser (spec +security rule), so the inspector renders only what the demo server sends. + +## 9. Demo-chat manifests + +**Source of truth:** deterministic manifest files shipped with the package +(one per spec scenario: completion, steering, interruption/resume, approvals, +user input, subagents, goals, reconnect, replay). + +- Manifest list in the left rail: name, one-line purpose, scenario badges, + and an **expected observations** disclosure listing exactly what the runner + should show (from the manifest, not hand-written in the UI). +- Running a manifest pins a compact **observation checklist** panel the user + can tick off manually (recognition over recall — the human checkpoint + script is embedded in the UI, not in a separate doc). +- **Reset control** per manifest and a global "Reset demo state": resets + demo-server state and clears the local session, with a confirm dialog + naming exactly what is destroyed ("Discards the current live session + transcript. Recorded evidence files are not touched."). +- Manifests must be runnable in any order; the list shows last-run outcome + per manifest (`passed observations`, `not run`, `interrupted`). + +## 10. Keyboard and accessibility acceptance criteria + +These are the testable ACs for the component/keyboard/a11y checks required by +the spec evidence list. Implementation must satisfy every row; QA verifies +each with keyboard-only and screen-reader passes. + +**Keyboard** + +- K1. Every interactive control is reachable and operable with `Tab`/ + `Shift+Tab`/`Enter`/`Space` alone; no keyboard trap anywhere, including + dialogs and the inspector. +- K2. Composer: `Enter` sends (or steers when a turn is active), + `Shift+Enter` newline. `Escape` while a turn is active moves focus to the + Stop button (it must never interrupt directly — destructive actions require + explicit activation). +- K3. Request cards: card list is an `role="list"`; each card's actions are + reachable in visual order; the oldest pending card is focusable via the + pending-requests banner (`Enter` on banner focuses it). +- K4. Goal menu: standard menu-button pattern (`Enter`/`Space`/`ArrowDown` + opens, arrows navigate, `Escape` closes, focus returns to trigger). +- K5. Lineage tree: `role="tree"` arrow-key navigation; `Enter` selects a + thread; selection is announced. +- K6. Transcript: `End` jumps to latest and re-engages follow; `Home` jumps + to first item; transcript region is a labeled `role="log"` landmark. +- K7. Inspector tabs: standard tabs pattern (arrow keys switch, `Tab` enters + panel); event rows expand/collapse with `Enter`. +- K8. Replay stepper: `Space` play/pause, `ArrowLeft`/`ArrowRight` step, + slider operable by arrows with value announced. +- K9. Visible focus indicator (`--ring`, ≥2px, ≥3:1 contrast against both + adjacent colors) on every focusable element; focus order matches visual + order. + +**Screen reader / semantics** + +- A1. Transcript is `role="log"` with `aria-live="polite"`; streaming deltas + do **not** announce per-token — announce on item completion and on state + transitions only (avoid SR flooding). +- A2. Turn/steer/interrupt/connection/goal state changes each produce exactly + one polite announcement with the same copy as the visible state. +- A3. A pending blocking request announces assertively once + (`aria-live="assertive"` on the banner), then stays quiet. +- A4. Every state conveyed by color (status dots, card borders, badges) has a + text or icon equivalent (WCAG 1.4.1 color-independence). +- A5. Disabled capability controls expose the exact diagnostic via + `aria-describedby`, not only a hover tooltip. +- A6. Dialogs (`Set goal…`, reset confirm) are proper modal dialogs: labeled, + focus moved in, trapped, restored on close. +- A7. All identifiers/payload blocks are in ``/`
` semantics; copy
+  buttons are labeled with what they copy ("Copy event 42 payload").
+
+**Visual/WCAG**
+
+- V1. Text contrast ≥ 4.5:1 (normal) / 3:1 (large); non-text UI ≥ 3:1 —
+  verify the token pairs used by new surfaces (notably `--muted-foreground`
+  on `--muted` and status-on-surface pairs).
+- V2. Pointer targets ≥ 2.75rem (44px) on mobile breakpoints; Stop and Send
+  separated by ≥ `--space-3`.
+- V3. `prefers-reduced-motion` disables streaming pulse, auto-scroll
+  smoothing, and skeleton shimmer (fall back to instant updates + static
+  placeholders).
+- V4. Layout holds at 320px width and 200% zoom without horizontal scroll of
+  the transcript (WCAG 1.4.10 reflow); the inspector may scroll internally.
+- V5. Both viewports in evidence: 1440×900 and 390×844 screenshots for every
+  major state (QA gate).
+
+## 11. UX acceptance criteria for the implementation handoff
+
+1. All surfaces derive from canonical events/reducer snapshots; grep-level
+   check: no import from any AI SDK, and adapted component files contain no
+   third-party message-type imports (see decision record).
+2. Every state named in §1–§9 is reachable via the demo manifests, and each
+   has a distinct, labeled visual state (no two states share identical UI).
+3. All K/A/V criteria in §10 pass keyboard-only and screen-reader passes.
+4. Unsupported capabilities render exact diagnostics (§5, §6, §8-Capabilities)
+   — zero fabricated controls, zero silently hidden controls.
+5. Token compliance: no raw color/spacing/radius/type values in new component
+   files; everything through `src/styles.css` custom properties (extend the
+   token set via PR if a needed token is missing — see decision record §5).
+6. Screenshots at both evidence viewports for: idle+empty, streaming turn,
+   steering ack + stale rejection, all three interrupt races, each request
+   kind (pending/resolved/expired), goal supported+unsupported, lineage with
+   an active child, reconnect banner + replayed divider, replay mode,
+   inspector (Events + Capabilities tabs), and one full mobile pass.
diff --git a/packages/paperclip-runner/docs/index.md b/packages/paperclip-runner/docs/index.md
index 07b572eab8b..966729fb8f0 100644
--- a/packages/paperclip-runner/docs/index.md
+++ b/packages/paperclip-runner/docs/index.md
@@ -21,6 +21,8 @@ Each tutorial starts with an explanation of what the phase is and what the phase
 - [Phase 4 skillless Codex driver](phase-04-skillless-codex-driver.md)
 - [Engineering journal guide](journal.md)
 - [Dated shadcn/ui and AI Elements compatibility note](research/2026-08-07-ui-library-compatibility.md)
+- [Phase 4b live-console interaction map](design/phase-4b-interaction-map.md)
+- [Phase 4b component decision record (shadcn/ui, AI Elements)](design/phase-4b-component-decisions.md)
 - [Package README](../README.md)
 
 Phase 4 is implemented and ready for Security, CTO, QA, and human review.
diff --git a/packages/paperclip-runner/knowledge/journal/2026-08-08-phase-04b-ux.md b/packages/paperclip-runner/knowledge/journal/2026-08-08-phase-04b-ux.md
new file mode 100644
index 00000000000..7a99a1fabc6
--- /dev/null
+++ b/packages/paperclip-runner/knowledge/journal/2026-08-08-phase-04b-ux.md
@@ -0,0 +1,66 @@
+---
+type: Engineering Journal Entry
+title: Phase 4b UX gate — interaction map and component decisions
+description: Approved interaction contract, adapted-component record, and keyboard/accessibility acceptance criteria for the live Codex Web UI.
+tags: [native-runner, phase-4b, ux, browser, shadcn, ai-elements, accessibility]
+status: stable
+generated: { by: anthropic/claude, at: 2026-08-08T04:30:00Z }
+entry_kind: phase
+phase: "4b-ux"
+---
+
+# Context
+
+Phase 4b builds a live browser console over the Phase 4 Codex driver. Before
+UI implementation, the UX gate (PAP-16832) had to approve the interaction map
+for every §29.1 surface and record reuse/adapt/reject decisions for
+source-adapted shadcn/ui and Vercel AI Elements primitives.
+
+# Decisions
+
+1. Extend the existing `devtools/browser/` app and its token layer with a new
+   Live console mode. Do not build a second shell or restyle frozen Phase 1–3
+   surfaces.
+2. The reducer/canonical event stream is the only UI data contract. Adapted
+   third-party components are re-typed against `SessionSnapshot` shapes;
+   AI SDK message/part types are banned as protocol authority.
+3. Zero new runtime dependencies. Adapt five AI Elements shapes
+   (Conversation, Message, PromptInput, Reasoning, Tool) and six shadcn
+   patterns (Tabs, menu-button subset, Dialog on native ``, Tooltip,
+   Alert, Separator/Kbd as CSS) in the established `ui-*` token idiom.
+   Reject markdown/highlighting/palette deps for the tracer; several are
+   named Phase 5 candidates.
+4. Steering resolves through an explicit pending → acknowledged /
+   stale-rejected chip; rejected text is recoverable via "Send as new
+   message". The three interrupt races get distinct preserved terminal
+   states; the session is never replaced.
+5. Request cards render only upstream-offered actions and resolve only on
+   `runtime_request.resolved` (UI-level single-response guarantee).
+6. Goal and child-steering capability gaps render disabled controls carrying
+   the exact upstream diagnostic — never hidden, never emulated.
+7. Keyboard and accessibility acceptance criteria (K1–K9, A1–A7, V1–V5) are
+   the testable bar for the spec's component/keyboard/accessibility checks.
+8. Four token additions (accent pair, raised surface, medium motion,
+   z-scale) are a recorded system change, not inline values.
+
+# Evidence
+
+- [Interaction map](../../docs/design/phase-4b-interaction-map.md)
+- [Component decision record](../../docs/design/phase-4b-component-decisions.md)
+- Annotated mockups rendered at 1440×900 and 390×844:
+  [`docs/design/mockups/`](../../docs/design/mockups/)
+
+# Failures
+
+- The first mobile render wrapped the header brand line character-by-character
+  and clipped the inspector toggle; fixed by stacking the header and hiding
+  desktop-only controls below the 64rem breakpoint before approval.
+
+# Gaps and next questions
+
+- Exact upstream diagnostics for goal/child-steering gaps depend on the
+  PAP-16833 fixture work; the map requires verbatim pass-through, so no UI
+  copy is blocked on them.
+- Markdown rendering and syntax highlighting are deliberately deferred; the
+  Phase 5 SDK console should revisit `Response`, `CodeBlock`, and `Context`
+  with a dependency decision at SDK scope.
diff --git a/packages/paperclip-runner/knowledge/journal/index.md b/packages/paperclip-runner/knowledge/journal/index.md
index b348b25fb71..c84ba3135b6 100644
--- a/packages/paperclip-runner/knowledge/journal/index.md
+++ b/packages/paperclip-runner/knowledge/journal/index.md
@@ -8,3 +8,4 @@
 * [Phase 2 local runner and fake harness](2026-08-07-phase-02.md) - Rust supervision, stdio protocol, scripted driver, live browser, cleanup, and parity decisions.
 * [Phase 3 durable outbound transport and recovery](2026-08-07-phase-03.md) - WebSocket authentication, durable ACK replay, command deduplication, and recovery diagnostics.
 * [Phase 4 skillless Codex app-server driver](2026-08-08-phase-04.md) - Direct app-server mapping, context isolation, semantic completion, real-model failures, and fixes.
+* [Phase 4b UX gate — interaction map and component decisions](2026-08-08-phase-04b-ux.md) - Approved live-console interaction contract, adapt/reject component record, and keyboard/accessibility acceptance criteria.
diff --git a/packages/paperclip-runner/knowledge/log.md b/packages/paperclip-runner/knowledge/log.md
index 5fc467a9979..b2bf87f17c4 100644
--- a/packages/paperclip-runner/knowledge/log.md
+++ b/packages/paperclip-runner/knowledge/log.md
@@ -14,3 +14,7 @@
   compact task envelope, isolated model context, semantic completion tools,
   canonical event/replay mapping, safe real-model example, tutorial, journal,
   and exact verification trace.
+* **Phase 4b UX gate**: Approved the live-console interaction map, the
+  shadcn/AI Elements adapt/reject decision record with a zero-new-runtime-
+  dependency rule, keyboard/accessibility acceptance criteria, four token
+  additions, and annotated desktop/mobile mockups.