import type { DaemonClient, FetchAgentHistoryOptions, FetchAgentHistoryPageInfo, } from "@getpaseo/client/internal/daemon-client"; import { useInfiniteQuery } from "@tanstack/react-query"; import { useCallback, useMemo, useSyncExternalStore } from "react"; import { useTranslation } from "react-i18next"; import type { AggregatedAgent } from "@/hooks/use-aggregated-agents"; import { getHostRuntimeStore, isHostRuntimeConnected, useHosts } from "@/runtime/host-runtime"; import { buildAgentDirectoryState } from "@/utils/agent-directory-sync"; import { agentHistoryQueryKey, allAgentHistoryQueryKey } from "./agent-history-query-key"; const AGENT_HISTORY_PAGE_LIMIT = 200; const AGENT_HISTORY_SORT: NonNullable = [ { key: "updated_at", direction: "desc" }, ]; const AGENT_HISTORY_ALL_HOSTS_FAILED_MESSAGE = "No connected hosts could load agent history"; export interface AgentHistoryResult { agents: AggregatedAgent[]; isLoading: boolean; isInitialLoad: boolean; isRevalidating: boolean; isError: boolean; hasMore: boolean; isLoadingMore: boolean; refreshAll: () => Promise; loadMore: () => void; } export interface AgentHistoryPage { agents: AggregatedAgent[]; pageInfo: FetchAgentHistoryPageInfo; } export type AgentHistoryClient = Pick; export interface AgentHistoryHost { serverId: string; serverLabel: string; client: AgentHistoryClient; } interface AgentHistoryBatchPage { agents: AggregatedAgent[]; pageInfoByServerId: Record; } type AgentHistoryCursorByServerId = Record; export async function fetchAgentHistoryPage(input: { client: AgentHistoryClient; serverId: string; cursor: string | null; }): Promise { const payload = await input.client.fetchAgentHistory({ sort: AGENT_HISTORY_SORT, page: input.cursor ? { limit: AGENT_HISTORY_PAGE_LIMIT, cursor: input.cursor } : { limit: AGENT_HISTORY_PAGE_LIMIT }, }); const { agents } = buildAgentDirectoryState({ serverId: input.serverId, entries: payload.entries, }); return { agents: Array.from(agents.values(), (agent) => ({ id: agent.id, serverId: input.serverId, serverLabel: input.serverId, title: agent.title ?? null, status: agent.status, lastActivityAt: agent.lastActivityAt, cwd: agent.cwd, workspaceId: agent.workspaceId, provider: agent.provider, pendingPermissionCount: agent.pendingPermissions.length, requiresAttention: agent.requiresAttention, attentionReason: agent.attentionReason, attentionTimestamp: agent.attentionTimestamp ?? null, archivedAt: agent.archivedAt ?? null, createdAt: agent.createdAt, labels: agent.labels, projectPlacement: agent.projectPlacement, })), pageInfo: payload.pageInfo, }; } function sortByLatestActivity(agents: AggregatedAgent[]): AggregatedAgent[] { return [...agents].sort((a, b) => b.lastActivityAt.getTime() - a.lastActivityAt.getTime()); } function getNextAgentHistoryPageParam( page: AgentHistoryBatchPage, ): AgentHistoryCursorByServerId | null { const cursorByServerId: AgentHistoryCursorByServerId = {}; for (const [serverId, pageInfo] of Object.entries(page.pageInfoByServerId)) { if (pageInfo.hasMore && pageInfo.nextCursor) { cursorByServerId[serverId] = pageInfo.nextCursor; } } return Object.keys(cursorByServerId).length > 0 ? cursorByServerId : null; } export async function fetchAgentHistoryBatch(input: { hosts: readonly AgentHistoryHost[]; cursorByServerId: AgentHistoryCursorByServerId | null; }): Promise { const cursorByServerId = input.cursorByServerId ?? {}; const hasCursorFilter = Object.keys(cursorByServerId).length > 0; const hostsToFetch = hasCursorFilter ? input.hosts.filter((host) => Object.hasOwn(cursorByServerId, host.serverId)) : input.hosts; const settledPages = await Promise.allSettled( hostsToFetch.map(async (host) => { const page = await fetchAgentHistoryPage({ client: host.client, serverId: host.serverId, cursor: cursorByServerId[host.serverId] ?? null, }); return { host, page }; }), ); const pages = settledPages.flatMap((result) => result.status === "fulfilled" ? [result.value] : [], ); if (pages.length === 0) { throw new Error(AGENT_HISTORY_ALL_HOSTS_FAILED_MESSAGE); } const agents = pages.flatMap(({ host, page }) => page.agents.map((agent) => Object.assign({}, agent, { serverLabel: host.serverLabel })), ); const pageInfoByServerId = Object.fromEntries( pages.map(({ host, page }) => [host.serverId, page.pageInfo]), ); return { agents: sortByLatestActivity(agents), pageInfoByServerId, }; } export function useAgentHistory(options: { serverId?: string | null; enabled?: boolean; }): AgentHistoryResult { const { t } = useTranslation(); const daemons = useHosts(); const runtime = getHostRuntimeStore(); const runtimeVersion = useSyncExternalStore( (onStoreChange) => runtime.subscribeAll(onStoreChange), () => runtime.getVersion(), () => runtime.getVersion(), ); const serverId = useMemo(() => { const value = options.serverId; return typeof value === "string" && value.trim().length > 0 ? value.trim() : null; }, [options.serverId]); const enabled = options.enabled ?? true; const targetHosts = useMemo(() => { void runtimeVersion; const serverLabelById = new Map(daemons.map((daemon) => [daemon.serverId, daemon.label])); const serverIds = serverId ? [serverId] : daemons.map((daemon) => daemon.serverId); const hosts: AgentHistoryHost[] = []; for (const targetServerId of serverIds) { const snapshot = runtime.getSnapshot(targetServerId); const client = runtime.getClient(targetServerId); if (!client || !isHostRuntimeConnected(snapshot)) { continue; } hosts.push({ serverId: targetServerId, serverLabel: serverLabelById.get(targetServerId) ?? targetServerId, client, }); } return hosts; }, [daemons, runtime, runtimeVersion, serverId]); const targetServerIds = useMemo(() => targetHosts.map((host) => host.serverId), [targetHosts]); const queryKey = useMemo( () => (serverId ? agentHistoryQueryKey(serverId) : allAgentHistoryQueryKey(targetServerIds)), [serverId, targetServerIds], ); const serverLabelById = useMemo( () => new Map(daemons.map((daemon) => [daemon.serverId, daemon.label])), [daemons], ); const historyQuery = useInfiniteQuery< AgentHistoryBatchPage, Error, { pages: AgentHistoryBatchPage[] }, readonly unknown[], AgentHistoryCursorByServerId | null >({ queryKey, enabled: Boolean(enabled && targetHosts.length > 0), staleTime: 30_000, initialPageParam: null, getNextPageParam: getNextAgentHistoryPageParam, queryFn: async ({ pageParam }) => { if (targetHosts.length === 0) { throw new Error(t("workspace.terminal.hostDisconnected")); } return fetchAgentHistoryBatch({ hosts: targetHosts, cursorByServerId: pageParam, }); }, }); const { data, fetchNextPage, hasNextPage, isError, isFetching, isFetchingNextPage, isLoading, refetch, } = historyQuery; const refreshAll = useCallback(async () => { if (!enabled || targetHosts.length === 0) { return; } await refetch(); }, [enabled, refetch, targetHosts.length]); const loadMore = useCallback(() => { if (!enabled || targetHosts.length === 0 || !hasNextPage || isFetchingNextPage) { return; } void fetchNextPage(); }, [enabled, fetchNextPage, hasNextPage, isFetchingNextPage, targetHosts.length]); const agents = useMemo(() => { const historyAgents = (data?.pages ?? []).flatMap((page) => page.agents); const labelledAgents = historyAgents.map((agent) => Object.assign({}, agent, { serverLabel: serverLabelById.get(agent.serverId) ?? agent.serverLabel, }), ); return sortByLatestActivity(labelledAgents); }, [data?.pages, serverLabelById]); const isInitialLoad = isLoading && agents.length === 0; const isRevalidating = isFetching && !isFetchingNextPage && agents.length > 0; return { agents, isLoading, isInitialLoad, isRevalidating, isError, hasMore: hasNextPage, isLoadingMore: isFetchingNextPage, refreshAll, loadMore, }; }