diff --git a/frontend/web/src/components/chat.tsx b/frontend/web/src/components/chat.tsx index e012d12..aa91786 100644 --- a/frontend/web/src/components/chat.tsx +++ b/frontend/web/src/components/chat.tsx @@ -168,8 +168,22 @@ export function MessageBubble({ * Non-greedy + multi-line so a tag may span newlines; a still-streaming, * unclosed `

` simply does not match and stays out until it closes. */ function splitParagraphs(content: string): string[] { - const re = /]*>([\s\S]*?)<\/p>/g; - return [...content.matchAll(re)].map((m) => m[1]); + // Preferred: the agent wraps each paragraph in

…

(design/07 §2). When it + // emits plain \n\n runs instead (the model often does), split on those too so + // each logical paragraph becomes its own bubble. But never cut inside an + // … / … block, and never split on a + // single \n (that is an in-line break, kept inside the paragraph). + const pRe = /]*>([\s\S]*?)<\/p>/g; + const pSegments = [...content.matchAll(pRe)].map((m) => m[1].trim()); + if (pSegments.length > 0) return pSegments; + + // Keep non-

content (action/narration + body) so we don't strip the tags. + const segments: string[] = []; + for (const part of content.split(/\n\s*\n/)) { + if (!part.trim()) continue; + segments.push(part.trim()); + } + return segments; } /** Chat-mode body: a Miuix surface shell around a whitespace-preserving span,