Repository navigation
fix(app): treat the reachable end as the end under desktop zoom - #60034
Conversation
There was a problem hiding this comment.
Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota.
An organization owner can choose the repository organization as the billing source for reviews requested by members with a paid Copilot license. Learn about Copilot code review billing.
There was a problem hiding this comment.
No blocking concerns. I reproduced the hang on the base branch and it is gone with this change. I ran the new zoom component test against the base source and the timeline stays hidden until the test times out. With this PR, all timeline-virtualizer component tests pass, including 25/25 over repeated runs. The timeline unit tests and tsc -b also pass, and the timeline-cache and mobile-scroll e2e suites fail the same 8 tests on the base branch as on this PR.
The loop now ends: the pinned pass still reports the offset and queues one more pass, but that pass reveals the timeline and clears coldPending, so the queued pass exits immediately. Unpinned mounts still wait for the range to catch up with the scroll, as before. The test is small and clear, and checking that the gap is bigger than 1 px confirms it really covers the zoom case.
Public API: unchanged (internal to the app timeline).
There was a problem hiding this comment.
Nothing new that needs fixing; my first review left no open points. I checked both halves of the new zoom test separately. Against the base branch, the timeline still stays hidden. Against your first-round commit, following isn't resumed after scrolling back to the bottom. Both pass on this head, 25/25 over repeated runs. The timeline-cache and mobile-scroll e2e suites (51/51), the timeline unit tests and tsc -b pass too.
atReachableEnd only probes by writing scrollTop when the view is between 0.5 and 4 px from the end, and it restores the position if the write moves it. The re-follow check waits for the scroll to settle and is cleared by any upward scroll, so momentum isn't interrupted and a deliberate upward nudge still unpins.
Public API: unchanged (internal to the app timeline).
Under desktop zoom (anything but 100%), opening a session could freeze the app (renderer unresponsive) or leave the timeline blank, and scrolling back down to the bottom never resumed following. Reported on 2.2.0 with a debug export; every renderer sample sat in the cold-mount settle loop in
virtualizer.tsx.Cause
Chrome rounds
scrollHeightandclientHeightto whole CSS pixels but scrolls in zoomed ones. Under zoom, the furthest offset it reaches can sit up to ~2px short ofscrollHeight - clientHeight(4px at 25%). Every "am I at the end?" check compared against that unreachable value.scrollToEnd(), TanStack keeps the offset it wrote (944) instead of the one the browser reached (942.857), because they are within its 1.5px read-back tolerance. The cold-mount settle pass waited for either distance to drop below 1px. That never happens, and each pass re-reported the offset, queuing the next pass as a microtask. Microtasks never yield, so the renderer hung.visibility: hidden.handleListScrollrequiresmaxScroll - scrollTop <= 0.5before it re-pins, which the browser can't reach.flowchart LR S[settle microtask] --> P[pinColdBottom] P -->|gap 1.14 > 0.5| E[scrollToEnd: writes 944, browser lands 942.857] E -->|scrollOffset 944 != scrollTop| R[reportOffset] R -->|queueMicrotask| S P --> G{gap > 1?} G -->|yes: wait| X[return]Captured live on the 2.2.0 code (zoom 175%):
{ "scrollHeight": 1493, "clientHeight": 549, "scrollTop": 942.857, "scrollOffset": 944, "intended": 944 }Fix
atReachableEnd(root): within the zoom slack (4px), only a write finds the end the browser reaches. A write that doesn't move proves the view is already there; one that does is undone before anything paints. At 100% zoom it returns from the existing 0.5px check without writing.Cold mount: a pinned mount reveals once
pinColdBottom()has written and reported the end, instead of re-measuring a distance that zoom can keep open. The "range must catch up" wait now applies only to unpinned mounts.Re-follow: a scroll that arrives within the slack marks the end as pending. Once the scroll settles (TanStack's
isScrolling→ false, no finger down),atReachableEnddecides whether to pin. Probing mid-scroll would cancel momentum.Post-resize
scrollToEnd: usesatReachableEnd, so under zoom it no longer reopens a settled scroll-to-end on every resize. That reopening is the case its comment says can fight keyboard scrolling.Visual
Notes
Added by the opencode bot (AI-generated).
Related: #51423, #58127