Transcript viewer: read an archived Claude Code session in the browser #64
No reviewers
Labels
No labels
bug
documentation
duplicate
enhancement
good first issue
help wanted
invalid
question
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
iterative-works/claude-code-query!64
Loading…
Reference in a new issue
No description provided.
Delete branch "cc-chat"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Batch close of the
cc-chateffort worktree: one slice,01-transcript-viewer.The slice
Goal (frozen card). I can point the viewer at one of my own archived Claude Code sessions
and read that conversation in the browser — my turns, the agent's turns, and what the agent
did — accurately enough that I use it instead of reading the raw JSONL.
Fit test. Open a real session and read it: nothing I know happened is missing, nothing
foreign is present, and "what happened in this session?" is answered correctly.
Fit passed on 2026-08-20, on Michal's own read in a browser.
What ships
ui/shared, cross-built to JVM and Scala.js):TranscriptEntry(user message / agent message / activity),Transcript, a hand-writtenTranscriptCodec,TranscriptEndpoint, andTranscriptFailurecarrying its own status.TranscriptApi) and pure serving logic (TranscriptHistory),JVM-only, so a consumer attaches
serverLogicand interprets them with whatever Tapir serverits effect system already runs. The browser bundle carries none of it.
ui/src/.../claudecode) — the only place that knows the vendor.<cc-chat>custom element (ui/js), Laminar, delivered as a content-hashed classpathresource from the jar. 192 KB gzip against the 250 KB CI gate.
ui.demo— an unpublished ZIO/Netty host that mounts the descriptions and reads the localarchive, which is the proof that
uinames neither ZIO nor a server.What the archive taught us
Every fidelity decision here was measured against the local transcript files, not argued:
userentry is not a person speaking.isMetamarks injected command and skill bodies;isCompactSummarymarks the summary written on compaction. The parser dropped both, so theviewer put 85 compaction summaries across 1916 files into Michal's mouth.
ContentBlockParserdiscarded every tool input, and readtool_resultcontent only in itsstring form — the archive writes 2196 as content blocks against 345 as strings.
tool_referenceblocks and screen tools with
imageblocks. All of them rendered as an empty row.emptiness is faithful; offering a disclosure that opens onto nothing was not.
parentToolUseId, recorded in ADR 0002 as knowledge to carry over,cannot happen: across 229818 entries, no entry carries that field and none is a sidechain.
Decisions recorded in ADR 0002
This settles the ADR's deferred normalized-history question.
vendor type, so a second agent tool is a second adapter and nothing else moves.
uispikeis deleted — the card forbids two generations coexisting, and the rule the ADR setsfor
proceduresbinds this repository first.Review
Seven skills in parallel at close (style, testing, security, scala3, architecture, api, zio).
Full report:
project-management/efforts/cc-chat/slices/01-transcript-viewer/review-slice-2026-08-20.md.One critical, found independently by two reviewers and fixed: an archive I/O failure put the
absolute path it failed on into the 500 body.
TranscriptFailure.Unreadablenow carries nopayload at all, so the leak is impossible rather than merely avoided; the host logs the cause on
its own side.
Five more fixed in-loop: a rejected id is echoed back bounded and without control characters;
the Netty binding is released on interrupt; a docstring claiming a boundary the data path does
not apply now says what is true; the asset read is
attemptBlocking; four untested refusalpaths in
ContentBlockParserare covered.Follow-ups (deferred deliberately, flushed to the effort map)
**and backticks;wrapper tags and ANSI codes show raw in activity bodies; nested tool inputs flatten into
an unreadable run-on; the reading column is split across the full page width.
this "versioned" and nothing in it is; the session probably belongs in the path rather
than a query parameter; the endpoint's bare
Stringinput makes a future paging cursorsource-breaking;
Schema.anyObjectleaves generated docs empty; the transcript responsesets no cache directive.
isMeta/isCompactSummaryshould be oneEntryOriginenum;JsonValue.ofshould return an enum rather thanAny.ConversationLogEntry(_, _, …)patterns break on everyenvelope field addition;
TranscriptElement.showis untested.Gates
818/818 tests,
checkFormatandfix --checkclean, bundle 192 KB gzip against the 250 KB gate.Refusals verified over HTTP: real session 200, unknown 404,
../../etc/passwd400 with nothingleaked, missing parameter 400.
🤖 Generated with Claude Code