The chat UI vertical: one rendering of an agent session #65
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!65
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?
The chat UI vertical: one rendering of an agent session, shared by every house project.
Two effort-local slices, reviewed and shipped as one branch — 51 commits, 4 644 insertions
across 44 files outside
project-management/.What this delivers
A
uimodule that turns a Claude Code session into a semanticcc-*HTML vocabulary, adefault stylesheet that is the whole appearance of a transcript, and a demo host that makes
both readable against a real local archive. A consumer embeds
<cc-chat session="…">, linkscc-chat.css, and ships — or drops the sheet and writes its own against the same tags.Slice 01 — transcript viewer
Goal: point the viewer at one of my own archived 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; "what happened in this session?" is answered correctly. Passed on
Michal's read.
Delivered: the vendor-neutral transcript model and its wire codec (cross-built); the
uimodule and its place in
build.mill; the Claude Code adapter, shaped against a measurement ofthe local archive; the history endpoint and its refusals; the
<cc-chat>element;bundle-from-jar delivery with a CI size gate; the demo host; and the deletion of the
uispikeprototype — the card forbids two generations coexisting.
Slice 02 — transcript presentation
Goal: read a long, dense session end to end — markdown as markdown, no scaffolding I never
typed, tool calls as structure, a picture as a picture — and change how the whole thing looks
from the host page alone, by editing one stylesheet, without touching or forking the element.
Fit test: open a compacted session with many tool calls, read it end to end, then restyle
it. Passed on Michal's read — "we can keep it like this".
Delivered, each step measured before it was built:
Markupdescribes thetree;
TranscriptMarkupis the vocabulary;cc-chat.cssships beside the bundle.Prose, read from markdown once on the JVM. Only thetranscript package can make one, so "prose HTML is ours or it is not inserted" is held by
construction, not by discipline.
left on a real 249-row session.
answers in the archive sit away from their call, so adjacency was not enough.
is its path.
rendering it. The bundle went from 185 543 bytes gzipped to 12 621, with the model, the
codec and Laminar all out of the browser.
wall-clock sits in a handful of pauses, so the page marks the pauses and the days instead of
printing a clock 179 times for 24 distinct values.
what is inside it:
9 tool calls · 55s · Bash 5, Read 4.keeps its rhythm whatever display a host gives the transcript.
Review
Eight reviewers over the whole batch — style, testing, security, scala3, composition,
architecture, api, zio. No critical findings. Eight of ten warnings fixed in loop, two
parked with their reason. Full report:
project-management/efforts/cc-chat/slices/02-transcript-presentation/review-slice-20260824-093349.md.The two that mattered:
sanitizer allows
data:beside http/https/mailto, so[open](data:text/html;base64,…)rendered a clickable
data:href into a document that reads mail — against the card's owninvariant that an artifact is a reference and never bytes. Two tests written first, both
failed, both pass.
ui.js, where a second producer could have been written in the one module with no tests. Itis JVM-only now;
ui/shared/srcholds the wire contract and nothing else.Follow-ups (parked, not blocking)
ui.jshas no test module, soTranscriptElementhas never been tested. The property itis trusted to hold — a session id can never become markup — is asserted only in prose. The
fix is infrastructure (jsdom-backed Scala.js tests), which is why it was deferred.
TranscriptApi.markup's error output cannot be decoded back into aTranscriptFailure.Deferred as a judgement: both available fixes are worse than the documented limitation.
parton the artifact endpoint bypasses the JSON error envelope.construction.
<cc-chat>.SO_REUSEADDR, so a restart insidethe
TIME_WAITwindow fails silently.The rest of the parked list is flushed into
project-management/efforts/cc-chat/map.md.Gate
./mill __.test— 823/823 green../mill __.checkFormat— clean. Bundle 12 621 bytesgzipped against the CI limit of 15 000.
No real transcript enters this repo. Every fixture is synthetic; real sessions are read at
runtime only, from the local machine.
🤖 Generated with Claude Code
https://claude.ai/code/session_01GfUk17CWqSK7D7ahc8DJww
Two things Michal found reading the page, and one the first of them uncovered. An open run had a rule under its heading and nothing at its foot, so a reader could see where the machinery started and not where it stopped. It is bracketed now: a rule under the heading, a rule under the last row it holds. The rhythm was missing because the sheet asked the container for it. The demo host writes `cc-transcript { display: block }` in its own chrome, unlayered, so it beat our `display: flex` and took the `gap` with it — every row on the page was spaced by margins alone, and two rounded grounds sat 0px apart. That is the consumer's stylesheet winning, exactly as intended, and it is why the row rhythm cannot live on the container: `display: block` is an ordinary thing for a project to write, and it should not cost every space on the page. A row owns the space above it now. Measured on a real session, the ladder is the same whether the transcript lays out as flex or as block: 12px between rows, 16px between two turns in a row, 20px above a mark where time passed. The host chrome no longer decides how a transcript lays itself out — it asks for a measure and centring, which is all it meant. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GfUk17CWqSK7D7ahc8DJww