Skip to content

Add markdown rendering to frontend chat messages#944

Merged
hyacinthus merged 3 commits intomainfrom
copilot/add-markdown-support-chat
Feb 11, 2026
Merged

Add markdown rendering to frontend chat messages#944
hyacinthus merged 3 commits intomainfrom
copilot/add-markdown-support-chat

Conversation

Copy link
Contributor

Copilot AI commented Feb 11, 2026

Chat messages rendered as plain text with no markdown, link auto-detection, or image previews.

Changes

  • Markdown rendering — Agent messages render via MarkdownRenderer with remark-gfm (tables, strikethrough, autolinks) and remark-breaks (preserves single newlines from LLM output)
  • Auto image preview — URLs ending in common image extensions (jpg/jpeg/png/webp/gif/bmp/ico/avif) render inline with click-to-open; stateful ImagePreview component falls back to text link on load error
  • Safe link handlingtarget="_blank" with noopener noreferrer applied only to external http(s) URLs; internal/relative/mailto links unaffected
  • SVG excluded from auto-preview (remote SVG attack surface)
  • URL pathname matching — Image extension check uses parsed URL.pathname, supporting querystrings like image.png?raw=1
  • enableBreaks prop — Opt-in remark-breaks for chat context; post pages continue using standard markdown line break semantics
// Before
<div className="whitespace-pre-wrap break-words">{msg.content}</div>

// After (agent messages)
<MarkdownRenderer className={markdownProseClass} enableBreaks>
  {msg.content}
</MarkdownRenderer>

User 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.

- 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>
Copilot AI changed the title [WIP] Add markdown support for local frontend chat messages Add markdown rendering to frontend chat messages Feb 11, 2026
Copilot AI requested a review from hyacinthus February 11, 2026 09:58
@hyacinthus hyacinthus marked this pull request as ready for review February 11, 2026 09:59
Copilot AI review requested due to automatic review settings February 11, 2026 09:59
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 MarkdownRenderer with remark-gfm, default external-link behavior, and auto image previews for common image URL extensions.
  • Updated agent chat UI to render agent messages via MarkdownRenderer with Tailwind Typography styling (user messages remain plain text).
  • Added remark-gfm dependency (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

Comment on lines 47 to 50
return (
<a href={href} target="_blank" rel="noopener noreferrer" {...props}>
{children}
</a>
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines 32 to 44
// 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>
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines 11 to 12
// Common image file extensions for auto-preview
const IMAGE_EXTENSIONS = /\.(jpg|jpeg|png|svg|webp|gif|bmp|ico|avif)$/i;
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines 553 to 559
{msg.role === "agent" ? (
<MarkdownRenderer className={markdownProseClass}>
{msg.content}
</MarkdownRenderer>
) : (
<div className="whitespace-pre-wrap break-words">{msg.content}</div>
)}
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines 21 to 26
function handleImageError(e: SyntheticEvent<HTMLImageElement>) {
const img = e.currentTarget;
const link = img.parentElement;
if (link) {
link.textContent = img.alt || img.src;
}
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
…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>
@hyacinthus hyacinthus merged commit e65ba20 into main Feb 11, 2026
@hyacinthus hyacinthus deleted the copilot/add-markdown-support-chat branch February 11, 2026 11:03
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