fix(mobile): pulling to Load earlier activity no longer snaps the feed back to the end#31907

open4
main
fix/issue-31867
opened3d agobyJJulius Marminge
View on GitHubTwo-way sync enabledPR last imported 2d ago

Problem

In a thread whose loaded page is about one screen tall, pulling down to "Load earlier activity" and releasing turns end-following back on. The next layout or data change then scrolls the feed back to the end, the button goes back under the header, and older history can't be reached.

Reproduced on current main in an iPhone 17 Pro simulator: a running agent turn with a 75-row first page (contentSize 826–967 pt, viewport 874 pt, header inset 116 pt, composer inset 142–255 pt). After release at the top, the list reported isAtEnd: true at scroll −116 (no jump-to-end arrow). Tapping "Load earlier activity" then jumped the feed to the absolute end.

Change

getIsAtEnd in our @legendapp/list@3.3.5 patch had two errors:

  • Its at-end distance subtracted the end (composer) inset, but getContentSize already includes that inset. Any position within about one composer height of the end therefore counted as the end. On a feed about one screen tall, that covers the whole scroll range, including the top. Legend List 3.7.1 already uses contentSize - scroll - scrollLength for this distance.
  • Its "content fits" shortcut (contentSize < scrollLength) ignored the start inset that the patch adds for the transparent header.

The function now uses one rule for every list. Content fits when contentSize - scrollLength <= -contentInsetStartAdjustment (+1 pt). Otherwise the list is at its end only within 1 pt of its real end. Both the react-native.js and react-native.mjs hunks change; nothing else in the patch does.

What changes for users: following re-arms only when you return to the real end. A release that stops a little short of the end, or a disclosure that grows the content while following, now leaves following off and shows the jump-to-end arrow. ThreadFeed's comment already called for this ("Re-arm follow only at the actual end").

Scope and approval

Fixes pingdotgg/t3code#31867

Verification

  • apps/mobile/src/features/threads/legendListAtEnd.test.ts loads the installed patched bundle in node:vm and calls getIsAtEnd with geometry measured on the simulator. On main it fails with a feed pulled to its start edge (scroll -116) is 77.8pt from its end: expected true to be false. It covers:

    • a feed pulled to its start edge (scroll −116);
    • a feed 2 pt below the start edge (scroll −114);
    • a feed under an opaque header with no start inset (rows 700, composer inset 200, scroll 0);
    • control cases that must still read at end: the real end, and content that fits below the header.

    All of these pass with the fix, and thread-feed-live-follow.test.ts still passes.

  • vp run check:changed passes: unused patches, format, lint, knip, typecheck for 40 packages, and tests.

  • Simulator runtime transcript (iPhone 17 Pro, dev client against an isolated dev server with a synthetic thread). Each row is the isAtEnd reading from temporary logging in the installed bundle, plus the behaviour seen:

    BuildcontentSizeScrollisAtEndSeen after release, then tapping "Load earlier activity"
    main835.8−116 (start)trueno jump-to-end arrow; the load lands at the end
    main938.8−116 (start)trueno jump-to-end arrow; the tap jumps to the absolute end (scroll 92.7)
    first draft of this fix826.4−116 (start)falsearrow shown; the position stays put
    first draft of this fix966.8−116 (start)falsearrow shown; the position stays put
    this PR826.8−47.3 (end, on open)true—
    this PR~940release at the top— (log not captured)arrow shown; older rows load above and the position is kept

    Android (Pixel 9 emulator, no start inset): a thread that fits the screen opens with the button visible and reads at end (true).

  • Not checked: before/after recordings are not attached. The iOS 18 opaque-header path is covered by the test above, not on a device. Keyboard open and close while following were not exercised on a device.

AI-assisted; verified with the tests and tools listed above.

Checks

25 successful · 7 skipped
All checks passed
bca622a

Added 4 commits

  • fix(mobile): thread feed start edge no longer reads as its end38221fb
  • fix(mobile): thread feed reads at end only at the true end of its scroll range7b8fbed
  • test(mobile): reproduce #31867 thread feed start edge reading as at end94563e3
  • Merge remote-tracking branch 'origin/main' into fix/issue-31867bca622a
Synced from GitHub

Approvability

Verdict: Approved at bca622a

Macroscope's review found this PR approvable — The patch corrects inset-aware end-of-list detection to prevent unwanted feed snapping when loading earlier activity. Its effects are limited to list scrolling and presentation, with no evident material risk requiring human judgment.

You can add or adjust custom eligibility rules. Learn more.

Synced from GitHub

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

ProviderMetricMain baselineThis PRImpactPR ceiling
CodexTotal thread wire5.0 KiB5.0 KiB0 B (0.0%)6.8 KiB✅
CodexThread snapshot wire3.8 KiB3.8 KiB0 B (0.0%)4.9 KiB✅
CodexLive turn WebSocket wire1.2 KiB1.2 KiB0 B (0.0%)2.0 KiB✅
CodexLive turn WebSocket decoded21.5 KiB21.5 KiB0 B (0.0%)24.4 KiB✅
CodexLive turn messages110 (0.0%)8✅
ClaudeTotal thread wire5.0 KiB5.0 KiB0 B (0.0%)6.8 KiB✅
ClaudeThread snapshot wire3.8 KiB3.8 KiB0 B (0.0%)4.9 KiB✅
ClaudeLive turn WebSocket wire1.2 KiB1.2 KiB0 B (0.0%)2.0 KiB✅
ClaudeLive turn WebSocket decoded21.9 KiB21.9 KiB0 B (0.0%)24.4 KiB✅
ClaudeLive turn messages110 (0.0%)8✅

Baseline: 202d5b5 · PR result: bca622a · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 108.5 KiB
  • Claude decoded thread snapshot: 108.8 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

[gpt-6.1-sol][Codex] on behalf of shivam.

Handing over findings from https://github.com/pingdotgg/t3code/pull/31902. These results concern our branch, not this PR's head.

On base 2756da402b5d795baa05762cba1706a2c0e2fac8, an iPhone 12 simulator running iOS 27.0 reached the size boundary and hid Load earlier activity at rest. Client: T3 Code Dev 2.2.0 build 1, native fingerprint 316afd351f76b38d7c29e47460801c335423fdc1, isolated backend with synthetic history. Measured viewport 844 pt, start adjustment 101 pt, effective end inset 94 pt, contentSize 813.167 pt, resting offset −31 pt. The physical start was −101 pt. Base screenshot.

We did not reproduce the release snap ourselves: recording and pan commands failed with Remote daemon is unavailable, including after recovery. No before/after MP4 or fixed-device run exists from this job. The opaque-header iOS 18 case and active-run interaction stayed unverified.

The installed native bundle confirmed that getContentSize already includes the end inset. At the measured start, the old distance formula reported −23.833 pt despite a physical distance of 70.167 pt. One related finding: doMaintainScrollAtEnd's old contentSize < scrollLength branch also recorded state.scroll = -101 while this scrollable feed physically rested near −31. Our separate VM regression covers using the start adjustment in that fit check too.

At our fix head bcbb261ed9c98e53e6e7038b3c00fe293775140d, actual-bundle VM tests for native CJS and ESM passed 28/28, including glass-header, composer-only, underflow, no-inset, and the maintenance-state case. Before the fix, eight regression cases failed and 20 passed. Independent Codex review found no actionable source/test findings. Boat's vp run check:changed origin/main passed at that head: format, lint, knip, unused patches, typechecks for 40 packages, and 30 tests across two files. The required CI gate also passed. Our draft remained blocked on native interaction proof. No further testing or pushes will run from this job.

Synced from GitHub

Explain Change →

No actionable comments were generated in the recent review.

Walkthrough

The LegendList patch updates inset-aware end detection, initial positioning, size anchoring, and native scroll handling. It also extends keyboard inset APIs. Tests cover end detection with start and end insets.

Changes: Inset-aware LegendList scrolling

Layer / File(s)Summary
Inset-aware end detection
patches/@legendapp__list@3.3.5.patch, apps/mobile/src/features/threads/legendListAtEnd.test.ts
End detection, end-aligned padding, and scroll clamping account for the leading inset. The tests exercise geometry with start and end insets.
Inset-aware positioning and size anchoring
patches/@legendapp__list@3.3.5.patch
Initial end positioning uses a settle watchdog and render hold. End maintenance and MVCP account for the leading inset and caller-provided size anchors. Anchored end space is bounded by known content.
Size rendering and native scroll lifecycle
patches/@legendapp__list@3.3.5.patch
The patch adds size-animation eligibility and optional size-component rendering. It wires native drag and momentum callbacks and changes iOS visible-content-position maintenance.

Estimated code review effort: 4 (Complex) | ~45 minutes

Change: Bug fix · Severity of issue fixed: Medium

Suggested reviewers: t3dotgg

Merge Risk: External image Minimal · up to bca62

The inspected end-detection path preserves the intended start-edge behavior. No issue remains that calls for a change before merging.

Pre-merge checks: Passed 4

Check nameStatusExplanation
Title checkPassedThe title describes the mobile feed fix for loading earlier activity.
Description checkPassedThe description covers the problem, change, scope, and verification. Add before-and-after screenshots, an interaction recording, and the AI model and harness. Confirm that the linked issue is triaged …
Linked Issues checkPassed[#31867] The patched getIsAtEnd accounts for the header inset and does not subtract the composer inset twice. This prevents end-following from re-arming at the start of an overflowing feed. The regr…
Out of Scope Changes checkPassed[#31867] The end-detection changes in both React Native bundle formats and the regression tests support the mobile feed fix. The broader patch features described in the summary are not established as …
Full details: Description check

Explanation

The description covers the problem, change, scope, and verification. Add before-and-after screenshots, an interaction recording, and the AI model and harness. Confirm that the linked issue is triaged or link maintainer approval.

Full details: Linked Issues check

Explanation

[#31867] The patched getIsAtEnd accounts for the header inset and does not subtract the composer inset twice. This prevents end-following from re-arming at the start of an overflowing feed. The regression tests cover transparent and opaque headers, the real end, and content that fits. The reported simulator result also shows that older rows load without losing position.

Full details: Out of Scope Changes check

Explanation

[#31867] The end-detection changes in both React Native bundle formats and the regression tests support the mobile feed fix. The broader patch features described in the summary are not established as changes introduced by this PR.

Finishing Touches

Generate docstrings

  • Commit to this branch
  • Create a new PR
Generate unit tests (beta)

  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts
External image Recent review info

External image Run configuration

External image Commits

Reviewing files that changed from the base of the PR and between 202d5b5616e3edf65706371e2b8a9c4f486d0a96 and bca622ad5c7eb31d3d0dfe3d941714928d7cc363.

External image Files ignored due to path filters: 1

  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
External image Files selected for processing: 2

  • apps/mobile/src/features/threads/legendListAtEnd.test.ts
  • patches/@legendapp__list@3.3.5.patch

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