From 2c6d4e528317ffed90dd2884bed88787c7fd2098 Mon Sep 17 00:00:00 2001 From: "paseo-bot[bot]" <266920839+paseo-bot[bot]@users.noreply.github.com> Date: Mon, 28 Sep 2026 22:09:16 +0200 Subject: [PATCH] fix(app): keep line breaks in streamed replies when a chunk ends in indentation (#5577) While a reply streams, presentation reparses only the growing last Markdown block: the cached block text plus the new chunk. splitMarkdownBlocks drops a trailing whitespace-only line, and only trailing "\n"s were added back, so a reply that paused on "]\n " cached the block as "]". The next chunk " B -->" then continued the previous line, joining two lines of a code block. Codex streams a line break and the next line's indent as separate deltas, so its Mermaid diagrams hit this and failed to render. History reparses the whole text, which is why a reload showed the reply correctly. Keep every trailing blank line the split drops on the last block. Closes #5456 Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com> (cherry picked from commit f4ba16a0b3fcd4b205708efff43b9e0ceefb4823) --- packages/app/src/agent-stream/presentation.test.ts | 11 +++++++++++ packages/app/src/agent-stream/presentation.ts | 6 ++++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/packages/app/src/agent-stream/presentation.test.ts b/packages/app/src/agent-stream/presentation.test.ts index 786cd5192b..5e625a9028 100644 --- a/packages/app/src/agent-stream/presentation.test.ts +++ b/packages/app/src/agent-stream/presentation.test.ts @@ -210,6 +210,17 @@ describe("stream presentation through installed plugins", () => { expect(result.head).toMatchObject([{ text: "```ts\nconst a = 1;\n\nconst b = 2;" }]); }); + // Codex streams a line break and the next line's indent as their own chunks. + it("keeps a line break when a chunk ends in the next line's indent", () => { + const harness = streamHarness(); + harness.send(assistant("```mermaid\nflowchart TD\n A[Start] --> B[Review]\n")); + harness.send(assistant(" ")); + const result = harness.send(assistant(" B --> C[Finish]\n```")); + expect(result.head).toMatchObject([ + { text: "```mermaid\nflowchart TD\n A[Start] --> B[Review]\n B --> C[Finish]\n```" }, + ]); + }); + // One rendering path: a fetched message is the same block group as a streamed one, // which is what lets find, scroll-to-message and history reveal address it by id. // A link reference definition stays with the paragraph that uses it, so the split diff --git a/packages/app/src/agent-stream/presentation.ts b/packages/app/src/agent-stream/presentation.ts index 0e83a0f50c..8300996348 100644 --- a/packages/app/src/agent-stream/presentation.ts +++ b/packages/app/src/agent-stream/presentation.ts @@ -112,8 +112,10 @@ export function createStreamPresentation() { const index = prefix.length + offset; let blockText = text; if (offset === textBlocks.length - 1) { - const trailingNewlines = /\n+$/.exec(item.text)?.[0] ?? ""; - blockText += trailingNewlines; + // The split drops trailing blank lines, including a line break followed only + // by the next line's indent. Keep them so the next append continues that line. + const trailingBlankLines = /\n\s*$/.exec(item.text)?.[0] ?? ""; + blockText += trailingBlankLines; } const existing = previous?.[index]; const id = `${item.id}:block:${index}`;