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
@@ -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<void>;
sendAgentAudio: (
agentId: string,
audioBlob: Blob,
requestId?: string,
options?: { mode?: "transcribe_only" | "auto_run" }
) => Promise<void>;
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<typeof useSessionStore.getState>) => {
if (!serverId) {
return null;
}
return state.sessions[serverId] ?? null;
},
[serverId]
);
const sessionState = useSessionStore(selectSession);
const { connectionStates } = useDaemonConnections();
const alertedDaemonsRef = useRef<Set<string>>(new Set());
const loggedDaemonsRef = useRef<Set<string>>(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<DaemonSession | null>(() => {
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);
}