[open-swe] feat: Add copy button to chat messages in v2 thread view (#286)

* Apply patch

* Apply patch

* Apply patch

* Apply patch

* cr

* Apply patch

* Apply patch

* Apply patch

* cr

* small styling changes

---------

Co-authored-by: open-swe-dev[bot] <open-swe-dev@users.noreply.github.com>
Co-authored-by: bracesproul <braceasproul@gmail.com>
This commit is contained in:
open-swe[bot] 2025-06-20 13:39:48 -07:00 • committed by GitHub
parent 91d5b0b087
commit 095c7fb58a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -5,8 +5,18 @@ import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Textarea } from "@/components/ui/textarea";
import { ArrowLeft, GitBranch, Send, User, Bot } from "lucide-react";
import {
ArrowLeft,
GitBranch,
Send,
User,
Bot,
Copy,
CopyCheck,
} from "lucide-react";
import { TooltipIconButton } from "@/components/ui/tooltip-icon-button";
import { getMessageContentString } from "@open-swe/shared/messages";
import { AnimatePresence, motion } from "framer-motion";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { ThreadSwitcher } from "./thread-switcher";
import { ThreadDisplayInfo } from "./types";
@ -21,6 +31,53 @@ import { DO_NOT_RENDER_ID_PREFIX } from "@open-swe/shared/constants";
const PROGRAMMER_ASSISTANT_ID = process.env.NEXT_PUBLIC_PROGRAMMER_ASSISTANT_ID;
const PLANNER_ASSISTANT_ID = process.env.NEXT_PUBLIC_PLANNER_ASSISTANT_ID;
function MessageCopyButton({ content }: { content: string }) {
const [copied, setCopied] = useState(false);
const handleCopy = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
e.stopPropagation();
navigator.clipboard.writeText(content);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
return (
<TooltipIconButton
onClick={(e) => handleCopy(e)}
variant="ghost"
tooltip="Copy content"
className="size-6 p-1"
>
<AnimatePresence
mode="wait"
initial={false}
>
{copied ? (
<motion.div
key="check"
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.8 }}
transition={{ duration: 0.15 }}
>
<CopyCheck className="h-3 w-3 text-green-500" />
</motion.div>
) : (
<motion.div
key="copy"
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.8 }}
transition={{ duration: 0.15 }}
>
<Copy className="h-3 w-3" />
</motion.div>
)}
</AnimatePresence>
</TooltipIconButton>
);
}
interface ThreadViewProps {
stream: ReturnType<typeof useStream<ManagerGraphState>>;
displayThread: ThreadDisplayInfo;
@ -126,7 +183,7 @@ export function ThreadView({
{filteredMessages.map((message) => (
<div
key={message.id}
className="flex gap-3"
className="group flex gap-3"
>
<div className="flex-shrink-0">
{message.type === "human" ? (
@ -139,7 +196,7 @@ export function ThreadView({
</div>
)}
</div>
<div className="flex-1 space-y-1">
<div className="relative flex-1 space-y-1">
<div className="flex items-center gap-2">
<span className="text-muted-foreground text-xs font-medium">
{message.type === "human" ? "You" : "Agent"}
@ -148,6 +205,11 @@ export function ThreadView({
<div className="text-foreground text-sm leading-relaxed">
{getMessageContentString(message.content)}
</div>
<div className="absolute right-0 -bottom-5 opacity-0 transition-opacity group-hover:opacity-100">
<MessageCopyButton
content={getMessageContentString(message.content)}
/>
</div>
</div>
</div>
))}