Fix native chat anchoring while streaming

This commit is contained in:
Mohamed Boudra
2026-06-09 11:45:03 +07:00
parent 6c3e2bd703
commit 0967557846
2 changed files with 51 additions and 24 deletions

View File

@@ -608,6 +608,47 @@ describe("bottom anchor controller driver", () => {
expect(harness.driver.getSnapshot().mode).toBe("sticky-bottom");
});
it("keeps native sticky content changes anchored when measured height is unchanged", () => {
const harness = createDriverHarness({
transportBehavior: {
verificationDelayFrames: 2,
verificationRetryMode: "recheck",
},
measurementState: createMeasurementState({
containerKey: "native-virtualized",
viewportWidth: 390,
viewportHeight: 546,
contentHeight: 546,
offsetY: 0,
viewportMeasuredForKey: "native-virtualized",
contentMeasuredForKey: "native-virtualized",
}),
});
harness.scrollToBottom.mockImplementation(() => {
harness.context.measurementState.offsetY = 0;
harness.context.nearBottom = true;
});
harness.driver.prepareForStickyContentChange();
expect(harness.scrollToBottom).toHaveBeenCalledTimes(1);
expect(harness.driver.getSnapshot()).toMatchObject({
mode: "sticky-bottom",
pendingVerification: {
requestId: null,
},
});
harness.context.measurementState.offsetY = 50;
harness.context.nearBottom = false;
harness.driver.handleScrollNearBottomChange({
nextIsNearBottom: false,
scrollDelta: 50,
});
expect(harness.driver.getSnapshot().mode).toBe("sticky-bottom");
});
});
describe("controller helper predicates", () => {

View File

@@ -244,29 +244,6 @@ function createBottomAnchorControllerDriver(
let stickyMeasurementRevision = 0;
let lastVerifiedStickyMeasurementRevision = 0;
const _getLogContext = (extra?: Record<string, unknown>) => {
const measurementState = input.getMeasurementState();
const distanceFromBottom = Math.max(
0,
measurementState.contentHeight - (measurementState.offsetY + measurementState.viewportHeight),
);
return {
agentId: input.getAgentId(),
requestReason: pendingRequest?.reason ?? null,
authoritativeHistoryReady: input.getIsAuthoritativeHistoryReady(),
contentHeight: measurementState.contentHeight,
viewportHeight: measurementState.viewportHeight,
offset: measurementState.offsetY,
distanceFromBottom,
renderStrategy: input.getRenderStrategy(),
blockedReason,
mode,
containerKey: measurementState.containerKey,
transportBehavior: input.getTransportBehavior(),
...extra,
};
};
const setBlockedReason = (nextBlockedReason: BottomAnchorBlockedReason | null) => {
if (blockedReason === nextBlockedReason) {
return;
@@ -327,7 +304,6 @@ function createBottomAnchorControllerDriver(
});
const scheduleVerification = (attemptContext: AttemptContext, delayFramesOverride?: number) => {
const _scheduledMeasurementState = input.getMeasurementState();
if (verificationHandle) {
input.cancelFrame(verificationHandle);
}
@@ -582,6 +558,16 @@ function createBottomAnchorControllerDriver(
return;
}
markStickyMeasurementChanged();
if (!pendingRequest) {
pendingVerification = { requestId: null, retries: 0 };
if (attemptHandle) {
input.cancelFrame(attemptHandle);
attemptHandle = null;
}
runAttempt(false);
return;
}
evaluate(false, "content_size_change");
},
handleScrollNearBottomChange(params) {
const { nextIsNearBottom, scrollDelta } = params;