diff --git a/docs/plans/2026-07-28-001-feat-markdown-message-rendering-plan.md b/docs/plans/2026-07-28-001-feat-markdown-message-rendering-plan.md new file mode 100644 index 0000000..a59f06b --- /dev/null +++ b/docs/plans/2026-07-28-001-feat-markdown-message-rendering-plan.md @@ -0,0 +1,446 @@ +--- +title: "feat: Render markdown in agent replies and chat messages" +type: feat +status: completed +date: 2026-07-28 +depth: standard +--- + +# feat: Render markdown in agent replies and chat messages + +## Summary + +Deuce's agent replies and chat messages currently render as raw, unformatted +text. When `@deuce` returns a response full of markdown (`##` headers, `**bold**`, +`###`, numbered lists, code fences), the user sees it all crammed together — +`…what this repo is.## This is the **Unify Agent Workspace**…` — because the +reply is dropped into a plain `
`/`

` with no markdown parsing and no +whitespace preservation. + +The fix wires up `react-markdown` + `remark-gfm` — **already in `package.json` +since the initial commit but never referenced in `src/`** — into a single shared +`` renderer, adds fenced-code syntax highlighting, adds scoped prose +CSS using existing Primer tokens, and applies the renderer across all message +surfaces (agent thread-drawer replies, chat bubbles for human + system messages). +The compact task-card summary stays a single truncated line via a pure +`toPlainText()` helper, since block markdown would break that layout. + +This is a **frontend-only rendering change**. No backend, message-storage, or Pi +agent-output changes. + +--- + +## Problem Frame + +**Reported symptom:** the agent's reply "looks pretty raw, but it seems like it +should have some formatting." The example is a full `@deuce` answer where every +markdown construct is inert and newlines have collapsed into a wall of text. + +**Root cause (confirmed by code read):** + +- The agent's reply text (`task.reply`) is rendered as a plain string in + `src/components/super-threads/AgentThreadDrawer.tsx:184` (the full reply, inside + `.q-resp .bd`) and `src/components/super-threads/AgentTaskCard.tsx:127` (a + one-line card summary). +- `.q-resp .bd` in `src/styles/globals.css:622` sets `line-height` but **no + `white-space` preservation** — so even literal `\n` between a sentence and a + `## ` header collapse to a single space, producing the run-together output. +- Chat bubbles in `src/components/chat/ChatView.tsx:249` render + `message.content` inside `

` — newlines + survive, but no markdown is parsed, so `**bold**` and lists stay literal. +- `react-markdown@^10.1.0` and `remark-gfm@^4.0.1` are dependencies but a repo + grep finds **zero references** in `src/`. There is even a leftover + `.q-resp code { … }` rule (`src/styles/globals.css:634`) that anticipated + markdown code spans. The wiring was simply never done. + +**Scope decisions (confirmed with requester):** + +- Render markdown across **all message text** via one shared component — agent + replies, system notices, and human-typed chat messages. +- Include **syntax highlighting** for fenced code blocks. + +--- + +## Requirements + +- **R1** — Agent replies in the thread drawer render markdown: headers, ordered + and unordered lists, bold/italic, inline code, fenced code blocks, blockquotes, + tables, and links display as formatted output, not literal syntax. +- **R2** — Human-typed chat messages and system notices render through the same + shared renderer, so formatting is consistent everywhere a message body appears. +- **R3** — Fenced code blocks are syntax-highlighted with a dark-mode theme that + fits the existing Primer palette; an unknown or missing language degrades + gracefully to an unhighlighted styled block. +- **R4** — Rendering is XSS-safe: model- and user-generated markdown cannot + inject executable HTML or `javascript:` URLs. Links open in a new tab with safe + `rel` attributes. +- **R5** — The compact task-card summary + (`AgentTaskCard.tsx`, terminal state) stays a single line — markdown is stripped + to readable plain text so headers/lists don't blow out the card layout. +- **R6** — No regression to intentionally-verbatim surfaces: tool output + (`.q-out` / Bash stdout / diffs) and the terminal/logs panels keep rendering as + raw monospace, untouched. + +--- + +## Key Technical Decisions + +- **KTD1 — One shared `` component, reusing installed deps.** Build a + single presentational `src/components/ui/markdown.tsx` wrapping `react-markdown` + with `remark-gfm`. Every message surface routes through it. No new + markdown-core dependency is added (the two are already present). This keeps + formatting behavior identical across chat and the agent drawer and gives one + place to enforce safety. + +- **KTD2 — XSS-safe by default; treat all message content as untrusted.** Do + **not** add `rehype-raw` and do **not** disable escaping. `react-markdown` + escapes raw HTML out of the box, so embedded `