diff --git a/libs/chat/src/lib/streaming/streaming-markdown.component.spec.ts b/libs/chat/src/lib/streaming/streaming-markdown.component.spec.ts index d26121f6b..394992c44 100644 --- a/libs/chat/src/lib/streaming/streaming-markdown.component.spec.ts +++ b/libs/chat/src/lib/streaming/streaming-markdown.component.spec.ts @@ -73,4 +73,32 @@ describe('ChatStreamingMdComponent', () => { expect(fixture.nativeElement.querySelector('p')).toBeNull(); expect(fixture.nativeElement.querySelector('h1')).toBeNull(); }); + + it('renders a paragraph for plain text WITHOUT a trailing newline (LLM-response shape)', () => { + // Regression: @cacheplane/partial-markdown@0.3 does not flush trailing + // text on finish() unless the buffer ends with '\n'. LLM responses + // typically omit the trailing newline. The component must push a + // sentinel newline before finish() so the message renders. + const fixture = TestBed.createComponent(HostComponent); + fixture.componentInstance.content.set('Hello — nice to meet you!'); + fixture.componentInstance.streaming.set(false); + fixture.detectChanges(); + const p = fixture.nativeElement.querySelector('p'); + expect(p).toBeTruthy(); + expect(p.textContent?.trim()).toBe('Hello — nice to meet you!'); + }); + + it('renders plain text when streaming flips to false (mirrored else-branch)', () => { + // The else-if branch (no content change, streaming flipped to false) + // must also push a sentinel newline before finish(). + const fixture = TestBed.createComponent(HostComponent); + fixture.componentInstance.content.set('Plain answer.'); + fixture.componentInstance.streaming.set(true); + fixture.detectChanges(); + fixture.componentInstance.streaming.set(false); + fixture.detectChanges(); + const p = fixture.nativeElement.querySelector('p'); + expect(p).toBeTruthy(); + expect(p.textContent?.trim()).toBe('Plain answer.'); + }); }); diff --git a/libs/chat/src/lib/streaming/streaming-markdown.component.ts b/libs/chat/src/lib/streaming/streaming-markdown.component.ts index 08c091595..c8a0ff073 100644 --- a/libs/chat/src/lib/streaming/streaming-markdown.component.ts +++ b/libs/chat/src/lib/streaming/streaming-markdown.component.ts @@ -99,12 +99,20 @@ export class ChatStreamingMdComponent { if (c.length > 0) this.parser.push(c); } if (!isStreaming && !this.finished) { + // @cacheplane/partial-markdown@0.3 does not flush trailing text on + // finish() unless the buffer ends with a newline. Plain LLM + // responses often omit the trailing newline, which causes the + // parser to emit a document with zero children — i.e. the message + // renders empty. Push a sentinel newline first to force the open + // paragraph closed before we finalize. + if (!c.endsWith('\n')) this.parser.push('\n'); this.parser.finish(); this.finished = true; } this.prior = c; } else if (!isStreaming && !this.finished) { // Streaming flipped to false without new content; ensure parser is finalized. + if (!this.prior.endsWith('\n')) this.parser.push('\n'); this.parser.finish(); this.finished = true; }