Web UI: tail-paginate session transcripts with a progress banner#288
Merged
Conversation
Opening a session in the web UI used to fetch the entire transcript in
one `session.transcript` round trip and render every event in a single
synchronous main-thread loop. For an `ultracode`-shaped session (1.5 GB
transcript on disk, millions of events) that meant several seconds of
wire wait, then a UI freeze while ~3.9M events parsed and built DOM,
covered only by a static skeleton shimmer.
Switch the first round trip to a tail page: a new `tail: Option<usize>`
field on `TranscriptParams` tells the daemon to return just the most
recent N events, and `read_transcript_tail` seeks backward through
`pty.log` style 64 KiB chunks to read those N events without scanning
the whole file. `total` comes from the live `transcript_count` counter,
so even on a multi-GB transcript the response is bounded and instant.
Then the web UI:
- Renders the tail (500 events) immediately, scrolled to the bottom —
the user sees the live tail almost as fast as the previous skeleton
would have appeared.
- If `total > tail.length`, pins a sticky progress banner
("Loading older history… 1,200 / 12,400 (10%)") to the top of the
pane and walks forward from `from: 0` in 500-event pages, prepending
each batch above the tail. Scroll position is preserved relative to
the bottom so prepends never make the visible content jump.
- Yields between pages with `requestAnimationFrame` so the page stays
responsive while history fills in.
The existing skeleton still flashes if the first tail fetch is slow
enough to notice (>150 ms).
Tests:
- Storage: 5 cases for `read_transcript_tail` (last-N order,
chunk-boundary safety, n>total, missing file, n=0).
- Session manager: `transcript(.., tail)` returns `total` from the live
counter and the correct trailing slice.
`Client::transcript` keeps its current shape (passes `tail: None`) so
the zarvis / MCP / TUI Rust callers don't need to opt in.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.
Problem
Opening a session in the web UI used to fetch the entire transcript in one
session.transcriptround trip and render every event in a single synchronous main-thread loop. For anultracode-shaped session (1.5 GB transcript on disk, ~3.9M events) that meant several seconds of wire wait, then a UI freeze while every event parsed and built DOM. The only feedback was a static skeleton shimmer that appeared after 150 ms and never updated.Fix
Tail-first: the first round trip returns just the most recent N events. Older pages then fill in upward without blocking the UI.
Server
TranscriptParamsgainstail: Option<usize>. When set,from/limitare ignored and the response is the last N events.Storage::read_transcript_tail(id, n)seeks backward in 64 KiB chunks until it has at leastn + 1newlines (so a partial first line never chops a real event), then parses only those lines. O(n × line size), not O(file size).SessionManager::transcripttakestotalfrom the livetranscript_countatomic, so even a multi-GB transcript responds with bounded latency.Web UI (
crates/daemon/assets/index.html){ session_id, tail: 500 }first and renders the tail immediately, scrolled to the bottom — the user sees the live tail almost as fast as the previous skeleton would have appeared.total > tail.length, pins a sticky progress banner ("Loading older history… 1,200 / 12,400 (10%)") to the top of the pane and walks forward fromfrom: 0in 500-event pages, prepending each batch above the tail. Scroll position is preserved relative to the bottom so prepends never make the visible content jump.requestAnimationFrameso the page stays responsive.Client::transcriptkeeps its current shape (passestail: None) so the zarvis / MCP / TUI Rust callers don't need to opt in.Test
read_transcript_tail— last-N order, chunk-boundary safety (lines longer than the chunk size), n > total, missing file, n = 0.transcript_tail_returns_last_n_with_live_totalassertstotalcomes from the live counter and the slice is the right tail.🤖 Generated with Claude Code