From ec5376932595141bdfb0b196443a6d5055a5ff76 Mon Sep 17 00:00:00 2001 From: ARIA Date: Fri, 21 Aug 2026 10:26:35 +0200 Subject: [PATCH] =?UTF-8?q?Chat=20bubbles:=20preserve=20single=20newlines?= =?UTF-8?q?=20in=20agent=20messages=20too=20=E2=80=94=20the=20hard-break?= =?UTF-8?q?=20rule=20from=20user=20bubbles=20now=20applies=20to=20agent=20?= =?UTF-8?q?replies=20as=20well,=20so=20status=20lines=20and=20model-wrappe?= =?UTF-8?q?d=20text=20render=20one=20line=20per=20field=20instead=20of=20c?= =?UTF-8?q?ollapsing=20into=20a=20wall=20of=20text;=20unit=20tests=20for?= =?UTF-8?q?=20preserveNewlinesAsHardBreaks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/iris/ui/screens/ChatScreen.kt | 7 ++++-- .../commonMain/kotlin/iris/util/Markdown.kt | 13 ++++++----- .../kotlin/iris/util/MarkdownTest.kt | 22 +++++++++++++++++++ 3 files changed, 34 insertions(+), 8 deletions(-) diff --git a/app/shared/src/commonMain/kotlin/iris/ui/screens/ChatScreen.kt b/app/shared/src/commonMain/kotlin/iris/ui/screens/ChatScreen.kt index acd9194..aa56925 100644 --- a/app/shared/src/commonMain/kotlin/iris/ui/screens/ChatScreen.kt +++ b/app/shared/src/commonMain/kotlin/iris/ui/screens/ChatScreen.kt @@ -2137,8 +2137,11 @@ private fun MessageBubble( // M8: render the agent's reply as markdown (bold / italic / // underscore, tables, highlighted code blocks). Leading // newlines are stripped so the text hugs the top of the - // bubble; the ▉ cursor is kept while streaming. - val displayText = msg.text.prepareForMarkdown() + if (msg.streaming) " ▉" else "" + // bubble; single newlines become hard breaks (models write + // status lines and wrapped text expecting a break per line, + // same as user input); the ▉ cursor is kept while streaming. + val displayText = msg.text.prepareForMarkdown().preserveNewlinesAsHardBreaks() + + if (msg.streaming) " ▉" else "" MarkdownText( text = displayText, color = textColor, diff --git a/app/shared/src/commonMain/kotlin/iris/util/Markdown.kt b/app/shared/src/commonMain/kotlin/iris/util/Markdown.kt index a6e1af1..cc9f53b 100644 --- a/app/shared/src/commonMain/kotlin/iris/util/Markdown.kt +++ b/app/shared/src/commonMain/kotlin/iris/util/Markdown.kt @@ -14,12 +14,13 @@ fun String.prepareForMarkdown(): String { } /** - * Converts the single newlines a user typed into markdown hard breaks (two - * trailing spaces) so they render as line breaks instead of being collapsed - * into spaces by the parser (GFM treats a lone newline inside a paragraph as a - * space). Lines inside fenced code blocks (``` / ~~~) are left untouched so the - * code content is not altered; blank lines are kept as-is since they already - * produce paragraph breaks. + * Converts single newlines into markdown hard breaks (two trailing spaces) so + * they render as line breaks instead of being collapsed into spaces by the + * parser (GFM treats a lone newline inside a paragraph as a space). Applied to + * both user input (lazy line breaks) and agent replies (models write status + * lines and wrapped text expecting a break per line). Lines inside fenced code + * blocks (``` / ~~~) are left untouched so the code content is not altered; + * blank lines are kept as-is since they already produce paragraph breaks. */ fun String.preserveNewlinesAsHardBreaks(): String { val lines = split("\n") diff --git a/app/shared/src/commonTest/kotlin/iris/util/MarkdownTest.kt b/app/shared/src/commonTest/kotlin/iris/util/MarkdownTest.kt index 36eb4e9..c0df4e2 100644 --- a/app/shared/src/commonTest/kotlin/iris/util/MarkdownTest.kt +++ b/app/shared/src/commonTest/kotlin/iris/util/MarkdownTest.kt @@ -39,4 +39,26 @@ class MarkdownTest { fun allBlankBecomesEmpty() { assertEquals("", "\n\n \n".prepareForMarkdown()) } + + @Test + fun convertsSingleNewlinesToHardBreaks() { + assertEquals("a \nb ", "a\nb".preserveNewlinesAsHardBreaks()) + } + + @Test + fun keepsBlankLinesAsParagraphBreaks() { + assertEquals("a \n\nb ", "a\n\nb".preserveNewlinesAsHardBreaks()) + } + + @Test + fun leavesFencedCodeBlocksUntouched() { + val md = "```kotlin\nval a = 1\nval b = 2\n```" + assertEquals(md, md.preserveNewlinesAsHardBreaks()) + } + + @Test + fun statusStyleLinesBecomeHardBreaks() { + val md = "**Title:** x\n**Model:** y" + assertEquals("**Title:** x \n**Model:** y ", md.preserveNewlinesAsHardBreaks()) + } } \ No newline at end of file