feat(replay): harden TUI browser replay and show Codex prompts#128
Conversation
Adds the pure-state side of step / play modes for the TUI replay panel:
- New `replayCursorEventIndex` / `replayPlaybackActive` / `replayPlaybackSpeed`
fields on AppState. Cursor null = overview mode (existing UX). Cursor set
= step mode. Active flag is the play/pause toggle on top of step mode.
- New `lib/replay-playback.ts` module with deterministic helpers:
enter / exit; step ±N events; jump to next/prev block boundary;
jump to next/prev "interesting" moment (peak minute, model switches,
block starts, outlier-cost events); toggle / tick play loop;
set speed (60×/240×/600× → 1/4/10 events per 100ms tick);
computePlaybackSummary for cumulative cost/tokens/cache/model-mix.
- Replay panel learns a `playback: ReplayPlaybackView | null` parameter:
when set, prepends a status line (cumulative cost, event N/M, cache
rate, ▶ playing / ⏸ paused), draws a ▼ playhead column on the
activity bar, marks the active block in amber, and shows an
"events near cursor" list (±2 around the cursor event).
- Help footer rotates between overview ("[s] enter step mode") and
playback ([n/p step, N/P block, i interesting, space play, …]).
This commit ships only the data + render plumbing. The next commit
wires the keyboard + the play-loop timer into tui/index.ts.
Tests: 25 cases in lib/replay-playback.test.ts covering boundaries,
clamping, block tracking, interesting-moment detection, tick stop at
end-of-day, no-op safety when cursor is null or report empty.
Routes replay-view input through a new handleReplayPlaybackInput dispatcher that activates only on the replay view: - s enter step mode (overview only) - s/Esc exit step mode (in step mode) - n / → next event (pauses if playing) - p / ← previous event - N / P next / previous flow-block boundary - i / I next / previous interesting moment - Home jump to first event - End jump to last event - space play / pause (overrides the existing block-toggle for replay-only) - 1/2/3 set playback speed to 60× / 240× / 600× Other keys (q, j/k for block scroll, h/l for date shift, view switches) fall through to the regular replay handlers, so step mode is purely additive. A module-level setInterval drives the play loop at 100ms ticks; the ticker advances `eventsPerTick(speed)` events per fire and stops itself when it reaches the end of the day OR the user pauses. Switching away from the replay view (or shifting date with h/l) calls `resetReplayInteraction`, which now also exits playback and clears the timer — so we never leave a runaway interval behind.
`tokenleak replay <date> --record day.cast` (alias --cast) renders
the day as an asciinema v2 cast file: open it with `asciinema play`
to get a cinematic playback of your AI session — every prompt's
cumulative cost, model switches, cache trend, and the active flow
block tick by in real time at the chosen speed.
Implementation:
- New packages/cli/src/replay-cast.ts: pure renderer that emits one
frame per real event with a screen-clear-then-redraw payload,
timed at (event.ts - dayStart) / speed seconds. Bursty stretches
scrub fast, idle stretches give natural pauses.
- Per-frame layout (plain text, fits 100x32 by default):
title + event counter
cost / tokens / cache · clock · current event
activity heatmap with a ▼ playhead column
active block info
events-near-cursor list (±2)
cumulative model-mix bar chart
- New CLI flags on `replay`:
--record / --cast <path> write a v2 cast file
--speed <number> playback speed (default 240)
- Mutual exclusion: `--record` and `--interactive` cannot be combined;
`--format / --output / --width / --port / --open / --speed` are
warned-and-ignored when the wrong mode is set.
- Empty days emit a single placeholder frame instead of failing.
Tests:
- packages/cli/src/replay-cast.test.ts: 8 cases — header shape,
per-event frame count, frame-tuple JSON shape, speed scaling,
screen-clear escape prefix, cumulative cost / model-mix in final
frame, empty-day placeholder.
- packages/cli/src/replay-cli.test.ts: 7 new cases for --record /
--cast / --speed parsing + validation (positive number, ≤10000,
required value).
Smoke: `tokenleak replay 2026-04-22 --record /tmp/day.cast --speed 240`
on a real 211-event day produced a 212-line valid cast that plays
cleanly under `asciinema play`.
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds asciinema cast recording and speed control to the replay CLI; implements cast generation and tests; extends the live replay server/template to support multi-day providers and a 91-day heatmap with prompt-detail UI; captures Codex prompts into UsageEvents; and adds cursor-driven playback controls, TUI integration, state, and tests. ChangesReplay + Playback + Codex prompt capture
Sequence Diagram(s)sequenceDiagram
participant User as CLI User
participant CLI as CLI (`cli.ts`)
participant Cast as buildReplayCast (`replay-cast.ts`)
participant FS as File System
User->>CLI: run with --record <path> --speed 240
CLI->>CLI: parse & validate flags (dateExplicit, speed)
CLI->>Cast: buildReplayCast(report, {speed:240})
Cast->>Cast: compute frames & render asciinema v2 output
Cast-->>CLI: asciinema string
CLI->>FS: write cast file
CLI-->>User: print "Wrote asciinema cast…" and exit
sequenceDiagram
participant TUIUser as TUI User
participant TUI as TUI (`index.ts`)
participant Playback as replay-playback (`replay-playback.ts`)
participant Panel as Replay Panel (`replay.ts`)
participant Server as Live Server (`replay-live-server.ts`)
TUIUser->>TUI: press 's' (enter playback)
TUI->>Playback: enterReplayPlayback(state)
Playback->>TUI: set cursor=0, active=true
TUI->>Panel: createReplayPanel(..., playback)
Panel-->>TUIUser: render playback UI with playhead
TUIUser->>TUI: press Space (toggle play)
TUI->>Playback: toggleReplayPlayback(state)
TUI->>TUI: start playback timer
TUI->>Playback: tickReplayPlayback(state)
Playback->>TUI: advance cursor by eventsPerTick()
TUI->>Panel: re-render with updated cursor
sequenceDiagram
participant Browser as Browser
participant Server as Live Server (`replay-live-server.ts`)
participant Provider as ReplayLiveDataProvider
participant Template as generateReplayLiveHtml (`replay-live-template.ts`)
Browser->>Server: GET /?date=YYYY-MM-DD
Server->>Provider: getReport(date)
Provider-->>Server: ReplayReport | null
Server->>Template: generateReplayLiveHtml(report, {heatmap, initialDate})
Template-->>Server: HTML
Server-->>Browser: 200 OK + HTML
Browser->>Server: GET /api/replay?date=YYYY-MM-DD
Server->>Provider: getReport(date)
Provider-->>Server: ReplayReport | null
alt report found
Server-->>Browser: 200 + JSON
else report missing
Server-->>Browser: 404
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Live testing surfaced two issues with PR #128's TUI playback view: 1. Glyph overlap. The playback overlay added 3 sections (status header, events-near-cursor, 2-line help) on top of an already-30-row panel. When the total exceeded the terminal viewport, opentui's flex layout silently *compressed* sibling rows on top of each other — the "Events near cursor" header collapsed onto the first event row, the "Pulse (tok/min)" label onto the y-axis label, the day-summary onto the peak label, and the 2-line help footer onto a single jumbled row. 2. The browser interactive scrub had no in-TUI affordance — new users never discover it. Fix: - In playback mode, drop the standalone pulse chart (the activity bar already shows the same shape with a ▼ playhead) and the day-summary line (its data lives in the playback status header). Shrink the flow-block list from 8 → 3 rows. Collapse the 2-line help footer to a single compact row. Total panel rows now ≲ 22 — comfortably fits on a standard 24-row terminal. - Always render a prominent "press [b] to open the interactive browser scrub" banner directly under the title, in BOTH overview and playback modes. Once a port is set on state, the banner switches to a one-line "✓ browser open at http://localhost:<port>/" status. The flow-block list, activity bar, status header, events-near-cursor list, and help row remain. The overlap is gone at terminals 80×24+. Tests (packages/tui/src/panels/replay.test.ts, 5 new cases): - overview mode renders the pulse chart + day summary - playback mode does NOT (and the help row collapses to one line) - the "press [b]" banner is visible in both modes - once liveServerPort is set, the banner becomes "browser open" status - null-report path still shows the banner
Implements the second half of the press-[b] affordance: the actual key
handler + server lifecycle.
- New `replayLiveServerPort: number | null` on AppState. Drives the
banner state in the panel.
- Press `b` from the replay view (works in both overview and playback
modes) → start startReplayLiveServer in-process, store the port,
spawn the OS open command (`open` on macOS, `xdg-open` on Linux,
`start` on Windows). Idempotent: a second `b` re-opens the browser
to the existing server.
- Server cleanup is bound to the TUI's lifecycle:
* `resetReplayInteraction` (called on view-switch and date-shift)
now also stops the server,
* the `q` quit handler stops it before destroying the renderer.
No orphan process after exit.
- The OS-open shim is duplicated from packages/cli/src/sharing/open.ts
(~10 lines) to avoid introducing a new shared package just for this.
The interactive replay page becomes multi-day. A GitHub-style 7×13
heatmap (last 90 days) sits above the cost odometer; click any day
to jump to that day's replay without restarting the CLI.
Server (packages/renderers/src/live/replay-live-server.ts):
- startReplayLiveServer now accepts ReplayReport | ReplayLiveDataProvider.
Single-day mode (the existing call shape) is unchanged — no heatmap
rendered, no new routes exposed.
- Multi-day mode adds:
* `?date=YYYY-MM-DD` on GET / → server-side render of that day
(no JS-side rebuild needed; full reload, but cheap on local data)
* `GET /api/replay?date=YYYY-MM-DD` → JSON ReplayReport (200) or
404 / 400 with structured error
* Empty-day fallback: if getReport returns null, the page renders
a "nothing happened on YYYY-MM-DD" empty state instead of erroring
Template (packages/renderers/src/live/replay-live-template.ts):
- New ReplayLiveHtmlOptions param on generateReplayLiveHtml.
- Heatmap is rendered server-side as anchor cells — clicks just navigate
to /?date=…, no JS required for the date switch.
- Layout: 7 rows (Sun–Sat) × 13 cols of 14×14px cells, gap 3px. Active
day has a thick emerald outline. Empty days are dim-bordered. Hover
shows date + tokens + cost. Month labels along the top.
CLI (packages/cli/src/cli.ts):
- `tokenleak replay [date] --interactive` becomes multi-day by default:
loads the last 90 days once, builds heatmap entries by grouping events
per date, passes a ReplayLiveDataProvider whose getReport re-runs
buildReplayReport on demand.
- New `--no-heatmap` escape hatch reverts to the single-day server.
- New `buildReplayHeatmap(providers)` helper.
Tests:
- packages/renderers/src/live/__tests__/replay-live-server.test.ts +7:
* heatmap renders + active cell marked + cell links use /?date=…
* /?date=X serves the requested day via getReport
* unknown day on GET / falls back to empty-state HTML (not 404)
* /api/replay 200 + JSON / 400 invalid / 404 missing
* single-day mode does NOT emit the heatmap section
- All existing tests stay green.
Smoke: `tokenleak replay 2026-04-22 --interactive` shows the heatmap;
clicking March 8 navigates the page in-place to that day's data.
|
Pushed three follow-up commits addressing the live-test feedback:
Tests: TUI 96→106, renderers 272→279. All green. Smoke verified end-to-end:
Yours to re-review. |
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (4)
packages/cli/src/replay-cast.ts (2)
51-51: HardcodedSHELL: '/bin/bash'won't match Windows/zsh/fish users.This metadata is mostly cosmetic in the cast file, but on a Windows host it's plainly wrong. Consider deriving from
process.env.SHELL(with a sane fallback) so the recorded environment reflects reality.♻️ Suggested change
- env: { TERM: 'xterm-256color', SHELL: '/bin/bash' }, + env: { + TERM: process.env.TERM ?? 'xterm-256color', + SHELL: process.env.SHELL ?? '/bin/sh', + },🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/cli/src/replay-cast.ts` at line 51, The hardcoded env entry sets SHELL to '/bin/bash' which is incorrect on many systems; update the cast metadata in replay-cast.ts where the env object is created (the env property) to derive the shell from the runtime: use process.env.SHELL with sensible fallbacks (e.g., process.env.COMSPEC on Windows, then '/bin/sh' or 'cmd.exe' as final fallback) so the recorded environment reflects the actual host shell.
186-191: Defensively clampfilledto the bar width.Mathematically
pct ≤ 1, so this is safe today, but ifMath.round(pct * barColumnWidth)ever exceedsbarColumnWidth(FP edge / future change to widths),' '.repeat(barColumnWidth - filled)will throw aRangeError. AMath.minmakes this loop bullet-proof.♻️ Suggested change
- const filled = Math.max(1, Math.round(pct * barColumnWidth)); + const filled = Math.min(barColumnWidth, Math.max(1, Math.round(pct * barColumnWidth)));🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/cli/src/replay-cast.ts` around lines 186 - 191, Clamp the computed bar fill to the bar width to avoid a negative repeat count: when building the bar in the loop over sortedMix (variables pct, filled, barColumnWidth), replace the current filled calculation with a defensively clamped value (e.g., use Math.min(barColumnWidth, Math.max(1, Math.round(pct * barColumnWidth)))) so that ' '.repeat(barColumnWidth - filled) cannot receive a negative length and the lines.push call remains safe.packages/cli/src/replay-cast.test.ts (1)
20-27:makeReport([])semantics are surprising.Passing
[]triggers the defaults branch, so the empty-day test then has to manually clearevents/flowBlocks/tokenVelocity/summary.totalEvents. A small tweak — e.g. acceptevents?: UsageEvent[]and treatundefined(not empty) as "use defaults" — would let the empty-day test simply callmakeReport([])and avoid the post-hoc mutation that left the originalpeakMinutepointing at a nonexistent event.Also applies to: 123-133
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/cli/src/replay-cast.test.ts` around lines 20 - 27, The makeReport function currently treats an empty array as "use defaults", which surprises callers and breaks tests that expect an explicit empty report (e.g., leaving peakMinute pointing at a non-existent event); change the signature to accept events?: UsageEvent[] and only use the default sample events when events is undefined (not when it's an empty array), update any internal logic that assumes non-empty events (including peakMinute, flowBlocks, tokenVelocity, and summary.totalEvents) so they handle an explicitly empty events array correctly, and apply the same change to the other occurrence referenced (lines ~123-133) so tests can call makeReport([]) to get a truly empty report without post-hoc mutations.packages/tui/src/lib/replay-playback.ts (1)
169-203:computePlaybackSummaryis O(n) per call — fine today, worth noting.This recomputes the cumulative aggregate from index 0 every time the cursor moves. At 600× with 100k events on a tick, that's 100k × ticks operations per second. Today's days are small enough that this is invisible, but if you ever support multi-day playback or much busier days, an incremental walk (advance the prior summary by
events[lastIdx+1..idx]) would keep playback smooth without changing the public API.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/tui/src/lib/replay-playback.ts` around lines 169 - 203, computePlaybackSummary currently recomputes aggregates from report.events[0..idx] on every call which is O(n) and will be slow for large or frequent cursor moves; change it to perform an incremental update: maintain a small cached state (lastIndex and cumulative aggregates such as cost, tokens, inputT, outputT, cacheR, cacheW, and modelMix) and when computePlaybackSummary(report, cursorIndex) is called, if cursorIndex >= lastIndex advance the cached aggregates by folding events[lastIndex+1..cursorIndex], or if cursorIndex < lastIndex roll back by subtracting events[cursorIndex+1..lastIndex] (or rebuild only when cursor moves backward rarely), then return the updated PlaybackSummary using the cached totals and cursorEvent; update the cache inside the same module (e.g., module-level variables or a lightweight LRU keyed by report id) so the public function signature of computePlaybackSummary remains unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/cli/src/cli.ts`:
- Around line 2486-2490: The getReport callback on serverArg currently always
returns a ReplayReport which masks not-found semantics; update
serverArg.getReport to call buildReplayReport(heatmapOutput.providers, d) and
return null when the produced ReplayReport represents "no data" (e.g., empty
segments/events or a provided isEmpty flag) so it matches the
ReplayLiveDataProvider.getReport contract and allows the server to return 404
for missing dates; reference serverArg, getReport, buildReplayReport,
ReplayReport and ensure the check matches the actual empty-report shape used
elsewhere.
In `@packages/renderers/src/live/replay-live-template.ts`:
- Around line 66-70: The heatmap anchor should use the replay window (or the
latest loaded entry) instead of wall-clock today; remove the todayStr comparison
and compute latestEntryDate from the provided replay window end (or from entries
only) and then set end = new Date(latestEntryDate + 'T00:00:00Z') and start =
new Date(end.getTime() - (HEATMAP_DAYS - 1) * 86_400_000). Update the logic
around todayStr, latestEntryDate, end, and start so the 13-week grid is based on
the replayEnd (or entries' max date) rather than new Date().toISOString().
In `@packages/tui/src/index.ts`:
- Around line 850-855: The TUI is calling startReplayLiveServer with a plain
ReplayReport (state.cachedReplayReport), which causes the old single-day view to
open; change the TUI launcher to construct a ReplayLiveDataProvider from
state.cachedReplayReport (same approach as the CLI path) and pass that
ReplayLiveDataProvider into startReplayLiveServer instead of the raw
ReplayReport so the multi-day heatmap/date navigation is available; keep
assigning the returned { port, stop } to state.replayLiveServerPort and
replayLiveServerStop and then call render and openUrlInBrowser as before.
In `@packages/tui/src/panels/replay.ts`:
- Around line 285-290: renderOverviewHelp currently advertises keys that only
work in playback; update renderOverviewHelp to show only active controls by
making it accept (or read) a boolean indicating whether step/playback is active
(e.g. isStepMode or playbackActive) and build the help line conditionally: when
not active render " [s] enter step/playback · [b] open browser", and when active
render the full " [s] enter step/playback · [n/p] step · [space] play · [i]
interesting · [b] open browser"; keep using truncate(line, contentWidth) and the
existing Box/Text/COLORS.dimWhite rendering.
---
Nitpick comments:
In `@packages/cli/src/replay-cast.test.ts`:
- Around line 20-27: The makeReport function currently treats an empty array as
"use defaults", which surprises callers and breaks tests that expect an explicit
empty report (e.g., leaving peakMinute pointing at a non-existent event); change
the signature to accept events?: UsageEvent[] and only use the default sample
events when events is undefined (not when it's an empty array), update any
internal logic that assumes non-empty events (including peakMinute, flowBlocks,
tokenVelocity, and summary.totalEvents) so they handle an explicitly empty
events array correctly, and apply the same change to the other occurrence
referenced (lines ~123-133) so tests can call makeReport([]) to get a truly
empty report without post-hoc mutations.
In `@packages/cli/src/replay-cast.ts`:
- Line 51: The hardcoded env entry sets SHELL to '/bin/bash' which is incorrect
on many systems; update the cast metadata in replay-cast.ts where the env object
is created (the env property) to derive the shell from the runtime: use
process.env.SHELL with sensible fallbacks (e.g., process.env.COMSPEC on Windows,
then '/bin/sh' or 'cmd.exe' as final fallback) so the recorded environment
reflects the actual host shell.
- Around line 186-191: Clamp the computed bar fill to the bar width to avoid a
negative repeat count: when building the bar in the loop over sortedMix
(variables pct, filled, barColumnWidth), replace the current filled calculation
with a defensively clamped value (e.g., use Math.min(barColumnWidth, Math.max(1,
Math.round(pct * barColumnWidth)))) so that ' '.repeat(barColumnWidth - filled)
cannot receive a negative length and the lines.push call remains safe.
In `@packages/tui/src/lib/replay-playback.ts`:
- Around line 169-203: computePlaybackSummary currently recomputes aggregates
from report.events[0..idx] on every call which is O(n) and will be slow for
large or frequent cursor moves; change it to perform an incremental update:
maintain a small cached state (lastIndex and cumulative aggregates such as cost,
tokens, inputT, outputT, cacheR, cacheW, and modelMix) and when
computePlaybackSummary(report, cursorIndex) is called, if cursorIndex >=
lastIndex advance the cached aggregates by folding
events[lastIndex+1..cursorIndex], or if cursorIndex < lastIndex roll back by
subtracting events[cursorIndex+1..lastIndex] (or rebuild only when cursor moves
backward rarely), then return the updated PlaybackSummary using the cached
totals and cursorEvent; update the cache inside the same module (e.g.,
module-level variables or a lightweight LRU keyed by report id) so the public
function signature of computePlaybackSummary remains unchanged.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 4c92f320-77b1-4b8b-a9b0-2c50924a81dc
📒 Files selected for processing (15)
packages/cli/src/cli.tspackages/cli/src/replay-cast.test.tspackages/cli/src/replay-cast.tspackages/cli/src/replay-cli.test.tspackages/cli/src/replay.tspackages/renderers/src/index.tspackages/renderers/src/live/__tests__/replay-live-server.test.tspackages/renderers/src/live/replay-live-server.tspackages/renderers/src/live/replay-live-template.tspackages/tui/src/index.tspackages/tui/src/lib/replay-playback.test.tspackages/tui/src/lib/replay-playback.tspackages/tui/src/lib/state.tspackages/tui/src/panels/replay.test.tspackages/tui/src/panels/replay.ts
Two issues from live testing:
1. Flow blocks looked invisible. The block ribbon below the velocity
histogram had a 2-SVG-unit minimum width clamp; on a 13-hour day
that's ~2px on screen, so short blocks (Quick Lookups, 30s–3min
Deep Flows) collapsed into invisible slivers and gave the user the
impression there were no blocks at all. Bumped min width to 10
SVG units (~12px on screen), centered the clamped block on its
true midpoint so visual position remains accurate, and bumped the
ribbon's height + fill/stroke opacity so it reads clearly against
the histogram. Also reshaped the timeline rows: histogram 12-122,
ribbon 138-164 (was 12-132 / 144-162).
2. Pressing [b] in the TUI made the terminal "stuck". Root cause:
startReplayLiveServer writes "Replay live at http://..." to stderr
on success, which corrupts the full-screen TUI render and leaves
the screen looking frozen. Added a `silent` option to
ReplayLiveServerOptions; the TUI's launchReplayBrowser passes
`{ silent: true }`. The TUI's banner is the user's feedback; no
stderr noise needed. Also swallowed the failure path silently for
the same reason — the TUI doesn't have an error toast yet, and
dumping to stderr would corrupt the screen.
No test changes — the existing tests cover both code paths and stay
green (TUI 106, renderers 279).
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (1)
packages/renderers/src/live/replay-live-template.ts (1)
66-70:⚠️ Potential issue | 🟠 MajorHeatmap still anchored to wall-clock today, not the replay window.
This was flagged on a previous commit and remains unchanged. When replaying an older day,
todayStrwins over the latest entry, sliding the 13-week grid forward to the real current date. The selectedactiveDatecan then end up outside the rendered 91-day window, even though the CLI loaded a 90-day window ending at the requested replay date.Proposed fix
- // Determine the day window: anchor on today (or the latest entry, whichever is later). - const todayStr = new Date().toISOString().slice(0, 10); - const latestEntryDate = entries.reduce((acc, e) => (e.date > acc ? e.date : acc), todayStr); + // Anchor on the active replay date (or the latest entry, whichever is later) + // so an older replay's selected day stays inside the visible 13-week window. + const latestEntryDate = entries.reduce( + (acc, e) => (e.date > acc ? e.date : acc), + activeDate, + ); const end = new Date(latestEntryDate + 'T00:00:00Z'); const start = new Date(end.getTime() - (HEATMAP_DAYS - 1) * 86_400_000);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/renderers/src/live/replay-live-template.ts` around lines 66 - 70, The heatmap anchoring uses wall-clock todayStr as the reducer initial value so real replay windows get pushed forward; change the logic to anchor on the latest entry date when entries exist: compute latestEntryDate from entries (e.g., reduce starting from the first entry or guard entries.length) and set end = new Date(latestEntryDate + 'T00:00:00Z') (fall back to todayStr only if entries is empty), then derive start from end using HEATMAP_DAYS so the 91-day grid folds around the replay data rather than the current date.
🧹 Nitpick comments (3)
packages/renderers/src/live/replay-live-template.ts (1)
888-900: Centered min-width clamp can overflow the right edge of the SVG.The centering math
xStart + trueWidth/2 - minWidth/2clamps to0on the left but not againstTL_Won the right. A short flow block whose midpoint is withinminWidth/2ofTL_W(e.g., a quick lookup at end-of-day) will render past x=1000 and get clipped bypreserveAspectRatio="none". Cheap fix:- const x = trueWidth < minWidth - ? Math.max(0, xStart + trueWidth / 2 - minWidth / 2) - : xStart; + const x = trueWidth < minWidth + ? Math.max(0, Math.min(TL_W - minWidth, xStart + trueWidth / 2 - minWidth / 2)) + : xStart;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/renderers/src/live/replay-live-template.ts` around lines 888 - 900, The centered min-width calculation for small blocks can overflow the right edge; in the flowBlocks.forEach block adjust the computed x (used when trueWidth < minWidth) to also clamp against the right boundary by ensuring x <= TL_W - minWidth. Update the logic around xStart / trueWidth / minWidth (the same place that sets x and w) to compute the centered value, then apply Math.max(0, ...) and a Math.min(TL_W - minWidth, ...) so the rectangle never extends past the right edge while still preventing negative x.packages/renderers/src/live/replay-live-server.ts (2)
111-147: Unhandled rejection fromgetReportwill surface as a 500 with no logging.Both
/and/api/replayawait arg.getReport(...)without atry/catch. If the provider throws (e.g., disk read failure, malformed event store), Bun.serve will return a generic 500 and — withsilent: trueset by the TUI per the PR description — there's no stderr breadcrumb either, so the TUI just sees a blank/error page with nothing to diagnose. Consider catching once at the handler boundary and returning a structured response (and optionally logging via a caller-supplied hook so silent mode stays clean).Sketch
return async (req: Request): Promise<Response> => { const url = new URL(req.url); + try { if (url.pathname === '/') { // ...existing logic... } if (url.pathname === '/api/replay') { // ...existing logic... } return new Response('not found', { status: 404 }); + } catch (err) { + if (!options.silent) { + process.stderr.write(`replay-live: getReport failed: ${String(err)}\n`); + } + if (url.pathname === '/api/replay') { + return Response.json({ error: 'internal error' }, { status: 500 }); + } + return new Response('internal error', { status: 500 }); + } };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/renderers/src/live/replay-live-server.ts` around lines 111 - 147, The handler awaits arg.getReport in two places and currently lets any throw bubble up to Bun, so wrap both calls to arg.getReport(...) in try/catch blocks at the handler boundary (the async function returned) to catch provider errors, call a logging hook if provided (e.g., if arg.onError is a function, invoke arg.onError(err, { date })) and then return a structured response: for the '/' route render an error HTML or fallback makeEmptyReport(date) and include a visible error banner, and for '/api/replay' return Response.json({ error: 'internal server error' }, { status: 500 }); ensure you preserve existing behavior when getReport returns null by keeping the makeEmptyReport/date assignment logic.
117-126: Whenrequested === initialDate, the handler skipsgetReportand serves the cached initial report — this could show stale data if the provider updates after startup.The condition
requested !== arg.initialDateat line 117 skips the provider lookup when navigating back to the initial day, servingarg.initialReportdirectly instead. This is a performance optimization for the typical single-load replay flow, but it means any data changes (e.g., live server restart with updated cache, new SQLite snapshot) won't be reflected when the user clicks the "current" heatmap cell.If the provider is expected to always be the authority, consider removing
&& requested !== arg.initialDateto make all date requests consistently go throughgetReport. If the optimization is intentional (e.g., for offline-first replay with stable snapshots), document that assumption.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/renderers/src/live/replay-live-server.ts` around lines 117 - 126, The current branch skips calling arg.getReport when requested === arg.initialDate, causing stale arg.initialReport to be served; update the condition so any valid ISO_DATE requested always calls arg.getReport (i.e., remove the "&& requested !== arg.initialDate" check) and keep the existing fallback logic that sets report to fresh or makeEmptyReport(requested) and date to requested; locate this change around the block using symbols requested, arg.initialDate, and arg.getReport in replay-live-server.ts.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/renderers/src/live/replay-live-template.ts`:
- Around line 78-93: The current column math (col = Math.floor(i / 7)) groups
days starting at start instead of at Sunday; fix by computing an adjusted index
that shifts days by the weekday of start so columns align to calendar weeks: let
startWeekday = start.getUTCDay(); for each index i compute const adjusted = i +
startWeekday; set col = Math.floor(adjusted / 7) and keep weekday =
d.getUTCDay(); push cells as before; also ensure the overall grid column count
(used by monthLabels/grid-template-columns) is derived from cells.at(-1)?.col +
1 so it grows when start isn’t a Sunday. This uses HEATMAP_DAYS, start, cells,
monthLabels and preserves tokens/cost/events population.
---
Duplicate comments:
In `@packages/renderers/src/live/replay-live-template.ts`:
- Around line 66-70: The heatmap anchoring uses wall-clock todayStr as the
reducer initial value so real replay windows get pushed forward; change the
logic to anchor on the latest entry date when entries exist: compute
latestEntryDate from entries (e.g., reduce starting from the first entry or
guard entries.length) and set end = new Date(latestEntryDate + 'T00:00:00Z')
(fall back to todayStr only if entries is empty), then derive start from end
using HEATMAP_DAYS so the 91-day grid folds around the replay data rather than
the current date.
---
Nitpick comments:
In `@packages/renderers/src/live/replay-live-server.ts`:
- Around line 111-147: The handler awaits arg.getReport in two places and
currently lets any throw bubble up to Bun, so wrap both calls to
arg.getReport(...) in try/catch blocks at the handler boundary (the async
function returned) to catch provider errors, call a logging hook if provided
(e.g., if arg.onError is a function, invoke arg.onError(err, { date })) and then
return a structured response: for the '/' route render an error HTML or fallback
makeEmptyReport(date) and include a visible error banner, and for '/api/replay'
return Response.json({ error: 'internal server error' }, { status: 500 });
ensure you preserve existing behavior when getReport returns null by keeping the
makeEmptyReport/date assignment logic.
- Around line 117-126: The current branch skips calling arg.getReport when
requested === arg.initialDate, causing stale arg.initialReport to be served;
update the condition so any valid ISO_DATE requested always calls arg.getReport
(i.e., remove the "&& requested !== arg.initialDate" check) and keep the
existing fallback logic that sets report to fresh or makeEmptyReport(requested)
and date to requested; locate this change around the block using symbols
requested, arg.initialDate, and arg.getReport in replay-live-server.ts.
In `@packages/renderers/src/live/replay-live-template.ts`:
- Around line 888-900: The centered min-width calculation for small blocks can
overflow the right edge; in the flowBlocks.forEach block adjust the computed x
(used when trueWidth < minWidth) to also clamp against the right boundary by
ensuring x <= TL_W - minWidth. Update the logic around xStart / trueWidth /
minWidth (the same place that sets x and w) to compute the centered value, then
apply Math.max(0, ...) and a Math.min(TL_W - minWidth, ...) so the rectangle
never extends past the right edge while still preventing negative x.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: b6177625-9e26-4f8e-acaf-d877a3ce0c9a
📒 Files selected for processing (3)
packages/renderers/src/live/replay-live-server.tspackages/renderers/src/live/replay-live-template.tspackages/tui/src/index.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- packages/tui/src/index.ts
Two issues from live testing: 1. The [b] key was a silent no-op when the cached single-day report had zero events — events.length > 0 gated the launcher. Switching to [o] (mnemonic: open) and dropping that gate so the key works whenever the user is on the replay view. 2. The interactive browser defaulted to today's date, so a quiet today rendered "0 flow blocks" even when the heatmap had plenty of data one cell to the left. When the date isn't passed explicitly on the CLI, fall back to the most recent day in the heatmap that has events. Also: empty heatmap cells were anchor links and would route the user to days with no data. Render them as <span> instead so a stray click can't land on a 0-block view.
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
packages/renderers/src/live/replay-live-template.ts (2)
53-53: Nit: kicker says "last 90 days" but the grid renders 91.
HEATMAP_DAYS = 91(13 × 7) but the kicker on line 138 reads// last 90 days. Either label as "last 13 weeks" or setHEATMAP_DAYS = 90and useMath.ceil(90 / 7) = 13columns. Cosmetic, but worth aligning so the copy matches the data.Also applies to: 138-138
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/renderers/src/live/replay-live-template.ts` at line 53, The HEATMAP_DAYS constant (HEATMAP_DAYS = 91) does not match the kicker copy that says "last 90 days"; update them to be consistent by either setting HEATMAP_DAYS = 90 and computing columns with Math.ceil(90 / 7) where the grid column count is derived, or keep HEATMAP_DAYS = 91 and change the kicker/copy to "last 13 weeks"; update the HEATMAP_DAYS constant and the kicker text (the comment that currently reads "last 90 days") and any other places that reference HEATMAP_DAYS or the kicker so the label matches the rendered data.
1-9: Verify the type import direction between template and server.
replay-live-template.tsimportsReplayHeatmapEntryfrom./replay-live-server, whilereplay-live-serveris the consumer that callsgenerateReplayLiveHtml(report, { heatmap, initialDate }). This creates a circular dependency between the two modules. It'simport type, so TS should erase it at runtime, but it's still fragile (e.g. if someone later removes thetypekeyword, or under certain bundler configs it can leak). Consider relocatingReplayHeatmapEntry(and any other shared types) into a small types module that both files import from.#!/bin/bash # Confirm the circular relationship and locate the type's definition. fd -e ts replay-live-server packages/renderers/src/live rg -nP "from ['\"]\./replay-live-(server|template)['\"]" packages/renderers/src/live rg -nP "export\s+(type\s+|interface\s+)?ReplayHeatmapEntry\b" packages/renderers/src🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/renderers/src/live/replay-live-template.ts` around lines 1 - 9, replay-live-template.ts currently imports the ReplayHeatmapEntry type from ./replay-live-server which creates a fragile circular dependency; extract ReplayHeatmapEntry (and any other types shared between replay-live-server and replay-live-template) into a small module (e.g., replay-live-types or similar) that exports the interface, update replay-live-server to export/consume from that types module and change replay-live-template to import type { ReplayHeatmapEntry } from the new types module, and ensure the new module uses only type/interface exports so TS erases it at runtime.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/renderers/src/live/replay-live-template.ts`:
- Line 170: The template sets const heatmap = options.heatmap ?? null which
treats an empty array as truthy and causes renderHeatmapSection to render an
empty grid; change the assignment to treat empty arrays as absent (e.g., set
heatmap to null unless options.heatmap is a non-empty array) so the conditional
`${heatmap ? renderHeatmapSection(...) : ''}` only renders when there are
heatmap entries; update the assignment referencing options.heatmap and any other
similar spots (also noted around the other occurrence) to use an Array.isArray +
length check or equivalent length-aware guard.
---
Nitpick comments:
In `@packages/renderers/src/live/replay-live-template.ts`:
- Line 53: The HEATMAP_DAYS constant (HEATMAP_DAYS = 91) does not match the
kicker copy that says "last 90 days"; update them to be consistent by either
setting HEATMAP_DAYS = 90 and computing columns with Math.ceil(90 / 7) where the
grid column count is derived, or keep HEATMAP_DAYS = 91 and change the
kicker/copy to "last 13 weeks"; update the HEATMAP_DAYS constant and the kicker
text (the comment that currently reads "last 90 days") and any other places that
reference HEATMAP_DAYS or the kicker so the label matches the rendered data.
- Around line 1-9: replay-live-template.ts currently imports the
ReplayHeatmapEntry type from ./replay-live-server which creates a fragile
circular dependency; extract ReplayHeatmapEntry (and any other types shared
between replay-live-server and replay-live-template) into a small module (e.g.,
replay-live-types or similar) that exports the interface, update
replay-live-server to export/consume from that types module and change
replay-live-template to import type { ReplayHeatmapEntry } from the new types
module, and ensure the new module uses only type/interface exports so TS erases
it at runtime.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 5ffe5376-2ccd-4591-9bac-eb60b42ea632
📒 Files selected for processing (6)
packages/cli/src/cli.tspackages/cli/src/replay-cli.test.tspackages/renderers/src/live/replay-live-template.tspackages/tui/src/index.tspackages/tui/src/panels/replay.test.tspackages/tui/src/panels/replay.ts
🚧 Files skipped from review as they are similar to previous changes (3)
- packages/tui/src/panels/replay.test.ts
- packages/cli/src/replay-cli.test.ts
- packages/tui/src/index.ts
Two follow-ups requested while testing the interactive replay: 1. The browser scrubber showed model/tokens/cost per event but not the prompt that drove it. Add a full-width "// prompt sent to model" card under the existing grid. Click an event row to pin the prompt panel; during playback the panel auto-follows the playhead until the user makes a manual selection. Prompt text comes from UsageEvent.prompt (already populated by Claude Code, truncated to 2,000 chars at parse time). For providers that don't capture prompts (Codex, Cursor, Pi), the panel renders a clear empty state instead of looking broken. 2. The "[o] open browser" affordance was Replay-only. Make it global: move the keypress handler out of handleReplayPlaybackInput into a new global helper called early in addInputHandler, and lazy-load cachedReplayReport via ensureReplayReport when the user presses [o] from a view that hasn't built one yet. The footer status bar now carries a bright-emerald "▶ [o] interactive replay" CTA chip on every non-modal view, swapping to "✓ replay open :PORT" once the server is running. The per-panel banner in replay.ts is removed since the footer covers it everywhere. Receipts sort moves from [o] to [S] to make room for the global launcher; help.ts and the status bar's receipts hint reflect the new binding.
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (4)
packages/tui/src/panels/replay.ts (1)
285-290:⚠️ Potential issue | 🟡 MinorOverview help advertises inactive controls before step mode starts.
Line 286 lists
[n/p],[space], and[i]whenplaybackis null; at that point only entering step/playback is actionable, so this is misleading.Suggested fix
function renderOverviewHelp(contentWidth: number) { - const line = ' [s] enter step/playback · [n/p] step · [space] play · [i] interesting'; + const line = ' [s] enter step/playback'; return Box( { flexDirection: 'column', width: '100%', paddingLeft: 1, paddingRight: 1 }, Text({ content: truncate(line, contentWidth), fg: COLORS.dimWhite }), ); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/tui/src/panels/replay.ts` around lines 285 - 290, renderOverviewHelp currently always advertises controls like `[n/p]`, `[space]`, and `[i]` even when playback is null; change it to only show actionable controls by adding a playback (or isStepMode) parameter to renderOverviewHelp (e.g., renderOverviewHelp(contentWidth: number, playback: Playback | null) or a boolean isStepMode), build the help line conditionally (when playback is null show only " [s] enter step/playback", otherwise include " [n/p] step · [space] play · [i] interesting"), then call truncate(line, contentWidth) as before; update all callers of renderOverviewHelp to pass the current playback/step-mode state and ensure the Box/Text construction remains unchanged.packages/renderers/src/live/replay-live-template.ts (3)
170-170:⚠️ Potential issue | 🟡 MinorTreat an empty heatmap array as absent.
[]is still truthy here, so a brand-new install can render a confusing empty heatmap panel with “0 active days” instead of omitting the section.Proposed fix
- const heatmap = options.heatmap ?? null; + const heatmap = + Array.isArray(options.heatmap) && options.heatmap.length > 0 + ? options.heatmap + : null;Also applies to: 1434-1434
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/renderers/src/live/replay-live-template.ts` at line 170, The current assignment const heatmap = options.heatmap ?? null treats an empty array as present; change it to treat empty arrays as absent by setting heatmap to null when options.heatmap is not an array or has length === 0 (e.g., use Array.isArray(options.heatmap) && options.heatmap.length > 0 ? options.heatmap : null); update the same logic at the other occurrence around the earlier heatmap handling (the other usage noted at 1434) so empty arrays don't render an empty heatmap panel.
77-93:⚠️ Potential issue | 🟠 MajorAlign heatmap columns to calendar weeks.
col = Math.floor(i / 7)still groups from an arbitrarystart, not from Sunday. On mid-week starts that mixes two different calendar weeks into one column, and the month labels/grid width drift with it.Proposed fix
+ const startWeekday = start.getUTCDay(); // 0 = Sun const cells: Array<{ date: string; tokens: number; cost: number; events: number; weekday: number; col: number }> = []; for (let i = 0; i < HEATMAP_DAYS; i++) { const d = new Date(start.getTime() + i * 86_400_000); const dateStr = d.toISOString().slice(0, 10); const weekday = d.getUTCDay(); // 0 = Sun - const col = Math.floor(i / 7); + const col = Math.floor((i + startWeekday) / 7); const e = byDate.get(dateStr); cells.push({ date: dateStr, tokens: e?.tokens ?? 0, @@ col, }); } + const columnCount = (cells[cells.length - 1]?.col ?? 0) + 1; @@ - for (let week = 0; week < Math.ceil(HEATMAP_DAYS / 7); week++) { + for (let week = 0; week < columnCount; week++) { const firstDay = cells.find((c) => c.col === week); if (!firstDay) continue; @@ - <div class="heatmap-months mono">${monthLabels.join('')}</div> - <div class="heatmap-grid">${cellHtml}</div> + <div class="heatmap-months mono" style="grid-template-columns:repeat(${columnCount}, 14px)">${monthLabels.join('')}</div> + <div class="heatmap-grid" style="grid-template-columns:repeat(${columnCount}, 14px)">${cellHtml}</div>Also applies to: 123-131, 156-157, 652-666
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/renderers/src/live/replay-live-template.ts` around lines 77 - 93, The heatmap columns are computed from the arbitrary start date (col = Math.floor(i / 7)) which misaligns calendar weeks; instead compute a Sunday-aligned week origin and derive col from the difference between each cell date and that Sunday. Specifically, before the loop compute a UTC Sunday start (e.g., copy start into firstSunday and subtract start.getUTCDay() days using UTC date arithmetic) and then for each date d compute col = Math.floor((d.getTime() - firstSunday.getTime()) / (7 * 86_400_000)); update the cells creation in the loop that uses start/HEATMAP_DAYS (the block building cells with date, tokens, cost, events, weekday, col) and apply the same Sunday-aligned col logic to the other similar blocks referenced in the review (the other occurrences around the later ranges).
66-70:⚠️ Potential issue | 🟠 MajorAnchor the heatmap to the replay window, not wall-clock today.
Line 68 still seeds the window with
todayStr, so replaying an older 90-day slice can shift the grid forward and push the requested day out of view.Proposed fix
- const todayStr = new Date().toISOString().slice(0, 10); - const latestEntryDate = entries.reduce((acc, e) => (e.date > acc ? e.date : acc), todayStr); + const latestEntryDate = entries.reduce( + (acc, e) => (e.date > acc ? e.date : acc), + activeDate, + );🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/renderers/src/live/replay-live-template.ts` around lines 66 - 70, The heatmap window is incorrectly anchored to wall-clock "todayStr"; change the latestEntryDate computation so it is seeded from the replay entries themselves instead of today. Specifically, remove the todayStr accumulator and compute latestEntryDate from entries (e.g. seed reduce with entries[0].date or otherwise derive the max date from entries) so that end/new Date(...) and start use the replay's latest entry; keep a safe fallback only if entries is empty. Ensure you update the variables latestEntryDate, end and start accordingly (functions/vars: entries, latestEntryDate, end, start, HEATMAP_DAYS).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/renderers/src/live/__tests__/replay-live-server.test.ts`:
- Around line 258-262: The multi-day fixture otherReport is inconsistent: only
date and events are updated while nested fields like flowBlocks, tokenVelocity,
and summary.peakMinute (copied from initial) still reference the old date;
update otherReport to rewrite any embedded dates/timestamps inside flowBlocks,
tokenVelocity entries, and summary.peakMinute to use otherDate (and adjust any
contained timestamp strings similarly) so all parts of the fixture (date,
events, flowBlocks, tokenVelocity, summary.peakMinute) are consistently set to
otherDate.
---
Duplicate comments:
In `@packages/renderers/src/live/replay-live-template.ts`:
- Line 170: The current assignment const heatmap = options.heatmap ?? null
treats an empty array as present; change it to treat empty arrays as absent by
setting heatmap to null when options.heatmap is not an array or has length === 0
(e.g., use Array.isArray(options.heatmap) && options.heatmap.length > 0 ?
options.heatmap : null); update the same logic at the other occurrence around
the earlier heatmap handling (the other usage noted at 1434) so empty arrays
don't render an empty heatmap panel.
- Around line 77-93: The heatmap columns are computed from the arbitrary start
date (col = Math.floor(i / 7)) which misaligns calendar weeks; instead compute a
Sunday-aligned week origin and derive col from the difference between each cell
date and that Sunday. Specifically, before the loop compute a UTC Sunday start
(e.g., copy start into firstSunday and subtract start.getUTCDay() days using UTC
date arithmetic) and then for each date d compute col = Math.floor((d.getTime()
- firstSunday.getTime()) / (7 * 86_400_000)); update the cells creation in the
loop that uses start/HEATMAP_DAYS (the block building cells with date, tokens,
cost, events, weekday, col) and apply the same Sunday-aligned col logic to the
other similar blocks referenced in the review (the other occurrences around the
later ranges).
- Around line 66-70: The heatmap window is incorrectly anchored to wall-clock
"todayStr"; change the latestEntryDate computation so it is seeded from the
replay entries themselves instead of today. Specifically, remove the todayStr
accumulator and compute latestEntryDate from entries (e.g. seed reduce with
entries[0].date or otherwise derive the max date from entries) so that end/new
Date(...) and start use the replay's latest entry; keep a safe fallback only if
entries is empty. Ensure you update the variables latestEntryDate, end and start
accordingly (functions/vars: entries, latestEntryDate, end, start,
HEATMAP_DAYS).
In `@packages/tui/src/panels/replay.ts`:
- Around line 285-290: renderOverviewHelp currently always advertises controls
like `[n/p]`, `[space]`, and `[i]` even when playback is null; change it to only
show actionable controls by adding a playback (or isStepMode) parameter to
renderOverviewHelp (e.g., renderOverviewHelp(contentWidth: number, playback:
Playback | null) or a boolean isStepMode), build the help line conditionally
(when playback is null show only " [s] enter step/playback", otherwise include "
[n/p] step · [space] play · [i] interesting"), then call truncate(line,
contentWidth) as before; update all callers of renderOverviewHelp to pass the
current playback/step-mode state and ensure the Box/Text construction remains
unchanged.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: d0f2dd21-a3f5-4686-9f08-b0b9ef0486d1
📒 Files selected for processing (8)
packages/renderers/src/live/__tests__/replay-live-server.test.tspackages/renderers/src/live/replay-live-template.tspackages/tui/src/index.tspackages/tui/src/panels/help.tspackages/tui/src/panels/replay.test.tspackages/tui/src/panels/replay.tspackages/tui/src/panels/status-bar.test.tspackages/tui/src/panels/status-bar.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- packages/tui/src/index.ts
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/tui/src/lib/replay-interaction.ts`:
- Around line 15-23: The scroll math in keepSelectedItemVisible (and the similar
blocks at the other two locations) assumes visibleCount > 0; when visibleCount
<= 0 the calculation can advance the offset incorrectly. Guard these
computations by early-returning the current scrollOffset (or treating
visibleCount as at least 1) when visibleCount <= 0, e.g., check visibleCount <=
0 at the top of keepSelectedItemVisible and the two analogous functions/blocks
(the ones at the ranges noted) and skip the adjustment so the offset is not
changed.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 2692eaa3-61d1-427d-93dc-d7eba8cff123
📒 Files selected for processing (14)
packages/cli/src/receipts.tspackages/mcp/src/tools/get-receipt-lines.tspackages/mcp/src/tools/index.tspackages/registry/src/__fixtures__/codex-current/sessions/2026/03/12/session-current.jsonlpackages/registry/src/providers/codex.test.tspackages/registry/src/providers/codex.tspackages/renderers/src/live/__tests__/replay-live-server.test.tspackages/renderers/src/live/replay-live-template.tspackages/tui/src/index.tspackages/tui/src/lib/replay-interaction.test.tspackages/tui/src/lib/replay-interaction.tspackages/tui/src/lib/replay-playback.test.tspackages/tui/src/lib/replay-playback.tspackages/tui/src/panels/receipts.ts
✅ Files skipped from review due to trivial changes (3)
- packages/mcp/src/tools/index.ts
- packages/tui/src/panels/receipts.ts
- packages/registry/src/fixtures/codex-current/sessions/2026/03/12/session-current.jsonl
🚧 Files skipped from review as they are similar to previous changes (5)
- packages/renderers/src/live/tests/replay-live-server.test.ts
- packages/tui/src/lib/replay-playback.test.ts
- packages/tui/src/index.ts
- packages/tui/src/lib/replay-playback.ts
- packages/renderers/src/live/replay-live-template.ts
What
This PR updates the replay experience across the TUI and browser live view.
oreplay launcher alive across ordinary TUI view switches.tokenleak replay --interactive.user_messageprompt text and carries it into replayUsageEvent.prompt, so Codex prompts show in the browser prompt panel like Claude prompts.Why
The TUI/browser replay integration had a couple of state mismatches: switching views could kill the live replay server, playback navigation could desync the active cursor event from the selected block, and the TUI launcher did not fully match the CLI heatmap-enabled browser replay. On top of that, Codex session logs already contained user prompts, but the Codex provider was only extracting token usage.
Validation
bun test packages/tui/src/lib/replay-interaction.test.ts packages/tui/src/lib/replay-playback.test.ts packages/tui/src/panels/replay.test.ts packages/tui/src/panels/status-bar.test.ts packages/renderers/src/live/__tests__/replay-live-server.test.tsbun run checkbun test packages/tui packages/renderers packages/clibun test packages/registry/src/providers/codex.test.ts packages/renderers/src/live/__tests__/replay-live-server.test.tsbun test packages/registry packages/renderers packages/tui packages/cli packages/mcpbun run buildand verifiedtokenleak replay --codex --format jsonemits prompt fields for current Codex events.Notes
There are unrelated untracked screenshot/log artifacts in the local checkout; they were not staged or pushed.
Summary by CodeRabbit
New Features
--record/--cast,--speed) and a written-file notice.--noHeatmap) and multi-day live server with date links and JSON API.Documentation
Tests