feat(chrome): render agent replies as safe Markdown - #190
Conversation
Display-time Markdown for agent bubbles only (marked + DOMPurify), keeping raw text in storage/SSE. Bundle chrome-client as a browser IIFE, serve with no-cache, and fail closed to plain text instead of string innerHTML.
Stop defaultChromeClientPath from serving raw src ESM when dist is missing, escape marked HTML tokens before sanitize, and reject empty sanitize fragments. Add path/404/bundle/XSS/static-contract coverage for the agent bubble path.
|
I found that it was quite difficult to read the wall of text that came from agent responses. I tried to keep the commit as lean and simple as possible. These improvements have meaningfully added to quality of life on my part as I've been using them and have kept me in the flow better. I hope it does the same for you and others. Please let me know if you have any questions or would like to see any changes. |
|
Automated reminder: thanks for the PR! This branch currently has a merge conflict with the base branch. When you get a chance, please rebase onto (or merge) the latest base branch, resolve the conflict, and push. After that, checks will re-run and the PR will get looked at again. Noted for lavish-axi#190 at |
|
Automated reminder: this PR still looks blocked on a rebase or merge conflict fix. If you are still interested, please rebase onto the current base branch, resolve the conflict, and push. If I do not hear back, I may close this as inactive. |
|
Speaking as Kun's firstmate: Holding this. I reviewed the Markdown/XSS path: agent bubbles render through marked + DOMPurify as a DocumentFragment, fail closed to I still cannot auto-merge. The branch is CONFLICTING with main, and even after a rebase this would be a default chrome behavior change — agent replies would render as Markdown with no opt-in. I am not rebasing a default-behavior PR. Please rebase onto current main if you still want this considered; the product call waits until the branch is mergeable. |


Intent
Ship Markdown rendering for agent-reply chat bubbles in Lavish chrome Conversation panel.
Goal: agent replies already travel as raw strings via --agent-reply; display them as safe readable Markdown (bold, italic, code, lists, paragraphs/breaks, http(s) links, unhighlighted fenced code) without changing poll protocol, storage schema, or agent-reply API. User bubbles stay plain text. Headings strip tags but keep text. Canonical source stays raw Markdown in chat[].text; render at display time only via addChat for role===agent.
Implementation: new src/chat-markdown.js (marked + DOMPurify) returning DocumentFragment or plainText fail-closed; chrome-client addChat attaches fragment via replaceChildren never innerHTML of HTML strings; esbuild IIFE bundle of chrome-client; serve built asset with Cache-Control no-cache via defaultChromeClientPath; THIRD-PARTY-NOTICES; bubble CSS; tests with jsdom + built-IIFE harness.
Code-review and no-mistakes hardenings already on branch: defaultChromeClientPath never serves raw src ESM; marked HTML escape; empty/break-only sanitize fragments fail closed to plainText; ordered-list start attr preserved; in-memory chrome harness IIFE; jsdom/engine alignment; extracted harness helper; complete Marked license notices.
Out of scope: user-bubble Markdown, syntax highlighting, images/tables/task lists, structured rich-reply protocol, server-side HTML storage.
Note: prior run failed only on push (fork_url missing); fork is now https://github.com/mikefwille/lavish-axi.git. Push must go to fork and open PR against kunchenguid/lavish-axi.
What Changed
Risk Assessment
✅ Low: The change is well-bounded, preserves raw chat storage and protocols, and uses a strict sanitized DocumentFragment rendering path with built-bundle coverage.
Testing
No separate baseline results were supplied. After routing pnpm through Node 22, the production build, focused automated suite, and real CLI→server/SSE→browser workflow passed; screenshot, DOM, persisted-state, and response-header evidence confirm safe display-time agent Markdown without changing raw storage or user-bubble behavior.
/var/folders/z1/5cf_zjjx293ggqphkx76ck680000gp/T/no-mistakes-evidence/01KXVQRCQA20V4WQ0TWZN8BY82/markdown-agent-reply-conversation.png)Evidence: Raw Markdown retained in chat[].text
Evidence: Live browser safety probe
Evidence: Built chrome-client response headers
Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
✅ **Review** - passed
✅ No issues found.
✅ **Test** - passed
✅ No issues found.
Initialpnpm run build && node --test test/chat-markdown.test.js test/chrome-client-queue.test.js test/server.test.js test/package-json.test.js(setup-only Node 20/Corepack failure; retried under Node 22)pnpm install --frozen-lockfileunder the installed Node 22 runtimepnpm run buildunder Node 22node --test test/chat-markdown.test.js test/chrome-client-queue.test.js test/server.test.js test/package-json.test.jsunder Node 22dist/cli.mjs <artifact> --no-open --no-gatewith isolated evidence state and portSent literal Markdown through the real Conversation composer using Playwrightdist/cli.mjs poll <artifact> --agent-reply "$AGENT_MD" --timeout-ms 1000Captured and visually inspectedmarkdown-agent-reply-conversation.pngProbed the rendered DOM for XSS execution, forbidden nodes, heading stripping, user formatting, and link hardeningInspected generatedstate.jsonand/chrome-client.jsresponse headersStopped the test server/browser, removed generated worktree artifacts, and confirmed a clean worktree✅ **Document** - passed
✅ No issues found.
✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.