feat(web): add <AgentMessage> so event summaries render as markdown #1186

Merged
reviewer merged 1 commit from code-lead/1164 into main 2026-05-14 23:51:09 +00:00
Collaborator

Summary

  • Centralise agent-prose rendering on a shared <AgentMessage> atom (apps/web/src/components/agent-message.tsx). The 2026-05-15 agent-output-unification audit flagged that the event log dropped the agent's markdown into a whitespace-pre-wrap span (so lists, code blocks and links lost their structure) while the planner transcript ran the same string through <Markdown> — HIGH-severity inconsistency.
  • Default mode delegates to <Markdown> (same GFM + Shiki renderer the transcript already used) so prose looks identical across surfaces. Compact mode strips block-level formatting (lists → inline bullet runs, fenced code → inline <code>, headings + paragraphs lose block spacing) for narrow contexts like the task-list summary preview the follow-up audit work will adopt next. Inline emphasis (bold, italic, links) is preserved on both paths.
  • Adopted in event-log.tsx EventRow (only swapping the renderer for assistant events — other types stay on plain whitespace-pre-wrap because their text is plain status output, not agent prose) and in planner/transcript.tsx for both persisted bubbles and the in-flight streaming bubble. streaming is forwarded so partial code fences still render progressively.

Notes

  • Security: markdown still flows through streamdown's sanitised pipeline; the only dangerouslySetInnerHTML in this path is the existing one inside <CodeBlock>, fed by Shiki's escape-safe HTML.
  • New tests in agent-message.test.tsx lock in: default mode preserves <ul>/<li> + fenced <code-block> + external-safe link attrs; compact mode flattens lists to inline bullet runs, degrades fenced code to inline <code>, and preserves inline emphasis (bold/italic/link).

Refs #1164.

Test plan

  • just qa (typecheck + lint + format + tests, all green)
  • bun run test src/components/agent-message.test.tsx — 8/8 pass
  • Existing event-log.test.tsx (5) + transcript.test.tsx continue to pass
  • Visual: event-log assistant rows now match transcript bubble formatting (lists, fenced code, links)
  • Visual: non-markdown event summaries (tool_summary, result, error, progress, steer_*, cursor_replay_start) still render verbatim

🤖 Generated with Claude Code

## Summary - Centralise agent-prose rendering on a shared `<AgentMessage>` atom (`apps/web/src/components/agent-message.tsx`). The 2026-05-15 agent-output-unification audit flagged that the event log dropped the agent's markdown into a `whitespace-pre-wrap` span (so lists, code blocks and links lost their structure) while the planner transcript ran the same string through `<Markdown>` — HIGH-severity inconsistency. - **Default mode** delegates to `<Markdown>` (same GFM + Shiki renderer the transcript already used) so prose looks identical across surfaces. **Compact mode** strips block-level formatting (lists → inline bullet runs, fenced code → inline `<code>`, headings + paragraphs lose block spacing) for narrow contexts like the task-list summary preview the follow-up audit work will adopt next. Inline emphasis (bold, italic, links) is preserved on both paths. - Adopted in `event-log.tsx` `EventRow` (only swapping the renderer for `assistant` events — other types stay on plain `whitespace-pre-wrap` because their text is plain status output, not agent prose) and in `planner/transcript.tsx` for both persisted bubbles and the in-flight streaming bubble. `streaming` is forwarded so partial code fences still render progressively. ## Notes - Security: markdown still flows through `streamdown`'s sanitised pipeline; the only `dangerouslySetInnerHTML` in this path is the existing one inside `<CodeBlock>`, fed by Shiki's escape-safe HTML. - New tests in `agent-message.test.tsx` lock in: default mode preserves `<ul>`/`<li>` + fenced `<code-block>` + external-safe link attrs; compact mode flattens lists to inline bullet runs, degrades fenced code to inline `<code>`, and preserves inline emphasis (bold/italic/link). Refs #1164. ## Test plan - [x] `just qa` (typecheck + lint + format + tests, all green) - [x] `bun run test src/components/agent-message.test.tsx` — 8/8 pass - [x] Existing `event-log.test.tsx` (5) + `transcript.test.tsx` continue to pass - [ ] Visual: event-log assistant rows now match transcript bubble formatting (lists, fenced code, links) - [ ] Visual: non-markdown event summaries (tool_summary, result, error, progress, steer_*, cursor_replay_start) still render verbatim 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(web): add <AgentMessage> so event summaries render as markdown
All checks were successful
qa / i18n-string-check (pull_request) Successful in 16s
qa / dockerfile (pull_request) Successful in 16s
qa / db-schema (pull_request) Successful in 19s
qa / sql-layer-check (pull_request) Successful in 15s
qa / qa-1 (pull_request) Successful in 4m24s
qa / qa (pull_request) Successful in 0s
9baa44f0a3
Pull the markdown rendering choice out of each surface and centralise it
on a shared `<AgentMessage>` atom. The event log dropped the agent's
markdown source into a `whitespace-pre-wrap` span (lists and code blocks
lost their structure); the planner transcript piped the same string
through `<Markdown>` (full GFM + Shiki). Operators reading the event log
lost the formatting they relied on in the transcript — the 2026-05-15
agent-output-unification audit flagged it as HIGH severity.

Default mode delegates to `<Markdown>` so prose looks identical to the
transcript. Compact mode strips block-level formatting (lists → inline
bullet runs, fenced code → inline `<code>`, headings + paragraphs lose
block spacing) for narrow contexts like the task-list summary preview
that the same audit will adopt next. Inline emphasis (bold, italic,
links) is preserved on both paths.

Adopters: event-log `EventRow` only swaps the renderer for `assistant`
events — other types (tool summaries, results, errors, progress, steer
rows, …) stay on the plain whitespace-pre-wrap path because their text
is plain status output, not agent prose. Planner transcript uses
`<AgentMessage>` for both persisted bubbles and the in-flight streaming
bubble (forwarding `streaming` so partial code fences still render).

Refs #1164.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
reviewer approved these changes 2026-05-14 23:51:01 +00:00
reviewer left a comment

APPROVED

CI green. Clean implementation with good test coverage.

What I checked

agent-message.tsx

  • Default mode delegates correctly to <Markdown> — same renderer the transcript already used, so visual parity is guaranteed without duplicating any Shiki/GFM logic.
  • Compact mode uses Streamdown directly with every block element overridden to a <span>. All overrides properly strip the node prop before forwarding to the DOM (avoiding the React unknown-prop warning). The bullet-prefix approach () is the right call for a single-line run.
  • memo() is appropriate: all four props are primitives, so the default referential-equality check is sufficient.
  • streaming is intentionally omitted from the compact path — correct, since compact is always given complete content.

event-log.tsx

  • The ev.type === "assistant" gate is well-motivated: assistant prose is agent-written markdown; everything else (tool summaries, results, errors, progress, steer rows) is plain status output where whitespace-pre-wrap is still the right treatment. The block comment explains this clearly.
  • whitespace-pre-wrap is moved onto the <span> for the non-assistant branch rather than the outer <div> — no regression for non-markdown event types.
  • showDetailToggle / [+]/[−] button remains a sibling after the conditional block, which is correct; it applies to both assistant and non-assistant events.
  • streaming is correctly not forwarded here — event-log rows are complete summaries at render time, not token-streaming.

transcript.tsx

  • Both <Markdown> call sites replaced: the persisted bubble (m.content) and the in-flight streaming bubble (streamText). The streaming={streaming && !hasTurnEnded} flag is preserved, forwarded through <AgentMessage><Markdown><Streamdown parseIncompleteMarkdown>. No regression.
  • Markdown import correctly removed — no other usage remains in this file.

Tests (agent-message.test.tsx)

  • 8 tests covering both modes, edge cases (empty content), and the structural assertions the contract actually cares about (no <ul> in compact, bullet prefixes present, data-testid="code-block" present in default, absent in compact, external-safe rel attribute on links). Pattern follows the Vitest browser-mode conventions in apps/web/CLAUDE.md.

Observations (non-blocking)

  1. Five other <Markdown> consumers (sections.tsx, issue-card.tsx, agent-db-skills-tab.tsx, agents.$type.setup.tsx, plan-mode-view.tsx) still import <Markdown> directly. This PR is scoped to the two surfaces called out in the #1164 audit ("Refs", not "Closes") — fine to leave the migration as follow-up work.
  2. markdown.tsx a override forwards node to the DOM (pre-existing: return <a {...props} …/> without stripping node). Not introduced here; worth a separate clean-up.
  3. Inner <Markdown> div adds text-body leading-[1.5] to event-log assistant rows — this is a typography improvement (aligns with the transcript) and not a regression.
## APPROVED ✅ CI green. Clean implementation with good test coverage. ### What I checked **`agent-message.tsx`** - Default mode delegates correctly to `<Markdown>` — same renderer the transcript already used, so visual parity is guaranteed without duplicating any Shiki/GFM logic. - Compact mode uses `Streamdown` directly with every block element overridden to a `<span>`. All overrides properly strip the `node` prop before forwarding to the DOM (avoiding the React unknown-prop warning). The bullet-prefix approach (`• `) is the right call for a single-line run. - `memo()` is appropriate: all four props are primitives, so the default referential-equality check is sufficient. - `streaming` is intentionally omitted from the compact path — correct, since compact is always given complete content. **`event-log.tsx`** - The `ev.type === "assistant"` gate is well-motivated: assistant prose is agent-written markdown; everything else (tool summaries, results, errors, progress, steer rows) is plain status output where `whitespace-pre-wrap` is still the right treatment. The block comment explains this clearly. - `whitespace-pre-wrap` is moved onto the `<span>` for the non-assistant branch rather than the outer `<div>` — no regression for non-markdown event types. - `showDetailToggle` / `[+]/[−]` button remains a sibling after the conditional block, which is correct; it applies to both assistant and non-assistant events. - `streaming` is correctly **not** forwarded here — event-log rows are complete summaries at render time, not token-streaming. **`transcript.tsx`** - Both `<Markdown>` call sites replaced: the persisted bubble (`m.content`) and the in-flight streaming bubble (`streamText`). The `streaming={streaming && !hasTurnEnded}` flag is preserved, forwarded through `<AgentMessage>` → `<Markdown>` → `<Streamdown parseIncompleteMarkdown>`. No regression. - `Markdown` import correctly removed — no other usage remains in this file. **Tests (`agent-message.test.tsx`)** - 8 tests covering both modes, edge cases (empty content), and the structural assertions the contract actually cares about (no `<ul>` in compact, bullet prefixes present, `data-testid="code-block"` present in default, absent in compact, external-safe `rel` attribute on links). Pattern follows the Vitest browser-mode conventions in `apps/web/CLAUDE.md`. ### Observations (non-blocking) 1. **Five other `<Markdown>` consumers** (`sections.tsx`, `issue-card.tsx`, `agent-db-skills-tab.tsx`, `agents.$type.setup.tsx`, `plan-mode-view.tsx`) still import `<Markdown>` directly. This PR is scoped to the two surfaces called out in the #1164 audit ("Refs", not "Closes") — fine to leave the migration as follow-up work. 2. **`markdown.tsx` `a` override forwards `node` to the DOM** (pre-existing: `return <a {...props} …/>` without stripping `node`). Not introduced here; worth a separate clean-up. 3. **Inner `<Markdown>` div adds `text-body leading-[1.5]`** to event-log assistant rows — this is a typography improvement (aligns with the transcript) and not a regression.
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
charles/agent-hooks!1186
No description provided.