fix(app): keep chat reading position stable while scrolling (#5945)

* fix(app): keep chat reading position stable while scrolling

Prepend restoration froze the reading offset while wheel input and delayed image measurements changed layout. Give the viewport one reading anchor, preserve concurrent user movement, and apply virtual row measurements before paint. Suspend automatic pagination while explicit message navigation settles.

* fix(app): type the reading anchor scroll intent explicitly

* fix(app): retain the reading anchor through virtualization startup

Keep the reader pinned while the first virtual range mounts. Strengthen the scroll regression detector to compare shared rows when the original anchor unmounts, and consume fixture replies only when they are emitted.

* test(app): distinguish delayed wheel input from idle scroll drift

CI frames can arrive long after their wheel events. Start idle assertions after the input driver finishes, and account for an image entered by wheel input expanding below the new reading line. Replayed all six CI captures without false positives while retaining the original 76 baseline jumps.

* test(app): bound image growth allowance to displaced reading rows

Require the entered image top to follow wheel input before attributing movement of a later row to its growth. Calibrate the detector against simultaneous viewport jumps and jumps of the growing row itself.

* test(app): clamp scroll expectations at the history boundary
This commit is contained in:
Mohamed Boudra
2026-10-03 10:12:01 +01:00
committed by GitHub
parent 5293ddac3f
commit 8aec0787d2
9 changed files with 773 additions and 224 deletions
@@ -12,7 +12,6 @@ import {
findMountedWindowStart,
getWebMountedRecentStreamItems,
getWebPartialVirtualizationThreshold,
shouldAdjustScrollForVirtualRowResize,
splitWebVirtualizedHistory,
type IndexedStreamItem,
} from "./web-virtualization";
@@ -170,55 +169,6 @@ describe("estimateStreamItemHeight", () => {
});
});
describe("virtual row resize anchoring", () => {
it("does not move the viewport when a visible row expands while detached", () => {
expect(
shouldAdjustScrollForVirtualRowResize({
isHistoryStartPrependActive: false,
rowStart: 1200,
scrollOffset: 1000,
remainingDistanceFromBottom: 5000,
bottomThreshold: 64,
}),
).toBe(false);
});
it("keeps the reading position when a row above the viewport changes size", () => {
expect(
shouldAdjustScrollForVirtualRowResize({
isHistoryStartPrependActive: false,
rowStart: 800,
scrollOffset: 1000,
remainingDistanceFromBottom: 5000,
bottomThreshold: 64,
}),
).toBe(true);
});
it("leaves history prepend and bottom following to their existing anchors", () => {
const baseInput = {
rowStart: 800,
scrollOffset: 1000,
remainingDistanceFromBottom: 5000,
bottomThreshold: 64,
};
expect(
shouldAdjustScrollForVirtualRowResize({
...baseInput,
isHistoryStartPrependActive: true,
}),
).toBe(false);
expect(
shouldAdjustScrollForVirtualRowResize({
...baseInput,
isHistoryStartPrependActive: false,
remainingDistanceFromBottom: 64,
}),
).toBe(false);
});
});
describe("web virtualization test overrides", () => {
it("uses defaults unless explicit positive integer overrides are present", () => {
const globalWithOverrides = globalThis as typeof globalThis & {