diff --git a/packages/app/src/components/split-container.tsx b/packages/app/src/components/split-container.tsx index 75c56ebd0..23215f177 100644 --- a/packages/app/src/components/split-container.tsx +++ b/packages/app/src/components/split-container.tsx @@ -72,6 +72,7 @@ interface SplitContainerProps { newTabAgentOptionId?: "__new_tab_agent__"; buildPaneContentModel: (input: { paneId: string; + isPaneFocused: boolean; tab: WorkspaceTabDescriptor; }) => WorkspacePaneContentModel; onFocusPane: (paneId: string) => void; @@ -751,6 +752,7 @@ function SplitPaneView({ activeTabDescriptor ? buildPaneContentModel({ paneId: pane.id, + isPaneFocused: isFocused, tab: activeTabDescriptor, }) : null, diff --git a/packages/app/src/panels/agent-panel.tsx b/packages/app/src/panels/agent-panel.tsx index 519c61f29..2c4f545be 100644 --- a/packages/app/src/panels/agent-panel.tsx +++ b/packages/app/src/panels/agent-panel.tsx @@ -1,5 +1,4 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; -import { useFocusEffect, useIsFocused } from "@react-navigation/native"; import { ActivityIndicator, Platform, Text, View } from "react-native"; import ReanimatedAnimated from "react-native-reanimated"; import { StyleSheet, useUnistyles } from "react-native-unistyles"; @@ -102,12 +101,13 @@ function useAgentPanelDescriptor( } function AgentPanel() { - const { serverId, target, openFileInWorkspace } = usePaneContext(); + const { serverId, target, isPaneFocused, openFileInWorkspace } = usePaneContext(); invariant(target.kind === "agent", "AgentPanel requires agent target"); return ( { openFileInWorkspace(filePath); }} @@ -142,10 +142,12 @@ function isNotFoundErrorMessage(message: string): boolean { function AgentPanelContent({ serverId, agentId, + isPaneFocused, onOpenWorkspaceFile, }: { serverId: string; agentId: string; + isPaneFocused: boolean; onOpenWorkspaceFile?: (input: { filePath: string }) => void; }) { const resolvedAgentId = agentId.trim() || undefined; @@ -185,6 +187,7 @@ function AgentPanelContent({ ["client"]>; isConnected: boolean; connectionStatus: HostRuntimeConnectionStatus; @@ -213,7 +218,8 @@ function AgentPanelBody({ const { isArchivingAgent } = useArchiveAgent(); const streamViewRef = useRef(null); const addImagesRef = useRef<((images: ImageAttachment[]) => void) | null>(null); - const isScreenFocused = useIsFocused(); + const clearOnAgentBlurRef = useRef<() => void>(() => {}); + const wasPaneFocusedRef = useRef(isPaneFocused); const reconnectToastArmedRef = useRef(false); const initAttemptTokenRef = useRef(0); const routeBottomAnchorRequestRef = useRef<{ @@ -314,21 +320,17 @@ function AgentPanelBody({ return filtered; }, [agentId, allPendingPermissions]); - const setFocusedAgentId = useCallback( - (focusedAgentId: string | null) => { - useSessionStore.getState().setFocusedAgentId(serverId, focusedAgentId); - }, - [serverId] - ); - const attentionController = useAgentAttentionClear({ agentId, client, isConnected, requiresAttention: agent?.requiresAttention, attentionReason: agent?.attentionReason, - isScreenFocused, + isScreenFocused: isPaneFocused, }); + useEffect(() => { + clearOnAgentBlurRef.current = attentionController.clearOnAgentBlur; + }, [attentionController.clearOnAgentBlur]); const { style: animatedKeyboardStyle } = useKeyboardShiftStyle({ mode: "translate", @@ -383,34 +385,38 @@ function AgentPanelBody({ } }, [connectionStatus, panelToast.api]); - useFocusEffect( - useCallback(() => { - if (!agentId || !isConnected || !hasSession) { - return; - } - ensureInitializedWithSyncErrorHandling("focus"); - }, [agentId, ensureInitializedWithSyncErrorHandling, hasSession, isConnected]) - ); + useEffect(() => { + if (!isPaneFocused || !agentId || !isConnected || !hasSession) { + return; + } + ensureInitializedWithSyncErrorHandling("focus"); + }, [ + agentId, + ensureInitializedWithSyncErrorHandling, + hasSession, + isConnected, + isPaneFocused, + ]); const isArchivingCurrentAgent = Boolean( agentId && isArchivingAgent({ serverId, agentId }) ); - useFocusEffect( - useCallback(() => { - if (!agentId) { - setFocusedAgentId(null); - return; - } + useEffect(() => { + if (wasPaneFocusedRef.current && !isPaneFocused) { + clearOnAgentBlurRef.current(); + } + wasPaneFocusedRef.current = isPaneFocused; + }, [isPaneFocused]); - setFocusedAgentId(agentId); - return () => { - attentionController.clearOnAgentBlur(); - setFocusedAgentId(null); - }; - }, [agentId, attentionController, setFocusedAgentId]) - ); + useEffect(() => { + return () => { + if (wasPaneFocusedRef.current) { + clearOnAgentBlurRef.current(); + } + }; + }, []); const isInitializing = agentId ? isInitializingFromMap !== false : false; const isHistorySyncing = useMemo(() => { @@ -797,7 +803,7 @@ function AgentPanelBody({ void; onCloseCurrentTab: () => void; onRetargetCurrentTab: (target: WorkspaceTabDescriptor["target"]) => void; @@ -25,6 +26,7 @@ export function buildWorkspacePaneContentModel({ tab, normalizedServerId, normalizedWorkspaceId, + isPaneFocused, onOpenTab, onCloseCurrentTab, onRetargetCurrentTab, @@ -40,6 +42,7 @@ export function buildWorkspacePaneContentModel({ serverId: normalizedServerId, workspaceId: normalizedWorkspaceId, tabId: tab.tabId, + isPaneFocused, target: tab.target, openTab: onOpenTab, closeCurrentTab: onCloseCurrentTab, diff --git a/packages/app/src/screens/workspace/workspace-screen.tsx b/packages/app/src/screens/workspace/workspace-screen.tsx index d8f714daf..103abb5ec 100644 --- a/packages/app/src/screens/workspace/workspace-screen.tsx +++ b/packages/app/src/screens/workspace/workspace-screen.tsx @@ -873,6 +873,24 @@ function WorkspaceScreenContent({ }), [uiTabs, workspaceLayout] ); + const setFocusedAgentId = useSessionStore((state) => state.setFocusedAgentId); + const focusedPaneAgentId = useMemo(() => { + const target = focusedPaneTabState.activeTab?.descriptor.target; + if (target?.kind !== "agent") { + return null; + } + return target.agentId; + }, [focusedPaneTabState.activeTab]); + + useEffect(() => { + setFocusedAgentId(normalizedServerId, focusedPaneAgentId); + }, [focusedPaneAgentId, normalizedServerId, setFocusedAgentId]); + + useEffect(() => { + return () => { + setFocusedAgentId(normalizedServerId, null); + }; + }, [normalizedServerId, setFocusedAgentId]); const ensureWorkspaceTab = useCallback( function ensureWorkspaceTab(target: WorkspaceTabTarget): string | null { @@ -1833,12 +1851,14 @@ function WorkspaceScreenContent({ (input: { tab: WorkspaceTabDescriptor; paneId?: string | null; + isPaneFocused: boolean; focusPaneBeforeOpen?: boolean; }) => buildWorkspacePaneContentModel({ tab: input.tab, normalizedServerId, normalizedWorkspaceId, + isPaneFocused: input.isPaneFocused, onOpenTab: (target) => { if (!persistenceKey) { return; @@ -1885,6 +1905,7 @@ function WorkspaceScreenContent({ ? buildPaneContentModel({ tab: activeTabDescriptor, paneId: focusedPaneTabState.pane?.id ?? null, + isPaneFocused: true, focusPaneBeforeOpen: false, }) : null, @@ -2159,13 +2180,14 @@ function WorkspaceScreenContent({ onCloseOtherTabs={handleCloseOtherTabsInPane} onSelectNewTabOption={handleSelectNewTabOption} newTabAgentOptionId={NEW_TAB_AGENT_OPTION_ID} - buildPaneContentModel={({ paneId, tab }) => - buildPaneContentModel({ - tab, - paneId, - focusPaneBeforeOpen: true, - }) - } + buildPaneContentModel={({ paneId, tab, isPaneFocused }) => + buildPaneContentModel({ + tab, + paneId, + isPaneFocused, + focusPaneBeforeOpen: true, + }) + } onFocusPane={(paneId) => { focusWorkspacePane(persistenceKey, paneId); }}