Markdown copy button for Plan and Assistant Responses (#694)

This commit is contained in:
Solomon
2025-09-12 10:18:51 +01:00
committed by GitHub
parent a086f82dfa
commit aa8741b47c
2 changed files with 77 additions and 4 deletions

View File

@@ -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

View File

@@ -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>
);
}