Skip to content

fix(mobile): a streaming reply reveals word by word, like the desktop - #29148

Merged
brennanb2025 merged 12 commits into
mainfrom
brennanb2025/chat-stream-phone-reveal
Oct 17, 2026
Merged

brennanb2025 merged 12 commits into
mainfrom
brennanb2025/chat-stream-phone-reveal

Conversation

@brennanb2025

@brennanb2025 brennanb2025 commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 8 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​195 $\color{#cf222e}{\Huge{\mathbf{−}}}$​3 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​192
Prod 15 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​673 $\color{#cf222e}{\Huge{\mathbf{−}}}$​409 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​264

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.

  • The pure reveal (native-chat-text-reveal.ts: per-chunk deadlines, word-boundary stepping, the spaceless-script rule) moved from the desktop renderer to src/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.
  • The bookkeeping that tells "a reply that began while you watched" from "a reply already written when you opened the chat", and remembers how far a row got, also moved to src/shared/native-chat-reply-reveals.ts. The desktop's React wrapper around it is unchanged.
  • The phone gets a thin hook (use-mobile-native-chat-paced-text.ts) and MobileNativeChatPacedProse. 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.
  • The chat overlay holds each chat's reveal memory and passes it down through context. The overlay stays mounted when the list does not, so a row the list remounts while scrolling, or a chat reopened after switching to the terminal view, resumes where it was instead of replaying. The overlay reads the phone's Reduce Motion setting once per chat.
  • use-reduced-motion.ts moved from mobile/src/onboarding/ to mobile/src/hooks/, since onboarding and chat now both use it.
  • While a block is revealing, the markdown renderer gets 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.
  • Terminal-backed chats: their live preview row never counted as part of the live turn on the phone, so it could not grow or be paced. It now does while the agent is working (it is the only row known to be in the live turn there). The phone also builds that row with the shared helper the desktop uses.

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:

  • Paced reveal: intended. The common pattern shows received text immediately. The desktop reveal was approved in feat(native-chat): make native chat replies flow in and the transcript glide when following #26238, and this extends the same behaviour to the phone.
  • Growing markdown: intended. During an ordinary animated reply, incomplete markdown is repaired and diagrams remain source until finished, matching the desktop. With Reduce Motion on, the phone keeps its previous immediate text and diagram timing.

Linked Issue

Discord report (no issue). Related to #28984 (host chunk cadence).

Visual Proof

Real QA on head 489aa7d5214 against 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.

Run Main: phone steps / largest jump This PR: phone steps / largest jump
Claude, ~300 words 11 steps for 11 updates / 421 chars 36–45 steps for 9–11 updates / 184–189 chars
Claude, long list (~9.5k chars) 16 steps / 1,371 chars 84 steps / 446 chars
Claude, Reduce Motion on one step per update (immediate) one step per update (immediate, unchanged)
OpenCode (Big Pickle) one step per update 106 steps for 67 updates, largest jump 457 → 158
  • Remounts: switching tabs away and back, scrolling the growing reply off-screen and back, and backgrounding the app mid-reply never replayed text that was already shown.
  • Long reply responsiveness (simulator): scrolling during a long reply had comparable JS hitches on both builds (over 150 ms: 10 on this PR, max 347 ms; 11 on main, max 291 ms). This is simulator evidence on a Mac, not a real-phone CPU or battery measurement.
  • Desktop: a fresh Claude chat ends cleanly on both main and this PR, with one "Worked for" label and no interrupted notice. The notice and doubled label seen during review came from replayed test output and do not reproduce with a real Claude reply.
  • Final text matched the desktop in every run.

Main, Claude mid-reply (whole chunks):

main Claude mid-reply

This PR, Claude mid-reply (revealing word by word):

PR Claude mid-reply

This PR, OpenCode mid-reply:

PR OpenCode mid-reply

This PR, long reply while scrolling:

PR long reply while scrolling

Desktop end state, this PR and main:

desktop PR end state

desktop main end state

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

  • Not applicable, or this change copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.

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

  • This PR is small and focused
  • I explained what changed and why (ELI5, the user-facing before/after, the mechanism, and why over the alternatives)
  • Before/after screenshots or videos attached for UI changes, or N/A with reason
  • Self-reviewed for correctness, security, and performance
  • Cross-platform, SSH/remote, and path/shortcut impact considered (or N/A)
  • pnpm lint, pnpm typecheck, pnpm test, and pnpm build pass (or CI will cover; local preferred)

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.
@brennanb2025
brennanb2025 force-pushed the brennanb2025/chat-stream-phone-reveal branch from 87bee30 to 73e7290 Compare October 15, 2026 12:35
@brennanb2025

brennanb2025 commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor Author

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 766bcf715951811ce17144a215042ca7e2797613. Two adversarial rounds plus correction and readiness checks fixed: reveal progress crossing prose blocks, replay of already-read text after offscreen prompts or resume, and completed prose animating beside a running tool. Architecture review kept the shared design. Since the last round the branch took three merges of main and one pure file move. A final bounded pass checked each against main's intent: #28906 ask rows, #28654 queued-message clear, #29075 user-message markdown/fold/images, and #28997's reduced-motion hook move. It found all of them preserved, with no new P0/P1/P2.

  • CI: full PR workflow green on 766bcf71595 (run 38985243742).
  • Real QA (iPhone simulator plus a hidden desktop app, real Claude and OpenCode, main vs this branch):
    • Claude, ~300 words: 11 whole-chunk steps on main (largest jump 421 chars) became 36–45 steps here (largest 184–189).
    • A ~9.5k-character list: 16 steps (largest 1,371) became 84 (largest 446).
    • Reduce Motion stays immediate on both.
    • Tab switches, offscreen scrolling and backgrounding mid-reply never replayed text already shown.
    • Scrolling during a long reply had comparable JS hitches on both builds (simulator evidence).
  • Resolved: the interrupted notice and doubled duration label from an earlier synthetic replay don't reproduce with a real Claude reply on main or this branch.
  • Not covered:
    • Real-phone CPU and battery cost (simulator only).
    • The chat/terminal toggle, which structured chats don't have on the phone. Tab switches covered remounting instead.
  • Pre-existing, not from this PR: after a tab switch the chat can reopen scrolled to the previous turn, on main and this branch alike.
  • Deferred (P2): each revealed step re-parses the settled Markdown. Simulator timing shows no user-visible cost; an incremental parser wasn't warranted without measured harm.

For Brennan's review

main this PR
Claude reply, mid-stream on the phone
OpenCode mid-stream / long reply while scrolling (PR)
Desktop end state (unchanged)

Places where judgment matters:

  1. mobile/src/session/MobileNativeChatMessage.tsx#L279-L285: only assistant prose is paced; user rows keep main's fold.
  2. mobile/src/session/MobileNativeChatPacedProse.tsx#L24: pacing starts only when the newest block is text, matching desktop.
  3. Intended difference from the common pattern: the approved 350 ms word reveal (the common pattern shows received text at once). No host cadence, saved data, wire, setting or default change.

…-main-sync

# Conflicts:
#	mobile/src/session/MobileNativeChatMessage.tsx
#	mobile/src/session/MobileNativeChatOverlay.tsx
@brennanb2025
brennanb2025 marked this pull request as ready for review October 16, 2026 01:27
@coderabbitai

coderabbitai Bot commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: e627d833-7b70-4bb5-ad17-2d01cb6353f6
📥 Commits

Reviewing files that changed from the base of the PR and between 16c2ed8 and 766bcf7.

📒 Files selected for processing: 23
  • mobile/src/session/MobileNativeChatMessage.paced-reveal.test.ts
  • mobile/src/session/MobileNativeChatMessage.tsx
  • mobile/src/session/MobileNativeChatOverlay.test.ts
  • mobile/src/session/MobileNativeChatOverlay.tsx
  • mobile/src/session/MobileNativeChatPacedProse.tsx
  • mobile/src/session/MobileNativeChatProse.tsx
  • mobile/src/session/__mocks__/mobile-prompt-controller.ts
  • mobile/src/session/mobile-native-chat-merged-snapshot-parity-hooks.test.tsx
  • mobile/src/session/mobile-native-chat-render-data.ts
  • mobile/src/session/mobile-native-chat-reply-reveals.ts
  • mobile/src/session/use-mobile-native-chat-paced-text.ts
  • mobile/src/session/use-mobile-native-chat-turn-disclosure-growing-row.test.tsx
  • mobile/src/session/use-mobile-native-chat-turn-disclosure.ts
  • src/renderer/src/components/native-chat/NativeChatPacedMarkdown.file-links.test.tsx
  • src/renderer/src/components/native-chat/NativeChatPacedMarkdown.mermaid.test.tsx
  • src/renderer/src/components/native-chat/native-chat-reply-reveals.ts
  • src/renderer/src/components/native-chat/native-chat-text-reveal.ts
  • src/renderer/src/components/native-chat/use-native-chat-paced-text.remount.test.tsx
  • src/renderer/src/components/native-chat/use-native-chat-paced-text.ts
  • src/renderer/src/components/sidebar/comment-markdown-word-fade.ts
  • src/shared/native-chat-reply-reveals.ts
  • src/shared/native-chat-text-reveal.test.ts
  • src/shared/native-chat-text-reveal.ts
💤 Files with no reviewable changes: 2
  • src/shared/native-chat-text-reveal.test.ts
  • src/renderer/src/components/native-chat/native-chat-text-reveal.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 2 remain after this review.

📝 Walkthrough

The change moves native chat text-reveal and reply-row tracking logic into shared modules. The renderer hook and mobile chat use the shared reveal logic. Mobile chat adds paced prose rendering, reduced-motion handling, and tests for reveal behavior and remounts. The mobile turn-disclosure path also marks only the synthetic streaming row as working when disclosure is disabled.

Priority: ⬇️ Low

Merge Risk: ⚪ Minimal · up to 766bc

No actionable issue was identified that should prevent merging after normal checks.

Pre-merge checks: Passed 3 · Failed 2

Failed Failed checks: 2 warnings

Check name Status Explanation Resolution
Description check Warning The description is detailed and covers the change, rationale, visual proof, testing, review notes, and checklist. However, it does not provide the required actual linked issue and explicitly states th… Add an actual issue link in the Linked Issue section, preferably using the required Fixes #<issue-number> format. Keep the existing testing and visual proof details, and complete any applicable conditional sections such as AI Disclosure i…
Docstring Coverage Warning Docstring coverage is 48.15% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 27 functions across 23 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 3 passed
Check name Status Explanation
Title check Passed The title clearly and concisely describes the main change: adding desktop-style word-by-word streaming reveals to mobile replies.
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.
Full details: Description check

Explanation

The description is detailed and covers the change, rationale, visual proof, testing, review notes, and checklist. However, it does not provide the required actual linked issue and explicitly states that no issue exists; the related issue reference is not a replacement.

Resolution

Add an actual issue link in the Linked Issue section, preferably using the required Fixes #&lt;issue-number&gt; format. Keep the existing testing and visual proof details, and complete any applicable conditional sections such as AI Disclosure if required for the contributor.

  • Fix all pre-merge checks with AI
✨ Finishing Touches: 💡 1
📝 Generate docstrings: 💡
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

…-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.
@brennanb2025
brennanb2025 merged commit b61b9da into main Oct 17, 2026
33 checks passed
@brennanb2025
brennanb2025 deleted the brennanb2025/chat-stream-phone-reveal branch October 17, 2026 01:55
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