Problem
The Factory visualization can only play back a hardcoded demo recording. Real orchestrated runs are viewable as static final-state snapshots, but there is no way to replay them as animations. This limits the system's usefulness for reviewing past runs and understanding how the orchestration pipeline progressed.
Context
- The server already has
GET /api/runs/:projectSlug/:runId which reads run-index.json + run-log.jsonl, folds events via foldEvents(), and returns the final CanonicalRunStatus
- The client's
PlaybackController accepts CanonicalRunStatus[] and plays them at fixed intervals — it's source-agnostic
- The demo uses
foldEvents(header, events.slice(0, idx + 1)) (fold-to-cursor) to generate intermediate snapshots from raw events — this same pattern works for any run
foldEvents, RunHeader, and RunEvent are already available in the client bundle via @codeassembly/run-core
- v3 runs have event logs (
run-log.jsonl); v1/v2 runs do not and cannot be replayed
Solution
Server: raw events endpoint
Add GET /api/runs/:projectSlug/:runId/events returning { header: RunHeader, events: RunEvent[] }. Extract the v3 read-and-parse logic from parseRunData() into a reusable parseRunRawData(runPath) that returns header + events without folding. Refactor parseRunData to call it then fold. v1/v2 runs return 404 with a descriptive error.
Client: player redesign (streaming-app mental model)
Redesign the player UI following a streaming-app model (like Amazon Prime / Apple TV with live channels):
- Source selection is separate from transport controls. Selecting a demo recording is like choosing a movie; clicking a completed run's "Replay" is like selecting a title to watch. These are source selection actions, not player actions.
- Player controls (play/pause/step/stop/speed) are their own pop-out panel (
PlayerPanel, replacing the old DemoControlPanel). The player appears only when a source is active and contains only transport controls — no recording selector dropdown.
- Live view is the default: clicking a live orchestration is like tuning into a live channel.
Add fetchRunEvents() to the client API. Create a shared generateSnapshots(header, events) utility producing one CanonicalRunStatus per event via fold-to-cursor. Create useRunPlayback hook that fetches events, generates snapshots, and feeds them into the existing PlaybackController.
Playback behavior
- Clicking "Replay" loads the run into the player. If the player is already playing, the new source auto-plays. Otherwise it loads in stopped state.
- If the run is already loaded, "Replay" is a no-op.
- The transport "Stop" button exits playback mode entirely (returns to live/static view). There is no intermediate "stopped but source loaded" state.
- Starting replay stops demo mode; starting demo stops replay.
Acceptance criteria
Key files
packages/run-core/src/parsers/run-data-parser.ts — parseRunData, new parseRunRawData
packages/run-core/src/run-data-parse-error.ts — extend RunDataParseErrorCategory with 'no_event_log'
packages/factory/src/server/routes/runs.ts — new events endpoint
packages/factory/src/client/api/client.ts — fetchRunEvents
packages/factory/src/client/playback/playback-controller.ts — existing controller, new PlaybackSource type
packages/factory/src/client/playback/generate-snapshots.ts — shared snapshot generation
packages/factory/src/client/hooks/usePlayback.ts — generalize to accept PlaybackSource
packages/factory/src/client/hooks/useRunPlayback.ts — new hook
packages/factory/src/client/components/PlayerPanel.tsx — renamed from DemoControlPanel, transport only
packages/factory/src/client/App.tsx — merge playback sources, separate source selection from player
Problem
The Factory visualization can only play back a hardcoded demo recording. Real orchestrated runs are viewable as static final-state snapshots, but there is no way to replay them as animations. This limits the system's usefulness for reviewing past runs and understanding how the orchestration pipeline progressed.
Context
GET /api/runs/:projectSlug/:runIdwhich readsrun-index.json+run-log.jsonl, folds events viafoldEvents(), and returns the finalCanonicalRunStatusPlaybackControlleracceptsCanonicalRunStatus[]and plays them at fixed intervals — it's source-agnosticfoldEvents(header, events.slice(0, idx + 1))(fold-to-cursor) to generate intermediate snapshots from raw events — this same pattern works for any runfoldEvents,RunHeader, andRunEventare already available in the client bundle via@codeassembly/run-corerun-log.jsonl); v1/v2 runs do not and cannot be replayedSolution
Server: raw events endpoint
Add
GET /api/runs/:projectSlug/:runId/eventsreturning{ header: RunHeader, events: RunEvent[] }. Extract the v3 read-and-parse logic fromparseRunData()into a reusableparseRunRawData(runPath)that returns header + events without folding. RefactorparseRunDatato call it then fold. v1/v2 runs return 404 with a descriptive error.Client: player redesign (streaming-app mental model)
Redesign the player UI following a streaming-app model (like Amazon Prime / Apple TV with live channels):
PlayerPanel, replacing the oldDemoControlPanel). The player appears only when a source is active and contains only transport controls — no recording selector dropdown.Add
fetchRunEvents()to the client API. Create a sharedgenerateSnapshots(header, events)utility producing oneCanonicalRunStatusper event via fold-to-cursor. CreateuseRunPlaybackhook that fetches events, generates snapshots, and feeds them into the existingPlaybackController.Playback behavior
Acceptance criteria
GET /api/runs/:projectSlug/:runId/eventsreturns header + events for v3 runsparseRunDatarefactored to useparseRunRawDatainternally (no behavior change)Key files
packages/run-core/src/parsers/run-data-parser.ts—parseRunData, newparseRunRawDatapackages/run-core/src/run-data-parse-error.ts— extendRunDataParseErrorCategorywith'no_event_log'packages/factory/src/server/routes/runs.ts— new events endpointpackages/factory/src/client/api/client.ts—fetchRunEventspackages/factory/src/client/playback/playback-controller.ts— existing controller, newPlaybackSourcetypepackages/factory/src/client/playback/generate-snapshots.ts— shared snapshot generationpackages/factory/src/client/hooks/usePlayback.ts— generalize to acceptPlaybackSourcepackages/factory/src/client/hooks/useRunPlayback.ts— new hookpackages/factory/src/client/components/PlayerPanel.tsx— renamed fromDemoControlPanel, transport onlypackages/factory/src/client/App.tsx— merge playback sources, separate source selection from player