Skip to content

feat(chrome): render agent replies as safe Markdown - #190

Open
mikefwille wants to merge 6 commits into
kunchenguid:mainfrom
mikefwille:fm/lavish-md-lfg-g1
Open

feat(chrome): render agent replies as safe Markdown#190
mikefwille wants to merge 6 commits into
kunchenguid:mainfrom
mikefwille:fm/lavish-md-lfg-g1

Conversation

@mikefwille

Copy link
Copy Markdown

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

  • Render agent replies as sanitized Markdown with readable emphasis, code, lists, paragraphs, fenced blocks, and hardened HTTP(S) links, while keeping user messages and stored chat text plain.
  • Bundle Marked and DOMPurify into the browser chrome client, serve only the built IIFE with cache revalidation, and fail clearly when the bundle is missing.
  • Add Markdown styling, dependency notices, documentation, and jsdom/built-bundle coverage for sanitization, fail-closed rendering, and XSS protections.

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.

  • Evidence: Rendered Conversation panel (local file: /var/folders/z1/5cf_zjjx293ggqphkx76ck680000gp/T/no-mistakes-evidence/01KXVQRCQA20V4WQ0TWZN8BY82/markdown-agent-reply-conversation.png)
Evidence: Raw Markdown retained in chat[].text
{
  "sessions": {
    "d99c496fb3f4c00b": {
      "key": "d99c496fb3f4c00b",
      "file": "/private/var/folders/z1/5cf_zjjx293ggqphkx76ck680000gp/T/no-mistakes-evidence/01KXVQRCQA20V4WQ0TWZN8BY82/markdown-chat-artifact.html",
      "url": "http://127.0.0.1:45287/session/d99c496fb3f4c00b",
      "status": "open",
      "pending_prompts": 0,
      "prompts": [],
      "layout_warnings": [],
      "delivered_layout_warning_keys": [],
      "dom_snapshot": "",
      "chat": [
        {
          "role": "user",
          "text": "**user Markdown stays literal** and `not code`",
          "at": "2026-07-18T23:26:58.117Z"
        },
        {
          "role": "user",
          "text": "**user Markdown stays literal** and *not italic*",
          "at": "2026-07-18T23:27:13.686Z"
        },
        {
          "role": "agent",
          "text": "# Update complete\n\n**Bold**, *italic*, and `inline code` render clearly.\n\n- First safe item\n- Second safe item\n\n[Open HTTPS docs](https://example.com/docs)\n\n`` `js\nconst ready = true;\n`` `\n\nUnsafe HTML stayed text: <script>window.__lavishXss = 1</script>\n\n[blocked link](javascript:alert(1))",
          "at": "2026-07-18T23:27:53.031Z"
        }
      ],
      "updated_at": "2026-07-18T23:27:53.034Z"
    }
  }
}
Evidence: Live browser safety probe
{"xssExecuted":false,"scriptNodes":0,"imageNodes":0,"headingNodes":0,"userFormattedNodes":0,"safeLink":{"href":"https://example.com/docs","target":"_blank","rel":"noopener noreferrer"},"blockedLinkHref":null}
Evidence: Built chrome-client response headers
HTTP/1.1 200 OK
cache-control: no-cache
Content-Type: application/javascript; charset=utf-8

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.

  • Initial pnpm run build &amp;&amp; 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-lockfile under the installed Node 22 runtime
  • pnpm run build under Node 22
  • node --test test/chat-markdown.test.js test/chrome-client-queue.test.js test/server.test.js test/package-json.test.js under Node 22
  • dist/cli.mjs &lt;artifact&gt; --no-open --no-gate with isolated evidence state and port
  • Sent literal Markdown through the real Conversation composer using Playwright
  • dist/cli.mjs poll <artifact> --agent-reply "$AGENT_MD" --timeout-ms 1000
  • Captured and visually inspected markdown-agent-reply-conversation.png
  • Probed the rendered DOM for XSS execution, forbidden nodes, heading stripping, user formatting, and link hardening
  • Inspected generated state.json and /chrome-client.js response headers
  • Stopped 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.

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.
@mikefwille

Copy link
Copy Markdown
Author

Here's a before and after:
image
image

@mikefwille

Copy link
Copy Markdown
Author

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.

@kunchenguid

kunchenguid commented Jul 31, 2026

Copy link
Copy Markdown
Owner

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 592193df.

@kunchenguid

Copy link
Copy Markdown
Owner

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.

@kunchenguid kunchenguid removed the wheelhouse:pending-contributor-action Managed by Wheelhouse label Aug 11, 2026
@kunchenguid

Copy link
Copy Markdown
Owner

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 textContent, allow only http(s) links with trusted rel/target, and user bubbles stay plain text. I am not requesting changes for an unsafe implementation.

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.

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.

2 participants