83 lines
5.6 KiB
TypeScript
83 lines
5.6 KiB
TypeScript
import { useCallback, useMemo, useRef, useState } from "react";
|
|
import useSWR from "swr";
|
|
import useSWRInfinite from "swr/infinite";
|
|
import type { AgentApiClient, AgentChatSessionSummary, AgentSessionPage } from "@/features/agent";
|
|
import { useWorkbenchSessionResources } from "./workbench-session-resources";
|
|
|
|
const TTL = 60_000;
|
|
|
|
export function useAgentHistory(client: AgentApiClient, scope: string) {
|
|
const resources = useWorkbenchSessionResources();
|
|
const [topicFilter, setTopicFilter] = useState<{ scope: string; id?: string }>({ scope });
|
|
const topicId = topicFilter.scope === scope ? topicFilter.id : undefined;
|
|
const setTopicId = useCallback((id: string | undefined) => setTopicFilter({ scope, id }), [scope]);
|
|
const [query, setQuery] = useState("");
|
|
const [open, setOpen] = useState(false);
|
|
const [enabled, setEnabled] = useState(false);
|
|
const [pendingUpdateKey, setPendingUpdateKey] = useState<string | null>(null);
|
|
const viewKey = JSON.stringify([scope, query, topicId]);
|
|
const view = useRef({ scope, query, topicId });
|
|
view.current = { scope, query, topicId };
|
|
const staged = useRef<{ key: string; page: AgentSessionPage } | null>(null);
|
|
const [orderVersion, setOrderVersion] = useState(0);
|
|
const key = useCallback((q: string, cursor?: string | null, topic?: string) => JSON.stringify(["history", scope, q, cursor ?? null, topic ?? null]), [scope]);
|
|
const fetchPage = useCallback((q: string, cursor?: string | null, force = false, topic?: string) => resources.cache.load(key(q, cursor, topic),
|
|
() => client.listSessionPage({ q, topicId: topic, includeCollaboration: true, cursor: cursor ?? undefined, signal: AbortSignal.timeout(15_000) }), TTL, { force }), [client, key, resources]);
|
|
const recent = useSWR(["agent-recent-sessions", scope], () => fetchPage(""), {
|
|
revalidateOnFocus: false, shouldRetryOnError: false,
|
|
});
|
|
const pages = useSWRInfinite<AgentSessionPage>(
|
|
(_index, previous: AgentSessionPage | null) => !enabled || (previous && !previous.hasMore) ? null
|
|
: ["agent-history-page", scope, query, previous?.nextCursor ?? null, topicId ?? null],
|
|
([, , q, cursor, topic]: [string, string, string, string | null, string | null]) => fetchPage(q, cursor, false, topic ?? undefined),
|
|
{ revalidateOnFocus: false, revalidateFirstPage: false, shouldRetryOnError: false, persistSize: false },
|
|
);
|
|
const sessions = useMemo(() => [...new Map((pages.data ?? (query || topicId ? [] : recent.data ? [recent.data] : []))
|
|
.flatMap((page) => page.sessions).map((session) => [session.id, session])).values()], [pages.data, query, recent.data, topicId]);
|
|
const refresh = useCallback(async () => {
|
|
const page = staged.current?.key === viewKey ? staged.current.page : await fetchPage(query, null, true, topicId);
|
|
if (view.current.scope !== scope || view.current.query !== query || view.current.topicId !== topicId) return;
|
|
staged.current = null;
|
|
await pages.mutate([page], { revalidate: false });
|
|
await pages.setSize(1);
|
|
if (!query && !topicId) await recent.mutate(page, { revalidate: false });
|
|
setPendingUpdateKey(null);
|
|
setOrderVersion((version) => version + 1);
|
|
}, [fetchPage, pages, query, recent, scope, topicId, viewKey]);
|
|
const ensure = useCallback(async () => {
|
|
setOpen(true);
|
|
setEnabled(true);
|
|
const stale = (resources.cache.peek(key(query, null, topicId))?.expiresAt ?? 0) <= Date.now();
|
|
const page = await fetchPage(query, null, false, topicId);
|
|
if (view.current.scope !== scope || view.current.query !== query || view.current.topicId !== topicId) return;
|
|
// Opening a window never invalidates a fresh page or hides existing content.
|
|
if (!pages.data?.length) await pages.mutate([page], { revalidate: false });
|
|
else if (stale) { staged.current = { key: viewKey, page }; setPendingUpdateKey(viewKey); }
|
|
if (!query && !topicId) await recent.mutate(page, { revalidate: false });
|
|
}, [fetchPage, key, pages, query, recent, resources, scope, topicId, viewKey]);
|
|
const update = useCallback(async (change: (sessions: AgentChatSessionSummary[]) => AgentChatSessionSummary[]) => {
|
|
resources.cache.updateMatching<AgentSessionPage>((cacheKey) => {
|
|
if (!cacheKey.startsWith('["history",')) return false;
|
|
const value = JSON.parse(cacheKey) as unknown[];
|
|
return value[0] === "history" && value[1] === scope;
|
|
}, (page) => ({ ...page, sessions: change(page.sessions) }));
|
|
const recentPage = resources.cache.peek<AgentSessionPage>(key(""))?.value;
|
|
if (recentPage) {
|
|
const next = { ...recentPage, sessions: change(recentPage.sessions) };
|
|
resources.cache.set(key(""), next, TTL);
|
|
if (view.current.scope === scope) await recent.mutate(next, { revalidate: false });
|
|
}
|
|
if (view.current.scope !== scope || view.current.query !== query || view.current.topicId !== topicId) return;
|
|
await pages.mutate((current) => current?.map((page) => ({ ...page, sessions: change(page.sessions) })), { revalidate: false });
|
|
// Search and later pages can now have different membership/order. Retain what is being read.
|
|
setPendingUpdateKey(viewKey);
|
|
}, [key, pages, query, recent, resources, scope, topicId, viewKey]);
|
|
return {
|
|
topicId, setTopicId, sessions, recentSessions: recent.data?.sessions ?? [], query, setQuery,
|
|
loading: (open ? pages.isLoading : recent.isLoading), error: pages.error ?? recent.error,
|
|
loadingMore: pages.isValidating, hasMore: pages.data?.at(-1)?.hasMore ?? recent.data?.hasMore ?? false,
|
|
loadMore: () => pages.setSize(pages.size + 1), refresh, ensure, update,
|
|
pendingUpdate: pendingUpdateKey === viewKey, isOpen: open, orderVersion, close: () => setOpen(false),
|
|
};
|
|
}
|