diff --git a/packages/app/playwright.webkit.tmp.config.ts b/packages/app/playwright.webkit.tmp.config.ts new file mode 100644 index 000000000..e399bfb49 --- /dev/null +++ b/packages/app/playwright.webkit.tmp.config.ts @@ -0,0 +1,30 @@ +import { defineConfig, devices } from "@playwright/test"; + +const baseURL = + process.env.E2E_BASE_URL ?? + `http://localhost:${process.env.E2E_METRO_PORT ?? "8081"}`; + +export default defineConfig({ + testDir: "./e2e", + globalSetup: "./e2e/global-setup.ts", + timeout: 60_000, + expect: { + timeout: 10_000, + }, + fullyParallel: false, + workers: 1, + retries: 0, + reporter: [["list"]], + use: { + baseURL, + trace: "retain-on-failure", + screenshot: "only-on-failure", + video: "retain-on-failure", + }, + projects: [ + { + name: "Desktop Safari", + use: { ...devices["Desktop Safari"] }, + }, + ], +}); diff --git a/packages/app/src/components/agent-stream-render-strategy.ts b/packages/app/src/components/agent-stream-render-strategy.ts index 1fcd91756..a4169efc4 100644 --- a/packages/app/src/components/agent-stream-render-strategy.ts +++ b/packages/app/src/components/agent-stream-render-strategy.ts @@ -245,6 +245,32 @@ function forceScrollContainerToBottom( } } +function logWebBottomAnchorTransport( + event: string, + details: Record +): void { + if (!(globalThis as { __DEV__?: boolean }).__DEV__) { + return; + } + console.debug("[BottomAnchorTransport]", event, details); +} + +function getResolvedWebScrollContainerMetrics( + refs: StreamRenderRefs +): Record { + const scrollNode = resolveWebScrollContainerNode(refs); + if (!scrollNode) { + return { hasScrollNode: false }; + } + return { + hasScrollNode: true, + scrollTop: scrollNode.scrollTop, + scrollHeight: scrollNode.scrollHeight, + clientHeight: scrollNode.clientHeight, + tagName: scrollNode.tagName, + }; +} + function createStreamRenderStrategy( config: StreamRenderStrategyConfig ): StreamRenderStrategy { @@ -376,12 +402,28 @@ function createForwardStreamStrategy(): StreamRenderStrategy { 0, metrics.contentHeight - metrics.viewportHeight ); + logWebBottomAnchorTransport("forward_scroll_to_bottom_start", { + animated, + bottomOffset, + metrics, + usedFlatList: Boolean(refs.flatListRef.current), + ...getResolvedWebScrollContainerMetrics(refs), + }); if (refs.flatListRef.current) { refs.flatListRef.current.scrollToEnd?.({ animated }); refs.flatListRef.current.scrollToOffset?.({ offset: bottomOffset, animated, }); + if (typeof requestAnimationFrame === "function") { + requestAnimationFrame(() => { + logWebBottomAnchorTransport("forward_scroll_to_bottom_after_flatlist", { + animated, + bottomOffset, + ...getResolvedWebScrollContainerMetrics(refs), + }); + }); + } return; } const usedAnchor = scrollAnchorIntoView({ refs, animated }); @@ -394,6 +436,16 @@ function createForwardStreamStrategy(): StreamRenderStrategy { animated, }); forceScrollContainerToBottom(refs, bottomOffset); + if (typeof requestAnimationFrame === "function") { + requestAnimationFrame(() => { + logWebBottomAnchorTransport("forward_scroll_to_bottom_after_scrollview", { + animated, + bottomOffset, + usedAnchor, + ...getResolvedWebScrollContainerMetrics(refs), + }); + }); + } }, scrollToOffset: ({ refs, offset, animated }) => { if (refs.flatListRef.current) { diff --git a/packages/app/src/components/use-bottom-anchor-controller.ts b/packages/app/src/components/use-bottom-anchor-controller.ts index ca5cfa9ac..b7076c9d5 100644 --- a/packages/app/src/components/use-bottom-anchor-controller.ts +++ b/packages/app/src/components/use-bottom-anchor-controller.ts @@ -233,6 +233,20 @@ function deriveRetryDisposition(input: { : "retry-scroll"; } +function getDetailedMeasurementState( + measurementState: ControllerMeasurementState +): Record { + return { + containerKey: measurementState.containerKey, + viewportWidth: measurementState.viewportWidth, + viewportHeight: measurementState.viewportHeight, + contentHeight: measurementState.contentHeight, + offsetY: measurementState.offsetY, + viewportMeasuredForKey: measurementState.viewportMeasuredForKey, + contentMeasuredForKey: measurementState.contentMeasuredForKey, + }; +} + function createBottomAnchorControllerDriver( input: CreateBottomAnchorControllerDriverInput ): BottomAnchorControllerDriver { @@ -344,18 +358,37 @@ function createBottomAnchorControllerDriver( }); if (verificationBlockedReason) { + input.log( + "attempt_verified", + getLogContext({ + verificationPhase: "blocked", + verificationBlockedReason, + retries: attemptContext.retries, + measurementState: getDetailedMeasurementState(measurementState), + }) + ); pendingVerification = attemptContext; setBlockedReason(verificationBlockedReason); return; } const verifiedNearBottom = input.isNearBottom(); + const retryDisposition = verifiedNearBottom + ? null + : deriveRetryDisposition({ + mode, + retries: attemptContext.retries, + verificationRetryMode: + input.getTransportBehavior().verificationRetryMode, + }); input.log( "attempt_verified", getLogContext({ verifiedNearBottom, retries: attemptContext.retries, + retryDisposition, + measurementState: getDetailedMeasurementState(measurementState), }) ); @@ -370,12 +403,6 @@ function createBottomAnchorControllerDriver( return; } - const retryDisposition = deriveRetryDisposition({ - mode, - retries: attemptContext.retries, - verificationRetryMode: input.getTransportBehavior().verificationRetryMode, - }); - if (retryDisposition === "retry-verify") { pendingVerification = { requestId: attemptContext.requestId, @@ -397,7 +424,11 @@ function createBottomAnchorControllerDriver( input.log( "attempt_failed", - getLogContext({ retries: attemptContext.retries }) + getLogContext({ + retries: attemptContext.retries, + retryDisposition, + measurementState: getDetailedMeasurementState(measurementState), + }) ); pendingVerification = null; if (isRequestAttempt && currentRequest) { @@ -443,10 +474,11 @@ function createBottomAnchorControllerDriver( kind: "attempt", callback: () => { attemptHandle = null; + const measurementState = input.getMeasurementState(); const nextBlockedReason = deriveBottomAnchorBlockedReason({ pendingRequest, isAuthoritativeHistoryReady: input.getIsAuthoritativeHistoryReady(), - measurementState: input.getMeasurementState(), + measurementState, pendingVerificationRequestId: pendingVerification?.requestId ?? null, }); setBlockedReason(nextBlockedReason); @@ -462,6 +494,16 @@ function createBottomAnchorControllerDriver( !shouldAttemptForPendingRequest && !shouldAttemptForStickyVerification ) { + input.log( + "attempt_started", + getLogContext({ + attemptPhase: "skipped", + nextBlockedReason, + shouldAttemptForPendingRequest, + shouldAttemptForStickyVerification, + measurementState: getDetailedMeasurementState(measurementState), + }) + ); return; }