diff --git a/app/src/utils/__tests__/messageSegmentation.test.ts b/app/src/utils/__tests__/messageSegmentation.test.ts index d49236c23c..5f9ae92d75 100644 --- a/app/src/utils/__tests__/messageSegmentation.test.ts +++ b/app/src/utils/__tests__/messageSegmentation.test.ts @@ -33,6 +33,32 @@ describe('segmentMessage', () => { expect(segments.every(s => s.length >= 40)).toBe(true); }); + it('merges a short first paragraph into the second paragraph', () => { + const text = + 'Ok.\n\n' + + 'This second paragraph is intentionally long enough to stand alone after merging.'; + const segments = segmentMessage(text); + + expect(segments.length).toBe(1); + expect(segments[0].startsWith('Ok.\n\n')).toBe(true); + expect(segments[0].length).toBeGreaterThanOrEqual(40); + }); + + it('forward-merges a short first paragraph yet keeps the remaining bubbles', () => { + // Three paragraphs where only the first is below MIN_SEGMENT_CHARS. This + // exercises the forward-merge branch in mergeTooShort while still returning + // multiple segments from the paragraph-split strategy (no leading stub). + const text = + 'Ok.\n\n' + + 'This second paragraph is intentionally long enough to stand alone as a bubble.\n\n' + + 'And a third paragraph that is also long enough to remain its own standalone bubble.'; + const segments = segmentMessage(text); + + expect(segments).toHaveLength(2); + expect(segments[0].startsWith('Ok.\n\n')).toBe(true); + expect(segments.every(s => s.length >= 40)).toBe(true); + }); + it('splits on sentence boundaries when no paragraph breaks exist', () => { const text = 'This is the first sentence with some content. This is the second sentence with more content. ' + diff --git a/app/src/utils/messageSegmentation.ts b/app/src/utils/messageSegmentation.ts index baf33c450e..4a91af14ff 100644 --- a/app/src/utils/messageSegmentation.ts +++ b/app/src/utils/messageSegmentation.ts @@ -59,7 +59,18 @@ export function getSegmentDelay(segment: string): number { // ─── helpers ───────────────────────────────────────────────────────────────── -/** Merge adjacent items that are shorter than MIN_SEGMENT_CHARS. */ +/** + * Merge adjacent items that are shorter than MIN_SEGMENT_CHARS. + * + * Previously only short segments following a prior segment were merged (into + * the preceding one). A short *first* segment was left as-is because the + * `result.length > 0` guard prevented it from being absorbed. This caused the + * first chat bubble to show fewer than MIN_SEGMENT_CHARS characters while all + * subsequent short segments were correctly consolidated. + * + * Fix: after the normal backward-merge pass, check whether the first segment + * is still too short and, if so, forward-merge it into the second segment. + */ function mergeTooShort(parts: string[], joiner: string): string[] { const result: string[] = []; for (const part of parts) { @@ -69,6 +80,14 @@ function mergeTooShort(parts: string[], joiner: string): string[] { result.push(part); } } + + // If the first segment is still shorter than the minimum, forward-merge it + // into the second segment (if one exists) so no leading stub bubble is shown. + if (result.length >= 2 && result[0].length < MIN_SEGMENT_CHARS) { + result[1] = result[0] + joiner + result[1]; + result.shift(); + } + return result; }