fix: copy only assistant response text (#14853)

Restore the pre-#14770 copy path so the message copy button
serializes TEXT parts only and skips tools, thinking, and errors.
This commit is contained in:
Marco Beretta 2026-08-15 03:24:21 +02:00 committed by GitHub
parent 6f05f2427b
commit 8640bf89ef
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 17 additions and 34 deletions

View file

@ -64,7 +64,7 @@ describe('useCopyToClipboard', () => {
});
});
it('copies errors and tool input and output with surrounding text', () => {
it('copies only response text and skips tools, errors, and thinking', () => {
const content = [
{ type: ContentTypes.TEXT, text: 'I checked the deployment.' },
{
@ -76,7 +76,9 @@ describe('useCopyToClipboard', () => {
output: '{"status":"failed"}',
},
},
{ type: ContentTypes.THINK, think: 'Let me reason about this' },
{ type: ContentTypes.ERROR, error: 'Deployment lookup failed' },
{ type: ContentTypes.TEXT, text: 'The service is down.' },
] as TMessageContentParts[];
const { result } = renderHook(() => useCopyToClipboard({ content }));
@ -85,12 +87,9 @@ describe('useCopyToClipboard', () => {
result.current(mockSetIsCopied);
});
const copiedText = mockCopy.mock.calls[0]?.[0];
expect(copiedText).toContain('I checked the deployment.');
expect(copiedText).toContain('get_deployment');
expect(copiedText).toContain('service');
expect(copiedText).toContain('status');
expect(copiedText).toContain('Deployment lookup failed');
expect(mockCopy).toHaveBeenCalledWith('I checked the deployment.\nThe service is down.', {
format: 'text/plain',
});
});
it('should reset isCopied after timeout', () => {

View file

@ -1,8 +1,7 @@
import { useCallback, useEffect, useRef } from 'react';
import copy from 'copy-to-clipboard';
import { SearchResultData } from 'librechat-data-provider';
import { ContentTypes, SearchResultData } from 'librechat-data-provider';
import type { TMessage } from 'librechat-data-provider';
import type { LocalizeFunction } from '~/common';
import {
SPAN_REGEX,
CLEANUP_REGEX,
@ -10,8 +9,6 @@ import {
STANDALONE_PATTERN,
INVALID_CITATION_REGEX,
} from '~/utils/citations';
import { formatMessageContent } from '~/hooks/Conversations/format';
import useLocalize from '~/hooks/useLocalize';
type Source = {
link: string;
@ -33,30 +30,18 @@ const refTypeMap: Record<string, string> = {
export function serializeMessageForClipboard({
text,
content,
localize,
}: Partial<Pick<TMessage, 'text' | 'content'>> & { localize: LocalizeFunction }): string {
}: Partial<Pick<TMessage, 'text' | 'content'>>): string {
if (!Array.isArray(content) || content.length === 0) {
return text ?? '';
}
return content
.filter((part) => part != null)
.map((part) => {
const formatted = formatMessageContent({
sender: '',
content: part,
format: 'text',
localize,
});
if (formatted.length === 0) {
return '';
}
const [label, value] = formatted;
return label ? `${label}:\n${value}` : value;
})
.filter((value) => value.trim().length > 0)
.join('\n');
return content.reduce((acc, curr, i) => {
if (curr?.type === ContentTypes.TEXT) {
const partText = typeof curr.text === 'string' ? curr.text : (curr.text?.value ?? '');
return acc + partText + (i === content.length - 1 ? '' : '\n');
}
return acc;
}, '');
}
export default function useCopyToClipboard({
@ -66,7 +51,6 @@ export default function useCopyToClipboard({
}: Partial<Pick<TMessage, 'text' | 'content'>> & {
searchResults?: { [key: string]: SearchResultData };
}) {
const localize = useLocalize();
const copyTimeoutRef = useRef<NodeJS.Timeout | null>(null);
useEffect(() => {
@ -84,7 +68,7 @@ export default function useCopyToClipboard({
}
setIsCopied(true);
const messageText = serializeMessageForClipboard({ text, content, localize });
const messageText = serializeMessageForClipboard({ text, content });
// Early return if no search data
if (!searchResults || Object.keys(searchResults).length === 0) {
@ -123,7 +107,7 @@ export default function useCopyToClipboard({
setIsCopied(false);
}, 3000);
},
[text, content, searchResults, localize],
[text, content, searchResults],
);
return copyToClipboard;