fix: copy button functionality in web browsers

- Add clipboard utility with fallback for non-secure contexts
- Implement modern Clipboard API with document.execCommand fallback
- Update copy buttons in code blocks, markdown, messages, and session list
- Add proper error handling and user feedback for copy operations

Fixes issue where copy buttons did not work in web browsers served over HTTP or without Clipboard API support
This commit is contained in:
bizzkoot
2026-01-04 20:00:22 +08:00
parent 4b05e698f8
commit 3c450c076a
5 changed files with 92 additions and 18 deletions

View File

@@ -2,6 +2,7 @@ import { createSignal, onMount, Show, createEffect } from "solid-js"
import type { Highlighter } from "shiki/bundle/full"
import { useTheme } from "../lib/theme"
import { getSharedHighlighter, escapeHtml } from "../lib/markdown"
import { copyToClipboard } from "../lib/clipboard"
const inlineLoadedLanguages = new Set<string>()
@@ -61,9 +62,11 @@ export function CodeBlockInline(props: CodeBlockInlineProps) {
}
const copyCode = async () => {
await navigator.clipboard.writeText(props.code)
setCopied(true)
setTimeout(() => setCopied(false), 2000)
const success = await copyToClipboard(props.code)
if (success) {
setCopied(true)
setTimeout(() => setCopied(false), 2000)
}
}
return (

View File

@@ -2,6 +2,7 @@ import { createEffect, createSignal, onMount, onCleanup } from "solid-js"
import { renderMarkdown, onLanguagesLoaded, initMarkdown, decodeHtmlEntities } from "../lib/markdown"
import type { TextPart, RenderCache } from "../types/message"
import { getLogger } from "../lib/logger"
import { copyToClipboard } from "../lib/clipboard"
const log = getLogger("session")
const markdownRenderCache = new Map<string, RenderCache>()
@@ -125,13 +126,20 @@ export function Markdown(props: MarkdownProps) {
const code = copyButton.getAttribute("data-code")
if (code) {
const decodedCode = decodeURIComponent(code)
await navigator.clipboard.writeText(decodedCode)
const success = await copyToClipboard(decodedCode)
const copyText = copyButton.querySelector(".copy-text")
if (copyText) {
copyText.textContent = "Copied!"
setTimeout(() => {
copyText.textContent = "Copy"
}, 2000)
if (success) {
copyText.textContent = "Copied!"
setTimeout(() => {
copyText.textContent = "Copy"
}, 2000)
} else {
copyText.textContent = "Failed"
setTimeout(() => {
copyText.textContent = "Copy"
}, 2000)
}
}
}
}

View File

@@ -3,6 +3,7 @@ import type { MessageInfo, ClientPart } from "../types/message"
import { partHasRenderableText } from "../types/message"
import type { MessageRecord } from "../stores/message-v2/types"
import MessagePart from "./message-part"
import { copyToClipboard } from "../lib/clipboard"
interface MessageItemProps {
record: MessageRecord
@@ -15,9 +16,9 @@ interface MessageItemProps {
onFork?: (messageId?: string) => void
showAgentMeta?: boolean
onContentRendered?: () => void
}
}
export default function MessageItem(props: MessageItemProps) {
export default function MessageItem(props: MessageItemProps) {
const [copied, setCopied] = createSignal(false)
const isUser = () => props.record.role === "user"
@@ -155,8 +156,8 @@ interface MessageItemProps {
const handleCopy = async () => {
const content = getRawContent()
if (!content) return
await navigator.clipboard.writeText(content)
setCopied(true)
const success = await copyToClipboard(content)
setCopied(success)
setTimeout(() => setCopied(false), 2000)
}

View File

@@ -10,6 +10,7 @@ import { formatShortcut } from "../lib/keyboard-utils"
import { showToastNotification } from "../lib/notifications"
import { deleteSession, loading, renameSession } from "../stores/sessions"
import { getLogger } from "../lib/logger"
import { copyToClipboard } from "../lib/clipboard"
const log = getLogger("session")
@@ -72,14 +73,14 @@ const SessionList: Component<SessionListProps> = (props) => {
const copySessionId = async (event: MouseEvent, sessionId: string) => {
event.stopPropagation()
try {
if (typeof navigator === "undefined" || !navigator.clipboard) {
throw new Error("Clipboard API unavailable")
const success = await copyToClipboard(sessionId)
if (success) {
showToastNotification({ message: "Session ID copied", variant: "success" })
} else {
showToastNotification({ message: "Unable to copy session ID", variant: "error" })
}
await navigator.clipboard.writeText(sessionId)
showToastNotification({ message: "Session ID copied", variant: "success" })
} catch (error) {
log.error(`Failed to copy session ID ${sessionId}:`, error)
showToastNotification({ message: "Unable to copy session ID", variant: "error" })