diff --git a/packages/app/src/agent-stream/bottom-anchor-controller.test.ts b/packages/app/src/agent-stream/bottom-anchor-controller.test.ts index 56e9e9ff2..f37b79a23 100644 --- a/packages/app/src/agent-stream/bottom-anchor-controller.test.ts +++ b/packages/app/src/agent-stream/bottom-anchor-controller.test.ts @@ -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", () => { diff --git a/packages/app/src/agent-stream/bottom-anchor-controller.ts b/packages/app/src/agent-stream/bottom-anchor-controller.ts index 8cab0e4e5..b9f77c13c 100644 --- a/packages/app/src/agent-stream/bottom-anchor-controller.ts +++ b/packages/app/src/agent-stream/bottom-anchor-controller.ts @@ -244,29 +244,6 @@ function createBottomAnchorControllerDriver( let stickyMeasurementRevision = 0; let lastVerifiedStickyMeasurementRevision = 0; - const _getLogContext = (extra?: Record) => { - 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;