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 c7ebfca273..66c070679d 100644 --- a/packages/app/src/agent-stream/history-start-pagination.test.ts +++ b/packages/app/src/agent-stream/history-start-pagination.test.ts @@ -1,3 +1,4 @@ +import { nativeHistoryStartGeometry } from "./native-history-start-geometry"; import { describe, expect, it, vi } from "vitest"; import { createHistoryStartPagination, @@ -202,3 +203,36 @@ it("keeps cached history visible after synchronization and subsequent pagination expect(h.requests).toHaveLength(2); h.pagination.dispose(); }); + +it.each([ + [true, 123, 0], + [false, 123, 0], + [true, 0, 123], + [false, 0, 123], +])( + "handles native padding when short history has older=%s", + (hasOlderHistory, topOverlayInset, bottomPadding) => { + const geometry = nativeHistoryStartGeometry({ + contentHeight: 825, + viewportHeight: 700, + offsetY: 0, + topOverlayInset, + bottomPadding, + }); + let requests = 0; + const pagination = createHistoryStartPagination({ + initiallyVisible: false, + read: () => ({ ...initial, ...geometry, hasOlderHistory }), + load: () => { + requests++; + return new Promise(() => {}); + }, + changed: () => {}, + scheduleSettle: () => {}, + }); + pagination.update(); + expect(requests).toBe(hasOlderHistory ? 1 : 0); + expect(pagination.state.visible).toBe(!hasOlderHistory); + pagination.dispose(); + }, +); diff --git a/packages/app/src/agent-stream/native-history-start-geometry.ts b/packages/app/src/agent-stream/native-history-start-geometry.ts new file mode 100644 index 0000000000..33ebcbfabd --- /dev/null +++ b/packages/app/src/agent-stream/native-history-start-geometry.ts @@ -0,0 +1,13 @@ +export function nativeHistoryStartGeometry(input: { + contentHeight: number; + viewportHeight: number; + offsetY: number; + topOverlayInset: number; + bottomPadding: number; +}) { + const contentHeight = input.contentHeight - 32 - input.topOverlayInset - input.bottomPadding; + return { + distanceFromHistoryStart: contentHeight - input.viewportHeight - input.offsetY, + isUnderfilled: contentHeight < input.viewportHeight, + }; +} diff --git a/packages/app/src/agent-stream/strategy-native.tsx b/packages/app/src/agent-stream/strategy-native.tsx index 93a940dd50..610f8f74d8 100644 --- a/packages/app/src/agent-stream/strategy-native.tsx +++ b/packages/app/src/agent-stream/strategy-native.tsx @@ -33,6 +33,7 @@ import { type HistoryStartPaginationInput, } from "./history-start-pagination"; import { useHistoryStartPagination } from "./use-history-start-pagination"; +import { nativeHistoryStartGeometry } from "./native-history-start-geometry"; import { useStreamTopOverlayInset } from "./top-overlay"; import { createHistoryStartSettleScheduler, @@ -132,13 +133,13 @@ function NativeStreamViewport(props: StreamRenderInput & { strategy: StreamStrat metrics.viewportMeasuredForKey === metrics.containerKey && metrics.contentMeasuredForKey === metrics.containerKey; return { - distanceFromHistoryStart: metrics.contentHeight - metrics.viewportHeight - metrics.offsetY, - isUnderfilled: - metrics.contentHeight - - 32 - - topOverlayInset - - streamKeyboardInset.contentContainerPaddingBottom < - metrics.viewportHeight, + ...nativeHistoryStartGeometry({ + contentHeight: metrics.contentHeight, + viewportHeight: metrics.viewportHeight, + offsetY: metrics.offsetY, + topOverlayInset, + bottomPadding: streamKeyboardInset.contentContainerPaddingBottom, + }), hasOlderHistory, isLoadingOlderHistory, isReady: diff --git a/packages/app/src/subagents/provider-store.test.ts b/packages/app/src/subagents/provider-store.test.ts index 596425a620..a28e9b00fd 100644 --- a/packages/app/src/subagents/provider-store.test.ts +++ b/packages/app/src/subagents/provider-store.test.ts @@ -1,5 +1,6 @@ +import { createHistoryStartPagination } from "@/agent-stream/history-start-pagination"; import { DaemonConnectionError } from "@getpaseo/client/internal/daemon-client"; -import { afterEach, describe, expect, test } from "vitest"; +import { afterEach, describe, expect, test, vi } from "vitest"; import type { ProviderSubagentDescriptorPayload } from "@getpaseo/protocol/messages"; import { loadOlderProviderSubagentTimeline, @@ -593,6 +594,68 @@ describe("projected child history", () => { expect(await request).toBe(true); expect(current().cursor?.startSeq).toBe(1); }); + test("bounds an older child request so a timeout permits another attempt", async () => { + vi.useFakeTimers(); + try { + useProviderSubagentStore + .getState() + .replaceTimeline(SERVER_ID, { ...response("B", 2, 2), hasOlder: true }); + const timeouts: Array = []; + const client = { + fetchProviderSubagentTimeline: ( + _parent: string, + _child: string, + options: { timeout?: number }, + ) => { + timeouts.push(options.timeout); + return new Promise((_resolve, reject) => + setTimeout(() => reject(new Error("timeout")), options.timeout ?? 60_000), + ); + }, + }; + let loading = false; + const pagination = createHistoryStartPagination({ + initiallyVisible: true, + read: () => ({ + distanceFromHistoryStart: 0, + isUnderfilled: true, + hasOlderHistory: true, + isLoadingOlderHistory: loading, + isReady: true, + progressKey: "e:2", + }), + load: async () => { + loading = true; + try { + return await loadOlderProviderSubagentTimeline({ + client, + serverId: SERVER_ID, + parentAgentId: PARENT_ID, + subagentId: SUBAGENT_ID, + limit: 100, + }); + } catch { + return false; + } finally { + loading = false; + } + }, + changed: () => {}, + scheduleSettle: () => {}, + }); + pagination.update(); + expect(timeouts).toEqual([10_000]); + await vi.advanceTimersByTimeAsync(10_000); + expect(pagination.state.status).toBe("failed"); + pagination.rearm(); + expect(timeouts).toEqual([10_000, 10_000]); + expect(pagination.state.status).toBe("loading"); + pagination.dispose(); + await vi.advanceTimersByTimeAsync(10_000); + } finally { + vi.useRealTimers(); + } + }); test("replaces overlapping fetched text and continues live streaming", () => { page("AB", 1, 2); stream(3, "C"); diff --git a/packages/app/src/subagents/provider-store.ts b/packages/app/src/subagents/provider-store.ts index f3869c0a73..48010aa0be 100644 --- a/packages/app/src/subagents/provider-store.ts +++ b/packages/app/src/subagents/provider-store.ts @@ -377,6 +377,7 @@ export async function loadOlderProviderSubagentTimeline({ direction: "before", cursor: { epoch: timeline.epoch, seq }, limit, + timeout: 10_000, }); if (payload.error) return false; useProviderSubagentStore.getState().replaceTimeline(serverId, payload);