diff --git a/packages/app/src/components/agent-stream-view.tsx b/packages/app/src/components/agent-stream-view.tsx index 00096d541..d88021437 100644 --- a/packages/app/src/components/agent-stream-view.tsx +++ b/packages/app/src/components/agent-stream-view.tsx @@ -636,6 +636,14 @@ export function AgentStreamView({ ] ); + // Keep maintainVisibleContentPosition shape and values stable to avoid + // reconfiguring RN's internal anchoring logic during streaming transitions. + // We rely on our own follow-tail logic, so keep threshold at 0. + const maintainVisibleContentPosition = useMemo( + () => ({ minIndexForVisible: 0, autoscrollToTopThreshold: 0 }), + [] + ); + // FlatList's ListHeaderComponent renders at the *bottom* when inverted. // Without explicit spacing, the newest "head" rows (like Thinking/Setup tool badges) // can butt up directly against the most recent stream item. @@ -709,12 +717,7 @@ export function AgentStreamView({ ListEmptyComponent={listEmptyComponent} ListHeaderComponent={listHeaderComponent} extraData={flatListExtraData} - maintainVisibleContentPosition={ - // Disable when streaming and user is at bottom - we handle auto-scroll ourselves - agent.status === "running" && isNearBottom - ? undefined - : { minIndexForVisible: 0, autoscrollToTopThreshold: 40 } - } + maintainVisibleContentPosition={maintainVisibleContentPosition} initialNumToRender={12} windowSize={10} scrollEnabled={Platform.OS !== "web" || expandedInlineToolCallIds.size === 0} diff --git a/packages/app/src/contexts/session-context.tsx b/packages/app/src/contexts/session-context.tsx index 5fa3ef68c..fe339dfa8 100644 --- a/packages/app/src/contexts/session-context.tsx +++ b/packages/app/src/contexts/session-context.tsx @@ -803,16 +803,22 @@ function SessionProviderInternal({ ) { setAgentTimelineCursor(serverId, (prev) => { const current = prev.get(agentId); - const next = new Map(prev); - if (!current || current.epoch !== epoch) { - next.set(agentId, { epoch, startSeq: seq, endSeq: seq }); + if (current && current.epoch === epoch) { + // Fast-path: seq only extends the range during streaming. + // Skip the Map copy when nothing actually changes. + if (seq >= current.startSeq && seq <= current.endSeq) { + return prev; + } + const next = new Map(prev); + next.set(agentId, { + epoch, + startSeq: Math.min(current.startSeq, seq), + endSeq: Math.max(current.endSeq, seq), + }); return next; } - next.set(agentId, { - epoch, - startSeq: Math.min(current.startSeq, seq), - endSeq: Math.max(current.endSeq, seq), - }); + const next = new Map(prev); + next.set(agentId, { epoch, startSeq: seq, endSeq: seq }); return next; }); } diff --git a/packages/app/src/stores/session-store.ts b/packages/app/src/stores/session-store.ts index 89a797864..07fe57c2b 100644 --- a/packages/app/src/stores/session-store.ts +++ b/packages/app/src/stores/session-store.ts @@ -546,11 +546,15 @@ export const useSessionStore = create()( return prev; } logSessionStoreUpdate("setFocusedAgentId", serverId, { agentId }); + return { ...prev, sessions: { ...prev.sessions, - [serverId]: { ...session, focusedAgentId: agentId }, + [serverId]: { + ...session, + focusedAgentId: agentId, + }, }, }; }); @@ -563,7 +567,8 @@ export const useSessionStore = create()( if (!session) { return prev; } - const nextMessages = typeof messages === "function" ? messages(session.messages) : messages; + const nextMessages = + typeof messages === "function" ? messages(session.messages) : messages; if (session.messages === nextMessages) { return prev; }