SIGN IN SIGN UP

feat(web): render the transcript as a conversation

The history panel showed every turn as escaped plain text in an identical row: an agent's markdown
arrived as literal `## Heading` and `**bold**`, a table as a wall of pipes, and a file edit as a
single sentence with no sign of what changed. It read as a log, not as the conversation it is.

Two renderers, both building DOM nodes rather than HTML strings. That is the point, not an
implementation detail: every character of transcript text lands in a `textContent`, the same
boundary ansiFragment already holds, so nothing a transcript contains can become markup and the
escaping is provable instead of remembered.

`mdFragment` is a hand-rolled subset -- the page is one file with no build step behind a CSP that
blocks every CDN. What it covers is measured, not guessed: across the 2,572 assistant text blocks
(838KB) in the 25 largest transcripts here, inline code appears in 43.8% of them, bold 32.6%,
bullets 11.2%, headings 10.3%, GFM tables 8.7% -- more often than fenced code at 5.2% -- ordered
lists 5.7%, rules 2.5%, italics 2.4%, quotes 1.4%, links 1.3%. Strikethrough appears in 0.1% and
is not supported. Two omissions are deliberate, because this content is code: `_underscore_`
emphasis would mangle snake_case_identifiers, and `*emphasis*` requires non-space just inside both
delimiters so `rename *.ts to *.tsx` is not swallowed into one italic run. Links are http/mailto
only, and a heading becomes a scoped div, not an h1 that would outrank the panel's own title.

`diffFragment` colours the +/- lines and moves the marker into its own gutter cell so the code
keeps its real indentation. A file edit is a `<details>` that opens on tap, headed by the counts of
the whole change even when the body is only its head; a failed call turns red and says why.

Around them the layout separates who spoke: a person's turn is a tinted bubble on its own side,
the agent's is full width, a repeated agent label is dropped, a note is muted, and a tool call is
still one line -- on the biggest session measured here 674 of 794 turns were tool calls. An open
diff is remembered by turn uuid, because typing in the filter re-renders the list on every
keystroke and a diff snapping shut under you is worse than the state it saves.

Tested where the renderers actually run: tests/test_web_history.py loads web/index.html over
file:// in chromium and asserts the DOM, including that markup in a transcript stays text. It
skips rather than fails without playwright, and run.sh runs it as its own step.

The demo worker's canned transcript moves to the same payload shape, diff included -- its whole
job is to show what the real panel shows.
N
Nick007 committed
06fcb5dd22864742659f8caa5bf34e50d61e9857
Parent: a157eec