Merge branch 'main' into fix/chatbox-scroll-menu-cd4e390d

This commit is contained in:
Lee Jackson 2026-04-20 20:22:04 +01:00 committed by GitHub
commit 0fd8c4645b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 26 additions and 53 deletions

View file

@ -272,8 +272,8 @@ function MermaidCopyButton({ source }: { source: string }) {
type="button"
className="absolute top-3.5 right-20 z-20 cursor-pointer text-muted-foreground transition-all hover:text-foreground"
title="Copy Mermaid source"
onClick={() => {
if (!copyToClipboard(source)) {
onClick={async () => {
if (!(await copyToClipboard(source))) {
return;
}
showCopied();
@ -306,8 +306,8 @@ function CodeBlockActions({
className={ACTION_BUTTON_CLASS}
title="Copy code"
disabled={disabled}
onClick={() => {
if (!copyToClipboard(source)) {
onClick={async () => {
if (!(await copyToClipboard(source))) {
return;
}
showCopied();

View file

@ -322,8 +322,8 @@ function ReasoningCopyButton({ startIndex, endIndex }: { startIndex: number; end
.join("\n");
});
const handleCopy = useCallback(() => {
if (copyToClipboard(reasoningText)) {
const handleCopy = useCallback(async () => {
if (await copyToClipboard(reasoningText)) {
setCopied(true);
if (resetRef.current) clearTimeout(resetRef.current);
resetRef.current = setTimeout(() => setCopied(false), COPY_RESET_MS);

View file

@ -701,9 +701,9 @@ const CopyButton: FC = () => {
const [copied, setCopied] = useState(false);
const resetTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const handleCopy = () => {
const handleCopy = async () => {
const text = aui.message().getCopyText();
if (copyToClipboard(text)) {
if (await copyToClipboard(text)) {
setCopied(true);
if (resetTimeoutRef.current) clearTimeout(resetTimeoutRef.current);
resetTimeoutRef.current = setTimeout(() => {

View file

@ -44,8 +44,8 @@ function CopyBtn({ text }: { text: string }) {
};
}, []);
const copy = useCallback(() => {
if (copyToClipboard(text)) {
const copy = useCallback(async () => {
if (await copyToClipboard(text)) {
setCopied(true);
if (timer.current) {
clearTimeout(timer.current);

View file

@ -34,8 +34,8 @@ function CopyBtn({ text }: { text: string }) {
};
}, []);
const copy = useCallback(() => {
if (copyToClipboard(text)) {
const copy = useCallback(async () => {
if (await copyToClipboard(text)) {
setCopied(true);
if (timer.current) {
clearTimeout(timer.current);

View file

@ -83,7 +83,7 @@ export function ApiKeyRow({
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => copyToClipboard(prefix)}>
<DropdownMenuItem onClick={async () => { await copyToClipboard(prefix); }}>
<HugeiconsIcon icon={Copy01Icon} className="size-3.5 mr-2" />
Copy prefix
</DropdownMenuItem>

View file

@ -17,8 +17,8 @@ export function KeyRevealCard({
}) {
const [copied, setCopied] = useState(false);
const handleCopy = () => {
if (copyToClipboard(rawKey)) {
const handleCopy = async () => {
if (await copyToClipboard(rawKey)) {
setCopied(true);
setTimeout(() => setCopied(false), 1800);
}

View file

@ -39,8 +39,8 @@ function CopyableCommand({
};
}, []);
const handleCopy = () => {
if (!copyToClipboard(command)) {
const handleCopy = async () => {
if (!(await copyToClipboard(command))) {
return;
}
setCopied(true);

View file

@ -67,8 +67,8 @@ export function UsageExamples() {
[],
);
const handleCopy = () => {
if (copyToClipboard(snippets[lang])) {
const handleCopy = async () => {
if (await copyToClipboard(snippets[lang])) {
setCopied(true);
setTimeout(() => setCopied(false), 1800);
}

View file

@ -32,50 +32,23 @@ function copyWithExecCommand(text: string): boolean {
}
}
export function copyToClipboard(text: string): boolean {
export async function copyToClipboard(text: string): Promise<boolean> {
if (typeof text !== "string" || text.length === 0) {
return false;
}
if (typeof document !== "undefined" && document.queryCommandSupported?.("copy") !== false) {
if (copyWithExecCommand(text)) return true;
}
// Async fallback for environments where execCommand is entirely unsupported
// but the Clipboard API is available (rare; kept for original contract parity).
if (typeof navigator?.clipboard?.writeText === "function") {
navigator.clipboard.writeText(text).then(
() => {},
() => {},
);
return true;
}
return false;
}
export async function copyToClipboardAsync(text: string): Promise<boolean> {
if (typeof text !== "string" || text.length === 0) {
return false;
}
// Prefer the async Clipboard API: avoids focus disruption in Radix
// focus-trapped dialogs where execCommand always fails.
// Primary: async Clipboard API
if (typeof navigator?.clipboard?.writeText === "function") {
try {
await navigator.clipboard.writeText(text);
return true;
} catch {
// Clipboard API rejected (e.g. NotAllowedError, permission policy).
// User activation is still valid through promise chains per spec, so
// execCommand can succeed for callers outside focus-trapped dialogs.
// Inside a Radix modal the focus trap will block textarea.focus() and
// execCommand returns false harmlessly.
return copyWithExecCommand(text);
} catch (error) {
console.warn("Async clipboard API failed, falling back to execCommand", error);
// Clipboard API rejected (NotAllowedError, insecure context, etc.)
// Fall through to execCommand fallback.
}
}
// No Clipboard API (older browser / non-secure context): still in the
// original user-gesture frame, so execCommand can work.
// Fallback: execCommand (works in Safari when called during user gesture)
return copyWithExecCommand(text);
}