Markdown copy button for Plan and Assistant Responses (#694)
This commit is contained in:
@@ -247,6 +247,7 @@ const CollapsibleEntry: React.FC<{
|
||||
<MarkdownRenderer
|
||||
content={content}
|
||||
className="whitespace-pre-wrap break-words"
|
||||
enableCopyButton={false}
|
||||
/>
|
||||
) : (
|
||||
content
|
||||
@@ -261,6 +262,7 @@ const CollapsibleEntry: React.FC<{
|
||||
<MarkdownRenderer
|
||||
content={firstLine}
|
||||
className="whitespace-pre-wrap break-words"
|
||||
enableCopyButton={false}
|
||||
/>
|
||||
) : (
|
||||
firstLine
|
||||
@@ -318,6 +320,7 @@ const PlanPresentationCard: React.FC<{
|
||||
<MarkdownRenderer
|
||||
content={plan}
|
||||
className="whitespace-pre-wrap break-words"
|
||||
enableCopyButton
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -531,6 +534,7 @@ function DisplayConversationEntry({ entry, expansionKey }: Props) {
|
||||
<MarkdownRenderer
|
||||
content={isNormalizedEntry(entry) ? entry.content : ''}
|
||||
className="whitespace-pre-wrap break-words flex flex-col gap-1 font-light"
|
||||
enableCopyButton={entryType.type === 'assistant_message'}
|
||||
/>
|
||||
) : isNormalizedEntry(entry) ? (
|
||||
entry.content
|
||||
|
||||
@@ -1,12 +1,26 @@
|
||||
import ReactMarkdown, { Components } from 'react-markdown';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { memo, useMemo, useState, useCallback } from 'react';
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from '@/components/ui/tooltip.tsx';
|
||||
import { Button } from '@/components/ui/button.tsx';
|
||||
import { Check, Clipboard } from 'lucide-react';
|
||||
import { writeClipboardViaBridge } from '@/vscode/bridge';
|
||||
|
||||
interface MarkdownRendererProps {
|
||||
content: string;
|
||||
className?: string;
|
||||
enableCopyButton?: boolean;
|
||||
}
|
||||
|
||||
function MarkdownRenderer({ content, className = '' }: MarkdownRendererProps) {
|
||||
function MarkdownRenderer({
|
||||
content,
|
||||
className = '',
|
||||
enableCopyButton = false,
|
||||
}: MarkdownRendererProps) {
|
||||
const components: Components = useMemo(
|
||||
() => ({
|
||||
code: ({ children, ...props }) => (
|
||||
@@ -71,9 +85,64 @@ function MarkdownRenderer({ content, className = '' }: MarkdownRendererProps) {
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
const [copied, setCopied] = useState(false);
|
||||
const handleCopy = useCallback(async () => {
|
||||
try {
|
||||
await writeClipboardViaBridge(content);
|
||||
setCopied(true);
|
||||
window.setTimeout(() => setCopied(false), 1400);
|
||||
} catch {
|
||||
// noop – bridge handles fallback
|
||||
}
|
||||
}, [content]);
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<ReactMarkdown components={components}>{content}</ReactMarkdown>
|
||||
<div className={`relative group`}>
|
||||
{enableCopyButton && (
|
||||
<div className="sticky top-2 z-10 pointer-events-none">
|
||||
<div className="flex justify-end pr-1">
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="relative">
|
||||
<Button
|
||||
type="button"
|
||||
aria-label={copied ? 'Copied!' : 'Copy as Markdown'}
|
||||
title={copied ? 'Copied!' : 'Copy as Markdown'}
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={handleCopy}
|
||||
className="pointer-events-auto opacity-0 group-hover:opacity-100 group-hover:delay-200 delay-0 transition-opacity duration-150 h-8 w-8 rounded-md bg-background/95 backdrop-blur border border-border shadow-sm"
|
||||
>
|
||||
{copied ? (
|
||||
<Check className="h-4 w-4 text-green-600" />
|
||||
) : (
|
||||
<Clipboard className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
{copied && (
|
||||
<div
|
||||
className="absolute -right-1 mt-1 translate-y-1.5 select-none text-[11px] leading-none px-2 py-1 rounded bg-green-600 text-white shadow pointer-events-none"
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
>
|
||||
Copied
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{copied ? 'Copied!' : 'Copy as Markdown'}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className={className}>
|
||||
<ReactMarkdown components={components}>{content}</ReactMarkdown>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user