225 lines
7.3 KiB
TypeScript
225 lines
7.3 KiB
TypeScript
import { useCallback, useEffect, useMemo } from "react";
|
|
import { useMutation, useQuery, useQueryClient, type QueryClient } from "@tanstack/react-query";
|
|
import { useTranslation } from "react-i18next";
|
|
import type { AgentProvider, ProviderSnapshotEntry } from "@getpaseo/protocol/agent-types";
|
|
import type { DaemonClient } from "@getpaseo/client/internal/daemon-client";
|
|
import { useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-runtime";
|
|
import { useSessionStore } from "@/stores/session-store";
|
|
import { queryClient as singletonQueryClient } from "@/query/query-client";
|
|
import { agentCommandsQueryRoot } from "@/hooks/agent-commands-query";
|
|
import {
|
|
isProvidersSnapshotHomeScope,
|
|
normalizeProvidersSnapshotCwd,
|
|
providersSnapshotQueryKey,
|
|
providersSnapshotQueryRoot,
|
|
providersSnapshotRequestOptions,
|
|
} from "@/hooks/providers-snapshot-query";
|
|
|
|
type GetProvidersSnapshotResult = Awaited<ReturnType<DaemonClient["getProvidersSnapshot"]>>;
|
|
type RefreshProvidersSnapshotResult = Awaited<ReturnType<DaemonClient["refreshProvidersSnapshot"]>>;
|
|
|
|
export { providersSnapshotQueryKey, providersSnapshotQueryRoot };
|
|
|
|
export type ProvidersSnapshotClient = Pick<
|
|
DaemonClient,
|
|
"getProvidersSnapshot" | "refreshProvidersSnapshot"
|
|
>;
|
|
|
|
export interface ProvidersSnapshotUpdateMessage {
|
|
type: "providers_snapshot_update";
|
|
payload: {
|
|
cwd?: string;
|
|
entries: ProviderSnapshotEntry[];
|
|
generatedAt: string;
|
|
};
|
|
}
|
|
|
|
export async function fetchProvidersSnapshot(input: {
|
|
client: ProvidersSnapshotClient;
|
|
cwd: string | null;
|
|
}): Promise<GetProvidersSnapshotResult> {
|
|
return input.client.getProvidersSnapshot(providersSnapshotRequestOptions({ cwd: input.cwd }));
|
|
}
|
|
|
|
export async function refreshAndApplyProvidersSnapshot(input: {
|
|
client: ProvidersSnapshotClient;
|
|
queryClient: QueryClient;
|
|
serverId: string;
|
|
cwd: string | null;
|
|
providers?: AgentProvider[];
|
|
}): Promise<RefreshProvidersSnapshotResult> {
|
|
const refreshResult = await input.client.refreshProvidersSnapshot(
|
|
providersSnapshotRequestOptions({ cwd: input.cwd, providers: input.providers }),
|
|
);
|
|
const snapshot = await fetchProvidersSnapshot({ client: input.client, cwd: input.cwd });
|
|
input.queryClient.setQueryData(providersSnapshotQueryKey(input.serverId, input.cwd), snapshot);
|
|
void input.queryClient.invalidateQueries({
|
|
queryKey: agentCommandsQueryRoot(input.serverId),
|
|
exact: false,
|
|
});
|
|
if (isProvidersSnapshotHomeScope(input.cwd)) {
|
|
void input.queryClient.invalidateQueries({
|
|
queryKey: providersSnapshotQueryRoot(input.serverId),
|
|
exact: false,
|
|
});
|
|
}
|
|
return refreshResult;
|
|
}
|
|
|
|
export function applyProvidersSnapshotUpdate(input: {
|
|
serverId: string;
|
|
queryClient: QueryClient;
|
|
message: ProvidersSnapshotUpdateMessage;
|
|
}): void {
|
|
if (input.message.type !== "providers_snapshot_update") {
|
|
return;
|
|
}
|
|
const queryKey = providersSnapshotQueryKey(input.serverId, input.message.payload.cwd);
|
|
input.queryClient.setQueryData(queryKey, {
|
|
entries: input.message.payload.entries,
|
|
generatedAt: input.message.payload.generatedAt,
|
|
requestId: "providers_snapshot_update",
|
|
});
|
|
void input.queryClient.invalidateQueries({
|
|
queryKey: agentCommandsQueryRoot(input.serverId),
|
|
exact: false,
|
|
});
|
|
}
|
|
|
|
export type SelectorOpenRefetchDecision = "refetch-stale" | "refetch-always";
|
|
|
|
export function selectorOpenRefetchDecision(input: {
|
|
entries: ProviderSnapshotEntry[] | undefined;
|
|
selectedProvider: AgentProvider | null | undefined;
|
|
}): SelectorOpenRefetchDecision {
|
|
if (!input.selectedProvider) {
|
|
return "refetch-stale";
|
|
}
|
|
const selectedEntry = input.entries?.find((entry) => entry.provider === input.selectedProvider);
|
|
if (!selectedEntry || selectedEntry.status === "loading") {
|
|
return "refetch-always";
|
|
}
|
|
return "refetch-stale";
|
|
}
|
|
|
|
interface UseProvidersSnapshotResult {
|
|
entries: ProviderSnapshotEntry[] | undefined;
|
|
isLoading: boolean;
|
|
isFetching: boolean;
|
|
isRefreshing: boolean;
|
|
error: string | null;
|
|
supportsSnapshot: boolean;
|
|
refresh: (providers?: AgentProvider[]) => Promise<void>;
|
|
refetchIfStale: (selectedProvider?: AgentProvider | null) => void;
|
|
}
|
|
|
|
interface UseProvidersSnapshotOptions {
|
|
enabled?: boolean;
|
|
cwd?: string | null;
|
|
}
|
|
|
|
export function useProvidersSnapshot(
|
|
serverId: string | null,
|
|
options: UseProvidersSnapshotOptions = {},
|
|
): UseProvidersSnapshotResult {
|
|
const { t } = useTranslation();
|
|
const queryClient = useQueryClient();
|
|
const client = useHostRuntimeClient(serverId ?? "");
|
|
const isConnected = useHostRuntimeIsConnected(serverId ?? "");
|
|
const enabled = options.enabled ?? true;
|
|
const cwd = normalizeProvidersSnapshotCwd(options.cwd);
|
|
const supportsSnapshot = useSessionStore(
|
|
(state) => state.sessions[serverId ?? ""]?.serverInfo?.features?.providersSnapshot === true,
|
|
);
|
|
|
|
const queryKey = useMemo(() => providersSnapshotQueryKey(serverId, cwd), [cwd, serverId]);
|
|
|
|
const snapshotQuery = useQuery({
|
|
queryKey,
|
|
enabled: Boolean(enabled && supportsSnapshot && serverId && client && isConnected),
|
|
staleTime: 60_000,
|
|
queryFn: async () => {
|
|
if (!client) {
|
|
throw new Error(t("workspace.terminal.hostDisconnected"));
|
|
}
|
|
return fetchProvidersSnapshot({ client, cwd });
|
|
},
|
|
});
|
|
|
|
const refreshMutation = useMutation({
|
|
mutationFn: async (providers?: AgentProvider[]) => {
|
|
if (!client || !serverId) {
|
|
return;
|
|
}
|
|
await refreshAndApplyProvidersSnapshot({
|
|
client,
|
|
queryClient,
|
|
serverId,
|
|
cwd,
|
|
providers,
|
|
});
|
|
},
|
|
});
|
|
const { mutateAsync: refreshSnapshot, isPending: isRefreshing } = refreshMutation;
|
|
|
|
useEffect(() => {
|
|
if (!enabled || !supportsSnapshot || !client || !isConnected || !serverId) {
|
|
return;
|
|
}
|
|
|
|
return client.on("providers_snapshot_update", (message) => {
|
|
if (message.type !== "providers_snapshot_update") {
|
|
return;
|
|
}
|
|
applyProvidersSnapshotUpdate({ serverId, queryClient, message });
|
|
});
|
|
}, [client, enabled, isConnected, queryClient, serverId, supportsSnapshot]);
|
|
|
|
const refresh = useCallback(
|
|
async (providers?: AgentProvider[]) => {
|
|
await refreshSnapshot(providers);
|
|
},
|
|
[refreshSnapshot],
|
|
);
|
|
|
|
const refetchIfStale = useCallback(
|
|
(selectedProvider?: AgentProvider | null) => {
|
|
const decision = selectorOpenRefetchDecision({
|
|
entries: snapshotQuery.data?.entries,
|
|
selectedProvider,
|
|
});
|
|
if (decision === "refetch-always") {
|
|
void queryClient.refetchQueries({ queryKey, type: "active" });
|
|
return;
|
|
}
|
|
void queryClient.refetchQueries({ queryKey, type: "active", stale: true });
|
|
},
|
|
[queryClient, queryKey, snapshotQuery.data?.entries],
|
|
);
|
|
|
|
return {
|
|
entries: snapshotQuery.data?.entries ?? undefined,
|
|
isLoading: snapshotQuery.isLoading,
|
|
isFetching: snapshotQuery.isFetching,
|
|
isRefreshing,
|
|
error: snapshotQuery.error instanceof Error ? snapshotQuery.error.message : null,
|
|
supportsSnapshot,
|
|
refresh,
|
|
refetchIfStale,
|
|
};
|
|
}
|
|
|
|
export function prefetchProvidersSnapshot(
|
|
serverId: string,
|
|
client: DaemonClient,
|
|
options: { cwd?: string | null } = {},
|
|
): void {
|
|
const cwd = normalizeProvidersSnapshotCwd(options.cwd);
|
|
const queryKey = providersSnapshotQueryKey(serverId, cwd);
|
|
void singletonQueryClient.prefetchQuery({
|
|
queryKey,
|
|
staleTime: 60_000,
|
|
queryFn: () => fetchProvidersSnapshot({ client, cwd }),
|
|
});
|
|
}
|