diff --git a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessage.svelte b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessage.svelte index 0c9ead61e..fa2a50bc5 100644 --- a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessage.svelte +++ b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessage.svelte @@ -7,7 +7,12 @@ ChatMessageSystem, ChatMessageUser } from '$lib/components/app/chat'; - import { REASONING_TAGS, ROUTES, SYSTEM_MESSAGE_PLACEHOLDER } from '$lib/constants'; + import { + AGENTIC_TEXT_COPY_SEPARATOR, + REASONING_TAGS, + ROUTES, + SYSTEM_MESSAGE_PLACEHOLDER + } from '$lib/constants'; import { setChatMessageActionsContext, setChatMessageEditContext } from '$lib/contexts'; import { AgenticSectionType, AttachmentType, MessageRole } from '$lib/enums'; import { DatabaseService } from '$lib/services/database.service'; @@ -237,6 +242,24 @@ } function handleCopy() { + // Agentic sessions render as a single entry anchored on the first assistant + // turn, whose own content is typically just the first tool call. Copy the + // text sections of the whole session so the clipboard matches the visible + // response instead of the anchor turn. + if (message.role === MessageRole.ASSISTANT) { + const sections = deriveAgenticSections(message, toolMessages, [], false); + const text = sections + .filter((section) => section.type === AgenticSectionType.TEXT) + .map((section) => section.content) + .join(AGENTIC_TEXT_COPY_SEPARATOR); + + if (text) { + chatActions.copy(message, text); + + return; + } + } + chatActions.copy(message); } diff --git a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessages.svelte b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessages.svelte index 45b863d66..4750a9f7c 100644 --- a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessages.svelte +++ b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessages.svelte @@ -29,10 +29,10 @@ refreshAllMessages(); }, - copy: async (message: DatabaseMessage) => { + copy: async (message: DatabaseMessage, contentOverride?: string) => { const asPlainText = Boolean(currentConfig.copyTextAttachmentsAsPlainText); const clipboardContent = formatMessageForClipboard( - message.content, + contentOverride ?? message.content, message.extra, asPlainText ); diff --git a/tools/ui/src/lib/constants/agentic.constants.ts b/tools/ui/src/lib/constants/agentic.constants.ts index e57104e8a..33fb30f1b 100644 --- a/tools/ui/src/lib/constants/agentic.constants.ts +++ b/tools/ui/src/lib/constants/agentic.constants.ts @@ -20,6 +20,10 @@ export const SEARCH_SUMMARY = { // wraps mid-paragraph. export const RESULT_STAT_SEPARATOR = ' - '; +// Separator between the assistant text sections of a grouped agentic +// session when they are joined for the clipboard. +export const AGENTIC_TEXT_COPY_SEPARATOR = '\n\n'; + export const DEFAULT_AGENTIC_CONFIG: AgenticConfig = { enabled: true, maxTurns: 100 diff --git a/tools/ui/src/lib/types/chat.d.ts b/tools/ui/src/lib/types/chat.d.ts index 274131dd4..86a868c33 100644 --- a/tools/ui/src/lib/types/chat.d.ts +++ b/tools/ui/src/lib/types/chat.d.ts @@ -249,7 +249,7 @@ export interface ChatMessageDeletionInfo { * refresh + user-action notification), passed to each ChatMessage as a prop. */ export interface ChatMessageActions { - copy: (message: DatabaseMessage) => void; + copy: (message: DatabaseMessage, contentOverride?: string) => void; delete: (message: DatabaseMessage) => void; navigateToSibling: (siblingId: string) => void; editWithBranching: (