Skip to content

feat(demo): Stage 2 PR-2 — PTY transcript replay + recorder tool#222

Merged
luokerenx4 merged 1 commit into
feat/ui-demo-stage2-skeletonfrom
feat/ui-demo-stage2-pty-replay
May 29, 2026
Merged

feat(demo): Stage 2 PR-2 — PTY transcript replay + recorder tool#222
luokerenx4 merged 1 commit into
feat/ui-demo-stage2-skeletonfrom
feat/ui-demo-stage2-pty-replay

Conversation

@luokerenx4
Copy link
Copy Markdown
Contributor

Summary

The demo terminal now plays back a recorded PTY transcript through xterm.js instead of showing a static "terminal disabled" stub. Workspaces without a registered transcript still fall back to the stub from PR-1.

Stacks on top of PR #221 (Vercel + skeleton).

What changed

Transcript format

// ui/src/demo/types.ts
interface Transcript {
  label: string
  durationMs: number
  defaultSpeed?: number          // 1.0 = real-time
  frames: readonly TranscriptFrame[]
}
interface TranscriptFrame {
  atMs: number                   // milliseconds since start
  bytesB64: string               // base64-encoded raw PTY bytes
}

bytesB64 is the raw bytes the PTY WebSocket sent down. ANSI escapes, cursor positioning, colors all replay 1:1 because they ride along inside the bytes — no re-parsing needed.

Replay component

ui/src/demo/DemoTerminalReplay.tsx — instantiated by Terminal.tsx when VITE_DEMO_MODE is on. Looks up a transcript by wsId; renders the stub from PR-1 if none registered. For sessions WITH a transcript:

  • Spins up xterm with the same theme + addons (FitAddon, WebLinks, WebGL) as the real Terminal
  • Polls the container until it has real dimensions before fitting + writing — fixes an xterm "Cannot read properties of undefined (reading 'dimensions')" race in Viewport.syncScrollArea that fires when fit() or term.write() runs before the renderer is fully laid out, AND avoids the "default 80×24 stuck at narrow width" failure mode where writes go to a 0-cell terminal
  • Schedules term.write at recorded timestamps via requestAnimationFrame
  • End of transcript → ↻ Replay again button

Recorder tool

ui/src/demo/recorder/recorder.ts — dev-mode helper that monkey-patches window.WebSocket to tap PTY frames. Usage:

// In browser DevTools console while running `pnpm dev` (NOT demo mode):
__demoRecord.start()
// interact with terminal as normal
__demoRecord.stop('claude-research-may-2026')
// browser downloads transcript-claude-research-may-2026-<ts>.json

main.tsx auto-loads it in dev (not demo, not prod) so the recorder is always available alongside the real backend. URLs not matching /api/workspaces/pty pass through unchanged — limited blast radius.

See ui/src/demo/recorder/README.md for the full record-and-register flow.

Placeholder transcript

ui/src/demo/fixtures/transcripts/welcome.ts — hand-crafted Claude-Code-style intro: banner, typed user query, tool calls, multi-paragraph response describing OpenAlice's architecture. Placeholder only — PR-3 swaps this out for a real recorded session captured via the recorder tool.

Registered in fixtures/transcripts/index.ts keyed to DEMO_WORKSPACE_ID. Adding more workspaces with their own transcripts is a one-line addition there.

Verification

Check Result
pnpm -F open-alice-ui exec tsc -b clean
pnpm -F open-alice-ui build:demo 277KB demo chunk + 2.83MB main, no console warnings beyond pre-existing chunk-size info
pnpm -F open-alice-ui build (prod) zero __demoRecord / DemoTerminalReplay / transcriptsByWorkspace / welcomeTranscript / startRecording / stopRecording / VITE_DEMO_MODE refs in dist (the single recorder substring match is an unrelated audio_get_recorder_count string inside a transitive dep)
pnpm dev:demo + Playwright transcript plays end-to-end, ANSI colors + bold + dim render correctly, line wrapping respects container width, "Replay again" button restarts cleanly

Screenshot at end of replay attached in the PR thread (final-frame state shows the agent's full response with proper tool-call formatting and bullet styling).

Test plan

  • tsc -b clean
  • Build prod (no demo flag) — zero demo refs in dist
  • Build demo + Playwright smoke
  • Replay completes; ↻ button works
  • Manual: user runs pnpm dev, exercises __demoRecord.start() / stop() on a real terminal session, replaces welcome.ts placeholder with the real transcript (= PR-3's job to land)

Out of scope (deferred)

  • The real recorded transcript (PR-3 captures it once the user runs a flagship session)
  • Multiple transcripts per workspace, transcript selection UI (Stage 3 if ever)
  • Speed controls / scrubber (current replay is auto-play, fixed real-time)

Boundary touch

None — no trading / auth / broker / migrations code modified.

🤖 Generated with Claude Code

The Terminal in demo mode now plays back a recorded PTY transcript using
xterm.js, replacing the static "terminal disabled" stub for workspaces
that have a registered transcript fixture. Stage 1's stub remains the
fallback for any workspace without one.

Transcript format (`ui/src/demo/types.ts`): `{label, durationMs, frames:
[{atMs, bytesB64}]}`. bytesB64 is base64-encoded raw PTY output bytes;
ANSI escapes, colors, cursor moves all replay faithfully because they
ride along inside the bytes.

DemoTerminalReplay (`ui/src/demo/DemoTerminalReplay.tsx`) — looks up a
transcript by workspace id, instantiates xterm with the same theme +
addons as the real Terminal, polls the container until it has real
dimensions before fitting + writing (avoids xterm's "Cannot read
properties of undefined (reading 'dimensions')" race in
Viewport.syncScrollArea), then schedules `term.write` calls at the
recorded timestamps via rAF. End of transcript → "↻ Replay again"
button.

Recorder (`ui/src/demo/recorder/recorder.ts`) — dev-mode helper that
monkey-patches `window.WebSocket` to tap server→client frames on URLs
matching `/api/workspaces/pty`, captures `{atMs, bytesB64}` per frame,
and on `__demoRecord.stop(label)` packages them as a JSON download.
Main.tsx auto-loads it in dev mode (NOT demo mode, NOT prod) so the
console-driven recorder is available alongside the real backend.

Placeholder transcript (`fixtures/transcripts/welcome.ts`) — hand-
crafted Claude-Code-style intro with banner, typed query, tool calls,
and a multi-paragraph response describing OpenAlice's architecture.
PR-3 will replace this with a real recorded session captured via the
recorder tool.

Verified:
- pnpm -F open-alice-ui exec tsc -b clean
- pnpm -F open-alice-ui build:demo: 277KB demo chunk + 2.83MB main chunk
- pnpm -F open-alice-ui build (prod): zero __demoRecord/
  DemoTerminalReplay/transcriptsByWorkspace/welcomeTranscript/
  startRecording/stopRecording/VITE_DEMO_MODE refs in dist
- pnpm dev:demo + Playwright walk: transcript plays back end-to-end,
  ANSI colors + bold + dim render correctly, line wrapping respects
  container width, "Replay again" button re-runs from start

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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