diff --git a/apps/web/components/task/chat/message-list-native-scroll.ts b/apps/web/components/task/chat/message-list-native-scroll.ts index f929b248b1..40aa4aee92 100644 --- a/apps/web/components/task/chat/message-list-native-scroll.ts +++ b/apps/web/components/task/chat/message-list-native-scroll.ts @@ -1543,6 +1543,21 @@ type InitialScrollPositionEffectParams = Omit; }; +function handleEmptyTranscriptPlacement(params: { + hasEmptyTranscript: boolean; + envSwitchPlacementToken: number | null; + historyRefreshPending: boolean; + didInitialScroll: React.RefObject; + activationPendingRef: React.RefObject; +}): boolean { + if (!params.hasEmptyTranscript) return false; + if (params.envSwitchPlacementToken !== null && !params.historyRefreshPending) { + markInitialScrollConsumed(params.didInitialScroll, params.activationPendingRef); + completeEnvSwitchPlacement(params.envSwitchPlacementToken); + } + return true; +} + function runInitialScrollPositionEffect( params: InitialScrollPositionEffectParams, ): void | (() => void) { @@ -1565,13 +1580,13 @@ function runInitialScrollPositionEffect( activationPendingRef, } = params; if (!isVisible || (envSwitchPlacementToken !== null && isRestoringLayout)) return; - const hasEmptyTranscript = itemCount === 0; - if (envSwitchPlacementToken !== null && hasEmptyTranscript) { - if (historyRefreshPending) return; - markInitialScrollConsumed(didInitialScroll, activationPendingRef); - completeEnvSwitchPlacement(envSwitchPlacementToken); - return; - } + const hasEmptyTranscript = handleEmptyTranscriptPlacement({ + hasEmptyTranscript: itemCount === 0, + envSwitchPlacementToken, + historyRefreshPending, + didInitialScroll, + activationPendingRef, + }); if (didInitialScroll.current || hasEmptyTranscript) return; const element = scrollRef.current; if (!element) return; @@ -1605,7 +1620,11 @@ function runInitialScrollPositionEffect( if (phase === "provisional") provisionalTokenRef.current = envSwitchPlacementToken; }; - if (activationPendingRef.current) return scheduleAfterPanelRestore(applyInitialScroll); + const isEnvSwitchSettling = + envSwitchPlacementToken !== null && provisionalTokenRef.current === envSwitchPlacementToken; + if (activationPendingRef.current && !isEnvSwitchSettling) { + return scheduleAfterPanelRestore(applyInitialScroll); + } applyInitialScroll(); } diff --git a/apps/web/components/task/chat/message-list-native.test.tsx b/apps/web/components/task/chat/message-list-native.test.tsx index cedf3d5fe5..28d8ce69d7 100644 --- a/apps/web/components/task/chat/message-list-native.test.tsx +++ b/apps/web/components/task/chat/message-list-native.test.tsx @@ -86,6 +86,7 @@ const HARNESS_RENDER_ERROR = "harness did not render"; const NATIVE_SCROLL_MANAGEMENT_TEST_ID = "native-scroll-management-container"; const AUTO_SCROLL_CONTAINER_TEST_ID = "auto-scroll-container"; const CACHED_MESSAGE_ID = "cached-message"; +const SETTLED_MESSAGE_ID = "settled-message"; const TEST_MESSAGES = [{} as Message]; /** Always returns false: the harness never locks programmatic scrolling. */ const NEVER_LOCKED = () => false; @@ -412,18 +413,18 @@ describe("useNativeScrollManagement transcript pagination", () => { metrics.scrollHeight = 1400; rerender( , ); - act(() => { - for (let frame = frames.shift(); frame; frame = frames.shift()) frame(0); - }); expect(scrollContainer.scrollTop).toBe(1400); expect(mockDockviewState.pendingChatInitialPlacement).toBeNull(); + act(() => { + for (let frame = frames.shift(); frame; frame = frames.shift()) frame(0); + }); } finally { vi.unstubAllGlobals(); } @@ -476,17 +477,17 @@ describe("useNativeScrollManagement transcript pagination", () => { metrics.scrollTop = 915; rerender( , ); - act(() => { - for (let frame = frames.shift(); frame; frame = frames.shift()) frame(0); - }); expect(scrollContainer.scrollTop).toBe(320); expect(scrollContainer.scrollTop).not.toBe(810); + act(() => { + for (let frame = frames.shift(); frame; frame = frames.shift()) frame(0); + }); } finally { vi.unstubAllGlobals(); } @@ -556,19 +557,19 @@ describe("useNativeScrollManagement transcript pagination", () => { metrics.scrollTop = 480; rerender( , ); - act(() => { - for (let frame = frames.shift(); frame; frame = frames.shift()) frame(0); - }); expect(metrics.scrollTop).toBe(480); expect(mockDockviewState.pendingChatInitialPlacement).toBeNull(); + act(() => { + for (let frame = frames.shift(); frame; frame = frames.shift()) frame(0); + }); } finally { vi.unstubAllGlobals(); }