fix: preserve Markdown blocks when copying messages (#2944)

* fix: preserve Markdown blocks when copying messages

* fix: preserve blank lines when copying code

---------

Co-authored-by: Iuliia Ivashko <yulia.ivashko@gmail.com>
This commit is contained in:
ChangeHow
2026-08-27 19:37:49 +03:00
committed by GitHub
co-authored by Iuliia Ivashko
parent 2a1c10cb3e
commit 7aae5a6634
6 changed files with 277 additions and 41 deletions
+51
View File
@@ -1,6 +1,9 @@
import { afterEach, describe, expect, test } from 'bun:test';
import { marked } from 'marked';
import { copyMarkdownToClipboard } from './clipboard';
import { flattenAssistantTextParts } from './messages/messageText';
const originalNavigator = Object.getOwnPropertyDescriptor(globalThis, 'navigator');
const originalClipboardItem = Object.getOwnPropertyDescriptor(globalThis, 'ClipboardItem');
@@ -84,4 +87,52 @@ describe('copyMarkdownToClipboard', () => {
expect(result).toEqual({ ok: true, method: 'clipboard' });
expect(fallbackText).toBe('# title');
});
test('assistant copy payload keeps Markdown block separation in every clipboard format', async () => {
let writtenItem: { data: Record<string, Blob> } | undefined;
class FakeClipboardItem {
static supports(type: string): boolean {
return type === 'text/markdown';
}
readonly data: Record<string, Blob>;
constructor(data: Record<string, Blob>) {
this.data = data;
}
}
Object.defineProperty(globalThis, 'ClipboardItem', { configurable: true, value: FakeClipboardItem });
Object.defineProperty(globalThis, 'navigator', {
configurable: true,
value: {
clipboard: {
write: async (items: Array<{ data: Record<string, Blob> }>) => {
writtenItem = items[0];
},
},
},
});
const parts = [
{ id: 'p0', sessionID: 's', messageID: 'm', type: 'text', text: '第一段' },
{ id: 'p1', sessionID: 's', messageID: 'm', type: 'text', text: '第二段' },
{ id: 'p2', sessionID: 's', messageID: 'm', type: 'text', text: '```js\nconsole.log(1)\n\n\nconsole.log(2)\n```' },
{ id: 'p3', sessionID: 's', messageID: 'm', type: 'text', text: '第三段' },
];
// Same path as ChatMessage.tsx handleCopyMessage:
const text = flattenAssistantTextParts(parts as Parameters<typeof flattenAssistantTextParts>[0]);
const html = marked.parse(text, { gfm: true, breaks: false }) as string;
const result = await copyMarkdownToClipboard(text, html);
const expected = '第一段\n\n第二段\n\n```js\nconsole.log(1)\n\n\nconsole.log(2)\n```\n\n第三段';
expect(result).toEqual({ ok: true, method: 'clipboard' });
expect(await writtenItem?.data['text/plain']?.text()).toBe(expected);
expect(await writtenItem?.data['text/markdown']?.text()).toBe(expected);
const htmlText = await writtenItem?.data['text/html']?.text();
expect(htmlText).toContain('<p>第一段</p>');
expect(htmlText).toContain('<p>第二段</p>');
expect(htmlText).not.toContain('<p>第一段\n第二段</p>');
});
});