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
bug(mobile): feed scrolls the reader away from their position while a turn is running #17
On both the iPad and the iPhone, while a thread has a running turn and I am reading earlier content, the feed scrolls me away from where I was. Reported 2026-10-04 around 18:30 UTC, same build as #16 (Release archive of feat/ios-fork-build, includes 7345a87552). Exact thread, scroll position before and after, and whether it needs a tool result or only text to be appended: not recorded. Whether web has the same behavior: not reported.
What the code shows
Not yet known. The mobile feed in apps/mobile/src/features/threads/ThreadFeed.tsx rests at the newest message with maintainScrollAtEnd and a live-follow latch (described in docs/fork/README.md under the pull-to-reload design). Unverified: whether the latch releases when I scroll up, and whether row height changes from streamed content move the viewport.
Likely trigger
Not yet known. Same family as #16, where a live thread's feed also lays out wrong on the iPhone and blocks the iPad. Treat as one suspected area, not one cause.
Then fix the cause in the feed. Expected acceptance: a scroll position I set stays put while appends arrive, and following the bottom resumes only when I scroll back to it. That intended behavior is not recorded in docs/fork/; the default is standard chat behavior (follow only when at the bottom).
Surfaces: mobile (iPad and iPhone). Web and desktop need a check, not an assumed fix.
Diagnostic for next time
Thread id, UTC time, whether a turn was running, and whether the scroll jump happened on text-only appends or on tool rows.
Lead only, 2026-10-04 (harness on feat/ios-repro-harness). The feed scroll trace (__T3_DEBUG__ "feed") works through the probe. In one run opening a finished replayed thread on the iPad simulator, the offset went 16150 to 0 with follow: true and no drag, about 13 s after the open. It coincided with an agent-device screenshot, so it may be an artifact; I did not repeat it. The running-turn case from this issue is not reproduced yet (the live runs were dominated by debug-build JS cost, see #16).
Superseded by the next comment: the initialScrollAtEnd lead below was wrong (it still jumped with the prop off in later runs). The cause is in the keyboard controller, not LegendList.
Investigation 2026-10-04 (UTC 21:00 to 22:10), iPad Pro 11-inch (M5) simulator, debug dev client, isolated server on a VACUUM INTO copy, replay of thread 07e27b3f (27 image results, 9 presented rows, content about 19,100 px). Branch fix/mobile-feed-scroll-jump @ f17e99fbf3 (trace additions only, no fix). No effect on existing data.
New from me, 2026-10-04: the jump "often but not always happens in the middle of a scrolling gesture". That points at the follow-off path (maintain-visible-content-position turns on at drag start). My reproduction below is the follow-on path, so it may be a sibling of the reported symptom, not the same one.
Reproduced (follow on, no drag, nothing streaming): open the finished replayed thread; the list lands at the end (offset about 18,400), then 12 to 25 s after the list mounts the offset goes to exactly 0 while follow is true and atEnd flips false. Content height is unchanged (19,131) and the thread is left at its top. 8 of 9 runs with the probe attached (counting runs where only maintainScrollAtEnd or alignItemsAtEnd was switched off). It also happens with no debugger attached: screenshots 8 s and 48 s after opening both show the top of the thread.
What it is not (each from the trace, [t3-feed] namespace):
not a user gesture (drag false on every sample);
not a list remount (one empty to filled mount, at open);
not a composer or floating-status inset change (the inset trace is quiet at the jump);
not a JS-side scrollTo/scrollToEnd as far as I could trace (my tracer saw no calls at all, so this is weak);
not maintainScrollAtEnd (switched off, still jumps) and not alignItemsAtEnd (switched off, still jumps).
Where it points:initialScrollAtEnd. With it switched off, 4 of 5 runs had no jump to 0, but one run still jumped, so it is a strong lead, not a confirmed cause. Reading @legendapp/list 3.3.5: after the first positioning LegendList keeps its end target alive and, on later layout or inset events, runs a per-frame end correction that moves the offset natively through its scroll-adjust sentinel (which would explain no JS scroll call). Each jump follows a re-render burst of the thread route about 0.5 s earlier. Flipping the prop to false once the list first reaches the end did not reliably help (1 of 2 runs still jumped), because LegendList only clears the preserved target on a data change.
whether upstream main has the same behavior (not checked);
web and desktop (not checked).
Harness notes for the next run: start Metro with APP_VARIANT=development or the manifest request takes 10 s and the dev client times out; agent-device scroll bottom hangs the XCTest runner on a long-text thread (swipe works); the probe's jump detector ignores samples taken during a drag, so analyze scroll.series in the report for mid-gesture jumps.
Next: a Release simulator build measured without the inspector (needs a coordinated xcodebuild, other sessions are building), then replay plus scripted drags to reproduce the mid-gesture case. A candidate fix has to hold up on both the follow-on and follow-off paths and on short threads (alignItemsAtEnd, header inset).
Status: Verified (follow-on variant), cause localized to the list's initial-scroll target but not proven, no fix yet, reported symptom not yet reproduced.
Cause found and fixed, 2026-10-04 (UTC 17:00 to 01:00), branch fix/feed-scroll-jump (merged to fork/prod as d64f5de, patch commit 4bc9889). Merged, not deployed: the iPad needs a rebuild to carry it.
New from me, 2026-10-04: the jump "always happens almost immediately on the first touch of the thread area after returning to the thread or from another app; scrolling back to the bottom, no more jumping for a while".
Cause. The feed is wrapped in react-native-keyboard-controller's KeyboardChatScrollView. When the composer overlay height changes (extraContentPadding), and on every keyboard event, it shifted the list on iOS by writing contentOffset through animated props. That leaves a stale contentOffset on the native scroll view, and the next React commit of the list resets it to 0: the follow toggle on a touch, or any re-render burst. In the replay thread the offset went 18377 to 0, exactly the header-inset top, with no JS scroll call. The write is logged: contentOffsetY tracks the offset during the composer-inset animation at landing, then the jump follows with no further writes. Returning to the app fires a keyboard event (height 0, duration 383 ms) that writes contentOffsetY = the current offset; the first touch about 8 s later reset it to 0. Why it feels like "the first touch, then fine": after the reset the stale prop is gone until the next composer-height change or keyboard event.
Superseded earlier lead:initialScrollAtEnd (jumped anyway in later runs). Also ruled out: maintainScrollAtEnd, alignItemsAtEnd, maintain-visible-content-position toggling at drag start, remount, user gesture.
Fix.patches/react-native-keyboard-controller@1.22.4.patch: composer-height shifts (useExtraContentPadding) and keyboard-event shifts (useChatKeyboard/index.ios) now call reanimated scrollTo instead of writing contentOffsetY. The patch edits the package's src/ (what Metro bundles) and lib/module and lib/commonjs. Guard test: apps/mobile/src/lib/keyboardControllerPatch.test.ts fails if a version bump drops either file's patch (checked against the old package: both files fail).
Measured (iPad simulator, debug dev client, replay of thread 07e27b3f, 19,131 px):
before: cold open jumped 18377 to 0 in 10 of 10 runs (first touch in the one run that had it landed at the touch);
after: 0 of 8 cold opens, 7 of them with a touch (112 to 123 drag samples, no jump); 0 of 3 runs that leave the app for another one and return, then touch; the first swipe moves the reading position about 500 px and the second returns to the end.
Note: my first fix test edited the package's lib/ and showed no effect, because Metro bundles src/; the measurements above are on the src/ patch.
Review. A fresh reviewer could not disprove the fix. It found the keyboard-event path still exposed (now fixed and measured above), noted the original guard test was weak (now covers both files), and asked for a check of visual wobble from the loss of atomic inset plus offset commits.
Not verified, so this stays open as fixed-unverified:
on-screen keyboard open and close at the end of a long thread, and visual wobble of the composer-height animation and floating status control (the simulator's accessibility runner hangs on this thread, so press could not open the keyboard);
the live-stream plus mid-gesture case (the debug build's JS thread saturates; needs a Release simulator build);
iPhone, Android (the Android path is unchanged), web and desktop (they do not use this component);
whether upstream has the same bug (the patched package is the same upstream version).
Next: rebuild the iPad app from fork/prod (needs vp i first for the patch), then check the keyboard open and close and a live stream on the device. Severity unchanged: Medium.
Deployed 2026-10-04 (UTC): Release archive built from feat/ios-fork-build @ a358db9a41 (= fork/prod @ 044aaf4 plus the fork identity) and installed on the iPad (com.nohat.t3code, team YNSNP252C8). Checked: the archive signed by the fork team, and its JavaScript bundle contains the patched keyboard code (applyOffset). The device reports the app installed; its version string (1.3.1, build 1) did not change, so the revision cannot be read from the device. The build also carries the papercut capture work from feat/papercuts-ipad.
Status: fixed, awaiting your check on the device (the cases I could not run on the simulator: keyboard open and close at the end of a long thread, composer-height animation wobble, a live stream while you scroll, and iPhone). Not closed; I will close it after a soak window with no recurrence. Please tell me if the jump recurs, and when.
Closing as validated by use, 2026-10-04: about 20 minutes on the iPad with the new build and no jump. Fix is on fork/prod (merge d64f5de, patch commit 4bc9889); the Release build with it is installed on the iPad. Cause, traps, wrong leads, and the simulator reproduction recipe are recorded in docs/fork/mobile.md ("Traps in the iPad feed") and docs/fork/defect-resolution.md (iPad and mobile).
Not separately measured, so reopen or file a new issue if they show up: keyboard open and close at the end of a long thread, wobble of the composer-height animation, a live stream while scrolling, iPhone. Feed trace additions and probe changes stay on fix/mobile-feed-scroll-jump for the repro harness branch (feat/ios-repro-harness).
Activity
nohat commentedon Oct 4, 2026
Lead only, 2026-10-04 (harness on
feat/ios-repro-harness). The feed scroll trace (__T3_DEBUG__"feed") works through the probe. In one run opening a finished replayed thread on the iPad simulator, the offset went 16150 to 0 withfollow: trueand no drag, about 13 s after the open. It coincided with anagent-devicescreenshot, so it may be an artifact; I did not repeat it. The running-turn case from this issue is not reproduced yet (the live runs were dominated by debug-build JS cost, see #16).nohat commentedon Oct 4, 2026
Superseded by the next comment: the
initialScrollAtEndlead below was wrong (it still jumped with the prop off in later runs). The cause is in the keyboard controller, not LegendList.Investigation 2026-10-04 (UTC 21:00 to 22:10), iPad Pro 11-inch (M5) simulator, debug dev client, isolated server on a
VACUUM INTOcopy, replay of thread07e27b3f(27 image results, 9 presented rows, content about 19,100 px). Branchfix/mobile-feed-scroll-jump@ f17e99fbf3 (trace additions only, no fix). No effect on existing data.New from me, 2026-10-04: the jump "often but not always happens in the middle of a scrolling gesture". That points at the follow-off path (maintain-visible-content-position turns on at drag start). My reproduction below is the follow-on path, so it may be a sibling of the reported symptom, not the same one.
Reproduced (follow on, no drag, nothing streaming): open the finished replayed thread; the list lands at the end (offset about 18,400), then 12 to 25 s after the list mounts the offset goes to exactly 0 while
followis true andatEndflips false. Content height is unchanged (19,131) and the thread is left at its top. 8 of 9 runs with the probe attached (counting runs where only maintainScrollAtEnd or alignItemsAtEnd was switched off). It also happens with no debugger attached: screenshots 8 s and 48 s after opening both show the top of the thread.What it is not (each from the trace,
[t3-feed]namespace):dragfalse on every sample);scrollTo/scrollToEndas far as I could trace (my tracer saw no calls at all, so this is weak);maintainScrollAtEnd(switched off, still jumps) and notalignItemsAtEnd(switched off, still jumps).Where it points:
initialScrollAtEnd. With it switched off, 4 of 5 runs had no jump to 0, but one run still jumped, so it is a strong lead, not a confirmed cause. Reading@legendapp/list3.3.5: after the first positioning LegendList keeps its end target alive and, on later layout or inset events, runs a per-frame end correction that moves the offset natively through its scroll-adjust sentinel (which would explain no JS scroll call). Each jump follows a re-render burst of the thread route about 0.5 s earlier. Flipping the prop to false once the list first reaches the end did not reliably help (1 of 2 runs still jumped), because LegendList only clears the preserved target on a data change.Not established:
mainhas the same behavior (not checked);Harness notes for the next run: start Metro with
APP_VARIANT=developmentor the manifest request takes 10 s and the dev client times out;agent-device scroll bottomhangs the XCTest runner on a long-text thread (swipeworks); the probe's jump detector ignores samples taken during a drag, so analyzescroll.seriesin the report for mid-gesture jumps.Next: a Release simulator build measured without the inspector (needs a coordinated
xcodebuild, other sessions are building), then replay plus scripted drags to reproduce the mid-gesture case. A candidate fix has to hold up on both the follow-on and follow-off paths and on short threads (alignItemsAtEnd, header inset).Status: Verified (follow-on variant), cause localized to the list's initial-scroll target but not proven, no fix yet, reported symptom not yet reproduced.
nohat commentedon Oct 5, 2026
Cause found and fixed, 2026-10-04 (UTC 17:00 to 01:00), branch
fix/feed-scroll-jump(merged tofork/prodas d64f5de, patch commit 4bc9889). Merged, not deployed: the iPad needs a rebuild to carry it.New from me, 2026-10-04: the jump "always happens almost immediately on the first touch of the thread area after returning to the thread or from another app; scrolling back to the bottom, no more jumping for a while".
Cause. The feed is wrapped in react-native-keyboard-controller's
KeyboardChatScrollView. When the composer overlay height changes (extraContentPadding), and on every keyboard event, it shifted the list on iOS by writingcontentOffsetthrough animated props. That leaves a stalecontentOffseton the native scroll view, and the next React commit of the list resets it to 0: the follow toggle on a touch, or any re-render burst. In the replay thread the offset went 18377 to 0, exactly the header-inset top, with no JS scroll call. The write is logged:contentOffsetYtracks the offset during the composer-inset animation at landing, then the jump follows with no further writes. Returning to the app fires a keyboard event (height 0, duration 383 ms) that writescontentOffsetY= the current offset; the first touch about 8 s later reset it to 0. Why it feels like "the first touch, then fine": after the reset the stale prop is gone until the next composer-height change or keyboard event.Superseded earlier lead:
initialScrollAtEnd(jumped anyway in later runs). Also ruled out:maintainScrollAtEnd,alignItemsAtEnd, maintain-visible-content-position toggling at drag start, remount, user gesture.Fix.
patches/react-native-keyboard-controller@1.22.4.patch: composer-height shifts (useExtraContentPadding) and keyboard-event shifts (useChatKeyboard/index.ios) now call reanimatedscrollToinstead of writingcontentOffsetY. The patch edits the package'ssrc/(what Metro bundles) andlib/moduleandlib/commonjs. Guard test:apps/mobile/src/lib/keyboardControllerPatch.test.tsfails if a version bump drops either file's patch (checked against the old package: both files fail).Measured (iPad simulator, debug dev client, replay of thread
07e27b3f, 19,131 px):lib/and showed no effect, because Metro bundlessrc/; the measurements above are on thesrc/patch.Review. A fresh reviewer could not disprove the fix. It found the keyboard-event path still exposed (now fixed and measured above), noted the original guard test was weak (now covers both files), and asked for a check of visual wobble from the loss of atomic inset plus offset commits.
Not verified, so this stays open as fixed-unverified:
presscould not open the keyboard);Next: rebuild the iPad app from
fork/prod(needsvp ifirst for the patch), then check the keyboard open and close and a live stream on the device. Severity unchanged: Medium.nohat commentedon Oct 5, 2026
Deployed 2026-10-04 (UTC): Release archive built from
feat/ios-fork-build@ a358db9a41 (=fork/prod@ 044aaf4 plus the fork identity) and installed on the iPad (com.nohat.t3code, teamYNSNP252C8). Checked: the archive signed by the fork team, and its JavaScript bundle contains the patched keyboard code (applyOffset). The device reports the app installed; its version string (1.3.1, build 1) did not change, so the revision cannot be read from the device. The build also carries the papercut capture work fromfeat/papercuts-ipad.Status: fixed, awaiting your check on the device (the cases I could not run on the simulator: keyboard open and close at the end of a long thread, composer-height animation wobble, a live stream while you scroll, and iPhone). Not closed; I will close it after a soak window with no recurrence. Please tell me if the jump recurs, and when.
nohat commentedon Oct 5, 2026
Closing as validated by use, 2026-10-04: about 20 minutes on the iPad with the new build and no jump. Fix is on
fork/prod(merge d64f5de, patch commit 4bc9889); the Release build with it is installed on the iPad. Cause, traps, wrong leads, and the simulator reproduction recipe are recorded indocs/fork/mobile.md("Traps in the iPad feed") anddocs/fork/defect-resolution.md(iPad and mobile).Not separately measured, so reopen or file a new issue if they show up: keyboard open and close at the end of a long thread, wobble of the composer-height animation, a live stream while scrolling, iPhone. Feed trace additions and probe changes stay on
fix/mobile-feed-scroll-jumpfor the repro harness branch (feat/ios-repro-harness).test(mobile): guard upstream's keyboard-controller patch instead of t…