From 8b87b0a4e2b0c78962bc9973fb131fbd5c7bda16 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Tue, 2 Dec 2025 14:15:32 +0000 Subject: [PATCH] Remove agent directory syncing, make it pure derived state MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Problem: Agent directory was still being synced from SessionProvider to Zustand store, violating the "SessionProvider is just a message handler" principle and creating unnecessary state duplication. Solution: Make agent directory pure derived state: - Removed agentDirectory from SessionStoreState - Removed setAgentDirectory/clearAgentDirectory actions - Changed getAgentDirectory to compute on-demand from session.agents - Removed buildAgentDirectoryEntries and syncing from SessionProvider - Updated useAggregatedAgents to derive from sessions directly How it works now: - Agent directory is computed on-demand from session.agents Map - lastActivityAt automatically updates when agents update via WebSocket - No syncing, no stale state, no overhead - Single source of truth: session.agents Benefits: - Eliminated redundant state (agentDirectory was duplicate of agents) - No sync overhead or complexity - Always fresh data (derived on read) - Simpler mental model (agents is the only source) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- packages/app/src/contexts/session-context.tsx | 29 +------ .../app/src/hooks/use-aggregated-agents.ts | 18 +++-- packages/app/src/stores/session-store.ts | 79 +++++-------------- 3 files changed, 33 insertions(+), 93 deletions(-) diff --git a/packages/app/src/contexts/session-context.tsx b/packages/app/src/contexts/session-context.tsx index 6dfe8b045..535bffd48 100644 --- a/packages/app/src/contexts/session-context.tsx +++ b/packages/app/src/contexts/session-context.tsx @@ -137,22 +137,6 @@ function normalizeAgentSnapshot(snapshot: AgentSnapshotPayload, serverId: string }; } -function buildAgentDirectoryEntries(serverId: string, agents: Map): AgentDirectoryEntry[] { - const entries: AgentDirectoryEntry[] = []; - for (const agent of agents.values()) { - entries.push({ - id: agent.id, - serverId, - title: agent.title ?? null, - status: agent.status, - lastActivityAt: agent.lastActivityAt, - cwd: agent.cwd, - provider: agent.provider, - }); - } - return entries; -} - const createExplorerState = () => ({ directories: new Map(), files: new Map(), @@ -231,8 +215,6 @@ export function SessionProvider({ children, serverUrl, serverId }: SessionProvid // Zustand store actions const initializeSession = useSessionStore((state) => state.initializeSession); const clearSession = useSessionStore((state) => state.clearSession); - const setAgentDirectory = useSessionStore((state) => state.setAgentDirectory); - const clearAgentDirectory = useSessionStore((state) => state.clearAgentDirectory); const setIsPlayingAudio = useSessionStore((state) => state.setIsPlayingAudio); const setMessages = useSessionStore((state) => state.setMessages); const setCurrentAssistantMessage = useSessionStore((state) => state.setCurrentAssistantMessage); @@ -1087,14 +1069,6 @@ export function SessionProvider({ children, serverUrl, serverId }: SessionProvid }; }, [ws, audioPlayer, serverId, setIsPlayingAudio, setMessages, setCurrentAssistantMessage, setAgentStreamState, setInitializingAgents, setAgents, setCommands, setPendingPermissions, setGitDiffs, setFileExplorer, setProviderModels, setHasHydratedAgents, updateConnectionStatus, getSession, saveDraftInput]); - // Sync agent directory - useEffect(() => { - const session = getSession(serverId); - if (!session) return; - const lightweightAgentDirectory = buildAgentDirectoryEntries(serverId, session.agents); - setAgentDirectory(serverId, lightweightAgentDirectory); - }, [serverId, getSession, setAgentDirectory]); - // Auto-flush queued messages when agent transitions from running -> not running useEffect(() => { const session = getSession(serverId); @@ -1546,9 +1520,8 @@ export function SessionProvider({ children, serverUrl, serverId }: SessionProvid useEffect(() => { return () => { clearSession(serverId); - clearAgentDirectory(serverId); }; - }, [serverId, clearSession, clearAgentDirectory]); + }, [serverId, clearSession]); const value = useMemo( () => ({ diff --git a/packages/app/src/hooks/use-aggregated-agents.ts b/packages/app/src/hooks/use-aggregated-agents.ts index a7a93e8d8..10fdc7f55 100644 --- a/packages/app/src/hooks/use-aggregated-agents.ts +++ b/packages/app/src/hooks/use-aggregated-agents.ts @@ -15,21 +15,27 @@ export interface AggregatedAgentsResult { export function useAggregatedAgents(): AggregatedAgentsResult { const { connectionStates } = useDaemonConnections(); - const agentDirectory = useSessionStore((state) => state.agentDirectory); + const sessions = useSessionStore((state) => state.sessions); return useMemo(() => { const allAgents: AggregatedAgent[] = []; - for (const [serverId, agents] of Object.entries(agentDirectory)) { - if (!agents || agents.length === 0) { + // Derive agent directory from all sessions + for (const [serverId, session] of Object.entries(sessions)) { + if (!session?.agents || session.agents.size === 0) { continue; } const serverLabel = connectionStates.get(serverId)?.daemon.label ?? serverId; - for (const agent of agents) { + for (const agent of session.agents.values()) { allAgents.push({ - ...agent, + id: agent.id, serverId, serverLabel, + title: agent.title ?? null, + status: agent.status, + lastActivityAt: agent.lastActivityAt, + cwd: agent.cwd, + provider: agent.provider, }); } } @@ -61,5 +67,5 @@ export function useAggregatedAgents(): AggregatedAgentsResult { }); return { agents: allAgents, isLoading }; - }, [agentDirectory, connectionStates]); + }, [sessions, connectionStates]); } diff --git a/packages/app/src/stores/session-store.ts b/packages/app/src/stores/session-store.ts index ae22873e4..6ec20cdb5 100644 --- a/packages/app/src/stores/session-store.ts +++ b/packages/app/src/stores/session-store.ts @@ -203,7 +203,6 @@ export interface SessionState { // Global store state interface SessionStoreState { sessions: Record; - agentDirectory: Record; } // Action types @@ -257,9 +256,7 @@ interface SessionStoreActions { // Hydration setHasHydratedAgents: (serverId: string, hydrated: boolean) => void; - // Agent directory - setAgentDirectory: (serverId: string, agents: AgentDirectoryEntry[]) => void; - clearAgentDirectory: (serverId: string) => void; + // Agent directory (derived from agents) getAgentDirectory: (serverId: string) => AgentDirectoryEntry[] | undefined; } @@ -288,29 +285,6 @@ function logSessionStoreUpdate( }); } -function areAgentDirectoriesEqual(left: AgentDirectoryEntry[] | undefined, right: AgentDirectoryEntry[]): boolean { - if (!left) { - return false; - } - if (left.length !== right.length) { - return false; - } - return left.every((entry, index) => { - const other = right[index]; - if (!other) { - return false; - } - return ( - entry.id === other.id && - entry.status === other.status && - entry.serverId === other.serverId && - entry.lastActivityAt.getTime() === other.lastActivityAt.getTime() && - entry.title === other.title && - entry.cwd === other.cwd && - entry.provider === other.provider - ); - }); -} // Helper to create initial session state function createInitialSessionState(serverId: string, ws: UseWebSocketReturn, audioPlayer: ReturnType): SessionState { @@ -339,7 +313,6 @@ function createInitialSessionState(serverId: string, ws: UseWebSocketReturn, aud export const useSessionStore = create()( subscribeWithSelector((set, get) => ({ sessions: {}, - agentDirectory: {}, // Session management initializeSession: (serverId, ws, audioPlayer) => { @@ -697,38 +670,26 @@ export const useSessionStore = create()( }); }, - // Agent directory - setAgentDirectory: (serverId, agents) => { - set((prev) => { - const existing = prev.agentDirectory[serverId]; - if (existing && areAgentDirectoriesEqual(existing, agents)) { - return prev; - } - logSessionStoreUpdate("setAgentDirectory", serverId, { agentCount: agents.length }); - return { - ...prev, - agentDirectory: { - ...prev.agentDirectory, - [serverId]: agents, - }, - }; - }); - }, - - clearAgentDirectory: (serverId) => { - set((prev) => { - if (!(serverId in prev.agentDirectory)) { - return prev; - } - logSessionStoreUpdate("clearAgentDirectory", serverId); - const nextDirectory = { ...prev.agentDirectory }; - delete nextDirectory[serverId]; - return { ...prev, agentDirectory: nextDirectory }; - }); - }, - + // Agent directory - derived from agents (computed on-demand) getAgentDirectory: (serverId) => { - return get().agentDirectory[serverId]; + const session = get().sessions[serverId]; + if (!session) { + return undefined; + } + + const entries: AgentDirectoryEntry[] = []; + for (const agent of session.agents.values()) { + entries.push({ + id: agent.id, + serverId, + title: agent.title ?? null, + status: agent.status, + lastActivityAt: agent.lastActivityAt, + cwd: agent.cwd, + provider: agent.provider, + }); + } + return entries; }, })) );