| ... | @@ -83,6 +83,8 @@ import { | ... | @@ -83,6 +83,8 @@ import { |
| 83 | LockOpenIcon, | 83 | LockOpenIcon, |
| 84 | Undo2Icon, | 84 | Undo2Icon, |
| 85 | XIcon, | 85 | XIcon, |
| | 86 | CopyIcon, |
| | 87 | CheckIcon, |
| 86 | } from "lucide-react"; | 88 | } from "lucide-react"; |
| 87 | import { Button } from "./ui/button"; | 89 | import { Button } from "./ui/button"; |
| 88 | import { Select, SelectItem, SelectPopup, SelectTrigger, SelectValue } from "./ui/select"; | 90 | import { Select, SelectItem, SelectPopup, SelectTrigger, SelectValue } from "./ui/select"; |
| ... | @@ -2333,6 +2335,28 @@ const PendingApprovalsPanel = memo(function PendingApprovalsPanel({ | ... | @@ -2333,6 +2335,28 @@ const PendingApprovalsPanel = memo(function PendingApprovalsPanel({ |
| 2333 | ); | 2335 | ); |
| 2334 | }); | 2336 | }); |
| 2335 | | 2337 | |
| | 2338 | const MessageCopyButton = memo(function MessageCopyButton({ text }: { text: string }) { |
| | 2339 | const [copied, setCopied] = useState(false); |
| | 2340 | |
| | 2341 | const handleCopy = useCallback(() => { |
| | 2342 | void navigator.clipboard.writeText(text); |
| | 2343 | setCopied(true); |
| | 2344 | setTimeout(() => setCopied(false), 2000); |
| | 2345 | }, [text]); |
| | 2346 | |
| | 2347 | return ( |
| | 2348 | <Button |
| | 2349 | type="button" |
| | 2350 | size="xs" |
| | 2351 | variant="outline" |
| | 2352 | onClick={handleCopy} |
| | 2353 | title="Copy message" |
| | 2354 | > |
| | 2355 | {copied ? <CheckIcon className="size-3 text-success" /> : <CopyIcon className="size-3" />} |
| | 2356 | </Button> |
| | 2357 | ); |
| | 2358 | }); |
| | 2359 | |
| 2336 | interface MessagesTimelineProps { | 2360 | interface MessagesTimelineProps { |
| 2337 | hasMessages: boolean; | 2361 | hasMessages: boolean; |
| 2338 | isWorking: boolean; | 2362 | isWorking: boolean; |
| ... | @@ -2549,7 +2573,7 @@ const MessagesTimeline = memo(function MessagesTimeline({ | ... | @@ -2549,7 +2573,7 @@ const MessagesTimeline = memo(function MessagesTimeline({ |
| 2549 | const canRevertAgentWork = revertTurnCountByUserMessageId.has(row.message.id); | 2573 | const canRevertAgentWork = revertTurnCountByUserMessageId.has(row.message.id); |
| 2550 | return ( | 2574 | return ( |
| 2551 | <div className="flex justify-end"> | 2575 | <div className="flex justify-end"> |
| 2552 | <div className="max-w-[80%] rounded-2xl rounded-br-sm border border-border bg-secondary px-4 py-3"> | 2576 | <div className="group relative max-w-[80%] rounded-2xl rounded-br-sm border border-border bg-secondary px-4 py-3"> |
| 2553 | {userImages.length > 0 && ( | 2577 | {userImages.length > 0 && ( |
| 2554 | <div className="mb-2 grid max-w-[420px] grid-cols-2 gap-2"> | 2578 | <div className="mb-2 grid max-w-[420px] grid-cols-2 gap-2"> |
| 2555 | {userImages.map( | 2579 | {userImages.map( |
| ... | @@ -2583,17 +2607,21 @@ const MessagesTimeline = memo(function MessagesTimeline({ | ... | @@ -2583,17 +2607,21 @@ const MessagesTimeline = memo(function MessagesTimeline({ |
| 2583 | </pre> | 2607 | </pre> |
| 2584 | )} | 2608 | )} |
| 2585 | <div className="mt-1.5 flex items-center justify-end gap-2"> | 2609 | <div className="mt-1.5 flex items-center justify-end gap-2"> |
| 2586 | {canRevertAgentWork && ( | 2610 | <div className="flex items-center gap-1.5 opacity-0 transition-opacity duration-200 focus-within:opacity-100 group-hover:opacity-100"> |
| 2587 | <Button | 2611 | {row.message.text && <MessageCopyButton text={row.message.text} />} |
| 2588 | type="button" | 2612 | {canRevertAgentWork && ( |
| 2589 | size="xs" | 2613 | <Button |
| 2590 | variant="outline" | 2614 | type="button" |
| 2591 | disabled={isRevertingCheckpoint || isWorking} | 2615 | size="xs" |
| 2592 | onClick={() => onRevertUserMessage(row.message.id)} | 2616 | variant="outline" |
| 2593 | > | 2617 | disabled={isRevertingCheckpoint || isWorking} |
| 2594 | <Undo2Icon className="size-3" /> | 2618 | onClick={() => onRevertUserMessage(row.message.id)} |
| 2595 | </Button> | 2619 | title="Revert to this message" |
| 2596 | )} | 2620 | > |
| | 2621 | <Undo2Icon className="size-3" /> |
| | 2622 | </Button> |
| | 2623 | )} |
| | 2624 | </div> |
| 2597 | <p className="text-right text-[10px] text-muted-foreground/30"> | 2625 | <p className="text-right text-[10px] text-muted-foreground/30"> |
| 2598 | {formatTimestamp(row.message.createdAt)} | 2626 | {formatTimestamp(row.message.createdAt)} |
| 2599 | </p> | 2627 | </p> |