diff --git a/web/src/components/Chat/CodeBlock.tsx b/web/src/components/Chat/CodeBlock.tsx index 8caa357..32f5b50 100644 --- a/web/src/components/Chat/CodeBlock.tsx +++ b/web/src/components/Chat/CodeBlock.tsx @@ -1,14 +1,16 @@ import { useState, useRef, type ReactNode } from 'react'; import { Copy, Check } from 'lucide-react'; +import { copyToClipboard } from '../../utils/clipboard'; export function CodeBlock({ className, children }: { className?: string; children: ReactNode }) { const [copied, setCopied] = useState(false); const codeRef = useRef(null); const language = className?.replace(/^.*?language-/, '').replace(/\s.*$/, '') || ''; - const handleCopy = () => { + const handleCopy = async () => { const text = codeRef.current?.textContent || ''; - navigator.clipboard.writeText(text); + const ok = await copyToClipboard(text); + if (!ok) return; setCopied(true); setTimeout(() => setCopied(false), 2000); }; diff --git a/web/src/pages/ChatPage.tsx b/web/src/pages/ChatPage.tsx index d24f5c7..38df59f 100644 --- a/web/src/pages/ChatPage.tsx +++ b/web/src/pages/ChatPage.tsx @@ -12,6 +12,7 @@ import { Loader2, PanelLeftOpen, PanelLeftClose, Files, ExternalLink } from 'luc import { api } from '../api/client'; import { useKeyboardShortcuts } from '../hooks/useKeyboardShortcuts'; import type { ShortcutDef } from '../utils/keyboard'; +import { copyToClipboard } from '../utils/clipboard'; import type { ChatMessage, TextBlockData } from '../types/chat'; const STATUS_LABELS: Record = { @@ -78,7 +79,7 @@ export function ChatPage() { section: 'chat', action: () => { const text = getLastAssistantText(useChatStore.getState().messages); - if (text) void navigator.clipboard.writeText(text); + if (text) void copyToClipboard(text); }, }, { diff --git a/web/src/utils/clipboard.ts b/web/src/utils/clipboard.ts new file mode 100644 index 0000000..a01713b --- /dev/null +++ b/web/src/utils/clipboard.ts @@ -0,0 +1,46 @@ +/** + * Copy text to clipboard with a fallback for non-secure contexts. + * + * `navigator.clipboard` is only exposed in *secure contexts* — HTTPS or + * `http://localhost`. When the app is accessed over plain HTTP via a LAN + * hostname or IP, or behind a proxy without a trusted cert, the entire + * `clipboard` object is `undefined` and any call throws synchronously. + * + * Falls back to the deprecated `document.execCommand('copy')` via a hidden + * off-screen `