Repository navigation
fix(mobile): a streaming reply reveals word by word, like the desktop - #29148
Conversation
The phone drew each streamed chunk as it landed, so a reply arrived in a few jumps. Move the desktop's paced reveal into src/shared and use it on both surfaces: the phone paces the growing block of the newest reply, remembers progress per chat list so a recycled row resumes, and draws everything at once with reduced motion. The terminal-backed chat's live row now counts as growing.
The list's turn resolver must not scan terminal-backed transcripts, and the overlay outlives the list across chat and terminal toggles, so it owns the reveal memory and the reduced-motion read. Also fix the moved hook's import in the onboarding route, and mock the OS read where tests render the overlay.
Their real modules reach React Native's Flow sources, which Bun cannot load; the other message row tests mock them the same way.
87bee30 to
73e7290
Compare
|
Problem. A Discord user said OpenCode chat replies on the Orca phone app "don't stream": a reply lands in a few large jumps instead of growing word by word. Measurement showed no layer drops text: OpenCode sends 8–14 large chunks (some models send the whole answer at the end), and the phone drew each received chunk whole, while the desktop already paces its reveal over 350 ms. User-facing change. Assistant replies on the phone now grow word by word, the same way they do on the desktop, using one reveal shared by both. Your own messages, and anything with Reduce Motion on, show immediately as before. This only paces text the phone has already received; it can't fill a gap before the agent sends text. Review status: loop finished on head
For Brennan's review
Places where judgment matters:
|
…-main-sync # Conflicts: # mobile/src/session/MobileNativeChatMessage.tsx # mobile/src/session/MobileNativeChatOverlay.tsx
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info
📝 Walkthrough
Priority: ⬇️ Low Merge Risk: ⚪ Minimal · up to
|
…-main-sync # Conflicts: # mobile/app/mobile-onboarding.tsx # mobile/src/hooks/use-reduced-motion.ts # mobile/src/onboarding/NotificationOnboardingPreview.tsx # mobile/src/onboarding/use-reduced-motion.ts # mobile/src/platform/use-reduced-motion.ts # mobile/src/session/MobileNativeChatMessage.tsx # mobile/src/session/MobileNativeChatOverlay.tsx # mobile/src/session/use-mobile-native-chat-turn-disclosure.ts
Keeps MobileNativeChatMessage.tsx under the 400-line limit after the main merge.






ELI5
On the phone, an agent's reply showed up in a few big jumps instead of growing word by word. The desktop already smooths the same chunks into a steady reading pace. The phone now uses that same reveal, so a reply grows word by word there too.
What Changed
The problem. A Discord user said an OpenCode chat reply on the Orca phone app "doesn't stream": it lands in a few big jumps. Measured on a test rig (about a 2,000-character reply), the host forwards every chunk it gets: OpenCode produced 8 text chunks that became 6 phone updates in 1.4 s; Claude produced 9–10 chunks in 6.6–7.9 s with gaps of up to 2.5 s. These are the original M4Air captures on main (
caa4155e61b), rechecked against their recorded runtime frames during review; they are not a new provider run on this branch. The desktop reveals each chunk over 350 ms at word boundaries (added in #26238), so the same chunks read as steady typing there. The phone had no reveal, so it drew each chunk whole the moment it arrived.Before: on the phone, a streaming reply jumps forward a paragraph at a time.
After: the reply grows word by word at the desktop's pace. With the phone's Reduce Motion setting on, text appears the moment it arrives, exactly as before.
Mechanism: one reveal, owned once.
native-chat-text-reveal.ts: per-chunk deadlines, word-boundary stepping, the spaceless-script rule) moved from the desktop renderer tosrc/shared/native-chat-text-reveal.ts. Its stepping logic moved with it: where a row starts when it mounts, how new text is received, and the frame loop with its deadline timer. The desktop hook now calls these; desktop behaviour is unchanged and its existing tests move with the code.src/shared/native-chat-reply-reveals.ts. The desktop's React wrapper around it is unchanged.use-mobile-native-chat-paced-text.ts) andMobileNativeChatPacedProse. One owner reveals the reply's combined text and maps it back to its original blocks, preserving their layout and attachments. Reveal frames update that prose subtree; the surrounding row, list, tools and composer do not update on each frame.use-reduced-motion.tsmoved frommobile/src/onboarding/tomobile/src/hooks/, since onboarding and chat now both use it.growing(open markup at the end is closed and a diagram stays code until done) and a half-revealed visual directive is held back, as on desktop.No wire change, no saved field, no new setting.
Why
One owner for the reveal instead of a separate copy per surface, so the phone and the desktop cannot drift. How often the host sends chunks is a separate fix (related to #28984); this PR leaves the host alone and fixes how the phone draws what it receives.
Differences from the common pattern:
Linked Issue
Discord report (no issue). Related to #28984 (host chunk cadence).
Visual Proof
Real QA on head
489aa7d5214against the main commit it merged (fd21909f152), on an iPhone simulator paired to a hidden desktop Orca, with real Claude and OpenCode replies. The phone's visible text was sampled every 100 ms.Main, Claude mid-reply (whole chunks):
This PR, Claude mid-reply (revealing word by word):
This PR, OpenCode mid-reply:
This PR, long reply while scrolling:
Desktop end state, this PR and main:
Testing
src/shared/native-chat-text-reveal.test.ts: the existing reveal tests, moved with the code, unchanged.mobile/src/session/MobileNativeChatMessage.paced-reveal.test.tsx(new): paced arrivals, remounts, multiple prose blocks, prompt pause/resume while offscreen, completed prose beside a running tool, and reduced motion.use-mobile-native-chat-turn-disclosure-growing-row.test.tsx: a terminal-backed chat's live preview row is the only row in the live turn.Full PR workflow green on
489aa7d5214(run 38791081427): all five unit shards, static analysis and typecheck, both packaging jobs, Mobile verify, and the aggregate.Real QA above (iPhone simulator + hidden desktop app, main vs this PR). An earlier pass on a second machine saw the same phone behaviour for Claude but was too loaded for trustworthy timing.
I manually tested these changes locally
Automated tests added/updated, or explained why not below
Review
Two review rounds made each reply's reveal owned once (so already-read text never replays after a question or an offscreen remount) and kept finished prose immediate beside a running tool. The design review kept the shared reveal. The isolated hidden-overlay replay concern is deferred because the production terminal controller clears hidden session identities and no reachable failure was found.
Agent skill upstream boundary
Notes
Platforms: same code on iOS and Android (QA ran on the iOS simulator; an earlier partial pass ran on Android). SSH/remote: phone rendering only, nothing host-side. Performance: a reveal step updates only the growing prose, at word boundaries, at most once a frame; the phone re-parses that block's markdown per step (about nine times as many parser calls during a reply), with no measurable responsiveness cost in the long-reply run above.
Provider note: how smooth a reply can look still depends on what the agent sends. Some free OpenCode models send their whole answer in one piece at the end; a reply that arrives already finished is shown at once on both phone and desktop. Host send cadence is tracked in #28984.
Author: @BrennanKB5
Checklist
N/Awith reasonpnpm lint,pnpm typecheck,pnpm test, andpnpm buildpass (or CI will cover; local preferred)