From ff456a8b4454639209ce485e21cadd3941c4285d Mon Sep 17 00:00:00 2001 From: chenjunlong1 Date: Sat, 25 Jul 2026 10:05:56 +0800 Subject: [PATCH] fix(web): preserve streamed text spacing --- .changeset/fix-web-stream-text-wrapping.md | 5 +++++ .../src/composables/messagesToTurns.ts | 7 ++++-- apps/kimi-web/test/turn-logic.test.ts | 22 +++++++++++++++++++ 3 files changed, 32 insertions(+), 2 deletions(-) create mode 100644 .changeset/fix-web-stream-text-wrapping.md diff --git a/.changeset/fix-web-stream-text-wrapping.md b/.changeset/fix-web-stream-text-wrapping.md new file mode 100644 index 0000000000..98ad1d194d --- /dev/null +++ b/.changeset/fix-web-stream-text-wrapping.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +web: Fix streamed assistant text rendering with unintended line breaks between chunks. diff --git a/apps/kimi-web/src/composables/messagesToTurns.ts b/apps/kimi-web/src/composables/messagesToTurns.ts index 82b170a2c8..0ddc025141 100644 --- a/apps/kimi-web/src/composables/messagesToTurns.ts +++ b/apps/kimi-web/src/composables/messagesToTurns.ts @@ -641,8 +641,11 @@ export function messagesToTurns( g.textParts.push(c.text); // Append to a trailing text block, else open a new one — so a tool // call between two text segments splits them into separate blocks. + // Stream chunks already contain the model's whitespace (including + // paragraph breaks). Inserting a newline here turns token-sized + // chunks into one line per word, especially for Chinese output. const last = g.blocks.at(-1); - if (last && last.kind === 'text') last.text += '\n' + c.text; + if (last && last.kind === 'text') last.text += c.text; else g.blocks.push({ kind: 'text', text: c.text }); } } else if (c.type === 'thinking') { @@ -651,7 +654,7 @@ export function messagesToTurns( // Ordered block too: thinking renders WHERE it happened in the turn, // merging consecutive segments (same rule as text blocks above). const last = g.blocks.at(-1); - if (last && last.kind === 'thinking') last.thinking += '\n' + c.thinking; + if (last && last.kind === 'thinking') last.thinking += c.thinking; else g.blocks.push({ kind: 'thinking', thinking: c.thinking }); } } else if (c.type === 'toolUse') { diff --git a/apps/kimi-web/test/turn-logic.test.ts b/apps/kimi-web/test/turn-logic.test.ts index 7c4557b4cf..732f3c3f58 100644 --- a/apps/kimi-web/test/turn-logic.test.ts +++ b/apps/kimi-web/test/turn-logic.test.ts @@ -102,6 +102,28 @@ describe('messagesToTurns', () => { expect(turns.map((turn) => turn.text)).toEqual(['one', 'two']); }); + it('preserves whitespace when merging streamed text around empty thinking parts', () => { + const turns = messagesToTurns( + [ + message('a1', 'assistant', [ + { type: 'text', text: 'one' }, + { type: 'thinking', thinking: '' }, + { type: 'text', text: ' ' }, + { type: 'thinking', thinking: '' }, + { type: 'text', text: 'two' }, + { type: 'thinking', thinking: '' }, + { type: 'text', text: ' ' }, + { type: 'text', text: 'three' }, + ]), + ], + [], + undefined, + false, + ); + + expect(turns[0]?.blocks).toEqual([{ kind: 'text', text: 'one two three' }]); + }); + it('renders compaction summaries as divider turns', () => { const turns = messagesToTurns( [