From b82e05af622763be6fa26a09bf8906d2430730d9 Mon Sep 17 00:00:00 2001 From: Roland Tannous Date: Fri, 10 Apr 2026 09:05:53 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20clipboard=20copy=20not=20working=20?= =?UTF-8?q?=E2=80=94=20use=20focus+select+execCommand=20in=20user=20gestur?= =?UTF-8?q?e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../frontend/src/features/chat/chat-page.tsx | 49 ++++++++++++------- 1 file changed, 30 insertions(+), 19 deletions(-) diff --git a/studio/frontend/src/features/chat/chat-page.tsx b/studio/frontend/src/features/chat/chat-page.tsx index 3195ff8402..ea64b39451 100644 --- a/studio/frontend/src/features/chat/chat-page.tsx +++ b/studio/frontend/src/features/chat/chat-page.tsx @@ -100,26 +100,37 @@ function HighlightedSnippet({ [language, source], ); - const handleCopy = useCallback(() => { - // navigator.clipboard requires HTTPS or localhost — Studio is often - // served over plain HTTP on a LAN IP, so fall back to execCommand. - try { - if (navigator.clipboard && window.isSecureContext) { - navigator.clipboard.writeText(source); - } else { - const ta = document.createElement("textarea"); - ta.value = source; - ta.style.position = "fixed"; - ta.style.left = "-9999px"; - document.body.appendChild(ta); - ta.select(); - document.execCommand("copy"); - document.body.removeChild(ta); + const handleCopy = useCallback( + (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + + // Always use the textarea/execCommand approach — it works on plain HTTP + // inside a synchronous user-gesture handler. navigator.clipboard is + // HTTPS-only on non-localhost origins and silently fails. + const ta = document.createElement("textarea"); + ta.value = source; + // Must be visible (off-screen but not display:none) for execCommand to work + ta.setAttribute("readonly", ""); + ta.style.position = "fixed"; + ta.style.left = "-9999px"; + ta.style.top = "-9999px"; + ta.style.opacity = "0"; + document.body.appendChild(ta); + ta.focus(); + ta.select(); + // setSelectionRange needed for iOS + ta.setSelectionRange(0, ta.value.length); + const ok = document.execCommand("copy"); + document.body.removeChild(ta); + + if (ok) { + setCopied(true); + setTimeout(() => setCopied(false), 2000); } - setCopied(true); - setTimeout(() => setCopied(false), 2000); - } catch { /* ignore */ } - }, [source]); + }, + [source], + ); // Streamdown memoizes/diffs markdown blocks and can hold onto previously // highlighted content when only an inline value inside the code fence