You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
perf(mobile): stop chat redraws per keystroke and page-wide row scans per scroll frame - #34323
Typing a prompt and scrolling back through a chat on Orca Mobile no longer get slower as the chat grows. Both did work proportional to the whole chat on every keystroke and every scroll frame (#33205).
The phone draws chat only in the over-the-air page (react-native-web inside the app's WebView, served by the desktop), so that is where the cost was.
Typing re-rendered the whole session screen. The draft text was subscribed inside useMobileNativeChatDrafts, which runs in useMobileNativeChatController at the top of MobileSessionRouteScreen. Each keystroke redrew the session screen and the chat view, re-ran every drawn row's props and memo check (hundreds once you've scrolled up), and ran the page list's layout-reading effects. Now the drafts hook returns the draft's key, and MobileNativeChatDraftComposer subscribes to the draft store and renders the same controlled MobileNativeChatComposer. A keystroke redraws only the composer. Typing, dictation, send and restore still write through the same draft store.
Scrolling scanned the whole page. On every scroll event the page list records the reader's place (mobileNativeChatReadingPlace), and to find the rows on screen it ran querySelectorAll('[data-testid^="chat-row:"]') over the scroller and read every drawn row's attribute: 9 to 12 thousand DOM nodes per frame, growing as you scroll up. The page list now marks its row box (data-chat-rows). The reading place takes that box's children plus any footer waiting rows after it, binary-searches them by position, and reads ids only for the rows it touches. The same rows, in the same order, count as anchors as before.
Since typing no longer commits the list, the page list's resize observer now also watches its own viewport. Without that, a reader parked in history moved when the composer changed height: in review, opening the / menu shifted the reading line by 301 px on the phone. With the observer it stays put.
The native FlatList path only picks up the draft-key wiring. Nothing changes on the wire, so mixed versions are unaffected.
Measurements
Release APK mobile-android-v0.0.59 on an Android 15 arm64 emulator, paired to a hidden dev desktop serving out/mobile-web built from main (6357bd7) or from this branch. The chat is a real 747-turn Claude transcript opened with claude --resume in a scratch repo. The bench drives the page's own WebView over CDP: keys through Input.dispatchKeyEvent, 500 px touch swipes through Input.synthesizeScrollGesture, and costs from Performance.getMetrics deltas. "4× CPU" is Emulation.setCPUThrottlingRate(4), which is closer to a mid-range phone than this Mac. Before and after builds were interleaved five times (before-* is main, after-* and final-5 are this branch, final-5 the last commit). The table uses the pair where both runs had 384 rows drawn after the load phase; the other pairs agree (log below).
main
this PR
Typing, chat just opened (24 rows): script per key
3.9 ms
1.7 ms
Typing, 384 rows drawn: script per key
5.9 ms
1.2 ms
Typing, 384 rows drawn, 4× CPU: main-thread per key, median / p95
46.7 / 102.0 ms
21.3 / 57.3 ms
Typing, 384 rows drawn, 4× CPU: script per key
29.7 ms
6.1 ms
Scrolling through already-drawn history: script per swipe, down / up
39.3 / 40.6 ms
20.6 / 25.9 ms
Same, 4× CPU
128.4 / 118.2 ms
79.6 / 66.3 ms
Typing costs 2 to 5 times less and no longer grows with how much of the chat is drawn. Script time while scrolling roughly halves. Total main-thread time per swipe drops less, about 5 to 10%, because on this emulator most of what is left is the browser's own hit testing and paint of a large page. Key-to-paint sits at 24 to 32 ms on both builds (the emulator paints in software), so it isn't counted as a result.
On the phone with this change, the row at screen centre held its offset to the pixel through 4 s of history loading, in three rounds of scrolling up.
Build the page from each tree with pnpm run build:mobile-web and keep a copy of out/mobile-web for each.
Launch a hidden dev desktop: ORCA_BACKGROUND_LAUNCH=1 ORCA_DEV_USER_DATA_PATH=/tmp/<dir> node config/scripts/run-electron-vite-dev.mjs -- --remote-debugging-port=<port>. Add a scratch repo and run claude --resume <long session> in its terminal. The transcript is copied into ~/.claude/projects/-private-tmp-<scratch>/.
Install the release APK on an emulator. Pair it with window.api.mobile.getPairingQR({ address: '10.0.2.2', connectionMode: 'local-only', rotate: true }) from the desktop page, sent with adb shell am start -d '<pairingUrl>'. Open the chat.
Run adb forward tcp:9599 localabstract:webview_devtools_remote_$(adb shell pidof com.stably.orca.mobile), then run the bench script below against localhost:9599.
To switch builds: swap out/mobile-web, restart the desktop (it caches the bundle per process), force-stop and relaunch the app, and reopen the chat.
…y row per scroll frame
Typing re-rendered the whole session screen because the draft text was
subscribed in the drafts hook at the route; the composer now subscribes by
draft key. The page list's reading place indexed every drawn row with a
page-wide querySelectorAll on each scroll event; it now binary-searches the
row box's children. The list also observes its viewport so composer resizes
keep a parked reader's place without a list commit.
The composer now receives a nullable draft key and reads its text from the mobile draft store. The draft-management hook exposes the key, and related controller, view, fixture, and test code uses it. Reading-position logic now searches tagged rows within a marked row container, skips untagged elements, and responds to viewport and content resizing.
Fixed issue severity: Medium
Priority: Normal
Merge Risk: Minimal · up to 17747
The change appears mergeable after normal checks. A production-path render test would give better protection against the typing lag returning.
Pre-merge checks: 4 · 1
Failed checks: 1 warning
Check name
Status
Explanation
Resolution
Docstring Coverage
Docstring coverage is 22.58% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Documentation-only edits and deletions within surv…
Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed checks: 4 passed
Check name
Status
Explanation
Title check
The title clearly and concisely summarizes the two main changes: reducing chat redraws during typing and avoiding page-wide row scans during scrolling.
Description check
The description is detailed and on topic. It explains the user impact, implementation, rationale, linked issue, visual proof status, test method, and performance measurements. It does not reproduce ev…
Linked Issues check
Issue #33205 directly reports lag while typing and scrolling in Orca Mobile. The PR addresses both paths. The draft subscription moves from the session screen to MobileNativeChatDraftComposer, so ty…
Out of Scope Changes check
The changed production files support the two performance fixes for #33205. Draft-key changes update the mobile chat controller, view, overlay, composer, and related draft tests. Row-box lookup and vie…
Full details: Description check
Explanation
The description is detailed and on topic. It explains the user impact, implementation, rationale, linked issue, visual proof status, test method, and performance measurements. It does not reproduce every template heading or checklist item, but the core required information is present.
Full details: Linked Issues check
Explanation
Issue #33205 directly reports lag while typing and scrolling in Orca Mobile. The PR addresses both paths. The draft subscription moves from the session screen to MobileNativeChatDraftComposer, so typing updates the composer without re-rendering the owner. The reading-place code searches the marked data-chat-rows box and nearby tagged footer rows instead of scanning all chat rows. The viewport resize observer preserves the reading position when composer height changes. Tests cover owner render isolation, reading-place lookup, viewport changes, and intervening turn-status rows. The issue has no additional coding acceptance criteria.
Full details: Out of Scope Changes check
Explanation
The changed production files support the two performance fixes for #33205. Draft-key changes update the mobile chat controller, view, overlay, composer, and related draft tests. Row-box lookup and viewport observation update the page list, reading-place code, and related tests. Native-path wiring and test-fixture changes support the same draft contract. The summary shows no unrelated feature, protocol change, or broad refactor.
Full details: Docstring Coverage
Explanation
Docstring coverage is 22.58% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Documentation-only edits and deletions within surviving functions are included; unchanged diff context and fully deleted functions are excluded. Analyzed 31 functions across 26 files.
Resolution
Write docstrings for the functions missing them to satisfy the coverage threshold.
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.
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 9 remain after this review.
The reason will be displayed to describe this comment to others. Learn more.
Minor · Functional Correctness · Quick win
Supported by static analysis
Script executed:
sed -n '350,410p' mobile/src/session/MobileNativeChatPageList.tsx
rg -n 'waitingRows|useMobileNativeChatTurnDisclosure|emptyStateView' mobile/src/session/MobileNativeChatView.tsx mobile/src/session/*turn* mobile/src/session/MobileNativeChatPageList.tsx
Repository: stablyai/orca
Length of output: 5240
Script executed:
sed -n '1,340p' mobile/src/session/use-mobile-native-chat-turn-disclosure.ts
printf'\n--- MobileNativeChatView ---\n'
sed -n '220,350p' mobile/src/session/MobileNativeChatView.tsx
printf'\n--- PageList props and setup ---\n'
sed -n '1,130p' mobile/src/session/MobileNativeChatPageList.tsx
Repository: stablyai/orca
Length of output: 20622
Script executed:
rg -n -F -- 'nativeChatMessagesWaitingBehindLiveTurn' src mobile
rg -n -F -- 'waitingRows' mobile/src/session/use-mobile-native-chat-turn-disclosure*.test* mobile/src/session/MobileNativeChatView.waiting-rows.test.ts
printf'\n--- helper ---\n'
helper=$(rg -l -F -- 'export function nativeChatMessagesWaitingBehindLiveTurn' src mobile | head -n 1)if [ -n"$helper" ];then
sed -n '1,260p'"$helper"fiprintf'\n--- disclosure tests around waiting cases ---\n'
sed -n '1,230p' mobile/src/session/use-mobile-native-chat-turn-disclosure-opening-send.test.tsx
printf'\n--- page-list reading branches ---\n'
rg -n 'mobileNativeChatReadingPlace|readingPlace|drawnRows|liveRows|rows.length|notePlace|drawAndFetch|restore' mobile/src/session/MobileNativeChatPageList.tsx
Repository: stablyai/orca
Length of output: 14329
Script executed:
printf'%s\n''--- MobileNativeChatView data producer ---'
sed -n '1,230p' mobile/src/session/MobileNativeChatView.tsx
printf'%s\n''--- projection and draw order definitions ---'
rg -n 'export function projectStructuredAgentSessionMessages|function projectStructuredAgentSessionMessages|export function nativeChatRowsInDrawOrder|function nativeChatRowsInDrawOrder' src/shared mobile/src/session
forfin$(rg -l -F -- 'export function projectStructuredAgentSessionMessages' src/shared mobile/src/session);do sed -n '1,280p'"$f";doneforfin$(rg -l -F -- 'export function nativeChatRowsInDrawOrder' src/shared mobile/src/session);do sed -n '1,220p'"$f";doneprintf'%s\n''--- page-list reading/restoration block ---'
sed -n '145,325p' mobile/src/session/MobileNativeChatPageList.tsx
Repository: stablyai/orca
Length of output: 29923
Script executed:
printf'%s\n''--- transient data producer ---'
rg -n -F -- 'export function buildMobileNativeChatTransientData' mobile/src/session
f=$(rg -l -F -- 'export function buildMobileNativeChatTransientData' mobile/src/session | head -n 1)
[ -n"$f" ] && sed -n '1,280p'"$f"printf'%s\n''--- MobileNativeChatView call sites ---'
rg -n 'MobileNativeChatView' mobile/src/session mobile/src | head -n 120
printf'%s\n''--- turn journal and messages wiring ---'
rg -n 'turnJournal=|turnJournal:|messages=|<MobileNativeChatView|buildMobileNativeChatTransientData' mobile/src/session --glob '*.tsx' --glob '*.ts'| head -n 240
Repository: stablyai/orca
Length of output: 29309
Script executed:
printf'%s\n''--- overlay call site ---'
sed -n '100,175p' mobile/src/session/MobileNativeChatOverlay.tsx
printf'%s\n''--- waiting-row integration test ---'
sed -n '1,230p' mobile/src/session/MobileNativeChatView.waiting-rows.test.ts
printf'%s\n''--- session message/journal declarations ---'
rg -n 'messages:|nativeChatTurnJournal|nativeChatTurnJournal:' mobile/src/session/use-mobile-native-chat-session.ts mobile/src/session/MobileNativeChatOverlay.tsx mobile/src/session --glob '*.ts' --glob '*.tsx'| head -n 220
Repository: stablyai/orca
Length of output: 36939
Script executed:
printf'%s\n''--- remaining overlay props ---'
sed -n '135,235p' mobile/src/session/MobileNativeChatOverlay.tsx
printf'%s\n''--- controller pending wiring ---'
rg -n 'pending=|pending:' mobile/src/session/use-mobile-native-chat-controller.ts mobile/src/session/MobileNativeChatOverlay.tsx mobile/src/session --glob '*.ts' --glob '*.tsx'| head -n 180
sed -n '80,175p' mobile/src/session/use-mobile-native-chat-controller.ts
printf'%s\n''--- opening-send predicate ---'
rg -n -F -- 'export function structuredAgentSessionOpeningSendIn' src/shared
f=$(rg -l -F -- 'export function structuredAgentSessionOpeningSendIn' src/shared | head -n 1)
[ -n"$f" ] && sed -n '1,240p'"$f"printf'%s\n''--- pending echo source ---'
rg -n 'MobileNativeChatPendingItem|build.*pending|pending.*messages|pendingEcho|pendingItems' mobile/src/session --glob '*.ts' --glob '*.tsx'| head -n 220
Repository: stablyai/orca
Length of output: 20178
Script executed:
printf'%s\n''--- mobile native chat drafts state ---'
sed -n '1,330p' mobile/src/session/native-chat/composer/use-mobile-native-chat-drafts.ts
printf'%s\n''--- draft pending types and send transitions ---'
rg -n 'acceptSend|holdUnconfirmedSend|pendingKey|pending:' mobile/src/session/native-chat/composer mobile/src/session/use-mobile-structured-agent-state.ts mobile/src/session/use-mobile-structured-send-with-outcome.ts mobile/src/session/use-mobile-native-chat-controller.ts
printf'%s\n''--- structured send/turn handover references ---'
rg -n 'handedOverAt|dispatchState.*pending|acceptedSequence|chatPending' mobile/src/session src/shared --glob '*.ts' --glob '*.tsx'| head -n 260
Repository: stablyai/orca
Length of output: 28823
Script executed:
printf'%s\n''--- controller structured/session bindings ---'
sed -n '230,300p' mobile/src/session/use-mobile-native-chat-controller.ts
sed -n '1,130p' mobile/src/session/use-mobile-structured-agent-state.ts
printf'%s\n''--- native session stream state ---'
sed -n '1,120p' mobile/src/session/use-mobile-native-chat-session.ts
sed -n '240,300p' mobile/src/session/use-mobile-native-chat-session.ts
printf'%s\n''--- pending retirement contract ---'
sed -n '1,220p' mobile/src/session/native-chat/mobile-native-chat-pending-retirement.ts
Repository: stablyai/orca
Length of output: 22307
Script executed:
printf'%s\n''--- reading place implementation ---'
sed -n '1,190p' mobile/src/session/mobile-native-chat-reading-place.ts
printf'%s\n''--- footer implementation ---'
sed -n '1,100p' mobile/src/session/mobile-native-chat-list-footer.tsx
printf'%s\n''--- page-list invocation and props ---'
rg -n 'MobileNativeChatRowList|MobileNativeChatPageList|emptyStateView' mobile/src/session/MobileNativeChatRowList.tsx mobile/src/session/MobileNativeChatPageList.tsx mobile/src/session/MobileNativeChatView.tsx
Repository: stablyai/orca
Length of output: 9029
Script executed:
sed -n '1,110p' mobile/src/session/MobileNativeChatRowList.tsx
Repository: stablyai/orca
Length of output: 2422
Keep the marked row box when waiting rows are present.
During an opening-send handover, chatPending can be the only transient row while the transcript is still empty. The waiting helper moves that row to turns.waitingRows, so rows.length === 0 is true while the footer still renders a tagged row.
The current branch omits the marked row box. mobileNativeChatReadingPlace then finds no rows, so resize restoration cannot preserve the reader’s position.
The test uses local Screen and Composer components and calls owner?.setComposerText('typed') directly. It does not render the production controller-to-overlay-to-view path.
The production path passes controller.setChatComposerText through MobileNativeChatOverlay to MobileNativeChatView. A draft subscription added to that controller or view can reintroduce screen redraws while this test still passes. The existing parity test also mocks MobileNativeChatDraftComposer and does not count renders.
Add a controller/view integration assertion that types through onChangeText and fails when the production owner rerenders per keystroke. This is a material, nonduplicative coverage improvement, not evidence of a current production failure.
Review info
Run configuration
Source mode: Auto
Repository: Unavailable
Branch: Unavailable
File: No configuration file from this repository applied
Commit: Unavailable
Layers: Repository UI
Review profile: CHILL
Plan: Advanced
Run ID: 961ea824-7807-4fd9-97e0-32c275f5d8f9
Commits
Reviewing files that changed from the base of the PR and between 9cd08fe and 1774724.
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 8 remain after this review.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Typing a prompt and scrolling back through a chat on Orca Mobile no longer get slower as the chat grows. Both did work proportional to the whole chat on every keystroke and every scroll frame (#33205).
The phone draws chat only in the over-the-air page (react-native-web inside the app's WebView, served by the desktop), so that is where the cost was.
Typing re-rendered the whole session screen. The draft text was subscribed inside
useMobileNativeChatDrafts, which runs inuseMobileNativeChatControllerat the top ofMobileSessionRouteScreen. Each keystroke redrew the session screen and the chat view, re-ran every drawn row's props and memo check (hundreds once you've scrolled up), and ran the page list's layout-reading effects. Now the drafts hook returns the draft's key, andMobileNativeChatDraftComposersubscribes to the draft store and renders the same controlledMobileNativeChatComposer. A keystroke redraws only the composer. Typing, dictation, send and restore still write through the same draft store.Scrolling scanned the whole page. On every scroll event the page list records the reader's place (
mobileNativeChatReadingPlace), and to find the rows on screen it ranquerySelectorAll('[data-testid^="chat-row:"]')over the scroller and read every drawn row's attribute: 9 to 12 thousand DOM nodes per frame, growing as you scroll up. The page list now marks its row box (data-chat-rows). The reading place takes that box's children plus any footer waiting rows after it, binary-searches them by position, and reads ids only for the rows it touches. The same rows, in the same order, count as anchors as before.Since typing no longer commits the list, the page list's resize observer now also watches its own viewport. Without that, a reader parked in history moved when the composer changed height: in review, opening the
/menu shifted the reading line by 301 px on the phone. With the observer it stays put.The native
FlatListpath only picks up the draft-key wiring. Nothing changes on the wire, so mixed versions are unaffected.Measurements
Release APK
mobile-android-v0.0.59on an Android 15 arm64 emulator, paired to a hidden dev desktop servingout/mobile-webbuilt frommain(6357bd7) or from this branch. The chat is a real 747-turn Claude transcript opened withclaude --resumein a scratch repo. The bench drives the page's own WebView over CDP: keys throughInput.dispatchKeyEvent, 500 px touch swipes throughInput.synthesizeScrollGesture, and costs fromPerformance.getMetricsdeltas. "4× CPU" isEmulation.setCPUThrottlingRate(4), which is closer to a mid-range phone than this Mac. Before and after builds were interleaved five times (before-*is main,after-*andfinal-5are this branch,final-5the last commit). The table uses the pair where both runs had 384 rows drawn after the load phase; the other pairs agree (log below).Typing costs 2 to 5 times less and no longer grows with how much of the chat is drawn. Script time while scrolling roughly halves. Total main-thread time per swipe drops less, about 5 to 10%, because on this emulator most of what is left is the browser's own hit testing and paint of a large page. Key-to-paint sits at 24 to 32 ms on both builds (the emulator paints in software), so it isn't counted as a result.
On the phone with this change, the row at screen centre held its offset to the pixel through 4 s of history loading, in three rounds of scrolling up.
Full A/B log (4 interleaved runs)
How to rerun
pnpm run build:mobile-weband keep a copy ofout/mobile-webfor each.ORCA_BACKGROUND_LAUNCH=1 ORCA_DEV_USER_DATA_PATH=/tmp/<dir> node config/scripts/run-electron-vite-dev.mjs -- --remote-debugging-port=<port>. Add a scratch repo and runclaude --resume <long session>in its terminal. The transcript is copied into~/.claude/projects/-private-tmp-<scratch>/.window.api.mobile.getPairingQR({ address: '10.0.2.2', connectionMode: 'local-only', rotate: true })from the desktop page, sent withadb shell am start -d '<pairingUrl>'. Open the chat.adb forward tcp:9599 localabstract:webview_devtools_remote_$(adb shell pidof com.stably.orca.mobile), then run the bench script below againstlocalhost:9599.out/mobile-web, restart the desktop (it caches the bundle per process), force-stop and relaunch the app, and reopen the chat.Visual Proof
N/A: nothing looks different. The change is how much work a keystroke and a scroll frame do; the numbers above are the proof.
Fixes #33205