Skip to content

feat(chat): render markdown in agent replies and chat messages - #42

Merged
clintberry merged 9 commits into
mainfrom
feat/markdown-message-rendering
Jul 28, 2026
Merged

feat(chat): render markdown in agent replies and chat messages#42
clintberry merged 9 commits into
mainfrom
feat/markdown-message-rendering

Conversation

@clintberry

Copy link
Copy Markdown
Contributor

Summary

@deuce's replies used to arrive as a wall of raw markdown — …what this repo is.## This is the **Unify Agent Workspace**… — with headers, lists, and bold rendered as literal characters and newlines collapsed. Now agent replies and chat messages render as formatted prose: headers, lists, tables, blockquotes, links, and syntax-highlighted code blocks.

The building blocks were already present — react-markdown and remark-gfm have been in package.json since the initial commit but were never referenced in src/. This wires them into one shared, XSS-safe renderer and applies it across every message surface. Frontend-only: no backend, message-storage, or agent-output changes.

What changed

  • Agent replies in the thread drawer render full markdown instead of a run-together string.
  • Chat messages (human-typed and system notices) render through the same renderer, so formatting is consistent everywhere.
  • Fenced code blocks are syntax-highlighted; unknown/absent languages fall back to a styled plain block.
  • The compact task-card summary stays a single line — a pure toPlainText() helper strips markdown so headers/lists don't blow out the card.

Design decisions

  • One shared <Markdown> component, XSS-safe by default. Raw HTML is escaped (no rehype-raw); links are protocol-allowlisted (http/https/mailto) and open in a new tab with noopener noreferrer nofollow. All message content — model-generated and human-typed — is treated as untrusted for rendering.
  • Syntax highlighting via react-syntax-highlighter (Prism async-light build + a curated language set, one-dark theme). Shiki was considered but ships a heavier WASM runtime than warranted for short chat snippets.
  • remark-breaks preserves a human's single newlines as line breaks — matching the old whitespace-pre-wrap behavior instead of collapsing them.
  • jsdom + @testing-library harness (new vitest.config.ts) added so the safety-critical rendering (HTML-injection inertness, javascript: link rejection) is actually asserted, not just claimed. The existing pure-logic suites stay green.

New dependencies

Runtime: react-syntax-highlighter, remark-breaks. Dev: jsdom, @testing-library/react, @testing-library/dom, @testing-library/jest-dom, @types/react-syntax-highlighter. All mainstream, widely-used packages.

Test plan

  • 67 Vitest tests pass (26 new across markdown, markdown-code, markdown-plain, MessageBubble, AgentThreadDrawer, and a render-harness smoke test). Safety tests assert an <img onerror> payload renders inert and javascript: links are dropped.
  • npx tsc -b clean, eslint 0 errors, vite build succeeds.

Post-Deploy Monitoring & Validation

No additional operational monitoring required — this is a frontend rendering change with no backend, runtime, or data impact. Validate visually after deploy: an agent reply with headers/lists/code renders formatted, and tool-output/terminal panels still render verbatim.

Known residuals / follow-ups

  • @mention highlighting is not yet unified into the markdown pipeline; the drawer prompt-echo still uses the Mentioned component (chat bubbles had no mention highlighting before, so no regression).
  • Prism languages are statically bundled (~831 kB build; a pre-existing >500 kB warning). Dynamic-import code-splitting is a future optimization.
  • Streaming/incremental markdown during a live run is deferred — replies render on completion.

Plan: docs/plans/2026-07-28-001-feat-markdown-message-rendering-plan.md


Compound Engineering
Claude Code

Adds jsdom + @testing-library/react + jest-dom and a vitest.config.ts with a
jsdom environment so component render tests can assert DOM behavior. Excludes
stale .worktrees/ duplicate suites. Existing pure-logic suites stay green.
Wires the already-installed react-markdown + remark-gfm into one shared
<Markdown> component for all message text. Raw HTML is escaped (no rehype-raw);
links are protocol-allowlisted and open in a new tab with noopener/noreferrer/
nofollow. Tests assert formatting, GFM tables/task lists, HTML-injection
inertness, and javascript: link rejection.
Adds react-syntax-highlighter (Prism async light + curated language set and
common aliases) as the code renderer for <Markdown>. Language-tagged fences are
highlighted with a dark theme; unknown/absent languages fall back to a styled
plain block; inline code stays a simple <code>.
Scoped under .md so prose rhythm doesn't leak into chat/card chrome. Styles
headings, lists (incl. GFM task lists), blockquotes, tables, links, inline code,
and fenced code blocks (frame + horizontal scroll) using Primer tokens.
Supersedes the old .q-resp code rule.
Routes the thread-drawer agent reply and chat message bubbles (human + system
notices) through the shared <Markdown> renderer. Terminal fallback strings,
tool output, expandable content, and the Mentioned prompt echo are untouched.
Exports MessageBubble for testing; adds ^_ unused-var ignore and drops
.worktrees from lint.
Adds toPlainText() (strips markdown syntax, collapses whitespace) and applies
it to the terminal task-card one-line summary so a markdown reply stays a clean
single line instead of leaking ## and list markers into the card.
The terminal no-reply fallback ('Run failed.'/'Run cancelled.'/'Done.') was
duplicated across the inline card and the thread drawer. Centralize it in
utils.ts so the phrasing lives in one place.
Code-review follow-ups:
- Add remark-breaks so a human message's single newlines render as line breaks
  (matching the old whitespace-pre-wrap behavior) instead of collapsing.
- toPlainText no longer strips underscores inside words, so snake_case
  identifiers survive in the compact card summary.
@clintberry
clintberry merged commit afd5310 into main Jul 28, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant