diff --git a/mobile/src/session/MobileNativeChatView.test.ts b/mobile/src/session/MobileNativeChatView.test.ts index 4214df7f521..5226f483d27 100644 --- a/mobile/src/session/MobileNativeChatView.test.ts +++ b/mobile/src/session/MobileNativeChatView.test.ts @@ -222,7 +222,12 @@ describe('MobileNativeChatView', () => { it('still follows the tail once the history page has settled', async () => { vi.useFakeTimers() try { - await render({ messages: [older, message], hasMore: false }) + await render({ messages: [message], hasMore: true }) + await act(async () => vi.runAllTimersAsync()) + // Drive a real paging cycle first: a hold that never releases would leave + // the chat permanently unable to follow the agent. + await update({ messages: [message], hasMore: true, loadingEarlier: true }) + await update({ messages: [older, message], hasMore: false, loadingEarlier: false }) await act(async () => vi.runAllTimersAsync()) scrollToEnd.mockClear() diff --git a/mobile/src/session/MobileNativeChatView.tsx b/mobile/src/session/MobileNativeChatView.tsx index 857cf3b73b0..21763f0d670 100644 --- a/mobile/src/session/MobileNativeChatView.tsx +++ b/mobile/src/session/MobileNativeChatView.tsx @@ -154,6 +154,8 @@ export function MobileNativeChatView({ // A short transcript leaves the reader both at the top and `atBottom`, so the // tail-follow paths would scroll away the history that just paged in. const keepHistoryPositionRef = useRef(Boolean(loadingEarlier)) + // Snapshot, not a live ref read: effects flush before native layout, so by the + // time `onContentSizeChange` runs the ref is already cleared. const keepHistoryPosition = keepHistoryPositionRef.current const { fontScale, pinchGesture } = useMobileNativeChatPinchGesture() useEffect(