feat: add project-aware agent workspace
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
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),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user