feat(chat): render markdown in agent replies and chat messages - #42
Merged
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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-markdownandremark-gfmhave been inpackage.jsonsince the initial commit but were never referenced insrc/. 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
toPlainText()helper strips markdown so headers/lists don't blow out the card.Design decisions
<Markdown>component, XSS-safe by default. Raw HTML is escaped (norehype-raw); links are protocol-allowlisted (http/https/mailto) and open in a new tab withnoopener noreferrer nofollow. All message content — model-generated and human-typed — is treated as untrusted for rendering.react-syntax-highlighter(Prism async-light build + a curated language set,one-darktheme). Shiki was considered but ships a heavier WASM runtime than warranted for short chat snippets.remark-breakspreserves a human's single newlines as line breaks — matching the oldwhitespace-pre-wrapbehavior instead of collapsing them.@testing-libraryharness (newvitest.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
markdown,markdown-code,markdown-plain,MessageBubble,AgentThreadDrawer, and a render-harness smoke test). Safety tests assert an<img onerror>payload renders inert andjavascript:links are dropped.npx tsc -bclean,eslint0 errors,vite buildsucceeds.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
@mentionhighlighting is not yet unified into the markdown pipeline; the drawer prompt-echo still uses theMentionedcomponent (chat bubbles had no mention highlighting before, so no regression).Plan:
docs/plans/2026-07-28-001-feat-markdown-message-rendering-plan.md