Skip to content

perf(mobile): sends respond instantly, thread opens stop freezing - #4882

Merged
t3dotgg merged 3 commits into
mainfrom
t3code/investigate-mobile-performance-lag
Jul 30, 2026
Merged

perf(mobile): sends respond instantly, thread opens stop freezing#4882
t3dotgg merged 3 commits into
mainfrom
t3code/investigate-mobile-performance-lag

Conversation

@t3dotgg

@t3dotgg t3dotgg commented Jul 29, 2026

Copy link
Copy Markdown
Member

On iOS, sending a message did nothing for 3+ seconds before showing "queued", opening a thread locked the app for ~1s before the transition, and starting a new thread froze before the composer appeared. A multi-agent investigation traced each symptom; this PR takes the highest-impact fixes that were safe to make small.

Send: feedback on the tap frame. The "queued" state waited on a durable JSON file write (with attachments inlined as base64) before anything changed on screen, and the draft only cleared after that resolved. The outbox atom now publishes synchronously with the write happening behind it (rolled back on failure, content restored to the draft), and the lock-screen Live Activity arms after the send instead of before. Sending now clears the composer and shows "queued" immediately; the file write and relay round trip happen behind that feedback.

Thread open: linear feed derivation. groupAdjacentActivities copied the accumulated group array per activity — O(k²) on long tool runs. It now appends into a mutable trailing group. Isolated benchmark: 2,000 activities in one turn drops 1.47ms → 0.009ms per pass (~160x); at 5,000 activities ~1.8ms → 0.03ms. This runs on every thread open and every streaming update, and pending approvals/user inputs re-sorted the full activity array twice more per update — now sorted once and shared.

New thread: keyboard no longer races the sheet. The native composer's autoFocus fired becomeFirstResponder the moment the view attached, forcing iOS keyboard bring-up (300ms+ cold) during the formSheet present animation. The existing InteractionManager-deferred focus takes over, so the sheet animates immediately and the keyboard rises after it settles.

Re-render hygiene. The v2 home thread list passed a fresh extraData object literal every render, forcing all visible FlatList rows to re-render on any HomeScreen render — now memoized. useThreadOutboxDrain (subscribed to all threads, projects, connections, and the outbox) ran directly in the root navigation layout, re-rendering every screen on each enqueue/shell/connection change — now hosted in a null-rendering leaf. The Android-only shortcuts hook also subscribed the iOS root to the active thread shell — now gated.

Estimated impact (release builds; ranked by user-visible effect):

  • Send: perceived latency drops from 3+ seconds to instantaneous queued feedback (the transport round trip still runs, but behind visible state).
  • Thread open: eliminates the quadratic + duplicate-sort share of the pre-transition freeze (up to ~2ms per pass per 2k activities, × N re-runs during catch-up replay) and stops whole-list row re-renders on the way out; the remaining mount cost (markdown expansion, schema decode) is follow-up work.
  • New thread: removes the main-thread keyboard stall from the present animation, which was the dominant visible beat.
  • Streaming/idle: outbox and shell updates no longer re-render the root tree, cutting steady-state render fan-out during active turns.

Deeper follow-ups identified but out of scope here: client-side event-stream batching, using the existing granular thread-detail slice atoms, skipping re-decode of warm snapshots, dropping the drain's redundant pre-flight settings RPCs, and file-referenced (not base64) attachments.

Written by Claude Fable 5 in Claude Code.

🤖 Generated with Claude Code


Note

Medium Risk
Optimistic outbox + confirm-before-drain changes message delivery ordering and failure recovery; incorrect rollback or stale guard logic could drop or duplicate sends, though new tests cover write failure and same-id retry cases.

Overview
Makes send feel immediate and cuts main-thread work and root re-renders that were freezing thread open and new task flows.

Thread outbox: Queued messages update the in-memory atom before the durable write finishes; the composer clears on the tap frame. Failed writes roll back the queue entry and merge text back into the draft (attachments via uncapped append). The drain calls confirmThreadOutboxMessageQueued and re-checks edit/busy state after that await so rolled-back or stale messages are never delivered.

Re-render isolation: useThreadOutboxDrain moves into a null ThreadOutboxDrainWorker so outbox/shell/connection churn does not re-render RootStackLayout. On iOS, launcher shortcut sync no longer subscribes to the active thread shell. Home v2 FlatList extraData is memoized.

Feed derivation: groupAdjacentActivities appends into a mutable trailing group (avoids quadratic copies). sortThreadActivities runs once and feeds both pending-approval and pending-user-input derivations.

UX timing: New-task composer autoFocus is off on iOS; focus stays deferred until after the sheet animation. Live Activity arms after onSendMessage completes.

Reviewed by Cursor Bugbot for commit 1d87beb. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Make mobile send respond instantly and stop thread opens from freezing

  • Queued messages in thread-outbox-manager.ts are now published to state synchronously before the durable write, so the draft clears immediately on send; failed writes roll back the message and restore the draft with an error.
  • use-thread-outbox-drain.ts uses the new confirmQueued API to verify the durable write succeeded before dispatching, preventing dispatch of rolled-back or stale messages.
  • ThreadOutboxDrainWorker in Stack.tsx isolates outbox drain subscriptions to a leaf component, preventing outbox state updates from re-rendering RootStackLayout.
  • ComposerEditor in NewTaskDraftScreen.tsx defers auto-focus until after the iOS sheet presentation animation completes.
  • FlatList.extraData in HomeScreen.tsx is memoized to avoid unnecessary row re-renders, and useRecentThreadShortcutSync in useAppShortcuts.ts skips active thread subscription on iOS.
  • Behavioral Change: onSendMessage now resolves immediately with the new messageId without waiting for the durable write; callers previously relying on post-send resolution timing may see different behavior.

Macroscope summarized 1d87beb.

Sending a message did nothing for seconds because the queued state waited
on a durable file write, and thread opens stalled on quadratic feed
grouping plus duplicated activity sorts. New-thread froze mid-sheet while
native autoFocus forced the keyboard up during the present animation.

- Publish outbox enqueues to the atom synchronously, write behind, roll
  back on failure; clear the composer draft on the tap frame
- Arm the Live Activity after the send instead of before it
- Make groupAdjacentActivities linear; sort activities once for
  approvals + user inputs instead of twice
- Focus the new-task composer after interactions (iOS) instead of via
  native autoFocus during the transition
- Memoize the v2 thread list's FlatList extraData
- Host useThreadOutboxDrain in a null-rendering leaf so outbox/shell
  updates stop re-rendering the root navigation layout
- Skip the active-thread shell subscription on iOS in useAppShortcuts
  (Android-only feature)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: aeb7de55-944c-4d9f-8f69-5f29340ccb5f

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:L 100-499 changed lines (additions + deletions). labels Jul 29, 2026
Comment thread apps/mobile/src/state/thread-outbox-manager.ts Outdated
Comment thread apps/mobile/src/state/use-thread-composer-state.ts

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 0b7c9d8. Configure here.

Comment thread apps/mobile/src/state/use-thread-composer-state.ts
@macroscopeapp

macroscopeapp Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

This PR changes the timing and ordering semantics of message enqueueing with an optimistic publish + rollback pattern, and adds complex race condition handling in the drain logic. While performance-focused and well-tested, these architectural changes to message queuing behavior warrant human review.

You can customize Macroscope's approvability policy. Learn more.

Review findings on the optimistic outbox:
- Roll back a failed enqueue by reference, not messageId, so a same-id
  retry that replaced the attempt survives the first attempt's failure
- Drain confirms pending writes settled (confirmQueued) before
  dispatching, so a message whose write later fails can never have
  already been delivered
- Failure rollback restores attachments via the uncapped append instead
  of the truncating merge path

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread apps/mobile/src/state/use-thread-outbox-drain.ts
…tion

The drain's threadBusy and editing-state guards were evaluated before
awaiting write confirmation, so a thread going busy or the user opening
the message in the editor during a slow write could still dispatch a
stale payload. Re-read both from the registry after the await and defer
to the next drain pass instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@t3dotgg
t3dotgg merged commit f0f16e4 into main Jul 30, 2026
17 checks passed
@t3dotgg
t3dotgg deleted the t3code/investigate-mobile-performance-lag branch July 30, 2026 08:55
github-actions Bot added a commit to omarcresp/t3code-flake that referenced this pull request Jul 30, 2026
## What's Changed
* fix(connect): suggest a serve command that matches how you ran connect by @t3dotgg in pingdotgg/t3code#4897
* fix(mobile): stop shared content errors in Personal Team builds by @t3dotgg in pingdotgg/t3code#4943
* perf(mobile): sends respond instantly, thread opens stop freezing by @t3dotgg in pingdotgg/t3code#4882
* fix(web): show Codex fast mode as a bolt by @t3dotgg in pingdotgg/t3code#4947
* docs: seed worktrees with a copy of real userdata instead of banning it by @t3dotgg in pingdotgg/t3code#4949


**Full Changelog**: pingdotgg/t3code@v0.0.32-nightly.20260730.954...v0.0.32-nightly.20260730.955

Upstream release: https://github.com/pingdotgg/t3code/releases/tag/v0.0.32-nightly.20260730.955
tarik02 added a commit to tarik02-org/t3code that referenced this pull request Jul 30, 2026
* fix(desktop): restore T3 Connect sign-in (pingdotgg#4809)

Co-authored-by: Julius Marminge <julius0216@outlook.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* Simplify files panel header (pingdotgg#4828)

* build(desktop): reduce installed app size by ~300MB (pingdotgg#4824)

* Update model version from claude-opus-4-8 to claude-opus-5 (pingdotgg#4832)

* Preserve the thread shell while detail loads (pingdotgg#4830)

* Reduce idle work and disk churn with native resource diagnostics (pingdotgg#2679)

Co-authored-by: codex <codex@users.noreply.github.com>

* fix(server): detect repositories after initialization (pingdotgg#4848)

* perf(server): merge separate staged/unstaged numstat calls into single diff HEAD --numstat (pingdotgg#4843)

* fix(git): disable external diff for review diff previews (pingdotgg#4854)

* Fix editable file focus and live syntax highlighting (pingdotgg#3979)

* fix(web): remember the rendered-markdown choice across threads (pingdotgg#4853)

Co-authored-by: Simon Doba <simon.doba@orbit.de>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>

* chore(release): prepare v0.0.31

* fix(mobile): reduce thread feed scroll jank (pingdotgg#4874)

* fix(web): restore sidebar v2 thread actions and terminal icon (pingdotgg#4712)

* fix(web): settle button now works on hover, not just right-click (pingdotgg#4905)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* fix(clients): disable add project while disconnected (pingdotgg#4834)

* fix(composer): hide default Codex service tier (pingdotgg#4784)

* docs: link iOS and Android app store downloads (pingdotgg#4902)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* fix(web): align remote server update action (pingdotgg#4731)

* fix(connect): suggest a serve command that matches how you ran connect (pingdotgg#4897)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* fix(mobile): stop shared content errors in Personal Team builds (pingdotgg#4943)

* perf(mobile): sends respond instantly, thread opens stop freezing (pingdotgg#4882)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* fix(web): show Codex fast mode as a bolt (pingdotgg#4947)

* docs: seed worktrees with a copy of real userdata instead of banning it (pingdotgg#4949)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* fix(mobile): support dragged images in the composer (pingdotgg#4953)

* fix(mobile): stop long iOS threads from jumping while scrolling up (pingdotgg#4867)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* fix(web): keep worktree default when switching a draft's machine (pingdotgg#4964)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* perf(mobile): reconnect environments immediately on resume (pingdotgg#4878)

* feat(web): pasting a huge screenshot now compresses it instead of erroring (pingdotgg#4967)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* feat(web): regenerate thread titles from sidebar (pingdotgg#4810)

* fix(web): show server update progress through reconnect (pingdotgg#4903)

---------

Co-authored-by: Alex <me@pixp.cc>
Co-authored-by: Julius Marminge <julius0216@outlook.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: wukko <me@wukko.me>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: Wout Stiens <71498452+StiensWout@users.noreply.github.com>
Co-authored-by: Utkarsh Patil <73941998+UtkarshUsername@users.noreply.github.com>
Co-authored-by: ohbentos <72638975+ohbentos@users.noreply.github.com>
Co-authored-by: Jake Leventhal <jakeleventhal@me.com>
Co-authored-by: Simon Doba <simon.doba@hotmail.de>
Co-authored-by: Simon Doba <simon.doba@orbit.de>
Co-authored-by: t3-code[bot] <269035359+t3-code[bot]@users.noreply.github.com>
Co-authored-by: Gabriel De Andrade <30420087+gabrielelpidio@users.noreply.github.com>
Co-authored-by: Jono Kemball <Noojuno@users.noreply.github.com>
Co-authored-by: Theo Browne <me@t3.gg>
Co-authored-by: Max Katz <me@maxkatz.me>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L 100-499 changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant