Add markdown rendering to frontend chat messages#944
Conversation
- Add remark-gfm for GFM auto-linking of URLs - Update MarkdownRenderer with GFM plugin, auto image preview for common image extensions (jpg/jpeg/png/svg/webp/gif/bmp/ico/avif), safe link handling (target=_blank, noopener noreferrer), and image error fallback - Use MarkdownRenderer for agent messages in chat with Tailwind Typography prose styling Co-authored-by: hyacinthus <488292+hyacinthus@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Adds richer rendering for agent-authored chat content by switching from plain-text display to Markdown (with GitHub Flavored Markdown features), including safer default link handling and inline image URL previews.
Changes:
- Enhanced
MarkdownRendererwithremark-gfm, default external-link behavior, and auto image previews for common image URL extensions. - Updated agent chat UI to render agent messages via
MarkdownRendererwith Tailwind Typography styling (user messages remain plain text). - Added
remark-gfmdependency (and lockfile updates).
Reviewed changes
Copilot reviewed 3 out of 4 changed files in this pull request and generated 5 comments.
| File | Description |
|---|---|
| frontend/src/components/ui/markdown-renderer.tsx | Adds GFM plugin, default <a> behavior, and auto image previews (affects all MarkdownRenderer consumers). |
| frontend/src/app/agent/[id]/ClientPage.tsx | Renders agent chat messages through MarkdownRenderer with prose styling. |
| frontend/package.json | Adds remark-gfm dependency. |
| frontend/package-lock.json | Locks new transitive deps for remark-gfm. |
Files not reviewed (1)
- frontend/package-lock.json: Language not supported
| return ( | ||
| <a href={href} target="_blank" rel="noopener noreferrer" {...props}> | ||
| {children} | ||
| </a> |
There was a problem hiding this comment.
The default <a> renderer forces target="_blank" for all links. This changes behavior for relative/internal links (and non-http schemes like mailto:) and can be surprising in post rendering where MarkdownRenderer is also used. Consider only applying target="_blank" to external http(s) URLs, or making this behavior configurable via a prop.
| // Auto-preview: if the URL ends with a common image extension, render as image | ||
| if (href && IMAGE_EXTENSIONS.test(href)) { | ||
| return ( | ||
| <a href={href} target="_blank" rel="noopener noreferrer" {...props}> | ||
| {/* eslint-disable-next-line @next/next/no-img-element */} | ||
| <img | ||
| src={href} | ||
| alt={typeof children === "string" ? children : href} | ||
| className="max-w-full rounded-md my-1" | ||
| loading="lazy" | ||
| onError={handleImageError} | ||
| /> | ||
| </a> |
There was a problem hiding this comment.
Auto-previewing image URLs will cause the browser to fetch remote images as soon as the message renders, which can leak user IP/user-agent to arbitrary third-party hosts. If that’s a concern, consider gating previews behind a click/consent, restricting to an allowlist/same-origin, or proxying images through a safe endpoint.
| // Common image file extensions for auto-preview | ||
| const IMAGE_EXTENSIONS = /\.(jpg|jpeg|png|svg|webp|gif|bmp|ico|avif)$/i; |
There was a problem hiding this comment.
Including svg in the auto-preview extension list can be risky: remote SVGs can reference external resources and have a larger attack surface than raster images. Consider removing svg from IMAGE_EXTENSIONS (or only allowing it from trusted/proxied sources) and, if you want to support querystrings like image.png?raw=1, consider matching based on URL pathname instead of a strict /$/ regex.
| {msg.role === "agent" ? ( | ||
| <MarkdownRenderer className={markdownProseClass}> | ||
| {msg.content} | ||
| </MarkdownRenderer> | ||
| ) : ( | ||
| <div className="whitespace-pre-wrap break-words">{msg.content}</div> | ||
| )} |
There was a problem hiding this comment.
Switching agent messages from whitespace-pre-wrap to Markdown means single newlines in msg.content will typically be collapsed (Markdown soft line breaks), which is a behavioral regression for many LLM-style outputs. If preserving newlines is desired, consider adding remark-breaks for chat messages or applying an appropriate white-space strategy for paragraphs in the markdown renderer used here.
| function handleImageError(e: SyntheticEvent<HTMLImageElement>) { | ||
| const img = e.currentTarget; | ||
| const link = img.parentElement; | ||
| if (link) { | ||
| link.textContent = img.alt || img.src; | ||
| } |
There was a problem hiding this comment.
handleImageError mutates the DOM directly (link.textContent = ...). In React this can lead to UI getting out of sync with component state/props on re-render. Prefer handling the error in React (e.g., render an ImagePreview component with local state that switches from <img> to a text link when onError fires).
…remark-breaks, React-idiomatic image error handling - Only apply target="_blank" to external http(s) URLs - Remove svg from auto-preview image extensions (security) - Match image extensions against URL pathname (supports querystrings) - Add remark-breaks for chat messages to preserve single newlines - Replace direct DOM mutation with stateful ImagePreview component Co-authored-by: hyacinthus <488292+hyacinthus@users.noreply.github.com>
Chat messages rendered as plain text with no markdown, link auto-detection, or image previews.
Changes
MarkdownRendererwithremark-gfm(tables, strikethrough, autolinks) andremark-breaks(preserves single newlines from LLM output)ImagePreviewcomponent falls back to text link on load errortarget="_blank"withnoopener noreferrerapplied only to externalhttp(s)URLs; internal/relative/mailto links unaffectedURL.pathname, supporting querystrings likeimage.png?raw=1enableBreaksprop — Opt-inremark-breaksfor chat context; post pages continue using standard markdown line break semanticsUser messages remain plain text. Dependencies added:
remark-gfm,remark-breaks.💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.