Remove agent directory syncing, make it pure derived state

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 <noreply@anthropic.com>
This commit is contained in:
Mohamed Boudra
2025-12-02 14:15:32 +00:00
parent f5c6bce54f
commit 8b87b0a4e2
3 changed files with 33 additions and 93 deletions

View File

@@ -137,22 +137,6 @@ function normalizeAgentSnapshot(snapshot: AgentSnapshotPayload, serverId: string
}; };
} }
function buildAgentDirectoryEntries(serverId: string, agents: Map<string, any>): 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 = () => ({ const createExplorerState = () => ({
directories: new Map(), directories: new Map(),
files: new Map(), files: new Map(),
@@ -231,8 +215,6 @@ export function SessionProvider({ children, serverUrl, serverId }: SessionProvid
// Zustand store actions // Zustand store actions
const initializeSession = useSessionStore((state) => state.initializeSession); const initializeSession = useSessionStore((state) => state.initializeSession);
const clearSession = useSessionStore((state) => state.clearSession); 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 setIsPlayingAudio = useSessionStore((state) => state.setIsPlayingAudio);
const setMessages = useSessionStore((state) => state.setMessages); const setMessages = useSessionStore((state) => state.setMessages);
const setCurrentAssistantMessage = useSessionStore((state) => state.setCurrentAssistantMessage); 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]); }, [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 // Auto-flush queued messages when agent transitions from running -> not running
useEffect(() => { useEffect(() => {
const session = getSession(serverId); const session = getSession(serverId);
@@ -1546,9 +1520,8 @@ export function SessionProvider({ children, serverUrl, serverId }: SessionProvid
useEffect(() => { useEffect(() => {
return () => { return () => {
clearSession(serverId); clearSession(serverId);
clearAgentDirectory(serverId);
}; };
}, [serverId, clearSession, clearAgentDirectory]); }, [serverId, clearSession]);
const value = useMemo<SessionContextValue>( const value = useMemo<SessionContextValue>(
() => ({ () => ({

View File

@@ -15,21 +15,27 @@ export interface AggregatedAgentsResult {
export function useAggregatedAgents(): AggregatedAgentsResult { export function useAggregatedAgents(): AggregatedAgentsResult {
const { connectionStates } = useDaemonConnections(); const { connectionStates } = useDaemonConnections();
const agentDirectory = useSessionStore((state) => state.agentDirectory); const sessions = useSessionStore((state) => state.sessions);
return useMemo(() => { return useMemo(() => {
const allAgents: AggregatedAgent[] = []; const allAgents: AggregatedAgent[] = [];
for (const [serverId, agents] of Object.entries(agentDirectory)) { // Derive agent directory from all sessions
if (!agents || agents.length === 0) { for (const [serverId, session] of Object.entries(sessions)) {
if (!session?.agents || session.agents.size === 0) {
continue; continue;
} }
const serverLabel = connectionStates.get(serverId)?.daemon.label ?? serverId; const serverLabel = connectionStates.get(serverId)?.daemon.label ?? serverId;
for (const agent of agents) { for (const agent of session.agents.values()) {
allAgents.push({ allAgents.push({
...agent, id: agent.id,
serverId, serverId,
serverLabel, 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 }; return { agents: allAgents, isLoading };
}, [agentDirectory, connectionStates]); }, [sessions, connectionStates]);
} }

View File

@@ -203,7 +203,6 @@ export interface SessionState {
// Global store state // Global store state
interface SessionStoreState { interface SessionStoreState {
sessions: Record<string, SessionState>; sessions: Record<string, SessionState>;
agentDirectory: Record<string, AgentDirectoryEntry[]>;
} }
// Action types // Action types
@@ -257,9 +256,7 @@ interface SessionStoreActions {
// Hydration // Hydration
setHasHydratedAgents: (serverId: string, hydrated: boolean) => void; setHasHydratedAgents: (serverId: string, hydrated: boolean) => void;
// Agent directory // Agent directory (derived from agents)
setAgentDirectory: (serverId: string, agents: AgentDirectoryEntry[]) => void;
clearAgentDirectory: (serverId: string) => void;
getAgentDirectory: (serverId: string) => AgentDirectoryEntry[] | undefined; 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 // Helper to create initial session state
function createInitialSessionState(serverId: string, ws: UseWebSocketReturn, audioPlayer: ReturnType<typeof useAudioPlayer>): SessionState { function createInitialSessionState(serverId: string, ws: UseWebSocketReturn, audioPlayer: ReturnType<typeof useAudioPlayer>): SessionState {
@@ -339,7 +313,6 @@ function createInitialSessionState(serverId: string, ws: UseWebSocketReturn, aud
export const useSessionStore = create<SessionStore>()( export const useSessionStore = create<SessionStore>()(
subscribeWithSelector((set, get) => ({ subscribeWithSelector((set, get) => ({
sessions: {}, sessions: {},
agentDirectory: {},
// Session management // Session management
initializeSession: (serverId, ws, audioPlayer) => { initializeSession: (serverId, ws, audioPlayer) => {
@@ -697,38 +670,26 @@ export const useSessionStore = create<SessionStore>()(
}); });
}, },
// Agent directory // Agent directory - derived from agents (computed on-demand)
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 };
});
},
getAgentDirectory: (serverId) => { 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;
}, },
})) }))
); );