Smoke test surfaced a UX bug: when the model fired multiple tool
rounds with interleaved text, the client concatenated every text
SSE event into one growing assistantContent string and rendered it
as a single chat bubble. Result: 'now.Spinning up...first boot...
The dev container is ready!' — three distinct narrative beats
mashed into one wall of run-on text with no visual breaks.
Server (app/api/chat/route.ts):
- Added assistantTextSegments[] alongside the legacy assistantText.
Each non-empty resp.text per round pushes one segment.
- assistantText is still produced (joined with blank lines) for
backward compat — old consumers still get a single-string content.
- finalMsg now persists textSegments[] so reloaded threads can
reconstruct per-round segmentation.
- Stop-marker / round-cap recovery / loop-break paths all push to
segments AND content, with the leading '\n\n' stripped from the
segment form so bubble joins look clean.
Client (components/vibn-chat/chat-panel.tsx):
- TimelineEntry gains a 'text' kind.
- text SSE events push a new TimelineEntry instead of growing a
single content string. Subsequent tool/thought events land in
between, so the renderer naturally groups text-tools-text-tools.
- New TimelineText component renders each segment as its own bubble
inline with thoughts and tool pills.
- MessageBubble's bottom content slot is now skipped for assistant
messages whose timeline has any text entries, so we don't
duplicate the prose below the timeline.
- loadThread() rehydrates timeline from persisted textSegments +
toolCalls so reload preserves bubble segmentation.
Backwards compat: messages without textSegments fall through to the
old single-bubble content rendering — no migration needed for
existing chat history.
Co-authored-by: Cursor <cursoragent@cursor.com>