diff --git a/openspec/changes/archive/2026-04-05-enable-rich-text-display/.openspec.yaml b/openspec/changes/archive/2026-04-05-enable-rich-text-display/.openspec.yaml new file mode 100644 index 0000000..c551aea --- /dev/null +++ b/openspec/changes/archive/2026-04-05-enable-rich-text-display/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-04-05 diff --git a/openspec/changes/archive/2026-04-05-enable-rich-text-display/design.md b/openspec/changes/archive/2026-04-05-enable-rich-text-display/design.md new file mode 100644 index 0000000..c5e54ec --- /dev/null +++ b/openspec/changes/archive/2026-04-05-enable-rich-text-display/design.md @@ -0,0 +1,57 @@ +## Context + +Assistant messages from the LLM contain markdown formatting (bold, code blocks, lists, headings) but are currently rendered as plain text via `@message.Content`. Markdig 1.1.1 is already in the stack spec but not yet wired up. The Blazor WASM client needs a rendering pipeline that converts markdown to HTML and displays it safely inside chat bubbles. + +## Goals / Non-Goals + +**Goals:** +- Render assistant message markdown as formatted HTML (headings, bold, italic, code, lists, tables, links) +- Sanitize HTML output to prevent XSS from LLM-generated content +- Style rendered elements to look good inside MudBlazor chat bubbles +- Maintain streaming performance — re-render as tokens arrive without flicker + +**Non-Goals:** +- Rendering user messages as markdown (they stay plain text) +- Syntax highlighting for code blocks (future enhancement) +- LaTeX/math rendering +- Image rendering from markdown + +## Decisions + +### D1: Use Markdig for markdown-to-HTML conversion + +Markdig is already in the stack spec. It's the standard .NET markdown library, runs in WASM, and supports GFM (GitHub Flavored Markdown) extensions out of the box. + +**Alternative**: Custom regex-based parsing — rejected, fragile and incomplete. + +### D2: Render via `MarkupString` in Blazor + +Blazor's `MarkupString` struct renders raw HTML in the component tree. We wrap the Markdig HTML output in `MarkupString` to display it. This replaces `@message.Content` with `@((MarkupString)renderedHtml)` for assistant messages only. + +**Alternative**: Use a third-party Blazor markdown component — rejected, adds a dependency when Markdig + MarkupString achieves the same result with less coupling. + +### D3: HTML sanitization via allowlist approach + +LLM output is untrusted. After Markdig converts markdown to HTML, we strip any tags/attributes not on an allowlist. Allowed: `p, h1-h6, strong, em, code, pre, ul, ol, li, a (href only), table, thead, tbody, tr, th, td, br, blockquote`. This prevents script injection without a heavy sanitizer dependency. + +**Alternative**: Use a NuGet sanitizer package (e.g., HtmlSanitizer) — viable but adds a dependency for a simple allowlist. If the allowlist proves insufficient, revisit. + +### D4: Create a MarkdownService for the conversion pipeline + +A `MarkdownService` class encapsulates the Markdig pipeline configuration, HTML sanitization, and caching. Registered as singleton in DI. This keeps the rendering logic out of the Razor component and makes it testable. + +### D5: Incremental rendering during streaming + +During streaming, the assistant message content grows token by token. Each time `StateHasChanged()` is called, the full content string is re-converted through Markdig. This is acceptable because: +- Markdig is fast (sub-millisecond for typical message sizes) +- Messages rarely exceed a few KB +- No DOM diffing overhead — Blazor handles this efficiently + +If performance becomes an issue, we can cache the last-known-good HTML prefix and only re-render the delta. + +## Risks / Trade-offs + +- **[XSS from LLM output]** → Mitigated by HTML sanitization allowlist. The allowlist is conservative — only structural tags, no script/style/event handlers. +- **[Streaming flicker]** → Low risk. Blazor's diffing minimizes DOM updates. If observed, add debouncing to StateHasChanged calls. +- **[Markdig WASM size]** → Markdig adds ~200KB to the WASM bundle. Already accepted in stack spec. +- **[Incomplete markdown support]** → Markdig supports GFM by default. Edge cases (nested tables, complex HTML blocks) may render imperfectly but are rare in LLM output. diff --git a/openspec/changes/archive/2026-04-05-enable-rich-text-display/proposal.md b/openspec/changes/archive/2026-04-05-enable-rich-text-display/proposal.md new file mode 100644 index 0000000..948abb5 --- /dev/null +++ b/openspec/changes/archive/2026-04-05-enable-rich-text-display/proposal.md @@ -0,0 +1,24 @@ +## Why + +Assistant messages currently render as plain text — markdown formatting (bold, code blocks, lists, headings) from the LLM appears as raw characters. With Semantic Kernel and tool calling now in place, responses are increasingly structured and harder to read without proper rendering. Markdig 1.1.1 is already in the stack but not wired up. + +## What Changes + +- Render assistant message content as HTML by converting markdown via Markdig +- Sanitize rendered HTML to prevent XSS (the LLM output is untrusted content) +- Style rendered markdown elements (code blocks, lists, tables) to fit the chat bubble aesthetic +- Keep user messages as plain text (they are short inputs, not markdown) + +## Capabilities + +### New Capabilities +- `rich-text-display`: Markdown-to-HTML rendering pipeline for assistant messages, including sanitization and styling + +### Modified Capabilities +- `chat-ui`: Assistant message display changes from plain text to rendered markdown + +## Impact + +- ChatAgent.Client: Chat.razor message rendering, new markdown service, CSS additions +- Dependencies: Markdig already in stack spec; may need an HTML sanitizer package +- No backend changes — this is purely client-side rendering diff --git a/openspec/changes/archive/2026-04-05-enable-rich-text-display/specs/chat-ui/spec.md b/openspec/changes/archive/2026-04-05-enable-rich-text-display/specs/chat-ui/spec.md new file mode 100644 index 0000000..9bb07b7 --- /dev/null +++ b/openspec/changes/archive/2026-04-05-enable-rich-text-display/specs/chat-ui/spec.md @@ -0,0 +1,20 @@ +## MODIFIED Requirements + +### Requirement: Message display + +The chat page SHALL display messages in a vertically scrolling list, with each message showing the sender role (user or assistant), the message content, and a visual distinction between user and assistant messages (e.g., alignment, color, or avatar). Assistant messages SHALL render content as formatted HTML converted from markdown; user messages SHALL display as plain text. + +#### Scenario: User message displayed + +- **WHEN** the user sends a message +- **THEN** the message appears in the message list aligned or styled to indicate it is from the user, rendered as plain text + +#### Scenario: Assistant message displayed + +- **WHEN** the assistant responds +- **THEN** the response appears in the message list with distinct styling from user messages, with markdown content rendered as formatted HTML + +#### Scenario: Message ordering + +- **WHEN** multiple messages exist in the conversation +- **THEN** messages are displayed in chronological order, oldest at top diff --git a/openspec/changes/archive/2026-04-05-enable-rich-text-display/specs/rich-text-display/spec.md b/openspec/changes/archive/2026-04-05-enable-rich-text-display/specs/rich-text-display/spec.md new file mode 100644 index 0000000..7c98af9 --- /dev/null +++ b/openspec/changes/archive/2026-04-05-enable-rich-text-display/specs/rich-text-display/spec.md @@ -0,0 +1,91 @@ +## ADDED Requirements + +### Requirement: Markdown rendering for assistant messages + +The system SHALL convert assistant message content from markdown to formatted HTML using Markdig, displaying headings, bold, italic, code blocks, lists, tables, links, and blockquotes with proper visual formatting. + +#### Scenario: Markdown bold and italic rendered + +- **WHEN** an assistant message contains `**bold**` or `*italic*` text +- **THEN** the text is displayed with bold or italic formatting respectively + +#### Scenario: Code block rendered + +- **WHEN** an assistant message contains a fenced code block (triple backticks) +- **THEN** the code is displayed in a monospace font within a visually distinct block + +#### Scenario: Inline code rendered + +- **WHEN** an assistant message contains inline code (single backticks) +- **THEN** the code is displayed in a monospace font with a subtle background + +#### Scenario: List rendered + +- **WHEN** an assistant message contains a markdown list (ordered or unordered) +- **THEN** the list is displayed with proper indentation and bullet/number markers + +#### Scenario: Heading rendered + +- **WHEN** an assistant message contains markdown headings (# through ######) +- **THEN** the headings are displayed with appropriate size and weight + +#### Scenario: Link rendered + +- **WHEN** an assistant message contains a markdown link `[text](url)` +- **THEN** the link is displayed as a clickable hyperlink opening in a new tab + +#### Scenario: Table rendered + +- **WHEN** an assistant message contains a markdown table +- **THEN** the table is displayed with borders, header row styling, and proper alignment + +### Requirement: HTML sanitization + +The system SHALL sanitize all HTML output from the markdown renderer to prevent cross-site scripting (XSS) attacks from LLM-generated content. + +#### Scenario: Script tags stripped + +- **WHEN** assistant message content contains ` world"); + Assert.DoesNotContain("