From 01ca2ecf242c1d2a240bfd1b73550ce88d36c73c Mon Sep 17 00:00:00 2001 From: Bohdan Triapitsyn Date: Sun, 5 Jul 2026 01:54:33 +0300 Subject: [PATCH] fix(chat): hide the load-older button once history is confirmed complete The button's visibility mixed the sync meta with the prefetch-cache hint; a stale prefetch entry (cursor recorded at the initial page) could keep the affordance alive after the user had already loaded to the top. Sync now exposes an explicit isComplete (positive confirmation from a fetch, distinct from !hasMore on unpopulated meta) and it overrides the prefetch hint, which stays in effect only before the meta knows anything. --- packages/ui/src/components/chat/ChatContainer.tsx | 11 +++++++++-- packages/ui/src/sync/use-sync.ts | 11 ++++++++++- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/packages/ui/src/components/chat/ChatContainer.tsx b/packages/ui/src/components/chat/ChatContainer.tsx index 6fc96c59..4451a41e 100644 --- a/packages/ui/src/components/chat/ChatContainer.tsx +++ b/packages/ui/src/components/chat/ChatContainer.tsx @@ -522,10 +522,17 @@ export const ChatContainer: React.FC = ({ autoOpenDraft = tr // History metadata — use sync's hasMore/isLoading const historyMeta = React.useMemo(() => { if (!currentSessionId) return null; - const prefetchHasMore = Boolean(sessionPrefetchInfo?.cursor) && sessionPrefetchInfo?.complete !== true; + // Sync's meta is authoritative once a fetch has confirmed the history + // is fully loaded — a stale prefetch-cache entry (cursor recorded at + // the initial page) must not keep the "load older" affordance alive + // after the user has already reached the top. + const syncComplete = sync.isComplete(currentSessionId); + const prefetchHasMore = !syncComplete + && Boolean(sessionPrefetchInfo?.cursor) + && sessionPrefetchInfo?.complete !== true; return { limit: sessionMessages.length, - complete: !(sync.hasMore(currentSessionId) || prefetchHasMore), + complete: syncComplete || !(sync.hasMore(currentSessionId) || prefetchHasMore), loading: sync.isLoading(currentSessionId), }; }, [currentSessionId, sessionMessages.length, sessionPrefetchInfo, sync]); diff --git a/packages/ui/src/sync/use-sync.ts b/packages/ui/src/sync/use-sync.ts index 4f741d68..66d3f788 100644 --- a/packages/ui/src/sync/use-sync.ts +++ b/packages/ui/src/sync/use-sync.ts @@ -555,6 +555,14 @@ export function useSync() { [getMetaFor], ) + // True only when a fetch has positively confirmed the history is fully + // loaded (no next cursor). Distinct from !hasMore(), which is also true for + // sessions whose meta simply hasn't been populated yet. + const isComplete = useCallback( + (sessionID: string) => getMetaFor(sessionID).complete, + [getMetaFor], + ) + // Optimistic add (for prompt submission) const optimisticAdd = useCallback( (input: { sessionID: string; directory?: string | null; message: Message; parts: Part[] }) => { @@ -610,11 +618,12 @@ export function useSync() { loadMore, hasMore, isLoading, + isComplete, optimistic: { add: optimisticAdd, remove: optimisticRemove, }, }), - [syncSession, loadMore, hasMore, isLoading, optimisticAdd, optimisticRemove], + [syncSession, loadMore, hasMore, isLoading, isComplete, optimisticAdd, optimisticRemove], ) }