Skip to content

feat(replay): the replay tails the session; the now line replaces the state diagram; clock axis, veil, thread focus - #103

Merged
lroolle merged 9 commits into
mainfrom
feat/replay-tail
Sep 1, 2026
Merged

feat(replay): the replay tails the session; the now line replaces the state diagram; clock axis, veil, thread focus#103
lroolle merged 9 commits into
mainfrom
feat/replay-tail

Conversation

@lroolle

@lroolle lroolle commented Aug 28, 2026

Copy link
Copy Markdown
Member

Replay rev 2: the replay tails the session, the now line replaces the state diagram

Eric's read of 0.45.0's stage: "the replay should tail the session; the transition stage is inaccurate and a bad representation of Claude Code agent work; refine the layout, the design, the live, the motion." Verified on a real 4h trace (1209 pairs, 25 sessions, 2 subagents) before changing anything:

  1. Replay did not tail. A landed pair redrew the strip but never moved replay.cursor — a reader parked at the live edge fell behind silently while the beat, the convo and the "state" froze at entry time.
  2. The diagram modelled the wrong machine. Six nodes / nine counted edges is a Markov chain; Claude Code's loop is prompt -> [model -> tools]* -> model -> answer. reply is an outcome not a state, waiting read 0 on every trace, human never lit, the counts were redundant (tools 160 = model>tools 160) or inconsistent (model>agents 1 vs agents>model 2), and model 164 . 33m 12s / tools 160 . 17m 31s restated the convo header's time chip verbatim (ui.md: a number is stated once).
  3. The moment was not on screen: scrubbing left the convo at turn 01, the playhead was invisible over the accent fill, no clock axis, every thread's pairs drawn in one model lane.

What changed

  • Tail: on a pair frame the page measures where the reader was BEFORE ingest; at the edge the cursor follows, the now line and beat move, the convo sticks to its bottom only if it was there. Behind the edge nothing moves and the live chip flips to a ⤓ live snap-back. history frames never move the cursor. Playback reaching the end of a live tape parks at the edge.
  • The NOW line (nowAt, built on stateAt): dot in the lane color, the state, what is running (tool names with repeats folded / running children by name / the failed status), held time only where the extent is a wire fact, absolute clock it began at. The dot beats only for a live in-flight request. The diagram, stateCounts and its transitions table are gone; soFar keeps the one figure nothing else states (the call tally).
  • Strip: clock axis (axisTicks, local time, day-major ticks drop a rule), the FUTURE is veiled instead of the past being tinted, playhead halo + flag, thread focus (selected thread full, others .other).
  • The convo follows the cursor: every seek/step/chapter/tick lands on its bottom, instantly.
  • Transport: ⏭ / Home / End, absolute clock before the offset, ● live / ⤓ live chip on live pages, ✕ exit only exits.
  • The beat names the loop's head (click = chapter); so far tally under it.

Spec rewritten in place: docs/design/replay-stage.md (rev 2 section explains each change against the three findings). Docs: web-ui.md, ui.md, SKILL.md, README, CLAUDE.md, CHANGELOG under Unreleased.

Verification

  • bun test: 847 pass / 0 fail. tsc clean. make build OK.
  • Real browser (host Chrome via claude-in-chrome) on the 2026-08-19 trace: veil, axis, playhead, now line + tooltip, beat head, convo at the moment after every seek, playback follows, present mode, both themes.
  • test-output/replay-tail/check.sh (static rev-2 surface, frozen fixture): 19 PASS / 0 FAIL.
  • test-output/replay-tail/check-live.sh — a traced claude -p run with a headless browser attached MID-RUN in replay: 17 PASS / 0 FAIL — a landed pair advanced the cursor at the edge, the convo stuck, ● live while tailing, model . thinking with the live class while a request was in flight, a pair landing while scrubbed back did NOT move the cursor, ⤓ live snapped back.
  • Two review passes (one by a reviewer agent): found and fixed the zero-width first render caching an empty clock row, the axis using today's tz offset on old data, a history merge desyncing the playhead from the lanes, the beat fade firing on telemetry pairs, a double session render per tailed pair, since in the future before the first pair, no tick floor on multi-day spans.

Not in this PR (spec "Not done"): folding the context overview and the strip into one component, a compressed-time axis for multi-day sessions, a mid-span DST shift on the ruler, per-call durations (P3 chunk timing).

🤖 Generated with Claude Code

lroolle and others added 6 commits August 29, 2026 07:17
The six-node diagram modelled the wrong machine: reply is an outcome, not
a state; waiting read 0 on every trace; the counts restated the convo
header's time chip verbatim. In its place one row — nowAt: the observed
state at the cursor, what is running, how long it has held (only where the
extent is a wire fact), and the absolute clock it began at. soFar keeps
the one figure nothing else states, the per-tool call tally. beatAt gains
the loop's head, so a step always names the task it serves. axisTicks
lands with them for the strip's clock ruler.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…d in focus

The strip could not say WHEN: no axis, every thread's spans drawn on top of
each other, and a 2px accent playhead over an accent-tinted past over
accent-hued model spans — invisible in any dense region.

- a clock row (axisTicks) above the human lane, labelled in local time,
  major ticks dropping a rule through every lane
- the veil: the FUTURE dims from the playhead to the right edge. The
  accent-tinted past (#rp-fill) is gone — it cost the model spans their
  contrast exactly where the reader looks
- the playhead gets a surface-color halo and a flag on the clock row
- thread focus: the rail's selection draws full, every other thread ghosts
- transport: ⏭ / End seek the end of the tape (the live edge on a live
  run), a live chip states whether the page is tailing or offers the snap
  back, the clock reads absolute local time before the tape offset, and
  ✕ exit means exit — nothing else

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ursor

Replay claimed live and was a snapshot: a landed pair redrew the strip but
never moved the cursor, so a reader parked at the edge fell behind in
silence while the strip grew.

- a pair frame measures wasAtEdge/wasAtBottom BEFORE ingest; at the edge
  the cursor moves to the new one, the now line and the beat follow, and
  the convo sticks to its bottom only if the reader was there. Behind the
  edge nothing moves and the chip flips to the snap-back
- history frames never move the cursor: the edge did not change
- playback reaching the end of a live tape parks at the edge instead of
  just stopping — from there the tail rule applies
- every other cursor change (seek, step, chapter, tick) scrolls the convo
  to its bottom instantly: the bottom IS the moment
- the beat gets the page's 160ms live-arrived fade on a tail advance only

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…gelog

The spec's "Revision 2" section is the brief this branch implemented; the
rest of the doc is now the current truth. web-ui.md, the SKILL, the README
and CLAUDE.md drop the diagram and pick up the now line, the clock axis,
the veil, thread focus and the tail; ui.md's replay rule says the future
is what dims, never the past.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…us; the spec names the edge, the instant, the ruler step

- a subagent selected in the rail drew its own agent span ghosted (the
  lane keyed off parentKey only); it now stays full
- axisTicks picks the FINEST step that clears 72px — the comment and
  the spec said coarsest, the code and its test always did finest
- the spec states what the harness had to guess: the transport's
  <length> is the strip's axis (stretched by an open start) while 'at
  the edge' is measured against the newest completed pair; a tools gap
  overlapping a running child reads agents; spans are half-open so the
  gap owns the instant at a pair's end; reading pages leave the live
  chip's skeleton span empty
- changelog: ascii separators, no UI glyphs

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1. The strip's measured frame width is part of its cache KEY. Entering
   replay from the requests tab renders once while #session-view is still
   display:none (the hash route is async): axisTicks(0) drew no clock and
   no span label got .w24, and the key-guard froze that draw forever. The
   route's own render now repairs it; a bounded rAF re-render backs it up.
2. The ruler's zone offset is measured on the DATA (new Date(span.t0)),
   not on today — a July trace read in January no longer rules an hour off
   its own tips. The comment claiming otherwise is corrected; a mid-span
   DST transition is named in the spec's "Not done".
3. A history frame re-reads the bar on the new axis: the playhead, the
   veil and the tape length were left on the pre-merge one.
4. The live-arrived fade fires only when the beat actually moved —
   telemetry, count_tokens probes and usage polls land at the edge too.
5. A tail advance renders the session view ONCE, at the new cursor,
   instead of once at the old cursor and again at the new one.
6. nowAt states no `since` for the idle before the first pair: stateAt
   bounds that hole by a clock the cursor has not reached.
7. axisTicks holds the 72px floor past the ladder's top rung — 30 days at
   900px drew 31 day-ticks 29px apart.
8. Tests: a bare expression now asserts; the history test merges a real
   earlier pair and checks the absolute clock held while the tape grew;
   the child-thread focus case is covered.
9. Nits: the live chip rewrites only when its state flips (it was
   destroying the button mid-hover); seekEnd's unreachable enterReplay
   branch is gone; the agents-lane comment is one sentence again; the
   duplicated focus test folds into oth(k, k2).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
lroolle and others added 3 commits August 29, 2026 07:38
…in the bar

Rev 2 deleted the state diagram on the argument that the strip already
carried the loop's shape. Eric's read: the strip shows the loop unrolled;
the picture of the machine was the thing worth keeping — "redesign it
accurately, live and horizontal, in the replay bar".

The loop row (#rp-now) sits between the strip and the transport, aligned
to the strip's gutter: Claude Code's machine — human -> model -> one
hand-off slot that reads tools / agents / waiting -> model ... -> human —
as fixed-pixel inline SVG, the state at the cursor lit with the edge it
came in by (prompt, calls, results, answer; a failed request is the model
chip in red with no edge), and beside it what is running, how long it has
held where that is a wire fact, and the absolute clock it began at. No
counts on it. Click seeks to the step that opened the state; at the live
edge the model chip beats while a request is in flight.

loopAt (src/replay.ts) builds on nowAt/stateAt: adjacency for a landed
request's edge, the protocol's edge for a live one, the answer edge only
when the reply actually landed. The stage's #stage-now row is gone — the
state is stated once. Spec: docs/design/replay-stage.md "Revision 2.1".

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…d's own time with idle folded

Rev 2.1's three 16px chips read as a legend, not a diagram. The loop row
is now Claude Code's loop as a flowchart: boxes human / model / the
hand-off slot (tools, agents or waiting), a `calls?` decision diamond —
the reply's tool_use blocks, a wire fact — and five labelled edges
(prompt, yes, results over the row, no · answer under it). The state at
the cursor lights its box, the edge it came in by lights and FLOWS (an
animated dash: the transition in progress, a deliberate second motion
owner while replaying), the model box beats only for a request in
flight. Fixed pixel geometry; prefers-reduced-motion drops both.

The strip's axis was the whole capture: on a 10h38m trace the selected
1h34m session sat in the left quarter and three quarters drew nothing.
The axis is the selected thread's own extent now (threadExtent), and a
gap in its busy time over five minutes folds to a 28px hatched break
with the skipped duration in the clock row (timeScale / scaleX / scaleT
in src/replay.ts — the one x<->t mapping every strip surface uses:
spans, ticks, veil, playhead, slice, drag, wheel zoom). The ruler rules
per busy segment, so a folded 8h never coarsens the ticks beside it;
break labels anchor inside the track. The transport's clock and length
stay real wall-clock.

Implemented by an Opus 5 worker against docs/design/replay-stage.md
("Revision 2.2"); verified in Chrome on the 2026-08-28 trace.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…replay tail

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@lroolle
lroolle merged commit 13c947a into main Sep 1, 2026
1 check passed
@lroolle
lroolle deleted the feat/replay-tail branch September 1, 2026 03:10
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