perf: eliminate cascading rerenders with granular Zustand selectors

- Delete useDaemonSession hook that selected entire session object
- Move drafts to top-level store (not nested in sessions)
- Refactor all components to use granular useSessionStore selectors
- Fix infinite loop bugs from selector fallbacks (use stable constants)

Before: 15-20 component rerenders per keystroke
After: ~1-2 rerenders per keystroke

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Mohamed Boudra
2025-12-03 16:37:23 +07:00
co-authored by Claude
parent 74c82a312e
commit 21d43bc249
11 changed files with 280 additions and 392 deletions
@@ -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<UseWebSocketReturn>(
() => ({
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<string, Agent>();
const cancelAgentRun = session?.cancelAgentRun ?? noopCancelAgentRun;
const getDraftInput = session?.getDraftInput ?? noopGetDraftInput;
const saveDraftInput = session?.saveDraftInput ?? noopSaveDraftInput;
const queuedMessagesByAgent = session?.queuedMessages ?? new Map<string, QueuedMessage[]>();
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<AudioDebugInfo | null>(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<number | null>(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<DictationToastConfig | null>(() => {
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<string, QueuedMessage[]>) => {
setQueuedMessages(serverId, (prev: Map<string, QueuedMessage[]>) => {
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 = (
<Pressable
onPress={handleRealtimePress}
disabled={!ws.isConnected && !isRealtimeMode}
disabled={!wsOrInert.isConnected && !isRealtimeMode}
style={[
styles.realtimeButton as any,
(isRealtimeMode ? styles.realtimeButtonActive : undefined) as any,
(!ws.isConnected && !isRealtimeMode ? styles.buttonDisabled : undefined) as any,
(!wsOrInert.isConnected && !isRealtimeMode ? styles.buttonDisabled : undefined) as any,
]}
>
{isRealtimeMode ? (
@@ -897,10 +913,10 @@ export function AgentInputArea({ agentId, serverId }: AgentInputAreaProps) {
const voiceButton = (
<Pressable
onPress={handleVoicePress}
disabled={!ws.isConnected || isRealtimeMode}
disabled={!wsOrInert.isConnected || isRealtimeMode}
style={[
styles.voiceButton as any,
(!ws.isConnected || isRealtimeMode ? styles.buttonDisabled : undefined) as any,
(!wsOrInert.isConnected || isRealtimeMode ? styles.buttonDisabled : undefined) as any,
(isDictating ? styles.voiceButtonRecording : undefined) as any,
]}
>
@@ -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) {
<View style={styles.leftButtonGroup}>
<Pressable
onPress={handlePickImage}
disabled={!ws.isConnected}
disabled={!wsOrInert.isConnected}
style={[
styles.attachButton as any,
(!ws.isConnected ? styles.buttonDisabled : undefined) as any,
(!wsOrInert.isConnected ? styles.buttonDisabled : undefined) as any,
]}
>
<Paperclip size={20} color={theme.colors.foreground} />
@@ -1020,12 +1036,12 @@ export function AgentInputArea({ agentId, serverId }: AgentInputAreaProps) {
{hasSendableContent && (
<Pressable
onPress={handleQueueCurrentInput}
disabled={!ws.isConnected}
disabled={!wsOrInert.isConnected}
accessibilityLabel="Queue message while agent is running"
accessibilityRole="button"
style={[
styles.queueButton as any,
(!ws.isConnected ? styles.buttonDisabled : undefined) as any,
(!wsOrInert.isConnected ? styles.buttonDisabled : undefined) as any,
]}
>
<ArrowUp size={20} color="white" />
@@ -1034,12 +1050,12 @@ export function AgentInputArea({ agentId, serverId }: AgentInputAreaProps) {
{realtimeButton}
<Pressable
onPress={handleCancelAgent}
disabled={!ws.isConnected || isCancellingAgent}
disabled={!wsOrInert.isConnected || isCancellingAgent}
accessibilityLabel={isCancellingAgent ? "Canceling agent" : "Stop agent"}
accessibilityRole="button"
style={[
styles.cancelButton as any,
(!ws.isConnected || isCancellingAgent ? styles.buttonDisabled : undefined) as any,
(!wsOrInert.isConnected || isCancellingAgent ? styles.buttonDisabled : undefined) as any,
]}
>
{isCancellingAgent ? (
@@ -1052,10 +1068,10 @@ export function AgentInputArea({ agentId, serverId }: AgentInputAreaProps) {
) : shouldShowSendButton ? (
<Pressable
onPress={handleSendMessage}
disabled={!ws.isConnected || isProcessing}
disabled={!wsOrInert.isConnected || isProcessing}
style={[
styles.sendButton as any,
(!ws.isConnected || isProcessing ? styles.buttonDisabled : undefined) as any,
(!wsOrInert.isConnected || isProcessing ? styles.buttonDisabled : undefined) as any,
]}
>
<ArrowUp size={20} color="white" />
+8 -7
View File
@@ -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<AggregatedAgent | null>(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) => {
@@ -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<SessionContextValue["setAgentMode"]>(() => {}, []);
const agents = session?.agents ?? new Map<string, Agent>();
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 (
@@ -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<UseWebSocketReturn>(
() => ({
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 ? (
<View style={stylesheet.permissionsContainer}>
{pendingPermissionItems.map((permission) => (
<PermissionRequestCard key={permission.key} permission={permission} ws={ws} />
<PermissionRequestCard key={permission.key} permission={permission} ws={wsOrInert} />
))}
</View>
) : null}
@@ -363,7 +374,7 @@ export function AgentStreamView({
</View>
</View>
);
}, [pendingPermissionItems, showWorkingIndicator, ws]);
}, [pendingPermissionItems, showWorkingIndicator, wsOrInert]);
const flatListData = useMemo(() => {
return [...streamItems].reverse();
@@ -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<string | null>(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<CreateAgentSessionSlice | null>(() => {
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<UseWebSocketReturn>(
() => ({
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;