13 KiB
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | must_haves | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 21-chat-foundation | 02 | execute | 1 |
|
|
true |
|
|
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.
<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_context>
@.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:
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 };
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:
/* -- 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"
<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>
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).
` 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:
- A toolbar bar at the top showing the language label (extracted from the
classNameon the child<code>element, e.g.,language-typescript->typescript) - A copy button in the toolbar that calls
navigator.clipboard.writeText(codeText)wherecodeTextis extracted by recursively flattening the children's text content - Copy button shows
Copyicon (lucide-react) by default, switches toCheckicon for 1500ms after a successful copy, then reverts - Toolbar background:
bg-card-- same as code block background, withborder-b border-border - Language label:
text-xs text-muted-foreground font-mono - Copy button:
Button variant="ghost" size="icon"withclassName="h-6 w-6",aria-label="Copy code"(changes to"Copied!"during success state) - For
preblocks without a code child (plain preformatted text), render a plain<pre>without the toolbar
Component signature:
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.
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:
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
<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>