Skip to content

fix: avoid empty assistant frame during streaming#168

Merged
benjamincanac merged 2 commits into
mainfrom
fix/streaming-indicator
Jul 2, 2026
Merged

fix: avoid empty assistant frame during streaming#168
benjamincanac merged 2 commits into
mainfrom
fix/streaming-indicator

Conversation

@benjamincanac

@benjamincanac benjamincanac commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Problem

On every message, a loading ("Thinking…") indicator flashed at the end of the streamed response, with a scroll jump.

The chat handler wrapped the model stream in createUIMessageStream({ execute → writer.merge(toUIMessageStream(...)) }). That wrapper's async writer.merge read-loop + double handleUIMessageStreamFinish pass surfaced the assistant message's start chunk before its first content part (start-step/reasoning), so useChat created an empty assistant message and UChatMessages showed its loading indicator until content arrived — reproducible on every response, amplified by extended thinking latency.

Fix

Stream the model directly with toUIMessageStream(result.stream) (the same shape the Nuxt UI playground uses) and persist via its own onEnd callback:

const stream = toUIMessageStream({
  stream: result.stream,
  sendSources: true,
  sendReasoning: true,
  onEnd: async ({ responseMessage }) => {
    await db.insert(schema.messages).values([{ id: responseMessage.id, chatId: chat.id, role: , parts: responseMessage.parts }]).onConflictDoNothing()
  }
})
return createUIMessageStreamResponse({ stream })

Without the wrapper there's no writer for the transient data-chat-title, so the client now refreshes the sidebar title on the first streaming transition instead — the title is generated and persisted server-side before streaming begins, so it's already available. onData handler removed accordingly.

No @nuxt/ui changes needed.

Validation

pnpm lint and pnpm typecheck pass. Verified manually: no indicator flash / scroll jump, messages persist, and the title still appears as the first response starts streaming.

Summary by CodeRabbit

  • Bug Fixes
    • Chat titles now update more reliably during streaming, especially for chats that don’t yet have a title.
    • Streaming responses continue to work as before, with improved saving of the final assistant message after completion.

Wrapping the model stream in `createUIMessageStream({ execute → writer.merge(toUIMessageStream(...)) })` emitted the assistant message's `start` chunk before its first content part, so `useChat` rendered an empty assistant message and the loading indicator flashed (with a scroll jump) on every response.

Stream the model directly with `toUIMessageStream(result.stream)` and persist via its `onEnd` callback instead. Without the wrapper there's no `writer` for the transient `data-chat-title`, so the client refreshes the sidebar title on the first `streaming` transition (the title is generated + persisted server-side before streaming starts).
@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c19eef1c-6dc6-4edf-adb1-7c5f8f5f33f5

📥 Commits

Reviewing files that changed from the base of the PR and between 410d8ad and ff27609.

📒 Files selected for processing (1)
  • server/api/chats/[id].post.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • server/api/chats/[id].post.ts

📝 Walkthrough

Walkthrough

The chat streaming route now streams through streamText and toUIMessageStream, then stores the final assistant response on completion. The chat page no longer reads title updates from streamed data and instead refreshes its title when streaming begins.

Changes

Chat streaming and title update refactor

Layer / File(s) Summary
Server streaming composition
server/api/chats/[id].post.ts
Streaming now goes through streamText and toUIMessageStream, with updated tool/provider options and end-of-stream persistence of responseMessage.
Client title update on status change
app/pages/chat/[id].vue
Removed onData handling for data-chat-title and added a watch(status) that refreshes chats and sets title when streaming starts for owners without a title.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ChatRoute as chats/[id].post.ts
  participant streamText
  participant UIStream as toUIMessageStream
  participant DB

  ChatRoute->>streamText: call with model, messages, tools, abortSignal
  streamText-->>ChatRoute: result.stream
  ChatRoute->>UIStream: convert result.stream
  UIStream-->>ChatRoute: responseMessage
  ChatRoute->>DB: insert responseMessage on stream end
Loading

Possibly related PRs

  • nuxt-ui-templates/chat#167: Also changes server/api/chats/[id].post.ts’s chat streaming implementation around AI-SDK v7 and streamText plumbing.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly reflects the main fix: preventing an empty assistant frame during streaming.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/streaming-indicator

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@server/api/chats/`[id].post.ts:
- Around line 135-141: Skip persisting empty assistant responses by guarding the
message insert in the onEnd handler. In server/api/chats/[id].post.ts, update
the onEnd callback around db.insert(schema.messages) so it only writes when
responseMessage.parts has content, preventing empty assistant frames from being
saved and reloaded; keep the existing insert logic for non-empty assistant
responses.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3635ff55-5a38-4049-a8f0-e888ff2ec207

📥 Commits

Reviewing files that changed from the base of the PR and between 57f84e6 and 410d8ad.

📒 Files selected for processing (2)
  • app/pages/chat/[id].vue
  • server/api/chats/[id].post.ts

Comment thread server/api/chats/[id].post.ts
On abort (client disconnects before any content) onEnd fires with an empty responseMessage; persisting it reloaded as an empty assistant frame. Guard the insert on responseMessage.parts.
@benjamincanac benjamincanac merged commit 2415c94 into main Jul 2, 2026
4 checks passed
@benjamincanac benjamincanac deleted the fix/streaming-indicator branch July 2, 2026 16:31
@dosstx

dosstx commented Jul 2, 2026

Copy link
Copy Markdown

This was an issue I did see but did not know how to describe. Thank you!!

@benjamincanac

benjamincanac commented Jul 2, 2026

Copy link
Copy Markdown
Contributor Author

Yes it happened after AI SDK v7 upgrade, took a while to figure out and I only found this workaround. It might be an issue in ai though, will investigate a bit more.

@benjamincanac

Copy link
Copy Markdown
Contributor Author

@dosstx After some investigation, I'm reverting these changes in favor of this fix in @nuxt/ui: nuxt/ui#6673 because the @ai-sdk/vue v4 package now stores messages in a shallowRef.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants