From 110af9e834551f8d8bfb308b1d1ded693457316b Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Wed, 25 Mar 2026 16:40:26 +0000 Subject: [PATCH] fix(site): fix agents sidebar not loading all pages when sentinel stays visible (#23613) --- .../components/Sidebar/AgentsSidebar.test.tsx | 44 +++++++++++++++++++ .../components/Sidebar/AgentsSidebar.tsx | 26 ++++++----- 2 files changed, 58 insertions(+), 12 deletions(-) diff --git a/site/src/pages/AgentsPage/components/Sidebar/AgentsSidebar.test.tsx b/site/src/pages/AgentsPage/components/Sidebar/AgentsSidebar.test.tsx index 92f21f0136..d761efd2cc 100644 --- a/site/src/pages/AgentsPage/components/Sidebar/AgentsSidebar.test.tsx +++ b/site/src/pages/AgentsPage/components/Sidebar/AgentsSidebar.test.tsx @@ -258,6 +258,50 @@ describe("AgentsSidebar load-more behavior", () => { expect(onLoadMore).toHaveBeenCalledTimes(1); }); + it("recreates the observer when isFetchingNextPage transitions to false so visible sentinels re-trigger", () => { + const onLoadMore = vi.fn(); + const { rerender } = render( + + + , + ); + + const countAfterMount = observeCount; + expect(countAfterMount).toBe(1); + + // Start fetching — observer is torn down. + rerender( + + + , + ); + + // Fetch completes — a fresh observer is created, firing + // an initial entry that detects the still-visible sentinel. + rerender( + + + , + ); + + expect(observeCount).toBe(countAfterMount + 1); + }); + it("does NOT render the sentinel when hasNextPage is false", () => { const onLoadMore = vi.fn(); render( diff --git a/site/src/pages/AgentsPage/components/Sidebar/AgentsSidebar.tsx b/site/src/pages/AgentsPage/components/Sidebar/AgentsSidebar.tsx index 2dd13a9181..6841ac9d12 100644 --- a/site/src/pages/AgentsPage/components/Sidebar/AgentsSidebar.tsx +++ b/site/src/pages/AgentsPage/components/Sidebar/AgentsSidebar.tsx @@ -1131,27 +1131,29 @@ const LoadMoreSentinel: FC<{ }> = ({ onLoadMore, isFetchingNextPage }) => { const sentinelRef = useRef(null); const onLoadMoreRef = useRef(onLoadMore); - const isFetchingNextPageRef = useRef(isFetchingNextPage); - // Keep refs in sync with the latest prop values so the - // observer callback always reads current state without - // needing to tear down and re-create the observer. + // Keep the callback ref in sync so the observer closure + // always calls the latest onLoadMore without needing to + // tear down and re-create the observer. useEffect(() => { onLoadMoreRef.current = onLoadMore; - isFetchingNextPageRef.current = isFetchingNextPage; - }, [onLoadMore, isFetchingNextPage]); + }, [onLoadMore]); useEffect(() => { + // Don't observe while a fetch is in progress. When the + // fetch completes this effect re-runs, creating a fresh + // observer whose initial entry detects the sentinel if + // it's still visible — fixing the case where loaded items + // don't push the sentinel out of view and the previous + // observer never re-fires. + if (isFetchingNextPage) return; + const el = sentinelRef.current; if (!el) return; const observer = new IntersectionObserver( (entries) => { - if ( - entries[0]?.isIntersecting && - !isFetchingNextPageRef.current && - onLoadMoreRef.current - ) { + if (entries[0]?.isIntersecting && onLoadMoreRef.current) { onLoadMoreRef.current(); } }, @@ -1159,7 +1161,7 @@ const LoadMoreSentinel: FC<{ ); observer.observe(el); return () => observer.disconnect(); - }, []); + }, [isFetchingNextPage]); return (