Skip to content

bug(mobile): feed scrolls the reader away from their position while a turn is running #17

Closed

Description

@nohat

Problem

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.

Proposed fixes

  1. Reproduce deterministically on a simulator by replaying a recorded event stream into a seeded thread while holding a scroll position, and log the scroll offset over time (the harness proposed under feat(mobile): instrumentable iPad simulator for reproducing and profiling iPad-only defects #4).
  2. 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.

Related

#16 (live-thread feed failure), #4 (simulator harness), #3. No fix branch.

Severity: Medium (recoverable, but it makes reading a running thread on the main surface unreliable).

Activity

  1. added
    bugSomething is broken or behaving incorrectly.
    on Oct 4, 2026
  2. nohat commented on Oct 4, 2026

    @nohat
    OwnerAuthor

    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).

  3. nohat commented on Oct 4, 2026

    @nohat
    OwnerAuthor

    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.

    Not established:

    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.

  4. nohat commented on Oct 5, 2026

    @nohat
    OwnerAuthor

    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.

  5. nohat commented on Oct 5, 2026

    @nohat
    OwnerAuthor

    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.

  6. nohat commented on Oct 5, 2026

    @nohat
    OwnerAuthor

    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).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething is broken or behaving incorrectly.

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions