Skip to content

fix(chat): stream updates never re-rendered — mutate the assistant message via its reactive proxy#161

Merged
ABB65 merged 1 commit into
mainfrom
fix/chat-stream-reactivity
Jul 17, 2026
Merged

fix(chat): stream updates never re-rendered — mutate the assistant message via its reactive proxy#161
ABB65 merged 1 commit into
mainfrom
fix/chat-stream-reactivity

Conversation

@ABB65

@ABB65 ABB65 commented Jul 17, 2026

Copy link
Copy Markdown
Member

Problem

Staging test after #159/#160: the final render is correct (chronological segments), but the live stream still showed only the Thinking spinner, with the whole turn popping in at once at the end.

Root cause is a Vue reactivity bug that predates the segment refactor — live streaming never actually rendered: `sendMessage` captured the assistant placeholder before `messages.value.push(...)` and the SSE reducer mutated that raw object. Raw-target writes bypass Vue's proxies, so no effect ever fired during the stream; the list only re-rendered when `isStreaming` flipped in `finally`.

Proven by a sync-flush watcher over the proxied message: on the old code it fires exactly once (the placeholder append) for a 4-event stream; with the fix it fires on every content event.

Change

One-liner in spirit: read the placeholder back through the reactive array (`messages.value[messages.value.length - 1]`) before handing it to `handleSSEEvent` and the abort/error checks. Every segment push and tool-call update now goes through the proxy → the list re-renders mid-stream and the `streamTick` scroll-follow has something to follow.

Test

  • New nuxt regression test counts reactive updates during a mocked stream (fails on the previous code with `1 >= 4`)
  • `pnpm test` — 1079 tests green; `pnpm lint` + `pnpm typecheck` clean

🤖 Generated with Claude Code

…ssage via its reactive proxy

The SSE reducer mutated the raw placeholder object captured before
messages.value.push(). Raw-target writes bypass Vue's proxies, so no
effect ever fired during a stream: the message list stayed frozen on
the Thinking spinner and the entire turn popped in at once when
isStreaming flipped at the end. This predates the segment refactor —
it is why live streaming never visibly worked.

Read the placeholder back through the reactive array before handing it
to the reducer, so every segment push and tool-call update triggers a
re-render (and the streamTick scroll-follow actually has something to
follow). Regression-tested with a sync-flush watcher that counts
reactive updates during a mocked stream.
@ABB65
ABB65 merged commit b00e689 into main Jul 17, 2026
2 checks passed
@ABB65
ABB65 deleted the fix/chat-stream-reactivity branch July 17, 2026 17:16
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.

1 participant