diff --git a/site/src/pages/AgentsPage/components/ChatConversation/ConversationTimeline.tsx b/site/src/pages/AgentsPage/components/ChatConversation/ConversationTimeline.tsx index 7caff5e8ed..02a5f21853 100644 --- a/site/src/pages/AgentsPage/components/ChatConversation/ConversationTimeline.tsx +++ b/site/src/pages/AgentsPage/components/ChatConversation/ConversationTimeline.tsx @@ -1,4 +1,4 @@ -import { FileTextIcon, PencilIcon } from "lucide-react"; +import { CopyIcon, FileTextIcon, PencilIcon } from "lucide-react"; import { type FC, Fragment, @@ -10,6 +10,8 @@ import { } from "react"; import type { UrlTransform } from "streamdown"; import type * as TypesGen from "#/api/typesGenerated"; +import { CheckIcon } from "#/components/AnimatedIcons/Check"; +import { Button } from "#/components/Button/Button"; import { CopyButton } from "#/components/CopyButton/CopyButton"; import { Spinner } from "#/components/Spinner/Spinner"; import { @@ -17,6 +19,7 @@ import { TooltipContent, TooltipTrigger, } from "#/components/Tooltip/Tooltip"; +import { useClipboard } from "#/hooks/useClipboard"; import { cn } from "#/utils/cn"; import { decodeInlineTextAttachment, @@ -437,6 +440,7 @@ const ChatMessageItem = memo<{ savingMessageId?: number | null; isAfterEditingMessage?: boolean; isLastAssistantMessage?: boolean; + isLastInConversation?: boolean; // When true, renders a gradient overlay inside the bubble // that fades text out toward the bottom. Used by the sticky // overlay to indicate truncated content. @@ -455,6 +459,7 @@ const ChatMessageItem = memo<{ savingMessageId, isAfterEditingMessage = false, isLastAssistantMessage = false, + isLastInConversation = false, fadeFromBottom = false, urlTransform, mcpServers, @@ -466,6 +471,8 @@ const ChatMessageItem = memo<{ const isSavingMessage = savingMessageId === message.id; const [previewImage, setPreviewImage] = useState(null); const [previewText, setPreviewText] = useState(null); + const [copyHovered, setCopyHovered] = useState(false); + const { showCopiedSuccess, copyToClipboard } = useClipboard(); if ( parsed.toolResults.length > 0 && parsed.toolCalls.length === 0 && @@ -612,9 +619,26 @@ const ChatMessageItem = memo<{ ) : ( - + -
+
- + + + + + + {showCopiedSuccess ? "Copied!" : "Copy message"} + +
) : undefined } @@ -1064,7 +1111,7 @@ export const ConversationTimeline = memo( if (lastAsstId != null && !isTurnActive) { lastAssistantPerTurnIds.add(lastAsstId); } - return parsedMessages.map(({ message, parsed }) => + return parsedMessages.map(({ message, parsed }, msgIdx) => message.role === "user" ? ( ( urlTransform={urlTransform} isAfterEditingMessage={afterEditingMessageIds.has(message.id)} isLastAssistantMessage={lastAssistantPerTurnIds.has(message.id)} + isLastInConversation={msgIdx === parsedMessages.length - 1} mcpServers={mcpServers} subagentTitles={subagentTitles} computerUseSubagentIds={computerUseSubagentIds}