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__"; newTabAgentOptionId?: "__new_tab_agent__";
buildPaneContentModel: (input: { buildPaneContentModel: (input: {
paneId: string; paneId: string;
isPaneFocused: boolean;
tab: WorkspaceTabDescriptor; tab: WorkspaceTabDescriptor;
}) => WorkspacePaneContentModel; }) => WorkspacePaneContentModel;
onFocusPane: (paneId: string) => void; onFocusPane: (paneId: string) => void;
@@ -751,6 +752,7 @@ function SplitPaneView({
activeTabDescriptor activeTabDescriptor
? buildPaneContentModel({ ? buildPaneContentModel({
paneId: pane.id, paneId: pane.id,
isPaneFocused: isFocused,
tab: activeTabDescriptor, tab: activeTabDescriptor,
}) })
: null, : null,
+39 -33
View File
@@ -1,5 +1,4 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useFocusEffect, useIsFocused } from "@react-navigation/native";
import { ActivityIndicator, Platform, Text, View } from "react-native"; import { ActivityIndicator, Platform, Text, View } from "react-native";
import ReanimatedAnimated from "react-native-reanimated"; import ReanimatedAnimated from "react-native-reanimated";
import { StyleSheet, useUnistyles } from "react-native-unistyles"; import { StyleSheet, useUnistyles } from "react-native-unistyles";
@@ -102,12 +101,13 @@ function useAgentPanelDescriptor(
} }
function AgentPanel() { function AgentPanel() {
const { serverId, target, openFileInWorkspace } = usePaneContext(); const { serverId, target, isPaneFocused, openFileInWorkspace } = usePaneContext();
invariant(target.kind === "agent", "AgentPanel requires agent target"); invariant(target.kind === "agent", "AgentPanel requires agent target");
return ( return (
<AgentPanelContent <AgentPanelContent
serverId={serverId} serverId={serverId}
agentId={target.agentId} agentId={target.agentId}
isPaneFocused={isPaneFocused}
onOpenWorkspaceFile={({ filePath }) => { onOpenWorkspaceFile={({ filePath }) => {
openFileInWorkspace(filePath); openFileInWorkspace(filePath);
}} }}
@@ -142,10 +142,12 @@ function isNotFoundErrorMessage(message: string): boolean {
function AgentPanelContent({ function AgentPanelContent({
serverId, serverId,
agentId, agentId,
isPaneFocused,
onOpenWorkspaceFile, onOpenWorkspaceFile,
}: { }: {
serverId: string; serverId: string;
agentId: string; agentId: string;
isPaneFocused: boolean;
onOpenWorkspaceFile?: (input: { filePath: string }) => void; onOpenWorkspaceFile?: (input: { filePath: string }) => void;
}) { }) {
const resolvedAgentId = agentId.trim() || undefined; const resolvedAgentId = agentId.trim() || undefined;
@@ -185,6 +187,7 @@ function AgentPanelContent({
<AgentPanelBody <AgentPanelBody
serverId={resolvedServerId} serverId={resolvedServerId}
agentId={resolvedAgentId} agentId={resolvedAgentId}
isPaneFocused={isPaneFocused}
client={runtimeClient} client={runtimeClient}
isConnected={runtimeIsConnected} isConnected={runtimeIsConnected}
connectionStatus={connectionStatus} connectionStatus={connectionStatus}
@@ -196,6 +199,7 @@ function AgentPanelContent({
function AgentPanelBody({ function AgentPanelBody({
serverId, serverId,
agentId, agentId,
isPaneFocused,
client, client,
isConnected, isConnected,
connectionStatus, connectionStatus,
@@ -203,6 +207,7 @@ function AgentPanelBody({
}: { }: {
serverId: string; serverId: string;
agentId?: string; agentId?: string;
isPaneFocused: boolean;
client: NonNullable<ReturnType<typeof useHostRuntimeSession>["client"]>; client: NonNullable<ReturnType<typeof useHostRuntimeSession>["client"]>;
isConnected: boolean; isConnected: boolean;
connectionStatus: HostRuntimeConnectionStatus; connectionStatus: HostRuntimeConnectionStatus;
@@ -213,7 +218,8 @@ function AgentPanelBody({
const { isArchivingAgent } = useArchiveAgent(); const { isArchivingAgent } = useArchiveAgent();
const streamViewRef = useRef<AgentStreamViewHandle>(null); const streamViewRef = useRef<AgentStreamViewHandle>(null);
const addImagesRef = useRef<((images: ImageAttachment[]) => void) | null>(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 reconnectToastArmedRef = useRef(false);
const initAttemptTokenRef = useRef(0); const initAttemptTokenRef = useRef(0);
const routeBottomAnchorRequestRef = useRef<{ const routeBottomAnchorRequestRef = useRef<{
@@ -314,21 +320,17 @@ function AgentPanelBody({
return filtered; return filtered;
}, [agentId, allPendingPermissions]); }, [agentId, allPendingPermissions]);
const setFocusedAgentId = useCallback(
(focusedAgentId: string | null) => {
useSessionStore.getState().setFocusedAgentId(serverId, focusedAgentId);
},
[serverId]
);
const attentionController = useAgentAttentionClear({ const attentionController = useAgentAttentionClear({
agentId, agentId,
client, client,
isConnected, isConnected,
requiresAttention: agent?.requiresAttention, requiresAttention: agent?.requiresAttention,
attentionReason: agent?.attentionReason, attentionReason: agent?.attentionReason,
isScreenFocused, isScreenFocused: isPaneFocused,
}); });
useEffect(() => {
clearOnAgentBlurRef.current = attentionController.clearOnAgentBlur;
}, [attentionController.clearOnAgentBlur]);
const { style: animatedKeyboardStyle } = useKeyboardShiftStyle({ const { style: animatedKeyboardStyle } = useKeyboardShiftStyle({
mode: "translate", mode: "translate",
@@ -383,34 +385,38 @@ function AgentPanelBody({
} }
}, [connectionStatus, panelToast.api]); }, [connectionStatus, panelToast.api]);
useFocusEffect( useEffect(() => {
useCallback(() => { if (!isPaneFocused || !agentId || !isConnected || !hasSession) {
if (!agentId || !isConnected || !hasSession) { return;
return; }
} ensureInitializedWithSyncErrorHandling("focus");
ensureInitializedWithSyncErrorHandling("focus"); }, [
}, [agentId, ensureInitializedWithSyncErrorHandling, hasSession, isConnected]) agentId,
); ensureInitializedWithSyncErrorHandling,
hasSession,
isConnected,
isPaneFocused,
]);
const isArchivingCurrentAgent = Boolean( const isArchivingCurrentAgent = Boolean(
agentId && agentId &&
isArchivingAgent({ serverId, agentId }) isArchivingAgent({ serverId, agentId })
); );
useFocusEffect( useEffect(() => {
useCallback(() => { if (wasPaneFocusedRef.current && !isPaneFocused) {
if (!agentId) { clearOnAgentBlurRef.current();
setFocusedAgentId(null); }
return; wasPaneFocusedRef.current = isPaneFocused;
} }, [isPaneFocused]);
setFocusedAgentId(agentId); useEffect(() => {
return () => { return () => {
attentionController.clearOnAgentBlur(); if (wasPaneFocusedRef.current) {
setFocusedAgentId(null); clearOnAgentBlurRef.current();
}; }
}, [agentId, attentionController, setFocusedAgentId]) };
); }, []);
const isInitializing = agentId ? isInitializingFromMap !== false : false; const isInitializing = agentId ? isInitializingFromMap !== false : false;
const isHistorySyncing = useMemo(() => { const isHistorySyncing = useMemo(() => {
@@ -797,7 +803,7 @@ function AgentPanelBody({
<AgentInputArea <AgentInputArea
agentId={agentId} agentId={agentId}
serverId={serverId} serverId={serverId}
autoFocus autoFocus={isPaneFocused}
isSubmitLoading={showPendingCreateSubmitLoading} isSubmitLoading={showPendingCreateSubmitLoading}
onAttentionInputFocus={attentionController.clearOnInputFocus} onAttentionInputFocus={attentionController.clearOnInputFocus}
onAttentionPromptSend={attentionController.clearOnPromptSend} onAttentionPromptSend={attentionController.clearOnPromptSend}
+1
View File
@@ -6,6 +6,7 @@ export interface PaneContextValue {
serverId: string; serverId: string;
workspaceId: string; workspaceId: string;
tabId: string; tabId: string;
isPaneFocused: boolean;
target: WorkspaceTabTarget; target: WorkspaceTabTarget;
openTab(target: WorkspaceTabTarget): void; openTab(target: WorkspaceTabTarget): void;
closeCurrentTab(): void; closeCurrentTab(): void;
@@ -15,6 +15,7 @@ export interface BuildWorkspacePaneContentModelInput {
tab: WorkspaceTabDescriptor; tab: WorkspaceTabDescriptor;
normalizedServerId: string; normalizedServerId: string;
normalizedWorkspaceId: string; normalizedWorkspaceId: string;
isPaneFocused: boolean;
onOpenTab: (target: WorkspaceTabDescriptor["target"]) => void; onOpenTab: (target: WorkspaceTabDescriptor["target"]) => void;
onCloseCurrentTab: () => void; onCloseCurrentTab: () => void;
onRetargetCurrentTab: (target: WorkspaceTabDescriptor["target"]) => void; onRetargetCurrentTab: (target: WorkspaceTabDescriptor["target"]) => void;
@@ -25,6 +26,7 @@ export function buildWorkspacePaneContentModel({
tab, tab,
normalizedServerId, normalizedServerId,
normalizedWorkspaceId, normalizedWorkspaceId,
isPaneFocused,
onOpenTab, onOpenTab,
onCloseCurrentTab, onCloseCurrentTab,
onRetargetCurrentTab, onRetargetCurrentTab,
@@ -40,6 +42,7 @@ export function buildWorkspacePaneContentModel({
serverId: normalizedServerId, serverId: normalizedServerId,
workspaceId: normalizedWorkspaceId, workspaceId: normalizedWorkspaceId,
tabId: tab.tabId, tabId: tab.tabId,
isPaneFocused,
target: tab.target, target: tab.target,
openTab: onOpenTab, openTab: onOpenTab,
closeCurrentTab: onCloseCurrentTab, closeCurrentTab: onCloseCurrentTab,
@@ -873,6 +873,24 @@ function WorkspaceScreenContent({
}), }),
[uiTabs, workspaceLayout] [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( const ensureWorkspaceTab = useCallback(
function ensureWorkspaceTab(target: WorkspaceTabTarget): string | null { function ensureWorkspaceTab(target: WorkspaceTabTarget): string | null {
@@ -1833,12 +1851,14 @@ function WorkspaceScreenContent({
(input: { (input: {
tab: WorkspaceTabDescriptor; tab: WorkspaceTabDescriptor;
paneId?: string | null; paneId?: string | null;
isPaneFocused: boolean;
focusPaneBeforeOpen?: boolean; focusPaneBeforeOpen?: boolean;
}) => }) =>
buildWorkspacePaneContentModel({ buildWorkspacePaneContentModel({
tab: input.tab, tab: input.tab,
normalizedServerId, normalizedServerId,
normalizedWorkspaceId, normalizedWorkspaceId,
isPaneFocused: input.isPaneFocused,
onOpenTab: (target) => { onOpenTab: (target) => {
if (!persistenceKey) { if (!persistenceKey) {
return; return;
@@ -1885,6 +1905,7 @@ function WorkspaceScreenContent({
? buildPaneContentModel({ ? buildPaneContentModel({
tab: activeTabDescriptor, tab: activeTabDescriptor,
paneId: focusedPaneTabState.pane?.id ?? null, paneId: focusedPaneTabState.pane?.id ?? null,
isPaneFocused: true,
focusPaneBeforeOpen: false, focusPaneBeforeOpen: false,
}) })
: null, : null,
@@ -2159,13 +2180,14 @@ function WorkspaceScreenContent({
onCloseOtherTabs={handleCloseOtherTabsInPane} onCloseOtherTabs={handleCloseOtherTabsInPane}
onSelectNewTabOption={handleSelectNewTabOption} onSelectNewTabOption={handleSelectNewTabOption}
newTabAgentOptionId={NEW_TAB_AGENT_OPTION_ID} newTabAgentOptionId={NEW_TAB_AGENT_OPTION_ID}
buildPaneContentModel={({ paneId, tab }) => buildPaneContentModel={({ paneId, tab, isPaneFocused }) =>
buildPaneContentModel({ buildPaneContentModel({
tab, tab,
paneId, paneId,
focusPaneBeforeOpen: true, isPaneFocused,
}) focusPaneBeforeOpen: true,
} })
}
onFocusPane={(paneId) => { onFocusPane={(paneId) => {
focusWorkspacePane(persistenceKey, paneId); focusWorkspacePane(persistenceKey, paneId);
}} }}