[eric] browser-feed: memoize rows + per-feed selector w/ result equality; hydrate new mid-run children (per-token full-history re-render = the browser-use lag)

This commit is contained in:
ciregenz
2026-07-15 16:18:23 -07:00
parent cfe338a0e5
commit a88549cbb3
@@ -155,7 +155,8 @@ const lightFeedColors: FeedColors = {
// Stable ref keeps shallowEqual happy when there are no browser sessions yet.
const EMPTY_STREAMING: Record<string, StreamingMessage> = Object.freeze({}) as Record<string, StreamingMessage>;
const selectBrowserSessions = createSelector(
// Factory, one selector PER FEED: a module-level createSelector has a cache of 1 shared by every mounted feed, so two feeds with different args thrash it and every render recomputes (and returns a fresh array identity, which defeats all downstream memoization).
const makeSelectBrowserSessions = () => createSelector(
[(state: RootState) => state.agents.sessions,
(_: RootState, parentSessionId: string) => parentSessionId,
(_: RootState, __: string, browserId?: string) => browserId],
@@ -166,6 +167,8 @@ const selectBrowserSessions = createSelector(
s.parent_session_id === parentSessionId &&
(!browserId || s.browser_id === browserId),
),
// Same members = same array identity: ANY session update rebuilds the sessions dict, and without this every unrelated agent:status re-ran formatMessage over the whole feed history.
{ memoizeOptions: { resultEqualityCheck: shallowEqual } },
);
const BrowserAgentInlineFeed: React.FC<Props> = ({ parentSessionId, browserId }) => {
@@ -176,6 +179,7 @@ const BrowserAgentInlineFeed: React.FC<Props> = ({ parentSessionId, browserId })
const scrollRef = useRef<HTMLDivElement>(null);
const fetchedForSession = useRef<string | null>(null);
const selectBrowserSessions = useMemo(makeSelectBrowserSessions, []);
const browserSessions = useAppSelector((state) =>
selectBrowserSessions(state, parentSessionId, browserId),
);
@@ -197,18 +201,18 @@ const BrowserAgentInlineFeed: React.FC<Props> = ({ parentSessionId, browserId })
shallowEqual,
);
// A child that arrived only through the trimmed session-list poll carries its message_count but no messages; fetch the full children so its history renders instead of showing a blank feed.
const needsChildFetch =
browserSessions.length === 0 ||
browserSessions.some((s) => (s.message_count ?? 0) > 0 && s.messages.length === 0);
// A child that arrived only through the trimmed session-list poll carries its message_count but no messages; fetch the full children so its history renders instead of showing a blank feed. Keyed by the unhydrated-children set (not one-shot per parent) so a NEW child appearing mid-run still hydrates, while the same set never refetches (no loop).
const unhydratedKey = browserSessions.length === 0
? `${parentSessionId}:empty`
: browserSessions.filter((s) => (s.message_count ?? 0) > 0 && s.messages.length === 0).map((s) => s.id).sort().join(',');
useEffect(() => {
if (needsChildFetch && fetchedForSession.current !== parentSessionId) {
fetchedForSession.current = parentSessionId;
dispatch(fetchBrowserAgentChildren(parentSessionId))
.unwrap()
.catch(() => { fetchedForSession.current = null; });
}
}, [needsChildFetch, parentSessionId, dispatch]);
if (!unhydratedKey.endsWith(':empty') && unhydratedKey === '') return;
if (fetchedForSession.current === unhydratedKey) return;
fetchedForSession.current = unhydratedKey;
dispatch(fetchBrowserAgentChildren(parentSessionId))
.unwrap()
.catch(() => { fetchedForSession.current = null; });
}, [unhydratedKey, parentSessionId, dispatch]);
const sessionsWithHistoricalEntries = useMemo(() => {
return browserSessions.map((session) => {
@@ -412,7 +416,8 @@ const BrowserAgentInlineFeed: React.FC<Props> = ({ parentSessionId, browserId })
);
};
const EntryRow: React.FC<{ entry: FeedEntry; accentColor: string; fc: FeedColors }> = ({ entry, accentColor, fc }) => {
// Memoized: the feed re-renders on every streamed token, and un-memoized rows re-render the ENTIRE lazy-loaded history per token (the "browser use = hella lag" bug).
const EntryRow = React.memo<{ entry: FeedEntry; accentColor: string; fc: FeedColors }>(({ entry, accentColor, fc }) => {
const c = useClaudeTokens();
if (entry.type === 'thought') {
@@ -493,7 +498,7 @@ const EntryRow: React.FC<{ entry: FeedEntry; accentColor: string; fc: FeedColors
}
return null;
};
});
const SessionStatusChip: React.FC<{ status: string }> = ({ status }) => {
const c = useClaudeTokens();