` 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`
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
Component signature:
```typescript
interface ChatCodeBlockProps {
children?: React.ReactNode;
className?: string;
[key: string]: unknown;
}
export function ChatCodeBlock({ children, className, ...props }: ChatCodeBlockProps): JSX.Element;
```
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.
```typescript
import Markdown from "react-markdown";
import remarkGfm from "remark-gfm";
import rehypeHighlight from "rehype-highlight";
import { ChatCodeBlock } from "./ChatCodeBlock";
import { cn } from "../lib/utils";
interface ChatMarkdownMessageProps {
content: string;
className?: string;
}
export function ChatMarkdownMessage({ content, className }: ChatMarkdownMessageProps) {
return (