refactor(app): drive agent panel focus from panes
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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);
|
||||||
}}
|
}}
|
||||||
|
|||||||
Reference in New Issue
Block a user