feat(web): add <AgentMessage> so event summaries render as markdown #1186
No reviewers
Labels
No labels
area:agents
area:dashboard
area:database
area:design
area:design-review
area:flows
area:infra
area:meta
area:security
area:sessions
area:webhook
area:workdir
security
type:bug
type:chore
type:meta
type:user-story
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
charles/agent-hooks!1186
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "code-lead/1164"
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?
Summary
<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 awhitespace-pre-wrapspan (so lists, code blocks and links lost their structure) while the planner transcript ran the same string through<Markdown>— HIGH-severity inconsistency.<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.event-log.tsxEventRow(only swapping the renderer forassistantevents — other types stay on plainwhitespace-pre-wrapbecause their text is plain status output, not agent prose) and inplanner/transcript.tsxfor both persisted bubbles and the in-flight streaming bubble.streamingis forwarded so partial code fences still render progressively.Notes
streamdown's sanitised pipeline; the onlydangerouslySetInnerHTMLin this path is the existing one inside<CodeBlock>, fed by Shiki's escape-safe HTML.agent-message.test.tsxlock 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 passevent-log.test.tsx(5) +transcript.test.tsxcontinue to pass🤖 Generated with Claude Code
APPROVED ✅
CI green. Clean implementation with good test coverage.
What I checked
agent-message.tsx<Markdown>— same renderer the transcript already used, so visual parity is guaranteed without duplicating any Shiki/GFM logic.Streamdowndirectly with every block element overridden to a<span>. All overrides properly strip thenodeprop 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.streamingis intentionally omitted from the compact path — correct, since compact is always given complete content.event-log.tsxev.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 wherewhitespace-pre-wrapis still the right treatment. The block comment explains this clearly.whitespace-pre-wrapis 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.streamingis correctly not forwarded here — event-log rows are complete summaries at render time, not token-streaming.transcript.tsx<Markdown>call sites replaced: the persisted bubble (m.content) and the in-flight streaming bubble (streamText). Thestreaming={streaming && !hasTurnEnded}flag is preserved, forwarded through<AgentMessage>→<Markdown>→<Streamdown parseIncompleteMarkdown>. No regression.Markdownimport correctly removed — no other usage remains in this file.Tests (
agent-message.test.tsx)<ul>in compact, bullet prefixes present,data-testid="code-block"present in default, absent in compact, external-saferelattribute on links). Pattern follows the Vitest browser-mode conventions inapps/web/CLAUDE.md.Observations (non-blocking)
<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.markdown.tsxaoverride forwardsnodeto the DOM (pre-existing:return <a {...props} …/>without strippingnode). Not introduced here; worth a separate clean-up.<Markdown>div addstext-body leading-[1.5]to event-log assistant rows — this is a typography improvement (aligns with the transcript) and not a regression.