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