--- 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" --- 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. @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/phases/21-chat-foundation/21-RESEARCH.md @.planning/phases/21-chat-foundation/21-UI-SPEC.md 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; export function useTheme(): { theme: Theme; toggleTheme: () => void }; ``` Task 1: Install rehype-highlight and add hljs theme CSS overrides ui/package.json, ui/src/index.css - ui/package.json (current dependencies) - ui/src/index.css (existing theme CSS variables and .dark/.theme-tokyo-night selectors) 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. 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" - 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 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). Task 2: Create ChatCodeBlock and ChatMarkdownMessage components ui/src/components/ChatCodeBlock.tsx, ui/src/components/ChatMarkdownMessage.tsx, ui/src/components/ChatMarkdownMessage.test.tsx - 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" - 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) 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 `

` 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 (
    
{content}
); } ``` 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 ``` cd /opt/nexus && pnpm vitest run ui/src/components/ChatMarkdownMessage.test.tsx 2>&1 | tail -5 - 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 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. - `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 - 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 After completion, create `.planning/phases/21-chat-foundation/21-02-SUMMARY.md`