---
phase: 21-chat-foundation
plan: 02
type: execute
wave: 1
depends_on: ["21-00"]
files_modified:
  - ui/src/components/ChatMarkdownMessage.tsx
  - ui/src/components/ChatCodeBlock.tsx
  - ui/src/index.css
  - ui/package.json
autonomous: true
requirements: [CHAT-02, CHAT-03, THEME-02]

must_haves:
  truths:
    - "Markdown messages render with syntax-highlighted code blocks"
    - "Code blocks show a language label and a one-click copy button"
    - "Code block highlighting changes correctly when switching between Catppuccin Mocha, Tokyo Night, and Catppuccin Latte"
  artifacts:
    - path: "ui/src/components/ChatMarkdownMessage.tsx"
      provides: "Markdown renderer with rehype-highlight"
      exports: ["ChatMarkdownMessage"]
    - path: "ui/src/components/ChatCodeBlock.tsx"
      provides: "Code block wrapper with copy button and language label"
      exports: ["ChatCodeBlock"]
  key_links:
    - from: "ui/src/components/ChatMarkdownMessage.tsx"
      to: "rehype-highlight"
      via: "rehypePlugins prop on react-markdown"
      pattern: "rehypePlugins.*rehypeHighlight"
    - from: "ui/src/components/ChatCodeBlock.tsx"
      to: "navigator.clipboard"
      via: "writeText call on copy button click"
      pattern: "navigator\\.clipboard\\.writeText"
    - from: "ui/src/index.css"
      to: "highlight.js themes"
      via: "CSS overrides per theme class (.dark .hljs, .theme-tokyo-night .hljs)"
      pattern: "\\.hljs"
---

<objective>
Install rehype-highlight and build the theme-aware markdown message renderer with code block copy functionality.

Purpose: Satisfy CHAT-02 (markdown rendering with syntax highlighting), CHAT-03 (copy button + language label on code blocks), and THEME-02 (theme-appropriate highlighting colors). This is the rendering layer used by the message list in later plans.
Output: ChatMarkdownMessage and ChatCodeBlock components, ready for use.
</objective>

<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/phases/21-chat-foundation/21-RESEARCH.md
@.planning/phases/21-chat-foundation/21-UI-SPEC.md

<interfaces>
From ui/src/components/MarkdownBody.tsx:
```typescript
import Markdown, { type Components } from "react-markdown";
import remarkGfm from "remark-gfm";
import { cn } from "../lib/utils";
import { useTheme, THEME_META } from "../context/ThemeContext";
```

From ui/src/context/ThemeContext.tsx:
```typescript
export type Theme = "catppuccin-mocha" | "tokyo-night" | "catppuccin-latte";
export const THEME_META: Record<Theme, { dark: boolean; label: string }>;
export function useTheme(): { theme: Theme; toggleTheme: () => void };
```
</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: Install rehype-highlight and add hljs theme CSS overrides</name>
  <files>ui/package.json, ui/src/index.css</files>
  <read_first>
    - ui/package.json (current dependencies)
    - ui/src/index.css (existing theme CSS variables and .dark/.theme-tokyo-night selectors)
  </read_first>
  <action>
Install the dependency:
```bash
cd /opt/nexus && pnpm --filter @paperclipai/ui add rehype-highlight
```

Then add highlight.js theme CSS overrides to `ui/src/index.css`. Do NOT import highlight.js CSS files directly — instead, add CSS custom property overrides scoped to each theme class. Add the following AFTER the existing theme variable blocks (after the `.theme-tokyo-night.dark` block), but BEFORE any component-specific styles:

```css
/* -- highlight.js syntax theme overrides (chat code blocks) ------------- */

/* Base hljs reset -- ensure code blocks use our themed variables */
.hljs {
  background: var(--code-block-bg, hsl(var(--card))) !important;
  color: var(--code-block-fg, hsl(var(--card-foreground))) !important;
}

/* Catppuccin Mocha (default dark) */
.dark .hljs { --code-block-bg: #1e1e2e; --code-block-fg: #cdd6f4; }
.dark .hljs-keyword { color: #cba6f7; }
.dark .hljs-string { color: #a6e3a1; }
.dark .hljs-number { color: #fab387; }
.dark .hljs-comment { color: #6c7086; font-style: italic; }
.dark .hljs-function { color: #89b4fa; }
.dark .hljs-title { color: #89b4fa; }
.dark .hljs-built_in { color: #f38ba8; }
.dark .hljs-type { color: #f9e2af; }
.dark .hljs-attr { color: #89dceb; }
.dark .hljs-variable { color: #cdd6f4; }
.dark .hljs-literal { color: #fab387; }
.dark .hljs-meta { color: #f5e0dc; }
.dark .hljs-selector-class { color: #89dceb; }
.dark .hljs-selector-tag { color: #cba6f7; }

/* Tokyo Night */
.theme-tokyo-night .hljs { --code-block-bg: #1a1b26; --code-block-fg: #a9b1d6; }
.theme-tokyo-night .hljs-keyword { color: #bb9af7; }
.theme-tokyo-night .hljs-string { color: #9ece6a; }
.theme-tokyo-night .hljs-number { color: #ff9e64; }
.theme-tokyo-night .hljs-comment { color: #565f89; font-style: italic; }
.theme-tokyo-night .hljs-function { color: #7aa2f7; }
.theme-tokyo-night .hljs-title { color: #7aa2f7; }
.theme-tokyo-night .hljs-built_in { color: #f7768e; }
.theme-tokyo-night .hljs-type { color: #e0af68; }
.theme-tokyo-night .hljs-attr { color: #73daca; }
.theme-tokyo-night .hljs-variable { color: #a9b1d6; }
.theme-tokyo-night .hljs-literal { color: #ff9e64; }
.theme-tokyo-night .hljs-meta { color: #c0caf5; }
.theme-tokyo-night .hljs-selector-class { color: #73daca; }
.theme-tokyo-night .hljs-selector-tag { color: #bb9af7; }

/* Catppuccin Latte (light) */
:root .hljs { --code-block-bg: #eff1f5; --code-block-fg: #4c4f69; }
:root .hljs-keyword { color: #8839ef; }
:root .hljs-string { color: #40a02b; }
:root .hljs-number { color: #fe640b; }
:root .hljs-comment { color: #9ca0b0; font-style: italic; }
:root .hljs-function { color: #1e66f5; }
:root .hljs-title { color: #1e66f5; }
:root .hljs-built_in { color: #d20f39; }
:root .hljs-type { color: #df8e1d; }
:root .hljs-attr { color: #179299; }
:root .hljs-variable { color: #4c4f69; }
:root .hljs-literal { color: #fe640b; }
:root .hljs-meta { color: #dc8a78; }
:root .hljs-selector-class { color: #179299; }
:root .hljs-selector-tag { color: #8839ef; }
```

IMPORTANT: The `.dark` selector matches Catppuccin Mocha. The `.theme-tokyo-night` selector overrides for Tokyo Night. The `:root` selector (without `.dark`) matches Catppuccin Latte. This aligns with the existing theme CSS variable structure in index.css.
  </action>
  <verify>
    <automated>cd /opt/nexus && grep -q "rehype-highlight" ui/package.json && grep -q "\.hljs-keyword" ui/src/index.css && grep -q "theme-tokyo-night .hljs" ui/src/index.css && echo "OK"</automated>
  </verify>
  <acceptance_criteria>
    - ui/package.json contains "rehype-highlight" in dependencies
    - ui/src/index.css contains `.dark .hljs-keyword { color: #cba6f7; }`
    - ui/src/index.css contains `.theme-tokyo-night .hljs-keyword { color: #bb9af7; }`
    - ui/src/index.css contains `:root .hljs-keyword { color: #8839ef; }`
    - The CSS block appears after existing theme variable blocks, not inside them
  </acceptance_criteria>
  <done>rehype-highlight is installed, and highlight.js theme CSS overrides exist in index.css for all three Nexus themes (Catppuccin Mocha, Tokyo Night, Catppuccin Latte).</done>
</task>

<task type="auto" tdd="true">
  <name>Task 2: Create ChatCodeBlock and ChatMarkdownMessage components</name>
  <files>ui/src/components/ChatCodeBlock.tsx, ui/src/components/ChatMarkdownMessage.tsx, ui/src/components/ChatMarkdownMessage.test.tsx</files>
  <behavior>
    - ChatMarkdownMessage renders plain text as a paragraph
    - ChatMarkdownMessage renders fenced code blocks with hljs classes applied by rehype-highlight
    - ChatCodeBlock extracts language from className "language-xxx" and displays it as a label
    - ChatCodeBlock renders a copy button with aria-label="Copy code"
  </behavior>
  <read_first>
    - ui/src/components/MarkdownBody.tsx (existing markdown component -- understand the Components override pattern, mermaid handling, and remark-gfm usage)
    - ui/src/context/ThemeContext.tsx (useTheme, THEME_META exports)
    - ui/src/lib/utils.ts (cn utility)
    - ui/src/components/ChatMarkdownMessage.test.tsx (test stub from Plan 00 -- fill in test expectations)
  </read_first>
  <action>
First, update `ui/src/components/ChatMarkdownMessage.test.tsx` to replace the `.todo` stubs with real test expectations. Use `renderToStaticMarkup` (same pattern as MarkdownBody.test.tsx) to verify:
- Plain text renders as `<p>` 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 `<code>` 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 `<pre>` 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 (
    <div className={cn("paperclip-markdown", className)}>
      <Markdown
        remarkPlugins={[remarkGfm]}
        rehypePlugins={[rehypeHighlight]}
        components={{
          pre: ChatCodeBlock,
        }}
      >
        {content}
      </Markdown>
    </div>
  );
}
```

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.

Run tests after implementation to verify:
```bash
pnpm vitest run ui/src/components/ChatMarkdownMessage.test.tsx
```
  </action>
  <verify>
    <automated>cd /opt/nexus && pnpm vitest run ui/src/components/ChatMarkdownMessage.test.tsx 2>&1 | tail -5</automated>
  </verify>
  <acceptance_criteria>
    - ui/src/components/ChatMarkdownMessage.tsx contains `import rehypeHighlight from "rehype-highlight"`
    - ui/src/components/ChatMarkdownMessage.tsx contains `rehypePlugins={[rehypeHighlight]}`
    - ui/src/components/ChatMarkdownMessage.tsx contains `pre: ChatCodeBlock`
    - ui/src/components/ChatMarkdownMessage.tsx exports `ChatMarkdownMessage`
    - ui/src/components/ChatCodeBlock.tsx contains `navigator.clipboard.writeText`
    - ui/src/components/ChatCodeBlock.tsx contains `aria-label` with "Copy code" or "Copied!"
    - ui/src/components/ChatCodeBlock.tsx extracts language from className pattern `language-`
    - ui/src/components/ChatCodeBlock.tsx uses `Check` and `Copy` icons from lucide-react
    - ChatMarkdownMessage tests pass via vitest
  </acceptance_criteria>
  <done>ChatMarkdownMessage renders markdown with syntax-highlighted code blocks via rehype-highlight. ChatCodeBlock shows a language label and copy button on every code block, with a 1500ms success state on copy. Tests pass.</done>
</task>

</tasks>

<verification>
- `pnpm --filter @paperclipai/ui exec -- tsc --noEmit` passes (type checks)
- `pnpm vitest run ui/src/components/ChatMarkdownMessage.test.tsx` passes
- Code blocks in ChatMarkdownMessage render with .hljs classes
- Copy button wires to navigator.clipboard.writeText
- Theme CSS in index.css covers all three themes
</verification>

<success_criteria>
- rehype-highlight installed in ui package
- ChatMarkdownMessage renders markdown with syntax highlighting
- ChatCodeBlock provides language label + copy button
- highlight.js theme overrides in index.css for Mocha, Tokyo Night, and Latte
- ChatMarkdownMessage tests pass
</success_criteria>

<output>
After completion, create `.planning/phases/21-chat-foundation/21-02-SUMMARY.md`
</output>
