diff --git a/packages/app/src/app/agent/[serverId]/[agentId].tsx b/packages/app/src/app/agent/[serverId]/[agentId].tsx
index a6645bb75..46e1e6179 100644
--- a/packages/app/src/app/agent/[serverId]/[agentId].tsx
+++ b/packages/app/src/app/agent/[serverId]/[agentId].tsx
@@ -19,13 +19,14 @@ import { BackHeader } from "@/components/headers/back-header";
import { AgentStreamView } from "@/components/agent-stream-view";
import { AgentInputArea } from "@/components/agent-input-area";
import { CreateAgentModal, ImportAgentModal, type CreateAgentInitialValues } from "@/components/create-agent-modal";
-import type { Agent, SessionContextValue } from "@/contexts/session-context";
import { useFooterControls } from "@/contexts/footer-controls-context";
import { useDaemonConnections } from "@/contexts/daemon-connections-context";
import type { ConnectionStatus } from "@/contexts/daemon-connections-context";
import { formatConnectionStatus } from "@/utils/daemons";
-import { useDaemonSession } from "@/hooks/use-daemon-session";
+import { useSessionStore } from "@/stores/session-store";
+import type { Agent } from "@/stores/session-store";
import { useDaemonRequest } from "@/hooks/use-daemon-request";
+import type { StreamItem } from "@/types/stream";
import type { SessionOutboundMessage } from "@server/server/messages";
import {
buildAgentNavigationKey,
@@ -35,6 +36,7 @@ import {
} from "@/utils/navigation-timing";
const DROPDOWN_WIDTH = 220;
+const EMPTY_STREAM_ITEMS: StreamItem[] = [];
type GitRepoInfoResponseMessage = Extract<
SessionOutboundMessage,
@@ -90,13 +92,11 @@ export default function AgentScreen() {
const resolvedAgentId = typeof agentId === "string" ? agentId : undefined;
const resolvedServerId = typeof serverId === "string" ? serverId : undefined;
const { connectionStates } = useDaemonConnections();
- const hasConnectionEntry = resolvedServerId ? connectionStates.has(resolvedServerId) : false;
- const session = useDaemonSession(resolvedServerId, {
- suppressUnavailableAlert: true,
- allowUnavailable: true,
- });
- const sessionServerId = session?.serverId ?? null;
+ // Check if session exists for this serverId
+ const hasSession = useSessionStore((state) =>
+ resolvedServerId ? !!state.sessions[resolvedServerId] : false
+ );
const connectionServerId = resolvedServerId ?? null;
const connection = connectionServerId ? connectionStates.get(connectionServerId) : null;
@@ -107,7 +107,7 @@ export default function AgentScreen() {
const lastConnectionError = connection?.lastError ?? null;
const handleBackToHome = useCallback(() => {
- const targetServerId = resolvedServerId ?? sessionServerId;
+ const targetServerId = resolvedServerId;
const targetAgentId = resolvedAgentId ?? null;
if (targetServerId && targetAgentId) {
startNavigationTiming(HOME_NAVIGATION_KEY, {
@@ -127,10 +127,10 @@ export default function AgentScreen() {
});
}
router.replace("/");
- }, [resolvedAgentId, resolvedServerId, router, sessionServerId]);
+ }, [resolvedAgentId, resolvedServerId, router]);
- const focusServerId = resolvedServerId ?? sessionServerId;
- const navigationStatus = session ? "ready" : "session_unavailable";
+ const focusServerId = resolvedServerId;
+ const navigationStatus = hasSession ? "ready" : "session_unavailable";
useFocusEffect(
useCallback(() => {
@@ -145,7 +145,7 @@ export default function AgentScreen() {
}, [focusServerId, navigationStatus, resolvedAgentId])
);
- if (!session) {
+ if (!hasSession || !resolvedServerId) {
return (
);
}
type AgentScreenContentProps = {
- session: import("@/hooks/use-daemon-session").DaemonSession;
+ serverId: string;
agentId?: string;
- routeServerId: string;
onBack: () => void;
};
-function AgentScreenContent({ session, agentId, routeServerId, onBack }: AgentScreenContentProps) {
+function AgentScreenContent({ serverId, agentId, onBack }: AgentScreenContentProps) {
const { theme } = useUnistyles();
const insets = useSafeAreaInsets();
const router = useRouter();
@@ -192,18 +188,53 @@ function AgentScreenContent({ session, agentId, routeServerId, onBack }: AgentSc
const [createAgentInitialValues, setCreateAgentInitialValues] =
useState();
- const {
- agents,
- agentStreamState,
- initializingAgents,
- pendingPermissions,
- initializeAgent,
- refreshAgent,
- setFocusedAgentId,
- ws,
- } = session;
const resolvedAgentId = agentId;
+ // Select only the specific agent
+ const agent = useSessionStore((state) =>
+ resolvedAgentId ? state.sessions[serverId]?.agents?.get(resolvedAgentId) : undefined
+ );
+
+ // Select only the specific stream state - use stable empty array to avoid infinite loop
+ const streamItemsRaw = useSessionStore((state) =>
+ resolvedAgentId ? state.sessions[serverId]?.agentStreamState?.get(resolvedAgentId) : undefined
+ );
+ const streamItems = streamItemsRaw ?? EMPTY_STREAM_ITEMS;
+
+ // Select only the specific initializing state
+ const isInitializingFromMap = useSessionStore((state) =>
+ resolvedAgentId ? (state.sessions[serverId]?.initializingAgents?.get(resolvedAgentId) ?? false) : false
+ );
+
+ // Select raw pending permissions - filter with useMemo to avoid new Map on every render
+ const allPendingPermissions = useSessionStore((state) =>
+ state.sessions[serverId]?.pendingPermissions
+ );
+ const pendingPermissions = useMemo(() => {
+ if (!allPendingPermissions || !resolvedAgentId) return new Map();
+ const filtered = new Map();
+ for (const [key, perm] of allPendingPermissions) {
+ if (perm.agentId === resolvedAgentId) {
+ filtered.set(key, perm);
+ }
+ }
+ return filtered;
+ }, [allPendingPermissions, resolvedAgentId]);
+
+ // Get ws for connection status
+ const ws = useSessionStore((state) => state.sessions[serverId]?.ws);
+
+ // Get methods
+ const methods = useSessionStore((state) => state.sessions[serverId]?.methods);
+ const initializeAgent = methods?.initializeAgent;
+ const refreshAgent = methods?.refreshAgent;
+ const setFocusedAgentId = useCallback(
+ (agentId: string | null) => {
+ useSessionStore.getState().setFocusedAgentId(serverId, agentId);
+ },
+ [serverId]
+ );
+
const { height: keyboardHeight } = useReanimatedKeyboardAnimation();
const bottomInset = useSharedValue(insets.bottom);
@@ -220,11 +251,6 @@ function AgentScreenContent({ session, agentId, routeServerId, onBack }: AgentSc
};
});
- const agent = resolvedAgentId ? agents.get(resolvedAgentId) : undefined;
- const streamItems = resolvedAgentId ? agentStreamState.get(resolvedAgentId) || [] : [];
- const agentPermissions = new Map(
- Array.from(pendingPermissions.entries()).filter(([_, perm]) => perm.agentId === resolvedAgentId)
- );
const agentModel = extractAgentModel(agent);
const modelDisplayValue = agentModel ?? "Unknown";
@@ -289,26 +315,28 @@ function AgentScreenContent({ session, agentId, routeServerId, onBack }: AgentSc
};
}, [resolvedAgentId, setFocusedAgentId]);
- const hasStreamState = resolvedAgentId ? agentStreamState.has(resolvedAgentId) : false;
- const initializationState = resolvedAgentId ? initializingAgents.get(resolvedAgentId) : undefined;
+ const hasStreamState = useSessionStore((state) =>
+ resolvedAgentId ? state.sessions[serverId]?.agentStreamState?.has(resolvedAgentId) ?? false : false
+ );
+
const isInitializing = resolvedAgentId
- ? initializationState !== undefined
- ? initializationState
+ ? isInitializingFromMap !== false
+ ? isInitializingFromMap
: !hasStreamState
: false;
useEffect(() => {
- if (!resolvedAgentId) {
+ if (!resolvedAgentId || !initializeAgent) {
return;
}
console.log("[AgentScreen] initializeAgent effect", {
agentId: resolvedAgentId,
hasStreamState,
- initializationState,
+ isInitializingFromMap,
});
- if (initializationState !== undefined) {
+ if (isInitializingFromMap !== false) {
return;
}
@@ -317,12 +345,12 @@ function AgentScreenContent({ session, agentId, routeServerId, onBack }: AgentSc
}
initializeAgent({ agentId: resolvedAgentId });
- }, [resolvedAgentId, initializeAgent, initializationState, hasStreamState]);
+ }, [resolvedAgentId, initializeAgent, isInitializingFromMap, hasStreamState]);
const agentControls = useMemo(() => {
if (!resolvedAgentId) return null;
- return ;
- }, [resolvedAgentId, routeServerId]);
+ return ;
+ }, [resolvedAgentId, serverId]);
useEffect(() => {
if (!agentControls || !agent || isInitializing) {
@@ -416,11 +444,11 @@ function AgentScreenContent({ session, agentId, routeServerId, onBack }: AgentSc
pathname: "/git-diff",
params: {
agentId: resolvedAgentId,
- serverId: routeServerId,
+ serverId: serverId,
},
});
}
- }, [resolvedAgentId, routeServerId, router, handleCloseMenu]);
+ }, [resolvedAgentId, serverId, router, handleCloseMenu]);
const handleBrowseFiles = useCallback(() => {
handleCloseMenu();
@@ -429,14 +457,14 @@ function AgentScreenContent({ session, agentId, routeServerId, onBack }: AgentSc
pathname: "/file-explorer",
params: {
agentId: resolvedAgentId,
- serverId: routeServerId,
+ serverId: serverId,
},
});
}
- }, [handleCloseMenu, resolvedAgentId, routeServerId, router]);
+ }, [handleCloseMenu, resolvedAgentId, serverId, router]);
const handleRefreshAgent = useCallback(() => {
- if (!resolvedAgentId) {
+ if (!resolvedAgentId || !refreshAgent) {
return;
}
handleCloseMenu();
@@ -475,7 +503,7 @@ function AgentScreenContent({ session, agentId, routeServerId, onBack }: AgentSc
isVisible={showCreateAgentModal}
onClose={handleCloseCreateAgentModal}
initialValues={createAgentInitialValues}
- serverId={routeServerId}
+ serverId={serverId}
/>
);
@@ -483,7 +511,7 @@ function AgentScreenContent({ session, agentId, routeServerId, onBack }: AgentSc
);
@@ -529,10 +557,10 @@ function AgentScreenContent({ session, agentId, routeServerId, onBack }: AgentSc
) : (
)}
diff --git a/packages/app/src/app/file-explorer.tsx b/packages/app/src/app/file-explorer.tsx
index 21b55bd4c..bb9b5533e 100644
--- a/packages/app/src/app/file-explorer.tsx
+++ b/packages/app/src/app/file-explorer.tsx
@@ -29,11 +29,11 @@ import {
X,
} from "lucide-react-native";
import { BackHeader } from "@/components/headers/back-header";
-import type { ExplorerEntry, SessionContextValue } from "@/contexts/session-context";
+import type { ExplorerEntry } from "@/stores/session-store";
import type { ConnectionStatus } from "@/contexts/daemon-connections-context";
import { useDaemonConnections } from "@/contexts/daemon-connections-context";
import { formatConnectionStatus } from "@/utils/daemons";
-import { useDaemonSession } from "@/hooks/use-daemon-session";
+import { useSessionStore } from "@/stores/session-store";
export default function FileExplorerScreen() {
const {
@@ -49,14 +49,14 @@ export default function FileExplorerScreen() {
}>();
const resolvedServerId = typeof serverId === "string" ? serverId : undefined;
const { connectionStates } = useDaemonConnections();
- const session = useDaemonSession(resolvedServerId, {
- suppressUnavailableAlert: true,
- allowUnavailable: true,
- });
+
+ const session = useSessionStore((state) =>
+ resolvedServerId ? state.sessions[resolvedServerId] : undefined
+ );
const connectionServerId = resolvedServerId ?? null;
const connection = connectionServerId ? connectionStates.get(connectionServerId) : null;
- const serverLabel = connection?.daemon.label ?? connectionServerId ?? session?.serverId ?? "Selected host";
+ const serverLabel = connection?.daemon.label ?? connectionServerId ?? resolvedServerId ?? "Selected host";
const connectionStatus = connection?.status ?? "idle";
const connectionStatusLabel = formatConnectionStatus(connectionStatus);
const lastError = connection?.lastError ?? null;
@@ -78,21 +78,19 @@ export default function FileExplorerScreen() {
return (
);
}
type FileExplorerContentProps = {
- session: import("@/hooks/use-daemon-session").DaemonSession;
+ serverId: string;
agentId?: string;
pathParamRaw?: string | string[];
fileParamRaw?: string | string[];
- routeServerId: string;
};
type FileExplorerSessionUnavailableProps = {
@@ -105,20 +103,29 @@ type FileExplorerSessionUnavailableProps = {
};
function FileExplorerContent({
- session,
+ serverId,
agentId,
pathParamRaw,
fileParamRaw,
- routeServerId,
}: FileExplorerContentProps) {
const { theme } = useUnistyles();
- const {
- agents,
- fileExplorer,
- requestDirectoryListing,
- requestFilePreview,
- navigateExplorerBack,
- } = session;
+
+ const agent = useSessionStore((state) =>
+ agentId && state.sessions[serverId]
+ ? state.sessions[serverId]?.agents.get(agentId)
+ : undefined
+ );
+
+ const explorerState = useSessionStore((state) =>
+ agentId && state.sessions[serverId]
+ ? state.sessions[serverId]?.fileExplorer.get(agentId)
+ : undefined
+ );
+
+ const methods = useSessionStore((state) => state.sessions[serverId]?.methods);
+ const requestDirectoryListing = methods?.requestDirectoryListing;
+ const requestFilePreview = methods?.requestFilePreview;
+ const navigateExplorerBack = methods?.navigateExplorerBack;
const [viewMode, setViewMode] = useState<"list" | "grid">("list");
const [selectedEntryPath, setSelectedEntryPath] = useState(null);
const pendingPathParamRef = useRef(null);
@@ -136,9 +143,6 @@ function FileExplorerContent({
const derivedDirectoryFromFile = normalizedFileParam
? deriveDirectoryFromFile(normalizedFileParam)
: null;
-
- const agent = agentId ? agents.get(agentId) : undefined;
- const explorerState = agentId ? fileExplorer.get(agentId) : undefined;
const history = explorerState?.history ?? [];
const lastKnownDirectory = history[history.length - 1];
const rememberedDirectory = explorerState?.lastVisitedPath;
@@ -237,7 +241,7 @@ function FileExplorerContent({
}, [activePath]);
useEffect(() => {
- if (!agentId || !initialTargetDirectory) {
+ if (!agentId || !initialTargetDirectory || !requestDirectoryListing) {
pendingPathParamRef.current = null;
return;
}
@@ -251,7 +255,7 @@ function FileExplorerContent({
}, [agentId, initialTargetDirectory, requestDirectoryListing]);
useEffect(() => {
- if (!agentId || !normalizedFileParam) {
+ if (!agentId || !normalizedFileParam || !requestFilePreview) {
pendingFileParamRef.current = null;
return;
}
@@ -281,7 +285,7 @@ function FileExplorerContent({
const handleEntryPress = useCallback(
(entry: ExplorerEntry) => {
- if (!agentId) {
+ if (!agentId || !requestDirectoryListing || !requestFilePreview) {
return;
}
@@ -330,13 +334,13 @@ function FileExplorerContent({
if (agentId) {
router.replace({
pathname: "/agent/[serverId]/[agentId]",
- params: { serverId: routeServerId, agentId },
+ params: { serverId, agentId },
});
return;
}
router.back();
- }, [agentId, routeServerId]);
+ }, [agentId, serverId]);
const handleBackNavigation = useCallback(() => {
if (!agentId) {
@@ -349,7 +353,7 @@ function FileExplorerContent({
return true;
}
- if ((explorerState?.history?.length ?? 0) > 1) {
+ if ((explorerState?.history?.length ?? 0) > 1 && navigateExplorerBack) {
navigateExplorerBack(agentId);
return true;
}
@@ -472,7 +476,7 @@ function FileExplorerContent({
const handleViewableItemsChanged = useCallback(
({ viewableItems }: { viewableItems: Array }) => {
- if (!agentId || viewMode !== "grid") {
+ if (!agentId || viewMode !== "grid" || !requestFilePreview) {
return;
}
diff --git a/packages/app/src/app/git-diff.tsx b/packages/app/src/app/git-diff.tsx
index 31db1209d..dd1901252 100644
--- a/packages/app/src/app/git-diff.tsx
+++ b/packages/app/src/app/git-diff.tsx
@@ -3,11 +3,10 @@ import { View, Text, ScrollView, ActivityIndicator } from "react-native";
import { useLocalSearchParams } from "expo-router";
import { StyleSheet } from "react-native-unistyles";
import { BackHeader } from "@/components/headers/back-header";
-import type { SessionContextValue } from "@/contexts/session-context";
import type { ConnectionStatus } from "@/contexts/daemon-connections-context";
import { useDaemonConnections } from "@/contexts/daemon-connections-context";
import { formatConnectionStatus } from "@/utils/daemons";
-import { useDaemonSession } from "@/hooks/use-daemon-session";
+import { useSessionStore } from "@/stores/session-store";
interface ParsedDiffFile {
path: string;
@@ -58,10 +57,8 @@ export default function GitDiffScreen() {
const { agentId, serverId } = useLocalSearchParams<{ agentId: string; serverId?: string }>();
const resolvedServerId = typeof serverId === "string" ? serverId : undefined;
const { connectionStates } = useDaemonConnections();
- const session = useDaemonSession(resolvedServerId, {
- suppressUnavailableAlert: true,
- allowUnavailable: true,
- });
+
+ const session = useSessionStore((state) => resolvedServerId ? state.sessions[resolvedServerId] : undefined);
const connectionServerId = resolvedServerId ?? null;
const connection = connectionServerId ? connectionStates.get(connectionServerId) : null;
@@ -83,7 +80,7 @@ export default function GitDiffScreen() {
return (
@@ -91,22 +88,30 @@ export default function GitDiffScreen() {
}
function GitDiffContent({
- session,
+ serverId,
agentId,
serverLabel,
}: {
- session: import("@/hooks/use-daemon-session").DaemonSession;
+ serverId: string;
agentId?: string;
serverLabel: string;
}) {
- const { agents, gitDiffs, requestGitDiff } = session;
const [isLoading, setIsLoading] = useState(true);
- const agent = agentId ? agents.get(agentId) : undefined;
- const diffText = agentId ? gitDiffs.get(agentId) : undefined;
+ const agent = useSessionStore((state) =>
+ agentId && serverId ? state.sessions[serverId]?.agents?.get(agentId) : undefined
+ );
+
+ const diffText = useSessionStore((state) =>
+ agentId && serverId ? state.sessions[serverId]?.gitDiffs?.get(agentId) : undefined
+ );
+
+ const requestGitDiff = useSessionStore((state) =>
+ serverId ? state.sessions[serverId]?.methods?.requestGitDiff : undefined
+ );
useEffect(() => {
- if (!agentId) {
+ if (!agentId || !requestGitDiff) {
setIsLoading(false);
return;
}
diff --git a/packages/app/src/app/settings.tsx b/packages/app/src/app/settings.tsx
index 70840dd86..25403029c 100644
--- a/packages/app/src/app/settings.tsx
+++ b/packages/app/src/app/settings.tsx
@@ -19,8 +19,7 @@ import { useDaemonConnections, type ConnectionStatus } from "@/contexts/daemon-c
import { formatConnectionStatus, getConnectionStatusTone } from "@/utils/daemons";
import { theme as defaultTheme } from "@/styles/theme";
import { BackHeader } from "@/components/headers/back-header";
-import { useSessionForServer } from "@/hooks/use-session-directory";
-import { useDaemonSession } from "@/hooks/use-daemon-session";
+import { useSessionStore } from "@/stores/session-store";
import type { UseWebSocketReturn } from "@/hooks/use-websocket";
const delay = (ms: number) =>
@@ -799,12 +798,8 @@ function DaemonCard({
: theme.colors.mutedForeground;
const badgeText = statusLabel;
const connectionError = typeof lastError === "string" && lastError.trim().length > 0 ? lastError.trim() : null;
- const daemonWs = useSessionForServer(
- daemon.id,
- useCallback((session) => session?.ws ?? null, [])
- );
- const fullSession = useDaemonSession(daemon.id, { allowUnavailable: true, suppressUnavailableAlert: true });
- const restartServerFn = fullSession?.restartServer ?? null;
+ const daemonWs = useSessionStore((state) => state.sessions[daemon.id]?.ws);
+ const restartServerFn = useSessionStore((state) => state.sessions[daemon.id]?.methods?.restartServer);
const [isRestarting, setIsRestarting] = useState(false);
const wsIsConnectedRef = useRef(daemonWs?.isConnected ?? false);
const isTesting = testState?.status === "testing";
diff --git a/packages/app/src/components/agent-input-area.tsx b/packages/app/src/components/agent-input-area.tsx
index 2070415cf..829e39b7f 100644
--- a/packages/app/src/components/agent-input-area.tsx
+++ b/packages/app/src/components/agent-input-area.tsx
@@ -31,9 +31,8 @@ import { RealtimeControls } from "./realtime-controls";
import { useImageAttachmentPicker } from "@/hooks/use-image-attachment-picker";
import { AUDIO_DEBUG_ENABLED } from "@/config/audio-debug";
import { AudioDebugNotice, type AudioDebugInfo } from "./audio-debug-notice";
-import { useDaemonSession } from "@/hooks/use-daemon-session";
+import { useSessionStore } from "@/stores/session-store";
import type { UseWebSocketReturn } from "@/hooks/use-websocket";
-import type { SessionContextValue, Agent } from "@/contexts/session-context";
import type { SessionOutboundMessage } from "@server/server/messages";
type QueuedMessage = {
@@ -44,11 +43,12 @@ type QueuedMessage = {
interface AgentInputAreaProps {
agentId: string;
- serverId?: string;
+ serverId: string;
}
const MIN_INPUT_HEIGHT = 50;
const MAX_INPUT_HEIGHT = 160;
+const EMPTY_ARRAY: readonly QueuedMessage[] = [];
const MAX_INPUT_WIDTH = 960;
const BASE_VERTICAL_PADDING = (FOOTER_HEIGHT - MIN_INPUT_HEIGHT) / 2;
// Android currently crashes inside ViewGroup.dispatchDraw when running Reanimated
@@ -85,7 +85,38 @@ type DictationToastConfig = {
export function AgentInputArea({ agentId, serverId }: AgentInputAreaProps) {
const { theme } = useUnistyles();
- const session = useDaemonSession(serverId, { allowUnavailable: true, suppressUnavailableAlert: true });
+
+ // [INVESTIGATION] Using granular selectors for WebSocket connection status
+ const ws = useSessionStore((state) => state.sessions[serverId]?.ws);
+
+ // [INVESTIGATION] Select only the specific agent (not all agents)
+ const agent = useSessionStore((state) =>
+ state.sessions[serverId]?.agents?.get(agentId)
+ );
+
+ // [INVESTIGATION] Get draft input - drafts are at TOP LEVEL (not in session)
+ const getDraftInput = useSessionStore((state) => state.getDraftInput);
+ const saveDraftInput = useSessionStore((state) => state.saveDraftInput);
+
+ // [INVESTIGATION] Get queued messages for this agent - use stable empty array
+ const queuedMessagesRaw = useSessionStore((state) =>
+ state.sessions[serverId]?.queuedMessages?.get(agentId)
+ );
+ const queuedMessages = queuedMessagesRaw ?? EMPTY_ARRAY;
+
+ // [INVESTIGATION] Get methods
+ const methods = useSessionStore((state) => state.sessions[serverId]?.methods);
+ const sendAgentMessage = methods?.sendAgentMessage;
+ const cancelAgentRun = methods?.cancelAgentRun;
+
+ // [INVESTIGATION] Use store action directly for setQueuedMessages
+ const setQueuedMessages = useSessionStore((state) => state.setQueuedMessages);
+
+ // Noop fallback for sendAgentAudio (required by useDictation)
+ const noopSendAgentAudio = useCallback(async () => {}, []);
+ const sendAgentAudio = methods?.sendAgentAudio ?? noopSendAgentAudio;
+
+ // Inert WebSocket fallback for when ws is undefined
const inertWebSocket = useMemo(
() => ({
isConnected: false,
@@ -101,21 +132,8 @@ export function AgentInputArea({ agentId, serverId }: AgentInputAreaProps) {
}),
[]
);
- const noopSendAgentMessage = useCallback(async () => {}, []);
- const noopSendAgentAudio = useCallback(async () => {}, []);
- const noopCancelAgentRun = useCallback(() => {}, []);
- const noopGetDraftInput = useCallback(() => undefined, []);
- const noopSaveDraftInput = useCallback(() => {}, []);
- const noopSetQueuedMessages = useCallback(() => {}, []);
- const ws = session?.ws ?? inertWebSocket;
- const sendAgentMessage = session?.sendAgentMessage ?? noopSendAgentMessage;
- const sendAgentAudio = session?.sendAgentAudio ?? noopSendAgentAudio;
- const agents = session?.agents ?? new Map();
- const cancelAgentRun = session?.cancelAgentRun ?? noopCancelAgentRun;
- const getDraftInput = session?.getDraftInput ?? noopGetDraftInput;
- const saveDraftInput = session?.saveDraftInput ?? noopSaveDraftInput;
- const queuedMessagesByAgent = session?.queuedMessages ?? new Map();
- const setQueuedMessagesByAgent = session?.setQueuedMessages ?? noopSetQueuedMessages;
+
+ const wsOrInert = ws ?? inertWebSocket;
const { startRealtime, stopRealtime, isRealtimeMode } = useRealtime();
const [userInput, setUserInput] = useState("");
@@ -125,7 +143,7 @@ export function AgentInputArea({ agentId, serverId }: AgentInputAreaProps) {
const [isCancellingAgent, setIsCancellingAgent] = useState(false);
const [audioDebugInfo, setAudioDebugInfo] = useState(null);
const [connectionStatus, setConnectionStatus] = useState(() =>
- ws.getConnectionState ? ws.getConnectionState() : { isConnected: ws.isConnected, isConnecting: ws.isConnecting }
+ wsOrInert.getConnectionState ? wsOrInert.getConnectionState() : { isConnected: wsOrInert.isConnected, isConnecting: wsOrInert.isConnecting }
);
const [lastSuccessToastAt, setLastSuccessToastAt] = useState(null);
@@ -180,7 +198,7 @@ export function AgentInputArea({ agentId, serverId }: AgentInputAreaProps) {
}, []);
const canStartDictation = useCallback(() => {
- const socketConnected = ws.getConnectionState ? ws.getConnectionState().isConnected : ws.isConnected;
+ const socketConnected = wsOrInert.getConnectionState ? wsOrInert.getConnectionState().isConnected : wsOrInert.isConnected;
const allowed = !isRealtimeMode && socketConnected;
console.log("[AgentInput] canStartDictation", {
allowed,
@@ -188,17 +206,17 @@ export function AgentInputArea({ agentId, serverId }: AgentInputAreaProps) {
wsConnected: socketConnected,
});
return allowed;
- }, [isRealtimeMode, ws]);
+ }, [isRealtimeMode, wsOrInert]);
const canConfirmDictation = useCallback(() => {
- const socketConnected = ws.getConnectionState ? ws.getConnectionState().isConnected : ws.isConnected;
+ const socketConnected = wsOrInert.getConnectionState ? wsOrInert.getConnectionState().isConnected : wsOrInert.isConnected;
const allowed = socketConnected;
console.log("[AgentInput] canConfirmDictation", {
allowed,
wsConnected: socketConnected,
});
return allowed;
- }, [ws]);
+ }, [wsOrInert]);
const {
isRecording: isDictating,
@@ -220,7 +238,7 @@ export function AgentInputArea({ agentId, serverId }: AgentInputAreaProps) {
} = useDictation({
agentId,
sendAgentAudio,
- ws,
+ ws: wsOrInert,
mode: "transcribe_only",
onTranscript: handleDictationTranscript,
onError: handleDictationError,
@@ -235,13 +253,13 @@ export function AgentInputArea({ agentId, serverId }: AgentInputAreaProps) {
}, [dictationPendingRequestId]);
useEffect(() => {
- if (!ws.subscribeConnectionStatus) {
+ if (!wsOrInert.subscribeConnectionStatus) {
return;
}
- return ws.subscribeConnectionStatus((status) => {
+ return wsOrInert.subscribeConnectionStatus((status) => {
setConnectionStatus(status);
});
- }, [ws]);
+ }, [wsOrInert]);
useEffect(() => {
if (dictationLastOutcome?.type === "success") {
@@ -274,9 +292,9 @@ export function AgentInputArea({ agentId, serverId }: AgentInputAreaProps) {
const dictationToast = useMemo(() => {
console.log('[AgentInputArea] Connection status:', {
isConnected: connectionStatus.isConnected,
- wsIsConnected: ws.isConnected,
- wsIsConnecting: ws.isConnecting,
- hasSession: !!session,
+ wsIsConnected: wsOrInert.isConnected,
+ wsIsConnecting: wsOrInert.isConnecting,
+ hasWs: !!ws,
});
if (!connectionStatus.isConnected) {
@@ -375,8 +393,8 @@ export function AgentInputArea({ agentId, serverId }: AgentInputAreaProps) {
}, [sendAgentMessage]);
async function handleSendMessage() {
- const socketConnected = ws.getConnectionState ? ws.getConnectionState().isConnected : ws.isConnected;
- if (!userInput.trim() || !socketConnected) return;
+ const socketConnected = wsOrInert.getConnectionState ? wsOrInert.getConnectionState().isConnected : wsOrInert.isConnected;
+ if (!userInput.trim() || !socketConnected || !sendAgentMessage) return;
const message = userInput.trim();
const imageAttachments = selectedImages.length > 0 ? selectedImages : undefined;
@@ -416,7 +434,7 @@ export function AgentInputArea({ agentId, serverId }: AgentInputAreaProps) {
console.log("[AgentInput] handleVoicePress", {
isDictating,
isRealtimeMode,
- wsConnected: ws.isConnected,
+ wsConnected: wsOrInert.isConnected,
});
if (isDictating || isRealtimeMode) {
return;
@@ -480,7 +498,7 @@ export function AgentInputArea({ agentId, serverId }: AgentInputAreaProps) {
if (!shouldShowAudioDebug) {
return;
}
- const unsubscribe = ws.on("transcription_result", (message: SessionOutboundMessage) => {
+ const unsubscribe = wsOrInert.on("transcription_result", (message: SessionOutboundMessage) => {
if (message.type !== "transcription_result") {
return;
}
@@ -506,7 +524,7 @@ export function AgentInputArea({ agentId, serverId }: AgentInputAreaProps) {
return () => {
unsubscribe();
};
- }, [shouldShowAudioDebug, ws]);
+ }, [shouldShowAudioDebug, wsOrInert]);
function isTextAreaLike(value: unknown): value is TextAreaHandle {
if (!value || typeof value !== "object") {
@@ -656,7 +674,6 @@ export function AgentInputArea({ agentId, serverId }: AgentInputAreaProps) {
});
}
- const agent = agents.get(agentId);
const isAgentRunning = agent?.status === "running";
agentStatusRef.current = agent?.status;
const hasText = userInput.trim().length > 0;
@@ -664,18 +681,17 @@ export function AgentInputArea({ agentId, serverId }: AgentInputAreaProps) {
const hasSendableContent = hasText || hasImages;
const shouldShowSendButton = !isAgentRunning && hasSendableContent;
const shouldShowVoiceControls = !hasSendableContent;
- const queuedMessages = queuedMessagesByAgent.get(agentId) ?? [];
const shouldHandleDesktopSubmit = IS_WEB;
const updateQueue = useCallback(
(updater: (current: QueuedMessage[]) => QueuedMessage[]) => {
- setQueuedMessagesByAgent((prev: Map) => {
+ setQueuedMessages(serverId, (prev: Map) => {
const next = new Map(prev);
next.set(agentId, updater(prev.get(agentId) ?? []));
return next;
});
},
- [agentId, setQueuedMessagesByAgent],
+ [agentId, serverId, setQueuedMessages],
);
useEffect(() => {
updateQueueRef.current = updateQueue;
@@ -702,23 +718,23 @@ export function AgentInputArea({ agentId, serverId }: AgentInputAreaProps) {
}
event.preventDefault();
if (isAgentRunning) {
- if (!hasSendableContent || !ws.isConnected) {
+ if (!hasSendableContent || !wsOrInert.isConnected) {
return;
}
handleQueueCurrentInput();
return;
}
- if (!shouldShowSendButton || isProcessing || !ws.isConnected) {
+ if (!shouldShowSendButton || isProcessing || !wsOrInert.isConnected) {
return;
}
void handleSendMessage();
}
useEffect(() => {
- if (!isAgentRunning || !ws.isConnected) {
+ if (!isAgentRunning || !wsOrInert.isConnected) {
setIsCancellingAgent(false);
}
- }, [isAgentRunning, ws.isConnected]);
+ }, [isAgentRunning, wsOrInert.isConnected]);
useEffect(() => {
if (!IS_WEB) {
@@ -738,7 +754,7 @@ export function AgentInputArea({ agentId, serverId }: AgentInputAreaProps) {
return;
}
- if (!isRealtimeMode && shouldShowVoiceControls && ws.isConnected) {
+ if (!isRealtimeMode && shouldShowVoiceControls && wsOrInert.isConnected) {
void handleVoicePress();
}
return;
@@ -759,7 +775,7 @@ export function AgentInputArea({ agentId, serverId }: AgentInputAreaProps) {
isDictating,
isRealtimeMode,
shouldShowVoiceControls,
- ws,
+ wsOrInert,
handleVoicePress,
handleSendRecording,
handleCancelRecording,
@@ -820,7 +836,7 @@ export function AgentInputArea({ agentId, serverId }: AgentInputAreaProps) {
if (isRealtimeMode) {
await stopRealtime();
} else {
- if (!ws.isConnected || !serverId) {
+ if (!wsOrInert.isConnected || !serverId) {
return;
}
await startRealtime(serverId);
@@ -834,7 +850,7 @@ export function AgentInputArea({ agentId, serverId }: AgentInputAreaProps) {
if (!agent || agent.status !== "running" || isCancellingAgent) {
return;
}
- if (!ws.isConnected) {
+ if (!wsOrInert.isConnected || !cancelAgentRun) {
return;
}
setIsCancellingAgent(true);
@@ -867,7 +883,7 @@ export function AgentInputArea({ agentId, serverId }: AgentInputAreaProps) {
async function handleSendQueuedNow(id: string) {
const item = queuedMessages.find((q) => q.id === id);
- if (!item || !ws.isConnected) return;
+ if (!item || !wsOrInert.isConnected || !sendAgentMessage) return;
updateQueue((current) => current.filter((q) => q.id !== id));
@@ -879,11 +895,11 @@ export function AgentInputArea({ agentId, serverId }: AgentInputAreaProps) {
const realtimeButton = (
{isRealtimeMode ? (
@@ -897,10 +913,10 @@ export function AgentInputArea({ agentId, serverId }: AgentInputAreaProps) {
const voiceButton = (
@@ -991,7 +1007,7 @@ export function AgentInputArea({ agentId, serverId }: AgentInputAreaProps) {
multiline
scrollEnabled={inputHeight >= MAX_INPUT_HEIGHT}
onContentSizeChange={handleContentSizeChange}
- editable={!isDictating && ws.isConnected}
+ editable={!isDictating && wsOrInert.isConnected}
onKeyPress={shouldHandleDesktopSubmit ? handleDesktopSubmitKeyPress : undefined}
/>
@@ -1001,10 +1017,10 @@ export function AgentInputArea({ agentId, serverId }: AgentInputAreaProps) {
@@ -1020,12 +1036,12 @@ export function AgentInputArea({ agentId, serverId }: AgentInputAreaProps) {
{hasSendableContent && (
@@ -1034,12 +1050,12 @@ export function AgentInputArea({ agentId, serverId }: AgentInputAreaProps) {
{realtimeButton}
{isCancellingAgent ? (
@@ -1052,10 +1068,10 @@ export function AgentInputArea({ agentId, serverId }: AgentInputAreaProps) {
) : shouldShowSendButton ? (
diff --git a/packages/app/src/components/agent-list.tsx b/packages/app/src/components/agent-list.tsx
index 03b5ee47b..a6e3612b3 100644
--- a/packages/app/src/components/agent-list.tsx
+++ b/packages/app/src/components/agent-list.tsx
@@ -6,7 +6,7 @@ import { formatTimeAgo } from "@/utils/time";
import { getAgentStatusColor, getAgentStatusLabel } from "@/utils/agent-status";
import { getAgentProviderDefinition } from "@server/server/agent/provider-manifest";
import { type AggregatedAgent } from "@/hooks/use-aggregated-agents";
-import { useDaemonSession } from "@/hooks/use-daemon-session";
+import { useSessionStore } from "@/stores/session-store";
import { buildAgentNavigationKey, startNavigationTiming } from "@/utils/navigation-timing";
interface AgentListProps {
@@ -18,14 +18,15 @@ interface AgentListProps {
export function AgentList({ agents, isRefreshing = false, onRefresh }: AgentListProps) {
const { theme } = useUnistyles();
const [actionAgent, setActionAgent] = useState(null);
- const actionSession = useDaemonSession(actionAgent?.serverId, {
- suppressUnavailableAlert: true,
- allowUnavailable: true,
- });
- const deleteAgent = actionSession?.deleteAgent;
+ // Get the methods for the specific server
+ const methods = useSessionStore((state) =>
+ actionAgent?.serverId ? state.sessions[actionAgent.serverId]?.methods : undefined
+ );
+ const deleteAgent = methods?.deleteAgent;
+
const isActionSheetVisible = actionAgent !== null;
- const isActionDaemonUnavailable = Boolean(actionAgent?.serverId && !actionSession);
+ const isActionDaemonUnavailable = Boolean(actionAgent?.serverId && !methods);
const handleAgentPress = useCallback(
(serverId: string, agentId: string) => {
diff --git a/packages/app/src/components/agent-status-bar.tsx b/packages/app/src/components/agent-status-bar.tsx
index 0a3d6ffcd..a29006689 100644
--- a/packages/app/src/components/agent-status-bar.tsx
+++ b/packages/app/src/components/agent-status-bar.tsx
@@ -1,36 +1,42 @@
import { View, Text, Pressable } from "react-native";
import { StyleSheet, useUnistyles } from "react-native-unistyles";
import { ChevronDown } from "lucide-react-native";
-import { useState, useCallback, useRef } from "react";
+import { useState, useRef } from "react";
import { ModeSelectorModal } from "./mode-selector-modal";
-import { useDaemonSession } from "@/hooks/use-daemon-session";
-import type { SessionContextValue, Agent } from "@/contexts/session-context";
+import { useSessionStore } from "@/stores/session-store";
interface AgentStatusBarProps {
agentId: string;
- serverId?: string;
+ serverId: string;
}
export function AgentStatusBar({ agentId, serverId }: AgentStatusBarProps) {
// INVESTIGATION: Log rerenders
const renderCountRef = useRef(0);
renderCountRef.current++;
- console.log(`[INVESTIGATION] AgentStatusBar render #${renderCountRef.current}`, { agentId });
+ console.log(`[INVESTIGATION] AgentStatusBar render #${renderCountRef.current}`, { agentId, serverId });
const { theme } = useUnistyles();
- const session = useDaemonSession(serverId, { allowUnavailable: true, suppressUnavailableAlert: true });
- const noopSetAgentMode = useCallback(() => {}, []);
- const agents = session?.agents ?? new Map();
- const setAgentMode = session?.setAgentMode ?? noopSetAgentMode;
- const [showModeSelector, setShowModeSelector] = useState(false);
- const agent = agents.get(agentId);
+ // Select only the specific agent (not all agents)
+ const agent = useSessionStore((state) =>
+ state.sessions[serverId]?.agents?.get(agentId)
+ );
+
+ // Get the setAgentMode action (actions are stable, won't cause rerenders)
+ const setAgentMode = useSessionStore((state) =>
+ state.sessions[serverId]?.methods?.setAgentMode
+ );
+
+ const [showModeSelector, setShowModeSelector] = useState(false);
if (!agent) {
return null;
}
function handleModeChange(modeId: string) {
- setAgentMode(agentId, modeId);
+ if (setAgentMode) {
+ setAgentMode(agentId, modeId);
+ }
}
return (
diff --git a/packages/app/src/components/agent-stream-view.tsx b/packages/app/src/components/agent-stream-view.tsx
index 60a5757fe..1f7322cfa 100644
--- a/packages/app/src/components/agent-stream-view.tsx
+++ b/packages/app/src/components/agent-stream-view.tsx
@@ -32,7 +32,7 @@ import type { StreamItem } from "@/types/stream";
import type { PendingPermission } from "@/types/shared";
import type { AgentPermissionResponse } from "@server/server/agent/agent-sdk-types";
import type { Agent } from "@/contexts/session-context";
-import { useDaemonSession } from "@/hooks/use-daemon-session";
+import { useSessionStore } from "@/stores/session-store";
import { useDaemonRequest } from "@/hooks/use-daemon-request";
import type { UseWebSocketReturn } from "@/hooks/use-websocket";
import type { SessionOutboundMessage } from "@server/server/messages";
@@ -79,7 +79,19 @@ export function AgentStreamView({
const isNearBottomRef = useRef(true);
const isUserScrollingRef = useRef(false);
const router = useRouter();
- const session = useDaemonSession(serverId, { allowUnavailable: true, suppressUnavailableAlert: true });
+
+ // Get serverId (fallback to agent's serverId if not provided)
+ const resolvedServerId = serverId ?? agent.serverId ?? "";
+
+ // Get ws for connection status
+ const ws = useSessionStore((state) => state.sessions[resolvedServerId]?.ws);
+
+ // Get methods for file operations
+ const methods = useSessionStore((state) => state.sessions[resolvedServerId]?.methods);
+ const requestDirectoryListing = methods?.requestDirectoryListing;
+ const requestFilePreview = methods?.requestFilePreview;
+
+ // Create inert websocket fallback if ws is null
const inertWebSocket = useMemo(
() => ({
isConnected: false,
@@ -93,10 +105,9 @@ export function AgentStreamView({
}),
[]
);
- const ws = session?.ws ?? inertWebSocket;
- const requestDirectoryListing = session?.requestDirectoryListing ?? (() => {});
- const requestFilePreview = session?.requestFilePreview ?? (() => {});
- const resolvedServerId = serverId ?? session?.serverId ?? agent.serverId ?? "";
+ const wsOrInert = ws ?? inertWebSocket;
+ const requestDirectoryListingOrInert = requestDirectoryListing ?? (() => {});
+ const requestFilePreviewOrInert = requestFilePreview ?? (() => {});
// Keep entry/exit animations off on Android due to RN dispatchDraw crashes
// tracked in react-native-reanimated#8422.
const shouldDisableEntryExitAnimations = Platform.OS === "android";
@@ -120,9 +131,9 @@ export function AgentStreamView({
return;
}
- requestDirectoryListing(agentId, normalized.directory);
+ requestDirectoryListingOrInert(agentId, normalized.directory);
if (normalized.file) {
- requestFilePreview(agentId, normalized.file);
+ requestFilePreviewOrInert(agentId, normalized.file);
}
router.push({
@@ -141,7 +152,7 @@ export function AgentStreamView({
},
});
},
- [agentId, requestDirectoryListing, requestFilePreview, resolvedServerId, router]
+ [agentId, requestDirectoryListingOrInert, requestFilePreviewOrInert, resolvedServerId, router]
);
const handleScroll = useCallback(
@@ -350,7 +361,7 @@ export function AgentStreamView({
{pendingPermissionItems.length > 0 ? (
{pendingPermissionItems.map((permission) => (
-
+
))}
) : null}
@@ -363,7 +374,7 @@ export function AgentStreamView({
);
- }, [pendingPermissionItems, showWorkingIndicator, ws]);
+ }, [pendingPermissionItems, showWorkingIndicator, wsOrInert]);
const flatListData = useMemo(() => {
return [...streamItems].reverse();
diff --git a/packages/app/src/components/create-agent-modal.tsx b/packages/app/src/components/create-agent-modal.tsx
index e5e3fc3d6..fba0afb55 100644
--- a/packages/app/src/components/create-agent-modal.tsx
+++ b/packages/app/src/components/create-agent-modal.tsx
@@ -65,7 +65,6 @@ import type { WSInboundMessage, SessionOutboundMessage } from "@server/server/me
import { formatConnectionStatus } from "@/utils/daemons";
import { trackAnalyticsEvent } from "@/utils/analytics";
import type { SessionContextValue } from "@/contexts/session-context";
-import { useDaemonSession } from "@/hooks/use-daemon-session";
import type { UseWebSocketReturn } from "@/hooks/use-websocket";
import { useSessionStore } from "@/stores/session-store";
@@ -263,26 +262,29 @@ function AgentFlowModal({
}, [serverId, daemonEntries]);
const [selectedServerId, setSelectedServerId] = useState(initialServerId);
- // Use useDaemonSession to get both state AND APIs
- const fullSession = useDaemonSession(selectedServerId, { allowUnavailable: true, suppressUnavailableAlert: true });
+ // [INVESTIGATION] Using useSessionStore directly with granular selectors
+ // Get the complete session state for the selected server
+ const sessionState = useSessionStore((state) =>
+ selectedServerId ? state.sessions[selectedServerId] : undefined
+ );
// Extract only what we need for CreateAgentSessionSlice
const session = useMemo(() => {
- if (!fullSession) {
+ if (!selectedServerId || !sessionState || !sessionState.methods) {
return null;
}
const slice: CreateAgentSessionSlice = {
- serverId: fullSession.serverId,
- ws: fullSession.ws,
- createAgent: fullSession.createAgent,
- resumeAgent: fullSession.resumeAgent,
- sendAgentAudio: fullSession.sendAgentAudio,
- agents: fullSession.agents,
- providerModels: fullSession.providerModels,
- requestProviderModels: fullSession.requestProviderModels,
+ serverId: selectedServerId,
+ ws: sessionState.ws,
+ createAgent: sessionState.methods.createAgent,
+ resumeAgent: sessionState.methods.resumeAgent,
+ sendAgentAudio: sessionState.methods.sendAgentAudio,
+ agents: sessionState.agents,
+ providerModels: sessionState.providerModels,
+ requestProviderModels: sessionState.methods.requestProviderModels,
};
return slice;
- }, [fullSession]);
+ }, [selectedServerId, sessionState]);
// Helper to get session state for background operations
// Note: This only returns state, not APIs. For calling methods, need to send WS messages directly.
@@ -324,7 +326,6 @@ function AgentFlowModal({
}
}, [daemonEntries, initialServerId, isVisible, selectedServerId]);
- const ws = session?.ws ?? null;
const inertWebSocket = useMemo(
() => ({
isConnected: false,
@@ -340,6 +341,7 @@ function AgentFlowModal({
}),
[]
);
+ const ws = session?.ws ?? null;
const effectiveWs: UseWebSocketReturn = ws ?? inertWebSocket;
const createAgent = session?.createAgent;
const resumeAgent = session?.resumeAgent;
diff --git a/packages/app/src/contexts/session-context.tsx b/packages/app/src/contexts/session-context.tsx
index 06953de31..b49d0b3eb 100644
--- a/packages/app/src/contexts/session-context.tsx
+++ b/packages/app/src/contexts/session-context.tsx
@@ -1662,7 +1662,7 @@ export function SessionProvider({ children, serverUrl, serverId }: SessionProvid
]
);
- // Sync imperative methods to Zustand store so useDaemonSession can access them without React Context
+ // Sync imperative methods to Zustand store so components can access them via selectors
// Memoize the methods object to avoid infinite re-renders (object reference must be stable)
const setSessionMethods = useSessionStore((state) => state.setSessionMethods);
const methods = useMemo(() => ({
diff --git a/packages/app/src/hooks/use-daemon-session.ts b/packages/app/src/hooks/use-daemon-session.ts
deleted file mode 100644
index 51326cf88..000000000
--- a/packages/app/src/hooks/use-daemon-session.ts
+++ /dev/null
@@ -1,180 +0,0 @@
-import { useCallback, useMemo, useRef } from "react";
-import { Alert } from "react-native";
-import { useDaemonConnections } from "@/contexts/daemon-connections-context";
-import { useSessionStore, type SessionState } from "@/stores/session-store";
-
-export class DaemonSessionUnavailableError extends Error {
- serverId: string;
-
- constructor(serverId: string) {
- super(`Host session "${serverId}" is unavailable`);
- this.name = "DaemonSessionUnavailableError";
- this.serverId = serverId;
- }
-}
-
-type UseDaemonSessionOptions = {
- suppressUnavailableAlert?: boolean;
- allowUnavailable?: boolean;
-};
-
-// Combined type: SessionState (from store) + imperative APIs (from context)
-export type DaemonSession = SessionState & {
- // Imperative APIs from context
- setVoiceDetectionFlags: (isDetecting: boolean, isSpeaking: boolean) => void;
- requestGitDiff: (agentId: string) => void;
- requestDirectoryListing: (agentId: string, path: string, options?: { recordHistory?: boolean }) => void;
- requestFilePreview: (agentId: string, path: string) => void;
- navigateExplorerBack: (agentId: string) => string | null;
- requestProviderModels: (provider: any, options?: { cwd?: string }) => void;
- restartServer: (reason?: string) => void;
- initializeAgent: (params: { agentId: string; requestId?: string }) => void;
- refreshAgent: (params: { agentId: string; requestId?: string }) => void;
- refreshSession: () => void;
- cancelAgentRun: (agentId: string) => void;
- sendAgentMessage: (
- agentId: string,
- message: string,
- images?: Array<{ uri: string; mimeType?: string }>
- ) => Promise;
- sendAgentAudio: (
- agentId: string,
- audioBlob: Blob,
- requestId?: string,
- options?: { mode?: "transcribe_only" | "auto_run" }
- ) => Promise;
- deleteAgent: (agentId: string) => void;
- createAgent: (options: {
- config: any;
- initialPrompt: string;
- git?: any;
- worktreeName?: string;
- requestId?: string;
- }) => void;
- resumeAgent: (options: { handle: any; overrides?: any; requestId?: string }) => void;
- setAgentMode: (agentId: string, modeId: string) => void;
- respondToPermission: (agentId: string, requestId: string, response: any) => void;
- // State getters/setters that components might use directly
- getDraftInput: (agentId: string) => any;
- saveDraftInput: (agentId: string, draft: any) => void;
- setFocusedAgentId: (agentId: string | null) => void;
- setMessages: (messages: any[] | ((prev: any[]) => any[])) => void;
- setQueuedMessages: (value: any) => void;
-};
-
-export function useDaemonSession(
- serverId?: string | null,
- options?: UseDaemonSessionOptions & { allowUnavailable?: false }
-): DaemonSession | null;
-export function useDaemonSession(
- serverId: string | null | undefined,
- options: UseDaemonSessionOptions & { allowUnavailable: true }
-): DaemonSession | null;
-export function useDaemonSession(serverId?: string | null, options?: UseDaemonSessionOptions) {
-
- const selectSession = useCallback(
- (state: ReturnType) => {
- if (!serverId) {
- return null;
- }
- return state.sessions[serverId] ?? null;
- },
- [serverId]
- );
- const sessionState = useSessionStore(selectSession);
- const { connectionStates } = useDaemonConnections();
- const alertedDaemonsRef = useRef>(new Set());
- const loggedDaemonsRef = useRef>(new Set());
- const { suppressUnavailableAlert = false, allowUnavailable = false } = options ?? {};
-
- // Get store actions (these are already stable from Zustand)
- const getDraftInputAction = useSessionStore((state) => state.getDraftInput);
- const saveDraftInputAction = useSessionStore((state) => state.saveDraftInput);
- const setFocusedAgentIdAction = useSessionStore((state) => state.setFocusedAgentId);
- const setMessagesAction = useSessionStore((state) => state.setMessages);
- const setQueuedMessagesAction = useSessionStore((state) => state.setQueuedMessages);
-
- // Create stable wrapper functions (must be before conditional returns)
- const boundGetDraftInput = useCallback(
- (agentId: string) => getDraftInputAction(agentId),
- [getDraftInputAction]
- );
- const boundSaveDraftInput = useCallback(
- (agentId: string, draft: any) => { saveDraftInputAction(agentId, draft); },
- [saveDraftInputAction]
- );
- const boundSetFocusedAgentId = useCallback(
- (agentId: string | null) => { if (serverId) setFocusedAgentIdAction(serverId, agentId); },
- [serverId, setFocusedAgentIdAction]
- );
- const boundSetMessages = useCallback(
- (messages: any) => { if (serverId) setMessagesAction(serverId, messages); },
- [serverId, setMessagesAction]
- );
- const boundSetQueuedMessages = useCallback(
- (value: any) => { if (serverId) setQueuedMessagesAction(serverId, value); },
- [serverId, setQueuedMessagesAction]
- );
-
- // Memoize the combined object to prevent infinite re-renders
- const combined = useMemo(() => {
- if (!serverId || !sessionState || !sessionState.methods) {
- return null;
- }
-
- return {
- ...sessionState,
- ...sessionState.methods,
- getDraftInput: boundGetDraftInput,
- saveDraftInput: boundSaveDraftInput,
- setFocusedAgentId: boundSetFocusedAgentId,
- setMessages: boundSetMessages,
- setQueuedMessages: boundSetQueuedMessages,
- };
- }, [
- serverId,
- sessionState,
- boundGetDraftInput,
- boundSaveDraftInput,
- boundSetFocusedAgentId,
- boundSetMessages,
- boundSetQueuedMessages,
- ]);
-
- if (!serverId) {
- return null;
- }
-
- if (combined) {
- console.log("[useDaemonSession] Debug", {
- serverId,
- hasSessionState: !!sessionState,
- hasMethods: !!sessionState?.methods,
- sessionStateKeys: sessionState ? Object.keys(sessionState).slice(0, 5) : [],
- });
- return combined;
- }
-
- // Handle unavailable session
- const connection = connectionStates.get(serverId);
- const label = connection?.daemon.label ?? serverId;
- const status = connection?.status ?? "unknown";
- const lastError = connection?.lastError ? `\n${connection.lastError}` : "";
- const message = `${label} isn't connected yet (${status}). Paseo reconnects automatically and will enable actions once it's back.${lastError}`;
-
- if (!suppressUnavailableAlert && !alertedDaemonsRef.current.has(serverId)) {
- alertedDaemonsRef.current.add(serverId);
- Alert.alert("Host unavailable", message.trim());
- }
-
- if (!loggedDaemonsRef.current.has(serverId)) {
- loggedDaemonsRef.current.add(serverId);
- console.warn(`[useDaemonSession] Session unavailable for daemon "${label}" (${status}).`);
- }
-
- if (allowUnavailable) {
- return null;
- }
-
- throw new DaemonSessionUnavailableError(serverId);
-}