feat(examples/chat): /stage — the real chat beside the real devtools, seekable to any recorded time (live-stage plan 2 of 3) - #1030
Merged
Merged
Conversation
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ory validation Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…cks checkpointIndex and histories; fixture moves out of the spec Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…h an authored hold Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…r pins one resume run and time-ordered events Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…d a seekable target time Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…re loading and pins the absolute-time gate Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… time, with hold and rewind The controller performs each crossed run's recorded action against the real LangGraph agent, draining the previous run to the next run's start first — the bridge's submit path aborts whatever stream is open, so acting early truncates the run being replaced. Rewinding resets the agent and the transport, then fast-forwards; seeks are coalesced so a burst in one frame performs each action once. Two departures from the plan's sketch, both forced by the real bridge: - The reported phase is the phase of the moment REACHED (t minus an epsilon), not the one about to begin. Runs are laid end to end, so a run's end is the next run's start, and phaseAt() would name the incoming run before it has rendered anything. - StageReplayTransport.getHistory now answers only the snapshot recorded at exactly the current replay position, never an earlier one. The bridge force-refreshes history at the close of every run (stream-manager.bridge.ts finalizeClosedAttempt → refreshHistory(true)) and projects it over the transcript, so an at-or-before snapshot snapped the replayed transcript back to whatever the recorder last captured. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… the run; phase boundary named; reload runs carry no events Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ript Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…double announcement Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…eekable by ?t= and by postMessage Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…he devtools open, docks responsively, and reports the approve stream as streaming Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The record host now resolves a submit/resume only once the run has closed AND its closing history refresh has landed. Resolving as soon as the agent reported "running" let the script start the next beat (a reload, which detaches and re-adopts the thread) while the first run was still streaming, which aborted it: run 0 recorded zero events and its thread had no checkpoints at reload time. A run that closes on an interrupt is the one exception — the bridge finalizes it as the interrupt arrives and skips the refresh, in replay exactly as in record — so the fixture spec expects a snapshot after every completed run except the one a resume follows. Take 6: 7 runs, 2,275 events, one list_backups → one delete_backups pause, a 116-character audit on resume, 9.1 MB pretty-printed like hero-replay.json. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…website fallback stage.spec.ts drives the committed replay through ?t=: the chat renders beside the devtools, the approve hold shows the inert interrupt panel over the backup table, and the end of the recording mounts the generated form with the thread in the Timeline tab. record-stage.config.ts + record-stage-stills.record.ts capture one still per beat at desktop (1200x720) and phone (390x650 at DPR 2, shipped 585 wide) for the website's non-pinned fallback; each still asserts its beat and its byte budget. The recorder exposed a gap: a seek applies a run in one burst, so the chat's last scroll write lands before the tool view, the A2UI surface and the interrupt panel render, leaving the newest content ~700px below the fold. The stage now pins the transcript to its bottom once a seek settles. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…e2e guards the pinned viewport The transcript pin scheduled one two-frame pair per seek and dropped any publish that landed while that pair was in flight, so content rendering after the pending pair's inner frame could stay below the fold. pinTranscript now keeps one pair in flight at a time and a publish during the pair re-arms exactly one more pair after it (pinPending). The destroy hook still cancels whichever frame is pending and clears the flag, so nothing runs after destroy. The docblock now also states that this deliberately overrides the chat's unpin-on-user-scroll for a scrubbed display surface. The pin still reaches into the chat's private .chat-scroll container. The proper fix is a public scrollToBottom() on ChatComponent (today the protected onScrollBubbleClick); that touches libs/ and is deferred, recorded as a TODO at the call site. The unit spec polls frame by frame instead of hard-coding "two frames deep", and gains a re-arm case whose scrollHeight grows after the first pair's inner frame. Mutation-checked: removing the re-arm fails only the new case; removing the publish() call fails both. The still recorder looks beats up by name (StageBeat) rather than by index, collects oversized files and asserts once after both loops so a failure cannot leave a half-rewritten set, and its header says deviceScaleFactor 2 applies to both sizes. stage.spec.ts asserts the interrupt panel and the A2UI surface are in the viewport, guarding the pin end to end, and explains the +1 on the hold boundary. The shared window augmentation moves to e2e/stage-globals.d.ts. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…nstead of racing the real one Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
blove
enabled auto-merge (squash)
September 6, 2026 03:10
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Contributor
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Plan 2 of the homepage live stage (
docs/superpowers/specs/2026-09-05-homepage-live-stage-design.md§4–6): the website will scrub one real run with scroll. This PR builds the thing it scrubs, in the demo app, verifiable on its own.What
/stage: real<chat>beside real<chat-debug>, replayingpublic/stage-replay.json(a live capture of four beats: stream with citations, reload + fork, the backup cleanup with its interrupt, the generated form) to any recorded time via?t=or thetplane-stagepostMessage protocol.StageReplayTransportgates recorded events behind a target time;StageControllerperforms each run's recorded action as time crosses it, holds at the interrupt for an authored window, and rewinds by reset + fast-forward.?record=1runs the four-beat script against the live backend throughStageRecordingTransport, which captures runs with their actions, history responses, and the thread id. The record host resolves a run only once its closing history refresh has landed; resolving earlier let the reload beat abort the first run mid-stream.scrollToBottom()onChatComponentis noted as the follow-up that removes the.chat-scrollcoupling.apps/website/public/screenshots/stage-*.webpfor the website's non-pinned fallback (phone stills are chat-only: below 768px the stage docks no devtools).examples/chat/angularproduction config no longer externalizes@threadplane/chat/debug; it ships in the lazy stage chunk. Initial bundle unchanged at 1.65 MB.Tests
Types, timeline, transport gate, controller against the real LangGraph agent (forward, reload restore, hold, resume, rewind, coalescing), recording transport, script order, bridge, component (incl. pin re-arm, mutation-checked); fixture spec pins the four beats and the guardrail (7/7 on take 6); e2e on the committed replay with viewport guards. Verified: demo unit suite 250/250, lint 0 errors, production build green,
stage.spec.ts+hero.spec.ts+interrupt-approval.spec.ts6/6 on aimock, website section-media spec 7/7.🤖 Generated with Claude Code