From b6a989bb51dbfeaaf7cd6184916bb4908965fe6b Mon Sep 17 00:00:00 2001 From: Nicolas Varrot Date: Wed, 11 Feb 2026 15:46:49 +0000 Subject: [PATCH] feat: add copy button on code blocks Hover over any fenced code block to reveal a floating copy-to-clipboard button (top-right corner). Provides visual feedback (checkmark) on success. Uses a custom ReactMarkdown
 component wrapper.
---
 src/components/ChatMessage.tsx |  7 +++++--
 src/components/CodeBlock.tsx   | 38 ++++++++++++++++++++++++++++++++++
 2 files changed, 43 insertions(+), 2 deletions(-)
 create mode 100644 src/components/CodeBlock.tsx

diff --git a/src/components/ChatMessage.tsx b/src/components/ChatMessage.tsx
index 0b1f447..9bdb4b7 100644
--- a/src/components/ChatMessage.tsx
+++ b/src/components/ChatMessage.tsx
@@ -4,6 +4,7 @@ import remarkGfm from 'remark-gfm';
 import rehypeHighlight from 'rehype-highlight';
 import type { ChatMessage as ChatMessageType, MessageBlock } from '../types';
 import { ThinkingBlock } from './ThinkingBlock';
+import { CodeBlock } from './CodeBlock';
 import { ToolCall } from './ToolCall';
 import { Bot, User, Wrench } from 'lucide-react';
 import { t, locale } from '../lib/i18n';
@@ -127,10 +128,12 @@ function getInternalBlocks(blocks: MessageBlock[]): MessageBlock[] {
   return blocks.filter(b => b.type === 'thinking' || b.type === 'tool_use' || b.type === 'tool_result');
 }
 
+const markdownComponents = { pre: CodeBlock };
+
 function renderTextBlocks(blocks: MessageBlock[]) {
   return getTextBlocks(blocks).map((block, i) => (
     
- + {autoFormatText((block as any).text)}
@@ -220,7 +223,7 @@ export function ChatMessageComponent({ message }: { message: ChatMessageType }) {/* User-visible text */} {message.blocks.length > 0 ? renderTextBlocks(message.blocks) : (
- + {autoFormatText(message.content)}
diff --git a/src/components/CodeBlock.tsx b/src/components/CodeBlock.tsx new file mode 100644 index 0000000..c32ff99 --- /dev/null +++ b/src/components/CodeBlock.tsx @@ -0,0 +1,38 @@ +import { useState, useCallback, type HTMLAttributes } from 'react'; +import { Check, Copy } from 'lucide-react'; + +/** + * Custom
 renderer for ReactMarkdown.
+ * Wraps code blocks with a floating copy button.
+ */
+export function CodeBlock(props: HTMLAttributes) {
+  const [copied, setCopied] = useState(false);
+
+  const handleCopy = useCallback(() => {
+    // Extract text from the nested  element
+    const code = (props.children as any)?.props?.children;
+    if (typeof code === 'string') {
+      navigator.clipboard.writeText(code).then(() => {
+        setCopied(true);
+        setTimeout(() => setCopied(false), 2000);
+      });
+    }
+  }, [props.children]);
+
+  return (
+    
+
+      
+    
+ ); +}