From 7f233460aa40c9ccca552c54afcf7be784b506ae Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Fri, 17 Apr 2026 12:52:39 +0200 Subject: [PATCH] fix(ui): stabilize infinite-stream callbacks + suppress empty-state flash - useInfiniteStream: wrap fetchNextPage and refresh in useCallback so InfiniteScrollArea's IntersectionObserver does not re-subscribe on every parent render. - InfiniteScrollArea: do not render 'End of stream' until at least one item has loaded and the initial query has settled (was flashing on mount before first fetch). - AgentHealth: pass isLoading + hasItems to both InfiniteScrollArea wrappers. --- ui/src/components/InfiniteScrollArea.tsx | 8 +++++++- ui/src/hooks/useInfiniteStream.ts | 16 +++++++++++----- ui/src/pages/AgentHealth/AgentHealth.tsx | 4 ++++ 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/ui/src/components/InfiniteScrollArea.tsx b/ui/src/components/InfiniteScrollArea.tsx index 7d085260..49b77c04 100644 --- a/ui/src/components/InfiniteScrollArea.tsx +++ b/ui/src/components/InfiniteScrollArea.tsx @@ -6,6 +6,8 @@ export interface InfiniteScrollAreaProps { onTopVisibilityChange?: (atTop: boolean) => void; isFetchingNextPage: boolean; hasNextPage: boolean; + isLoading?: boolean; + hasItems?: boolean; maxHeight?: number | string; children: ReactNode; /** Optional caller-owned scroll container ref (e.g. for scroll-to-top on refresh). */ @@ -18,6 +20,8 @@ export function InfiniteScrollArea({ onTopVisibilityChange, isFetchingNextPage, hasNextPage, + isLoading = false, + hasItems = true, maxHeight = 360, children, scrollRef, @@ -68,7 +72,9 @@ export function InfiniteScrollArea({ {children} ); } diff --git a/ui/src/hooks/useInfiniteStream.ts b/ui/src/hooks/useInfiniteStream.ts index 1285e9fa..47811222 100644 --- a/ui/src/hooks/useInfiniteStream.ts +++ b/ui/src/hooks/useInfiniteStream.ts @@ -1,5 +1,5 @@ import { useInfiniteQuery, useQueryClient } from '@tanstack/react-query'; -import { useMemo } from 'react'; +import { useCallback, useMemo } from 'react'; export interface StreamPage { data: T[]; @@ -52,14 +52,20 @@ export function useInfiniteStream(args: UseInfiniteStreamArgs): UseInfinit [query.data], ); + const fetchNextPage = useCallback(() => { + if (query.hasNextPage && !query.isFetchingNextPage) query.fetchNextPage(); + }, [query.hasNextPage, query.isFetchingNextPage, query.fetchNextPage]); + + const refresh = useCallback(() => { + queryClient.invalidateQueries({ queryKey: [...queryKey] }); + }, [queryClient, queryKey]); + return { items, - fetchNextPage: () => { - if (query.hasNextPage && !query.isFetchingNextPage) query.fetchNextPage(); - }, + fetchNextPage, hasNextPage: !!query.hasNextPage, isFetchingNextPage: query.isFetchingNextPage, isLoading: query.isLoading, - refresh: () => queryClient.invalidateQueries({ queryKey: [...queryKey] }), + refresh, }; } diff --git a/ui/src/pages/AgentHealth/AgentHealth.tsx b/ui/src/pages/AgentHealth/AgentHealth.tsx index a93035d5..d3ca863d 100644 --- a/ui/src/pages/AgentHealth/AgentHealth.tsx +++ b/ui/src/pages/AgentHealth/AgentHealth.tsx @@ -957,6 +957,8 @@ export default function AgentHealth() { onTopVisibilityChange={setIsLogAtTop} isFetchingNextPage={logStream.isFetchingNextPage} hasNextPage={logStream.hasNextPage} + isLoading={logStream.isLoading} + hasItems={logStream.items.length > 0} maxHeight={360} > {filteredLogs.length > 0 ? ( @@ -993,6 +995,8 @@ export default function AgentHealth() { onTopVisibilityChange={setIsTimelineAtTop} isFetchingNextPage={eventStream.isFetchingNextPage} hasNextPage={eventStream.hasNextPage} + isLoading={eventStream.isLoading} + hasItems={eventStream.items.length > 0} maxHeight={360} > {feedEvents.length > 0 ? (