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}
{isFetchingNextPage && Loading moreā¦
}
- {!hasNextPage && End of stream
}
+ {!hasNextPage && !isLoading && hasItems && (
+ End of stream
+ )}
);
}
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 ? (