diff --git a/packages/app/e2e/agent-timeline-pagination.spec.ts b/packages/app/e2e/agent-timeline-pagination.spec.ts index 31d381ed27..9b485caf3c 100644 --- a/packages/app/e2e/agent-timeline-pagination.spec.ts +++ b/packages/app/e2e/agent-timeline-pagination.spec.ts @@ -1,6 +1,7 @@ import { expect, test } from "./fixtures"; import { expectSameOlderHistoryLoadingOperation, + expectStableHistoryStartGutter, expectTimelineAtHistoryStart, expectTimelinePromptCentered, expectTimelinePromptNotMounted, @@ -27,6 +28,27 @@ import { } from "./helpers/timeline-pagination"; test.describe("Agent timeline pagination", () => { + test("keeps a fixed history-start gutter before, during, and after pagination", async ({ + page, + }) => { + const agent = await seedLongMockAgentTimeline({ turns: 40 }); + try { + const history = await holdOlderHistoryPages(page, agent); + await openAgentTimeline(page, agent); + await expectStableHistoryStartGutter(page); + + await userScrollsTimelineToHistoryStart(page); + await history.expectRequestedPages(1); + await expectStableHistoryStartGutter(page); + + history.releasePage(1); + await history.expectSettledWithRequestedPages(1); + await expectStableHistoryStartGutter(page); + } finally { + await agent.cleanup(); + } + }); + test("loads one page each time the user returns to history start", async ({ page }) => { test.setTimeout(120_000); const agent = await seedLongMockAgentTimeline({ turns: 80 }); diff --git a/packages/app/e2e/helpers/timeline-pagination.ts b/packages/app/e2e/helpers/timeline-pagination.ts index 2b00a980ba..866782521c 100644 --- a/packages/app/e2e/helpers/timeline-pagination.ts +++ b/packages/app/e2e/helpers/timeline-pagination.ts @@ -286,6 +286,14 @@ export async function rememberTimelineViewport(page: Page): Promise { + const gutter = page.getByTestId("older-history-slot"); + await expect(gutter).toBeAttached(); + await expect + .poll(() => gutter.evaluate((element) => element.getBoundingClientRect().height)) + .toBe(32); +} + export async function scrollTimelinePromptIntoView(page: Page, prompt: string): Promise { const timeline = page.locator('[data-testid="agent-chat-scroll"]:visible').first(); const row = timeline.getByTestId("user-message").filter({ hasText: prompt }); diff --git a/packages/app/src/agent-stream/history-start-pagination.test.ts b/packages/app/src/agent-stream/history-start-pagination.test.ts index 3a23fd18b3..475dbc771c 100644 --- a/packages/app/src/agent-stream/history-start-pagination.test.ts +++ b/packages/app/src/agent-stream/history-start-pagination.test.ts @@ -4,7 +4,6 @@ import { createHistoryStartPaginationState, evaluateHistoryStartPagination, isHistoryStartLoadingOperation, - isHistoryStartSlotReserved, rearmHistoryStartPagination, settleHistoryStartPagination, type HistoryStartPaginationInput, @@ -23,15 +22,11 @@ const visibleHistoryStart: HistoryStartPaginationInput = { }; describe("history start pagination", () => { - it("waits for user scroll intent before loading or reserving the loader slot", () => { + it("waits for user scroll intent before loading", () => { const dormant = createHistoryStartPaginationState(); const evaluated = evaluateHistoryStartPagination(dormant, visibleHistoryStart); expect(evaluated).toEqual({ state: { status: "dormant" }, shouldLoad: false }); - expect(isHistoryStartSlotReserved(evaluated.state, true)).toBe(false); - expect(isHistoryStartSlotReserved(rearmHistoryStartPagination(evaluated.state), true)).toBe( - true, - ); }); it("waits for anchored page geometry before authorizing another page", () => { diff --git a/packages/app/src/agent-stream/history-start-pagination.ts b/packages/app/src/agent-stream/history-start-pagination.ts index 8edbbe475a..46914ae3e1 100644 --- a/packages/app/src/agent-stream/history-start-pagination.ts +++ b/packages/app/src/agent-stream/history-start-pagination.ts @@ -34,13 +34,6 @@ export function rearmHistoryStartPagination( return state.status === "dormant" || state.status === "latched" ? { status: "ready" } : state; } -export function isHistoryStartSlotReserved( - state: HistoryStartPaginationState, - hasOlderHistory: boolean, -): boolean { - return state.status !== "dormant" && (hasOlderHistory || state.status !== "ready"); -} - export function abandonHistoryStartPaginationRequest( state: HistoryStartPaginationState, requestedProgressKey: string, diff --git a/packages/app/src/agent-stream/strategy-native.tsx b/packages/app/src/agent-stream/strategy-native.tsx index 1d0a7c17fa..788e4a38a7 100644 --- a/packages/app/src/agent-stream/strategy-native.tsx +++ b/packages/app/src/agent-stream/strategy-native.tsx @@ -36,7 +36,6 @@ import { createHistoryStartPaginationState, evaluateHistoryStartPagination, isHistoryStartLoadingOperation, - isHistoryStartSlotReserved, rearmHistoryStartPagination, settleHistoryStartPagination, type HistoryStartPaginationInput, @@ -59,9 +58,8 @@ const foregroundMutedColorMapping = (theme: Theme) => ({ const historyStartSlotStyle: ViewStyle = { alignItems: "center", justifyContent: "center", - minHeight: 32, - paddingTop: 4, - paddingBottom: 8, + height: 32, + flexShrink: 0, }; const HISTORY_START_SETTLE_FRAMES = 2; @@ -584,20 +582,16 @@ function NativeStreamViewport(props: StreamRenderInput & { strategy: StreamStrat const historyFooterContent = useMemo(() => { const isLoadingOperation = isHistoryStartLoadingOperation(historyStartPaginationState); - if (!isHistoryStartSlotReserved(historyStartPaginationState, hasOlderHistory)) { - return null; - } return ( - + {isLoadingOperation ? ( - + + + ) : null} ); - }, [hasOlderHistory, historyStartPaginationState]); + }, [historyStartPaginationState]); // RN's FlatList strictMode keeps its internal renderItem wrapper stable when // data or the live header changes, preserving the row identities above. diff --git a/packages/app/src/agent-stream/strategy-web.tsx b/packages/app/src/agent-stream/strategy-web.tsx index 3cd6cf88fa..3a83007e23 100644 --- a/packages/app/src/agent-stream/strategy-web.tsx +++ b/packages/app/src/agent-stream/strategy-web.tsx @@ -21,7 +21,6 @@ import { createHistoryStartPaginationState, evaluateHistoryStartPagination, isHistoryStartLoadingOperation, - isHistoryStartSlotReserved, rearmHistoryStartPagination, settleHistoryStartPagination, type HistoryStartPaginationInput, @@ -69,9 +68,8 @@ const historyStartSlotStyle: CSSProperties = { display: "flex", alignItems: "center", justifyContent: "center", - minHeight: 32, - paddingTop: 4, - paddingBottom: 8, + height: 32, + flexShrink: 0, }; const mountedHistoryRowStyle: CSSProperties = { @@ -785,20 +783,16 @@ function WebStreamViewport(props: StreamRenderInput & { isMobileBreakpoint: bool }, [renderLiveAuxiliary]); const historyStartSlot = useMemo(() => { const isLoadingOperation = isHistoryStartLoadingOperation(historyStartPaginationState); - if (!isHistoryStartSlotReserved(historyStartPaginationState, hasOlderHistory)) { - return null; - } return ( -
+
{isLoadingOperation ? ( - +
+ +
) : null}
); - }, [hasOlderHistory, historyStartPaginationState]); + }, [historyStartPaginationState]); const shouldRenderEmpty = !boundary.hasMountedHistory && !boundary.hasVirtualizedHistory &&