refactor(app): drive agent panel focus from panes

This commit is contained in:
Mohamed Boudra
2026-03-20 18:46:23 +07:00
parent c7fe944e73
commit e0f9b33d23
5 changed files with 74 additions and 40 deletions
@@ -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,
+39 -33
View File
@@ -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 (
<AgentPanelContent
serverId={serverId}
agentId={target.agentId}
isPaneFocused={isPaneFocused}
onOpenWorkspaceFile={({ filePath }) => {
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({
<AgentPanelBody
serverId={resolvedServerId}
agentId={resolvedAgentId}
isPaneFocused={isPaneFocused}
client={runtimeClient}
isConnected={runtimeIsConnected}
connectionStatus={connectionStatus}
@@ -196,6 +199,7 @@ function AgentPanelContent({
function AgentPanelBody({
serverId,
agentId,
isPaneFocused,
client,
isConnected,
connectionStatus,
@@ -203,6 +207,7 @@ function AgentPanelBody({
}: {
serverId: string;
agentId?: string;
isPaneFocused: boolean;
client: NonNullable<ReturnType<typeof useHostRuntimeSession>["client"]>;
isConnected: boolean;
connectionStatus: HostRuntimeConnectionStatus;
@@ -213,7 +218,8 @@ function AgentPanelBody({
const { isArchivingAgent } = useArchiveAgent();
const streamViewRef = useRef<AgentStreamViewHandle>(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({
<AgentInputArea
agentId={agentId}
serverId={serverId}
autoFocus
autoFocus={isPaneFocused}
isSubmitLoading={showPendingCreateSubmitLoading}
onAttentionInputFocus={attentionController.clearOnInputFocus}
onAttentionPromptSend={attentionController.clearOnPromptSend}
+1
View File
@@ -6,6 +6,7 @@ export interface PaneContextValue {
serverId: string;
workspaceId: string;
tabId: string;
isPaneFocused: boolean;
target: WorkspaceTabTarget;
openTab(target: WorkspaceTabTarget): void;
closeCurrentTab(): void;
@@ -15,6 +15,7 @@ export interface BuildWorkspacePaneContentModelInput {
tab: WorkspaceTabDescriptor;
normalizedServerId: string;
normalizedWorkspaceId: string;
isPaneFocused: boolean;
onOpenTab: (target: WorkspaceTabDescriptor["target"]) => 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,
@@ -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);
}}