` tag
+- A fenced code block (` ```typescript ... ``` `) produces output containing `hljs` class
+- The rendered output contains a copy button element with appropriate aria-label
+- Language label is extracted from the code fence
+
+Then create `ui/src/components/ChatCodeBlock.tsx`:
A `pre` component override for react-markdown that wraps code blocks with:
1. A toolbar bar at the top showing the language label (extracted from the `className` on the child `` element, e.g., `language-typescript` -> `typescript`)
2. A copy button in the toolbar that calls `navigator.clipboard.writeText(codeText)` where `codeText` is extracted by recursively flattening the children's text content
3. Copy button shows `Copy` icon (lucide-react) by default, switches to `Check` icon for 1500ms after a successful copy, then reverts
-4. Toolbar background: `bg-card` — same as code block background, with `border-b border-border`
+4. Toolbar background: `bg-card` -- same as code block background, with `border-b border-border`
5. Language label: `text-xs text-muted-foreground font-mono`
6. Copy button: `Button variant="ghost" size="icon"` with `className="h-6 w-6"`, `aria-label="Copy code"` (changes to `"Copied!"` during success state)
7. For `pre` blocks without a code child (plain preformatted text), render a plain `` without the toolbar
@@ -198,7 +211,7 @@ interface ChatCodeBlockProps {
export function ChatCodeBlock({ children, className, ...props }: ChatCodeBlockProps): JSX.Element;
```
-Create `ui/src/components/ChatMarkdownMessage.tsx`:
+Then create `ui/src/components/ChatMarkdownMessage.tsx`:
Builds on the existing `MarkdownBody` pattern but uses `rehype-highlight` for syntax highlighting and the custom `ChatCodeBlock` for code block rendering.
@@ -233,10 +246,15 @@ export function ChatMarkdownMessage({ content, className }: ChatMarkdownMessageP
The `paperclip-markdown` class ensures existing markdown prose styles from index.css apply (font-size 0.9375rem, line-height 1.6, heading styles, table styles, etc.).
-Do NOT duplicate mermaid handling from MarkdownBody — mermaid diagrams are not expected in chat responses for Phase 21. If needed later, it can be added.
+Do NOT duplicate mermaid handling from MarkdownBody -- mermaid diagrams are not expected in chat responses for Phase 21. If needed later, it can be added.
+
+Run tests after implementation to verify:
+```bash
+pnpm vitest run ui/src/components/ChatMarkdownMessage.test.tsx
+```