fix(frontend): fallback Streamdown clipboard copy (#3397)

* fix(frontend): fallback streamdown clipboard copy

* fix(frontend): address clipboard fallback review

* fix(frontend): normalize clipboard fallback rejection

* fix(frontend): harden clipboard fallback install

* fix(frontend): clarify clipboard fallback errors

* fix(frontend): cover clipboard fallback edge cases

* fix(frontend): tighten clipboard fallback cleanup

* fix(frontend): reduce clipboard fallback copy window

* fix(frontend): guard clipboard item fallback install

* fix(frontend): clean up clipboard fallback on selection errors

* Address clipboard fallback review feedback

* fix(frontend): guard clipboard fallback install during SSR
This commit is contained in:
Admire
2026-06-09 22:09:13 +08:00
committed by GitHub
parent 63ce88f874
commit 5b81588b87
9 changed files with 901 additions and 36 deletions
@@ -18,7 +18,8 @@ import {
} from "lucide-react";
import type { ComponentProps, HTMLAttributes, ReactElement } from "react";
import { createContext, memo, useContext, useEffect, useState } from "react";
import { Streamdown } from "streamdown";
import { ClipboardSafeStreamdown } from "./streamdown";
export type MessageProps = HTMLAttributes<HTMLDivElement> & {
from: UIMessage["role"];
@@ -302,11 +303,13 @@ export const MessageBranchPage = ({
);
};
export type MessageResponseProps = ComponentProps<typeof Streamdown>;
export type MessageResponseProps = ComponentProps<
typeof ClipboardSafeStreamdown
>;
export const MessageResponse = memo(
({ className, ...props }: MessageResponseProps) => (
<Streamdown
<ClipboardSafeStreamdown
className={cn(
"size-full [&>*:first-child]:mt-0 [&>*:last-child]:mb-0",
className,
@@ -10,9 +10,9 @@ import { cn } from "@/lib/utils";
import { BrainIcon, ChevronDownIcon } from "lucide-react";
import type { ComponentProps, ReactNode } from "react";
import { createContext, memo, useContext, useEffect, useState } from "react";
import { Streamdown } from "streamdown";
import { reasoningPlugins } from "@/core/streamdown/plugins";
import { Shimmer } from "./shimmer";
import { ClipboardSafeStreamdown } from "./streamdown";
type ReasoningContextValue = {
isStreaming: boolean;
@@ -178,7 +178,9 @@ export const ReasoningContent = memo(
)}
{...props}
>
<Streamdown {...reasoningPlugins}>{children}</Streamdown>
<ClipboardSafeStreamdown {...reasoningPlugins}>
{children}
</ClipboardSafeStreamdown>
</CollapsibleContent>
),
);
@@ -0,0 +1,17 @@
"use client";
import { type ComponentProps } from "react";
import { Streamdown } from "streamdown";
import { installClipboardFallback } from "@/core/clipboard";
export type ClipboardSafeStreamdownProps = ComponentProps<typeof Streamdown>;
// Only patch browser globals in client context; skip during SSR
if (typeof document !== "undefined") {
installClipboardFallback();
}
export function ClipboardSafeStreamdown(props: ClipboardSafeStreamdownProps) {
return <Streamdown {...props} />;
}
@@ -10,7 +10,6 @@ import {
} from "lucide-react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { toast } from "sonner";
import { Streamdown } from "streamdown";
import {
Artifact,
@@ -20,6 +19,7 @@ import {
ArtifactHeader,
ArtifactTitle,
} from "@/components/ai-elements/artifact";
import { ClipboardSafeStreamdown } from "@/components/ai-elements/streamdown";
import { Select, SelectItem } from "@/components/ui/select";
import {
SelectContent,
@@ -400,13 +400,13 @@ export function ArtifactFilePreview({
if (language === "markdown") {
return (
<div className="size-full px-4">
<Streamdown
<ClipboardSafeStreamdown
className="size-full"
{...streamdownPlugins}
components={{ a: ArtifactLink }}
>
{content ?? ""}
</Streamdown>
</ClipboardSafeStreamdown>
</div>
);
}
@@ -6,7 +6,6 @@ import {
XCircleIcon,
} from "lucide-react";
import { useMemo, useState } from "react";
import { Streamdown } from "streamdown";
import {
ChainOfThought,
@@ -14,6 +13,7 @@ import {
ChainOfThoughtStep,
} from "@/components/ai-elements/chain-of-thought";
import { Shimmer } from "@/components/ai-elements/shimmer";
import { ClipboardSafeStreamdown } from "@/components/ai-elements/streamdown";
import { Button } from "@/components/ui/button";
import { ShineBorder } from "@/components/ui/shine-border";
import { useI18n } from "@/core/i18n/hooks";
@@ -126,12 +126,12 @@ export function SubtaskCard({
{task.prompt && (
<ChainOfThoughtStep
label={
<Streamdown
<ClipboardSafeStreamdown
{...streamdownPluginsWithWordAnimation}
components={{ a: CitationLink }}
>
{task.prompt}
</Streamdown>
</ClipboardSafeStreamdown>
}
></ChainOfThoughtStep>
)}
@@ -1,9 +1,9 @@
"use client";
import { Streamdown } from "streamdown";
import { ClipboardSafeStreamdown } from "@/components/ai-elements/streamdown";
import { aboutMarkdown } from "./about-content";
export function AboutSettingsPage() {
return <Streamdown>{aboutMarkdown}</Streamdown>;
return <ClipboardSafeStreamdown>{aboutMarkdown}</ClipboardSafeStreamdown>;
}
@@ -10,8 +10,8 @@ import {
import Link from "next/link";
import { useDeferredValue, useId, useRef, useState } from "react";
import { toast } from "sonner";
import { Streamdown } from "streamdown";
import { ClipboardSafeStreamdown } from "@/components/ai-elements/streamdown";
import { Button } from "@/components/ui/button";
import {
Dialog,
@@ -639,12 +639,12 @@ export function MemorySettingsPage() {
<div className="text-muted-foreground mb-4 text-sm">
{summaryReadOnly}
</div>
<Streamdown
<ClipboardSafeStreamdown
className="size-full min-w-0 [overflow-wrap:anywhere] [&>*:first-child]:mt-0 [&>*:last-child]:mb-0"
{...streamdownPlugins}
>
{summariesToMarkdown(memory, filteredSectionGroups, t)}
</Streamdown>
</ClipboardSafeStreamdown>
</div>
) : null}