import { useMemo, useCallback, useSyncExternalStore } from "react"; import { useShallow } from "zustand/shallow"; import { useSessionStore } from "@/stores/session-store"; import type { AgentDirectoryEntry } from "@/types/agent-directory"; import type { Agent } from "@/stores/session-store"; import { getHostRuntimeStore, useHosts } from "@/runtime/host-runtime"; export interface AggregatedAgent extends AgentDirectoryEntry { serverId: string; serverLabel: string; } export interface AggregatedAgentsResult { agents: AggregatedAgent[]; isLoading: boolean; isInitialLoad: boolean; isRevalidating: boolean; refreshAll: () => void; } export function useAggregatedAgents(options?: { includeArchived?: boolean; }): AggregatedAgentsResult { const daemons = useHosts(); const runtime = getHostRuntimeStore(); const includeArchived = options?.includeArchived ?? false; const runtimeVersion = useSyncExternalStore( (onStoreChange) => runtime.subscribeAll(onStoreChange), () => runtime.getVersion(), () => runtime.getVersion(), ); const sessionAgents = useSessionStore( useShallow((state) => { const result: Record | undefined> = {}; for (const [serverId, session] of Object.entries(state.sessions)) { result[serverId] = session.agents; } return result; }), ); const refreshAll = useCallback(() => { runtime.refreshAllAgentDirectories(); }, [runtime]); const result = useMemo(() => { const allAgents: AggregatedAgent[] = []; const serverLabelById = new Map( daemons.map((daemon) => [daemon.serverId, daemon.label] as const), ); // Derive agent directory from all sessions for (const [serverId, agents] of Object.entries(sessionAgents)) { if (!agents || agents.size === 0) { continue; } const serverLabel = serverLabelById.get(serverId) ?? serverId; for (const agent of agents.values()) { if (!includeArchived && agent.archivedAt) { continue; } const nextAgent: AggregatedAgent = { id: agent.id, serverId, serverLabel, title: agent.title ?? null, terminal: agent.terminal, status: agent.status, lastActivityAt: agent.lastActivityAt, cwd: agent.cwd, provider: agent.provider, pendingPermissionCount: agent.pendingPermissions.length, requiresAttention: agent.requiresAttention, attentionReason: agent.attentionReason, attentionTimestamp: agent.attentionTimestamp, archivedAt: agent.archivedAt, createdAt: agent.createdAt, labels: agent.labels, }; allAgents.push(nextAgent); } } // Sort by: running agents first, then by most recent activity allAgents.sort((left, right) => { const leftRunning = left.status === "running"; const rightRunning = right.status === "running"; if (leftRunning && !rightRunning) { return -1; } if (!leftRunning && rightRunning) { return 1; } const leftTime = left.lastActivityAt.getTime(); const rightTime = right.lastActivityAt.getTime(); return rightTime - leftTime; }); // Check if we have any cached data const hasAnyData = allAgents.length > 0; // Align list loading with the runtime directory-sync machine. const isLoading = daemons.some((daemon) => { const status = runtime.getSnapshot(daemon.serverId)?.agentDirectoryStatus ?? "initial_loading"; return status === "initial_loading" || status === "revalidating"; }); const isInitialLoad = isLoading && !hasAnyData; const isRevalidating = isLoading && hasAnyData; return { agents: allAgents, isLoading, isInitialLoad, isRevalidating, }; }, [daemons, includeArchived, runtime, runtimeVersion, sessionAgents]); return { ...result, refreshAll, }; }