Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions web/src/components/Chat/CodeBlock.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLElement>(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);
};
Expand Down
3 changes: 2 additions & 1 deletion web/src/pages/ChatPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string> = {
Expand Down Expand Up @@ -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);
},
},
{
Expand Down
46 changes: 46 additions & 0 deletions web/src/utils/clipboard.ts
Original file line number Diff line number Diff line change
@@ -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 `<textarea>` — still works in every browser we care about.
*
* Mirrors the same non-secure-context fallback pattern already used in
* `utils/uuid.ts` for `crypto.randomUUID`.
*
* @returns true on success, false on failure.
*/
export async function copyToClipboard(text: string): Promise<boolean> {
if (typeof navigator !== 'undefined' && navigator.clipboard?.writeText) {
try {
await navigator.clipboard.writeText(text);
return true;
} catch {
// Fall through to legacy path.
}
}

if (typeof document === 'undefined') return false;

// Legacy fallback: off-screen textarea + execCommand('copy').
const ta = document.createElement('textarea');
ta.value = text;
ta.setAttribute('readonly', '');
ta.style.position = 'fixed';
ta.style.top = '-1000px';
ta.style.left = '-1000px';
ta.style.opacity = '0';
document.body.appendChild(ta);
try {
ta.select();
return document.execCommand('copy');
} catch {
return false;
} finally {
document.body.removeChild(ta);
}
}
Loading