Fix session history loading (#1468)

Fix chat history pagination and scroll preservation

Align session history loading with the expected scroll-up pagination UX while
keeping OpenChamber-specific initial message limits for constrained runtimes.

- Separate initial load sizes from older-history pagination size
- Load older messages automatically when scrolling near the top
- Continue fetching history until a visible older turn is available
- Preserve the current viewport synchronously during prepends
- Prevent history loading from fighting pinned-to-bottom follow behavior
- Remove delayed scroll-to-bottom correction that caused jumpbacks
- Fix the virtualizer fallback path that could render a large blank spacer
- Track oldest loaded message per pagination iteration to avoid redundant fetches
This commit is contained in:
Bohdan Triapitsyn
2026-05-30 02:03:41 +03:00
committed by GitHub
parent 4a1ebd98da
commit f9e9f30873
4 changed files with 196 additions and 73 deletions
@@ -156,6 +156,7 @@ type ChatViewportProps = {
handleMessageContentChange: (reason?: ContentChangeReason) => void; handleMessageContentChange: (reason?: ContentChangeReason) => void;
getAnimationHandlers: (messageId: string) => AnimationHandlers; getAnimationHandlers: (messageId: string) => AnimationHandlers;
handleLoadOlder: () => void; handleLoadOlder: () => void;
handleHistoryScroll: () => void;
scrollToBottom: () => void; scrollToBottom: () => void;
sessionQuestions: QuestionRequest[]; sessionQuestions: QuestionRequest[];
sessionPermissions: PermissionRequest[]; sessionPermissions: PermissionRequest[];
@@ -181,6 +182,7 @@ const ChatViewport = React.memo(({
handleMessageContentChange, handleMessageContentChange,
getAnimationHandlers, getAnimationHandlers,
handleLoadOlder, handleLoadOlder,
handleHistoryScroll,
scrollToBottom, scrollToBottom,
sessionQuestions, sessionQuestions,
sessionPermissions, sessionPermissions,
@@ -217,6 +219,7 @@ const ChatViewport = React.memo(({
hideTopShadow={isMobile && stickyUserHeader} hideTopShadow={isMobile && stickyUserHeader}
tabIndex={0} tabIndex={0}
onClick={focusScrollContainer} onClick={focusScrollContainer}
onScroll={handleHistoryScroll}
data-scroll-shadow="true" data-scroll-shadow="true"
data-scrollbar="chat" data-scrollbar="chat"
> >
@@ -280,6 +283,7 @@ const ChatViewport = React.memo(({
&& prev.handleMessageContentChange === next.handleMessageContentChange && prev.handleMessageContentChange === next.handleMessageContentChange
&& prev.getAnimationHandlers === next.getAnimationHandlers && prev.getAnimationHandlers === next.getAnimationHandlers
&& prev.handleLoadOlder === next.handleLoadOlder && prev.handleLoadOlder === next.handleLoadOlder
&& prev.handleHistoryScroll === next.handleHistoryScroll
&& prev.scrollToBottom === next.scrollToBottom && prev.scrollToBottom === next.scrollToBottom
&& prev.sessionQuestions === next.sessionQuestions && prev.sessionQuestions === next.sessionQuestions
&& prev.sessionPermissions === next.sessionPermissions && prev.sessionPermissions === next.sessionPermissions
@@ -655,7 +659,7 @@ export const ChatContainer: React.FC<ChatContainerProps> = ({ autoOpenDraft = tr
}, [handleMessageContentChange, sessionPermissions, sessionQuestions]); }, [handleMessageContentChange, sessionPermissions, sessionQuestions]);
const handleLoadOlder = React.useCallback(() => { const handleLoadOlder = React.useCallback(() => {
void loadEarlier(); void loadEarlier({ userInitiated: true });
}, [loadEarlier]); }, [loadEarlier]);
const navigation = useChatTurnNavigation({ const navigation = useChatTurnNavigation({
@@ -950,6 +954,7 @@ export const ChatContainer: React.FC<ChatContainerProps> = ({ autoOpenDraft = tr
handleMessageContentChange={handleMessageContentChange} handleMessageContentChange={handleMessageContentChange}
getAnimationHandlers={getAnimationHandlers} getAnimationHandlers={getAnimationHandlers}
handleLoadOlder={handleLoadOlder} handleLoadOlder={handleLoadOlder}
handleHistoryScroll={timelineController.handleHistoryScroll}
scrollToBottom={resumeToLatestInstant} scrollToBottom={resumeToLatestInstant}
sessionQuestions={sessionQuestions} sessionQuestions={sessionQuestions}
sessionPermissions={sessionPermissions} sessionPermissions={sessionPermissions}
@@ -1005,15 +1005,9 @@ const StaticHistoryList = React.memo(({ entries, shouldVirtualize, virtualRows,
} }
if (virtualRows.length === 0 && entries.length > 0) { if (virtualRows.length === 0 && entries.length > 0) {
const fallbackStart = Math.max(0, entries.length - MESSAGE_LIST_OVERSCAN * 2);
const fallbackEntries = entries.slice(fallbackStart);
const fallbackHeight = fallbackEntries.reduce((total, entry) => total + estimateHistoryEntryHeight(entry), 0);
const fallbackPaddingTop = Math.max(0, totalSize - fallbackHeight);
return ( return (
<div ref={contentRef} className="relative w-full"> <div ref={contentRef} className="relative w-full">
{fallbackPaddingTop > 0 ? <div aria-hidden="true" style={{ height: `${fallbackPaddingTop}px` }} /> : null} {entries.map((entry) => (
{fallbackEntries.map((entry) => (
<div <div
key={entry.key} key={entry.key}
data-turn-entry={entry.key} data-turn-entry={entry.key}
@@ -1108,7 +1102,7 @@ StreamingTailContent.displayName = 'StreamingTailContent';
const MessageList = React.forwardRef<MessageListHandle, MessageListProps>(({ const MessageList = React.forwardRef<MessageListHandle, MessageListProps>(({
sessionKey, sessionKey,
turnStart, turnStart,
disableStaging: _disableStaging, disableStaging = false,
messages, messages,
sessionIsWorking = false, sessionIsWorking = false,
activeStreamingMessageId = null, activeStreamingMessageId = null,
@@ -1123,7 +1117,6 @@ const MessageList = React.forwardRef<MessageListHandle, MessageListProps>(({
scrollRef, scrollRef,
}, ref) => { }, ref) => {
streamPerfCount('ui.message_list.render'); streamPerfCount('ui.message_list.render');
void _disableStaging;
const stickyUserHeader = useUIStore(state => state.stickyUserHeader); const stickyUserHeader = useUIStore(state => state.stickyUserHeader);
const chatRenderMode = useUIStore((state) => state.chatRenderMode); const chatRenderMode = useUIStore((state) => state.chatRenderMode);
const activityRenderMode = useUIStore((state) => state.activityRenderMode); const activityRenderMode = useUIStore((state) => state.activityRenderMode);
@@ -1320,7 +1313,7 @@ const MessageList = React.forwardRef<MessageListHandle, MessageListProps>(({
const grew = currentLen > previousLen; const grew = currentLen > previousLen;
const firstChanged = previousFirstKey !== currentFirstKey; const firstChanged = previousFirstKey !== currentFirstKey;
if (!shouldVirtualizeHistory || !grew || !firstChanged || previousLen === 0) { if (!shouldVirtualizeHistory || isLoadingOlder || disableStaging || !grew || !firstChanged || previousLen === 0) {
return; return;
} }
@@ -1379,36 +1372,9 @@ const MessageList = React.forwardRef<MessageListHandle, MessageListProps>(({
if (!shouldVirtualizeHistory) { if (!shouldVirtualizeHistory) {
return; return;
} }
const scrollEl = resolveScrollContainer();
const prevTotal = historyVirtualizer.getTotalSize();
const nearBottom = scrollEl && prevTotal > 0
? scrollEl.scrollTop + scrollEl.clientHeight >= prevTotal - 10
: false
historyVirtualizer.measure(); historyVirtualizer.measure();
}, [historyEntries.length, historyVirtualizer, shouldVirtualizeHistory]);
// measure() defers via useAnimationFrameWithResizeObserver.
// Wait two frames then, if we were near the estimated bottom, scroll
// to the real bottom after measurements settle.
let frame2: number | null = null;
const frame1 = requestAnimationFrame(() => {
frame2 = requestAnimationFrame(() => {
if (!nearBottom) return
const el = resolveScrollContainer()
if (!el) return
const target = Math.max(0, el.scrollHeight - el.clientHeight)
if (target > 0 && Math.abs(el.scrollTop - target) > 5) {
el.scrollTop = target
}
})
})
return () => {
cancelAnimationFrame(frame1)
if (frame2 !== null) {
cancelAnimationFrame(frame2)
}
}
}, [historyVirtualizer, resolveScrollContainer, shouldVirtualizeHistory]);
const scheduleVirtualMeasure = React.useCallback(() => { const scheduleVirtualMeasure = React.useCallback(() => {
if (!shouldVirtualizeHistory) { if (!shouldVirtualizeHistory) {
@@ -1673,7 +1639,7 @@ const MessageList = React.forwardRef<MessageListHandle, MessageListProps>(({
if (!applyAnchor()) { if (!applyAnchor()) {
const index = messageIndexMap.get(anchor.messageId); const index = messageIndexMap.get(anchor.messageId);
if (typeof index === 'number' && index < historyEntries.length) { if (typeof index === 'number' && index < historyEntries.length) {
scrollHistoryIndexIntoView(index, 'auto'); return scrollHistoryIndexIntoView(index, 'auto');
} }
} }
@@ -50,22 +50,26 @@ export interface UseChatTimelineControllerResult {
activeTurnId: string | null; activeTurnId: string | null;
showScrollToBottom: boolean; showScrollToBottom: boolean;
turnWindowModel: TurnWindowModel; turnWindowModel: TurnWindowModel;
loadEarlier: () => Promise<void>; loadEarlier: (options?: { userInitiated?: boolean }) => Promise<void>;
revealBufferedTurns: () => Promise<boolean>; revealBufferedTurns: () => Promise<boolean>;
resumeToBottom: () => void; resumeToBottom: () => void;
resumeToBottomInstant: () => Promise<void>; resumeToBottomInstant: () => Promise<void>;
scrollToTurn: (turnId: string, options?: { behavior?: ScrollBehavior }) => Promise<boolean>; scrollToTurn: (turnId: string, options?: { behavior?: ScrollBehavior }) => Promise<boolean>;
scrollToMessage: (messageId: string, options?: { behavior?: ScrollBehavior }) => Promise<boolean>; scrollToMessage: (messageId: string, options?: { behavior?: ScrollBehavior }) => Promise<boolean>;
handleHistoryScroll: () => void;
captureViewportAnchor: () => ViewportAnchor | null; captureViewportAnchor: () => ViewportAnchor | null;
restoreViewportAnchor: (anchor: ViewportAnchor) => boolean; restoreViewportAnchor: (anchor: ViewportAnchor) => boolean;
handleActiveTurnChange: (turnId: string | null) => void; handleActiveTurnChange: (turnId: string | null) => void;
} }
const TURN_MODEL_CACHE_MAX = 30 const TURN_MODEL_CACHE_MAX = 30
const HISTORY_SCROLL_THRESHOLD = 200
const VSCODE_TURN_MODEL_CACHE_MAX = 4 const VSCODE_TURN_MODEL_CACHE_MAX = 4
const VSCODE_TURN_MODEL_CACHE_MAX_MESSAGES = 30 const VSCODE_TURN_MODEL_CACHE_MAX_MESSAGES = 30
const MOBILE_TURN_MODEL_CACHE_MAX = 4 const MOBILE_TURN_MODEL_CACHE_MAX = 4
const MOBILE_TURN_MODEL_CACHE_MAX_MESSAGES = 30 const MOBILE_TURN_MODEL_CACHE_MAX_MESSAGES = 30
const HISTORY_RENDER_WAIT_TIMEOUT_MS = 250
const HISTORY_INTERACTION_GUARD_MS = 2000
const turnModelCache = new Map<string, { messages: ChatMessageEntry[]; model: TurnWindowModel }>() const turnModelCache = new Map<string, { messages: ChatMessageEntry[]; model: TurnWindowModel }>()
const getTurnModelCacheMax = () => { const getTurnModelCacheMax = () => {
if (isVSCodeRuntime()) return VSCODE_TURN_MODEL_CACHE_MAX if (isVSCodeRuntime()) return VSCODE_TURN_MODEL_CACHE_MAX
@@ -150,6 +154,8 @@ export const useChatTimelineController = ({
const initializedSessionRef = React.useRef<string | null>(null); const initializedSessionRef = React.useRef<string | null>(null);
const pendingRenderResolversRef = React.useRef<Array<() => void>>([]); const pendingRenderResolversRef = React.useRef<Array<() => void>>([]);
const pendingScrollRequestRef = React.useRef<PendingScrollRequest | null>(null); const pendingScrollRequestRef = React.useRef<PendingScrollRequest | null>(null);
const historyInteractionRef = React.useRef(false);
const historyInteractionTimerRef = React.useRef<number | null>(null);
const historySignals = React.useMemo(() => { const historySignals = React.useMemo(() => {
const defaultLimit = getMemoryLimits().HISTORICAL_MESSAGES; const defaultLimit = getMemoryLimits().HISTORICAL_MESSAGES;
@@ -178,10 +184,38 @@ export const useChatTimelineController = ({
messagesRef.current = messages; messagesRef.current = messages;
historyMetaRef.current = historyMeta; historyMetaRef.current = historyMeta;
const beginHistoryInteraction = React.useCallback(() => {
historyInteractionRef.current = true;
if (historyInteractionTimerRef.current !== null && typeof window !== 'undefined') {
window.clearTimeout(historyInteractionTimerRef.current);
historyInteractionTimerRef.current = null;
}
}, []);
const settleHistoryInteraction = React.useCallback(() => {
if (typeof window === 'undefined') {
historyInteractionRef.current = false;
return;
}
if (historyInteractionTimerRef.current !== null) {
window.clearTimeout(historyInteractionTimerRef.current);
}
historyInteractionTimerRef.current = window.setTimeout(() => {
historyInteractionTimerRef.current = null;
historyInteractionRef.current = false;
}, HISTORY_INTERACTION_GUARD_MS);
}, []);
React.useLayoutEffect(() => { React.useLayoutEffect(() => {
if (initializedSessionRef.current === sessionId) { if (initializedSessionRef.current === sessionId) {
return; return;
} }
if (historyInteractionTimerRef.current !== null && typeof window !== 'undefined') {
window.clearTimeout(historyInteractionTimerRef.current);
historyInteractionTimerRef.current = null;
}
historyInteractionRef.current = false;
initializedSessionRef.current = sessionId; initializedSessionRef.current = sessionId;
setTurnStart(getInitialTurnStart(turnWindowModel.turnCount)); setTurnStart(getInitialTurnStart(turnWindowModel.turnCount));
setIsLoadingOlder(false); setIsLoadingOlder(false);
@@ -204,7 +238,13 @@ export const useChatTimelineController = ({
setTurnStart((current) => { setTurnStart((current) => {
const previousInitial = getInitialTurnStart(previousTurnCount); const previousInitial = getInitialTurnStart(previousTurnCount);
const nextInitial = getInitialTurnStart(nextTurnCount); const nextInitial = getInitialTurnStart(nextTurnCount);
if (isPinnedRef.current && current === previousInitial) { if (
!historyInteractionRef.current
&& !isLoadingOlderRef.current
&& !pendingRevealWorkRef.current
&& isPinnedRef.current
&& current === previousInitial
) {
return nextInitial; return nextInitial;
} }
return clampTurnStart(current, nextTurnCount); return clampTurnStart(current, nextTurnCount);
@@ -228,6 +268,25 @@ export const useChatTimelineController = ({
}); });
}, []); }, []);
const waitForNextRenderCommitOrTimeout = React.useCallback((): Promise<void> => {
return new Promise<void>((resolve) => {
if (typeof window === 'undefined') {
resolve();
return;
}
let settled = false;
const finish = () => {
if (settled) return;
settled = true;
window.clearTimeout(timer);
resolve();
};
pendingRenderResolversRef.current.push(finish);
const timer = window.setTimeout(finish, HISTORY_RENDER_WAIT_TIMEOUT_MS);
});
}, []);
const resolvePendingScrollRequest = React.useCallback((value: boolean) => { const resolvePendingScrollRequest = React.useCallback((value: boolean) => {
const pending = pendingScrollRequestRef.current; const pending = pendingScrollRequestRef.current;
if (!pending) { if (!pending) {
@@ -271,6 +330,10 @@ export const useChatTimelineController = ({
React.useEffect(() => { React.useEffect(() => {
return () => { return () => {
if (historyInteractionTimerRef.current !== null && typeof window !== 'undefined') {
window.clearTimeout(historyInteractionTimerRef.current);
historyInteractionTimerRef.current = null;
}
resolvePendingRenderWaiters(); resolvePendingRenderWaiters();
resolvePendingScrollRequest(false); resolvePendingScrollRequest(false);
}; };
@@ -328,6 +391,7 @@ export const useChatTimelineController = ({
return false; return false;
} }
beginHistoryInteraction();
const container = scrollRef.current; const container = scrollRef.current;
if (container) { if (container) {
prePrependScrollRef.current = { prePrependScrollRef.current = {
@@ -343,10 +407,14 @@ export const useChatTimelineController = ({
return next > 0 ? next : 0; return next > 0 ? next : 0;
}); });
await waitForNextRenderCommit(); try {
setPendingRevealWork(false); await waitForNextRenderCommit();
return true; return true;
}, [captureViewportAnchor, scrollRef, waitForNextRenderCommit]); } finally {
setPendingRevealWork(false);
settleHistoryInteraction();
}
}, [beginHistoryInteraction, captureViewportAnchor, scrollRef, settleHistoryInteraction, waitForNextRenderCommit]);
const fetchOlderHistory = React.useCallback(async (input: { const fetchOlderHistory = React.useCallback(async (input: {
preserveViewport: boolean; preserveViewport: boolean;
@@ -374,6 +442,7 @@ export const useChatTimelineController = ({
}; };
} }
beginHistoryInteraction();
setIsLoadingOlder(true); setIsLoadingOlder(true);
try { try {
@@ -382,31 +451,112 @@ export const useChatTimelineController = ({
return false; return false;
} }
await loadMoreMessages(targetSessionId, 'up'); let loadedMessageCount = beforeMessageCount;
let loadedOldestMessageId = beforeOldestMessageId;
let loadedLimit = beforeLimit;
const beforeTurnCount = turnModelRef.current.turnCount;
const afterMessages = messagesRef.current; while (true) {
const afterMessageCount = afterMessages.length; await loadMoreMessages(targetSessionId, 'up');
const afterOldestMessageId = afterMessages[0]?.info?.id ?? null; if (sessionIdRef.current !== targetSessionId) {
const afterLimit = historyMetaRef.current?.limit ?? beforeLimit; return false;
const historyGrew = }
afterMessageCount > beforeMessageCount
|| (typeof beforeOldestMessageId === 'string'
&& typeof afterOldestMessageId === 'string'
&& beforeOldestMessageId !== afterOldestMessageId);
return historyGrew || afterLimit > beforeLimit; await waitForNextRenderCommitOrTimeout();
const afterMessages = messagesRef.current;
const afterMessageCount = afterMessages.length;
const afterOldestMessageId = afterMessages[0]?.info?.id ?? null;
const afterLimit = historyMetaRef.current?.limit ?? loadedLimit;
const messageGrowth =
afterMessageCount > loadedMessageCount
|| (typeof loadedOldestMessageId === 'string'
&& typeof afterOldestMessageId === 'string'
&& loadedOldestMessageId !== afterOldestMessageId)
|| afterLimit > loadedLimit;
const turnGrowth = turnModelRef.current.turnCount - beforeTurnCount;
if (turnGrowth > 0) {
return true;
}
if (!messageGrowth) {
return false;
}
if (!historySignalsRef.current.hasMoreAboveTurns) {
return true;
}
loadedMessageCount = afterMessageCount;
loadedOldestMessageId = afterOldestMessageId;
loadedLimit = afterLimit;
}
} finally { } finally {
setIsLoadingOlder(false); setIsLoadingOlder(false);
settleHistoryInteraction();
} }
}, [captureViewportAnchor, loadMoreMessages, scrollRef]); }, [beginHistoryInteraction, captureViewportAnchor, loadMoreMessages, scrollRef, settleHistoryInteraction, waitForNextRenderCommitOrTimeout]);
const loadEarlier = React.useCallback(async () => { const loadEarlier = React.useCallback(async (options?: { userInitiated?: boolean }) => {
if (await revealBufferedTurns()) { beginHistoryInteraction();
if (options?.userInitiated) {
releaseAutoFollow();
}
try {
if (await revealBufferedTurns()) {
return;
}
void (await fetchOlderHistory({ preserveViewport: true }));
} finally {
settleHistoryInteraction();
}
}, [beginHistoryInteraction, fetchOlderHistory, releaseAutoFollow, revealBufferedTurns, settleHistoryInteraction]);
const handleHistoryScroll = React.useCallback(() => {
const container = scrollRef.current;
if (!container) return;
if (isPinnedRef.current) return;
if (container.scrollTop >= HISTORY_SCROLL_THRESHOLD) return;
if (!historySignalsRef.current.canLoadEarlier) return;
if (isLoadingOlderRef.current || pendingRevealWorkRef.current) return;
void loadEarlier({ userInitiated: true });
}, [loadEarlier, scrollRef]);
React.useEffect(() => {
if (!sessionId || isLoadingOlder || pendingRevealWork) {
return;
}
if (!isPinned || !historySignals.canLoadEarlier) {
return;
}
if (typeof window === 'undefined') {
return; return;
} }
void (await fetchOlderHistory({ preserveViewport: true })); const frame = window.requestAnimationFrame(() => {
}, [fetchOlderHistory, revealBufferedTurns]); const container = scrollRef.current;
if (!container) return;
if (!isPinnedRef.current) return;
if (!historySignalsRef.current.canLoadEarlier) return;
if (isLoadingOlderRef.current || pendingRevealWorkRef.current) return;
if (container.scrollHeight > container.clientHeight + 1) return;
void loadEarlier();
});
return () => window.cancelAnimationFrame(frame);
}, [
historySignals.canLoadEarlier,
isLoadingOlder,
isPinned,
loadEarlier,
pendingRevealWork,
renderedMessages.length,
scrollRef,
sessionId,
]);
const scrollToTurn = React.useCallback(async ( const scrollToTurn = React.useCallback(async (
turnId: string, turnId: string,
@@ -552,6 +702,7 @@ export const useChatTimelineController = ({
resumeToBottomInstant, resumeToBottomInstant,
scrollToTurn, scrollToTurn,
scrollToMessage, scrollToMessage,
handleHistoryScroll,
captureViewportAnchor, captureViewportAnchor,
restoreViewportAnchor, restoreViewportAnchor,
handleActiveTurnChange, handleActiveTurnChange,
+12 -11
View File
@@ -22,9 +22,10 @@ import {
import { getSessionMaterializationStatus, materializeSessionSnapshots } from "./materialization" import { getSessionMaterializationStatus, materializeSessionSnapshots } from "./materialization"
const SKIP_PARTS = new Set(["patch", "step-start", "step-finish"]) const SKIP_PARTS = new Set(["patch", "step-start", "step-finish"])
const MESSAGE_PAGE_SIZE = 150 const INITIAL_MESSAGE_PAGE_SIZE = 150
const VSCODE_MESSAGE_PAGE_SIZE = 30 const VSCODE_INITIAL_MESSAGE_PAGE_SIZE = 30
const MOBILE_MESSAGE_PAGE_SIZE = 30 const MOBILE_INITIAL_MESSAGE_PAGE_SIZE = 30
const HISTORY_MESSAGE_PAGE_SIZE = 200
const VSCODE_INITIAL_PAGE_EXPANSION_LIMITS = [50, 80, 120] as const const VSCODE_INITIAL_PAGE_EXPANSION_LIMITS = [50, 80, 120] as const
const MAX_SEEN_DIRS = 30 const MAX_SEEN_DIRS = 30
const VSCODE_SESSION_CACHE_LIMIT = 4 const VSCODE_SESSION_CACHE_LIMIT = 4
@@ -53,12 +54,12 @@ const getEffectiveSessionCacheLimit = () => {
if (isMobileSurfaceRuntime()) return MOBILE_SESSION_CACHE_LIMIT if (isMobileSurfaceRuntime()) return MOBILE_SESSION_CACHE_LIMIT
return SESSION_CACHE_LIMIT return SESSION_CACHE_LIMIT
} }
const getEffectiveMessagePageSize = () => { const getInitialMessagePageSize = () => {
if (isVSCodeRuntime()) return VSCODE_MESSAGE_PAGE_SIZE if (isVSCodeRuntime()) return VSCODE_INITIAL_MESSAGE_PAGE_SIZE
if (isMobileSurfaceRuntime()) return MOBILE_MESSAGE_PAGE_SIZE if (isMobileSurfaceRuntime()) return MOBILE_INITIAL_MESSAGE_PAGE_SIZE
return MESSAGE_PAGE_SIZE return INITIAL_MESSAGE_PAGE_SIZE
} }
const getDefaultMeta = (): SyncMeta => ({ limit: getEffectiveMessagePageSize(), cursor: undefined, complete: false, loading: false }) const getDefaultMeta = (): SyncMeta => ({ limit: getInitialMessagePageSize(), cursor: undefined, complete: false, loading: false })
function getPrefetchMeta(directory: string, sessionID: string): SyncMeta | undefined { function getPrefetchMeta(directory: string, sessionID: string): SyncMeta | undefined {
const info = getSessionPrefetch(directory, sessionID) const info = getSessionPrefetch(directory, sessionID)
@@ -78,7 +79,7 @@ function sortParts(parts: Part[]) {
function isHeavyConstrainedSessionCache(state: Pick<State, "message" | "part">, sessionID: string): boolean { function isHeavyConstrainedSessionCache(state: Pick<State, "message" | "part">, sessionID: string): boolean {
const messages = state.message[sessionID] const messages = state.message[sessionID]
if (!messages || messages.length === 0) return false if (!messages || messages.length === 0) return false
return messages.length > getEffectiveMessagePageSize() return messages.length > getInitialMessagePageSize()
} }
function isUserMessage(message: Message): boolean { function isUserMessage(message: Message): boolean {
@@ -291,7 +292,7 @@ export function useSync() {
setMetaFor(sessionID, { loading: true }) setMetaFor(sessionID, { loading: true })
try { try {
const limit = options?.before ? getEffectiveMessagePageSize() : m.limit const limit = options?.before ? HISTORY_MESSAGE_PAGE_SIZE : m.limit
let page = await fetchMessages(sessionID, limit, options?.before) let page = await fetchMessages(sessionID, limit, options?.before)
// Constrained shells keep the initial page small for switch performance. Some // Constrained shells keep the initial page small for switch performance. Some
@@ -385,7 +386,7 @@ export function useSync() {
if (shouldSkipSessionPrefetch({ if (shouldSkipSessionPrefetch({
hasMessages: cachedReady, hasMessages: cachedReady,
info: prefetchInfo, info: prefetchInfo,
pageSize: getEffectiveMessagePageSize(), pageSize: getInitialMessagePageSize(),
})) return })) return
} }