From e6c438fd49bcb36054d421f3c7f1953bbf2ad51a Mon Sep 17 00:00:00 2001 From: Haileyesus Date: Mon, 9 Mar 2026 22:01:30 +0300 Subject: [PATCH] fix(chat): preserve fenced code blocks in plain-text copy output The plain-text copy path for assistant messages was lossy when the message contained fenced code blocks. We were unwrapping triple-backtick blocks first and then running the rest of the markdown cleanup against the extracted code. That caused valid code lines that start with markdown-like prefixes, such as `#`, `-`, `*`, or `1.`, to be rewritten as headings or list items instead of being copied verbatim. This change protects fenced code before the generic markdown normalization runs. Each fenced block is captured into a temporary array, replaced with a unique placeholder token, and then restored after the existing regex passes finish. The stored code trims only the trailing newline that comes from the fenced block wrapper, so the restored content stays clean without altering the actual code. As a result, "Copy as text" now preserves code inside fenced blocks exactly as rendered, while keeping the existing markdown-to-plain-text behavior unchanged for the rest of the message content. --- .../chat/view/subcomponents/MessageCopyControl.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/components/chat/view/subcomponents/MessageCopyControl.tsx b/src/components/chat/view/subcomponents/MessageCopyControl.tsx index dac02b7..aeacd45 100644 --- a/src/components/chat/view/subcomponents/MessageCopyControl.tsx +++ b/src/components/chat/view/subcomponents/MessageCopyControl.tsx @@ -14,7 +14,12 @@ type CopyFormatOption = { // Converts markdown into readable plain text for "Copy as text". const convertMarkdownToPlainText = (markdown: string): string => { let plainText = markdown.replace(/\r\n/g, '\n'); - plainText = plainText.replace(/```[\w-]*\n([\s\S]*?)```/g, '$1'); + const codeBlocks: string[] = []; + plainText = plainText.replace(/```[\w-]*\n([\s\S]*?)```/g, (_match, code: string) => { + const placeholder = `@@CODEBLOCK${codeBlocks.length}@@`; + codeBlocks.push(code.replace(/\n$/, '')); + return placeholder; + }); plainText = plainText.replace(/`([^`]+)`/g, '$1'); plainText = plainText.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '$1'); plainText = plainText.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1'); @@ -27,6 +32,7 @@ const convertMarkdownToPlainText = (markdown: string): string => { plainText = plainText.replace(/~~(.*?)~~/g, '$1'); plainText = plainText.replace(/<\/?[^>]+(>|$)/g, ''); plainText = plainText.replace(/\n{3,}/g, '\n\n'); + plainText = plainText.replace(/@@CODEBLOCK(\d+)@@/g, (_match, index: string) => codeBlocks[Number(index)] ?? ''); return plainText.trim(); };