fix(mobile): native chat streams replies token by token
Problem
Native chat (iOS and Android) showed a streaming reply only when the turn ended. "Working…" fades out, the screen stays empty under the prompt (about 13 s for a 50-line reply on the iOS simulator), then the whole reply appears at once. On Android it also landed below the fold without following.
Cause
renderRow read the live text from chat.snapshot, but the list's data (rows) does not change while a turn streams: deltas only go to snapshot.liveStreams. LegendList memoizes each cell on [itemKey, data, extraData], so the streaming row never re-rendered until the turn finished.
Fix
withRowTextputs each row's text into its list item. A row keeps the same{ row, text }object until its text changes, so only the streaming row's cell re-renders.extraDatawas not used because it would re-render every visible cell on every token.- Following the stream comes from the existing
maintainScrollAtEnd(dataChange), which now fires per token, as its comment already assumed. ConversationturnsmaintainScrollAtEndoff while a finger drags; otherwise each token pulled a reader scrolling up back to the end.
Verified (iOS 26.5 simulator, local stack, real Claude turns)
- A 50-line reply grows line by line; before, nothing showed until the turn ended.
- The view follows while at the bottom; scrolling up mid-stream holds still; ↓ re-pins and follows; a turn ending while scrolled up does not jump; a finished long chat opens at the bottom.
- Re-renders: during a 50-line turn the streaming row's cell rendered 182 times and no other cell rendered.
withRowText.test.tsfails without the cache and passes with it.bun test(mobile) 249 pass, biome clean, tsc unchanged (188 pre-existing), iOS and Android exports pass. iOS and Android runtime fingerprints unchanged (JS-only, ships over the air).
Not verified
- Android on a device (same code path; the export passes).
- Real-finger momentum: following resumes at drag end, so a flick still gliding inside the 15% threshold can re-pin.
- Separate, not from this change: a long chat sometimes opens partway up on the first cold open.
Summary by cubic
Fixes native chat streaming so replies appear token by token instead of only when the turn completes.
Previously, the list data never changed while a turn streamed, so LegendList never re-rendered the streaming row until the turn ended. Each row now carries its text in the list data, and a row keeps the same entry until its text changes, so only the streaming row's cell re-renders per token. The per-token data change also lets maintainScrollAtEnd follow the stream; it's now disabled while a finger drags so the reader isn't pulled back to the bottom.
Summary by CodeRabbit
- Bug Fixes
- Chat conversations no longer automatically stay pinned to the bottom while you drag through the message list. Existing scroll positioning resumes when you finish dragging.
- Updated message text is reflected in the conversation, while unchanged messages retain their existing display state.
- Performance
- Chat message lists reuse unchanged text entries, helping reduce unnecessary updates during rendering.