diff --git a/packages/app/src/app/agent/[id].tsx b/packages/app/src/app/agent/[id].tsx index 7201106d1..23e866ac1 100644 --- a/packages/app/src/app/agent/[id].tsx +++ b/packages/app/src/app/agent/[id].tsx @@ -15,7 +15,14 @@ export default function AgentScreen() { const { theme } = useUnistyles(); const insets = useSafeAreaInsets(); const { id } = useLocalSearchParams<{ id: string }>(); - const { agents, agentStreamState, pendingPermissions, respondToPermission, initializeAgent } = useSession(); + const { + agents, + agentStreamState, + initializingAgents, + pendingPermissions, + respondToPermission, + initializeAgent, + } = useSession(); const { registerFooterControls, unregisterFooterControls } = useFooterControls(); // Keyboard animation @@ -41,15 +48,29 @@ export default function AgentScreen() { Array.from(pendingPermissions.entries()).filter(([_, perm]) => perm.agentId === id) ); - // Agent is initializing if we don't have stream state yet - const isInitializing = id ? !agentStreamState.has(id) : false; + const hasStreamState = id ? agentStreamState.has(id) : false; + const initializationState = id ? initializingAgents.get(id) : undefined; + const isInitializing = id + ? initializationState !== undefined + ? initializationState + : !hasStreamState + : false; useEffect(() => { if (!id) { return; } + + if (initializationState !== undefined) { + return; + } + + if (hasStreamState) { + return; + } + initializeAgent({ agentId: id }); - }, [id, initializeAgent]); + }, [id, initializeAgent, initializationState, hasStreamState]); const agentControls = useMemo(() => { if (!id) return null; diff --git a/packages/app/src/components/agent-stream-view.tsx b/packages/app/src/components/agent-stream-view.tsx index 552bb7fbe..5aa41e7e9 100644 --- a/packages/app/src/components/agent-stream-view.tsx +++ b/packages/app/src/components/agent-stream-view.tsx @@ -41,14 +41,39 @@ export function AgentStreamView({ useState(null); const [isNearBottom, setIsNearBottom] = useState(true); const hasScrolledInitially = useRef(false); + const hasAutoScrolledOnce = useRef(false); + + useEffect(() => { + hasScrolledInitially.current = false; + hasAutoScrolledOnce.current = false; + }, [agentId]); // Scroll to bottom immediately on initial load, then animate for new messages useEffect(() => { - if (isNearBottom) { - const shouldAnimate = hasScrolledInitially.current; - scrollViewRef.current?.scrollToEnd({ animated: shouldAnimate }); - hasScrolledInitially.current = true; + if (streamItems.length === 0) { + return; } + + const scrollView = scrollViewRef.current; + if (!scrollView) { + return; + } + + if (!hasAutoScrolledOnce.current) { + // First load: jump to bottom without animation and remember we did it + scrollView.scrollToEnd({ animated: false }); + hasScrolledInitially.current = true; + hasAutoScrolledOnce.current = true; + return; + } + + if (!isNearBottom) { + return; + } + + const shouldAnimate = hasScrolledInitially.current; + scrollView.scrollToEnd({ animated: shouldAnimate }); + hasScrolledInitially.current = true; }, [streamItems, isNearBottom]); function handleOpenToolCallDetails(toolCall: SelectedToolCall) { diff --git a/packages/app/src/contexts/session-context.tsx b/packages/app/src/contexts/session-context.tsx index 2adf476ed..a7af862ac 100644 --- a/packages/app/src/contexts/session-context.tsx +++ b/packages/app/src/contexts/session-context.tsx @@ -100,6 +100,7 @@ interface SessionContextValue { setCurrentAssistantMessage: (message: string) => void; agentStreamState: Map; setAgentStreamState: (state: Map | ((prev: Map) => Map)) => void; + initializingAgents: Map; // Agents and commands agents: Map; @@ -153,6 +154,7 @@ export function SessionProvider({ children, serverUrl }: SessionProviderProps) { const [messages, setMessages] = useState([]); const [currentAssistantMessage, setCurrentAssistantMessage] = useState(""); const [agentStreamState, setAgentStreamState] = useState>(new Map()); + const [initializingAgents, setInitializingAgents] = useState>(new Map()); const [agents, setAgents] = useState>(new Map()); const [commands, setCommands] = useState>(new Map()); @@ -198,6 +200,7 @@ export function SessionProvider({ children, serverUrl }: SessionProviderProps) { setCommands(new Map(commandsList.map((c) => [c.id, c as Command]))); setAgentStreamState(new Map()); setAgentUpdates(new Map()); + setInitializingAgents(new Map()); }); // Agent created @@ -287,6 +290,12 @@ export function SessionProvider({ children, serverUrl }: SessionProviderProps) { ); return new Map(prev).set(agentId, reconstructedStream); }); + + setInitializingAgents((prev) => { + const next = new Map(prev); + next.set(agentId, false); + return next; + }); }); // Agent status update (mode changes, title changes, etc.) @@ -593,6 +602,12 @@ export function SessionProvider({ children, serverUrl }: SessionProviderProps) { }, [ws, audioPlayer, setIsPlayingAudio]); const initializeAgent = useCallback(({ agentId, requestId }: { agentId: string; requestId?: string }) => { + setInitializingAgents((prev) => { + const next = new Map(prev); + next.set(agentId, true); + return next; + }); + const msg: WSInboundMessage = { type: "session", message: { @@ -766,6 +781,7 @@ export function SessionProvider({ children, serverUrl }: SessionProviderProps) { setCurrentAssistantMessage, agentStreamState, setAgentStreamState, + initializingAgents, agents, setAgents, commands,