From 12d814d221849e85a32ea72d2e63b97855d902c4 Mon Sep 17 00:00:00 2001 From: Bohdan Triapitsyn Date: Wed, 3 Jun 2026 01:16:26 +0300 Subject: [PATCH] fix: preserve newlines in markdown-rendered user messages --- .../components/chat/message/parts/UserTextPart.tsx | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/packages/ui/src/components/chat/message/parts/UserTextPart.tsx b/packages/ui/src/components/chat/message/parts/UserTextPart.tsx index 94d4800b..811cbd43 100644 --- a/packages/ui/src/components/chat/message/parts/UserTextPart.tsx +++ b/packages/ui/src/components/chat/message/parts/UserTextPart.tsx @@ -49,6 +49,16 @@ const normalizeUserMessageRenderingMode = (mode: unknown): 'markdown' | 'plain' return mode === 'markdown' ? 'markdown' : 'plain'; }; +// In Markdown a single "\n" is a soft break (rendered as a space). Users type plain +// text where each newline is meant literally, so convert soft breaks into hard breaks +// (two trailing spaces) outside of fenced code blocks, where newlines are already literal. +const applyHardLineBreaks = (markdown: string): string => { + return markdown + .split(/(```[\s\S]*?```|~~~[\s\S]*?~~~)/g) + .map((segment, index) => (index % 2 === 1 ? segment : segment.replace(/ *\n/g, ' \n'))) + .join(''); +}; + const UserTextPart: React.FC = ({ part, messageId, agentMention }) => { const partWithText = part as PartWithText; const rawText = partWithText.text; @@ -150,6 +160,9 @@ const UserTextPart: React.FC = ({ part, messageId, agentMenti return `${prefix}[/${skillName}](${buildSkillHref(skillName)})`; }); + // Step 4: Preserve user newlines (markdown soft breaks would otherwise collapse to spaces) + content = applyHardLineBreaks(content); + return content; }, [agentMention, skillByName, textContent]);