Fix provider refresh scope and self-healing

This commit is contained in:
Mohamed Boudra
2026-04-21 12:41:28 +07:00
parent 92e701be8f
commit b01e274cce
54 changed files with 2057 additions and 694 deletions
@@ -161,6 +161,37 @@ describe("useAgentFormState", () => {
},
];
it("keeps provider, mode, and model unset on first open without preferences or explicit values", () => {
const resolved = __private__.resolveFormState(
undefined,
{},
null,
{
serverId: false,
provider: false,
modeId: false,
model: false,
thinkingOptionId: false,
workingDir: false,
},
{
serverId: null,
provider: null,
modeId: "",
model: "",
thinkingOptionId: "",
workingDir: "",
},
new Set<string>(),
makeProviderMap(TEST_CLAUDE_DEFINITION, TEST_CODEX_DEFINITION),
);
expect(resolved.provider).toBeNull();
expect(resolved.modeId).toBe("");
expect(resolved.model).toBe("");
expect(resolved.thinkingOptionId).toBe("");
});
it("does not auto-select a model on fresh drafts without preferences", () => {
const resolved = __private__.resolveFormState(
undefined,
@@ -374,7 +405,7 @@ describe("useAgentFormState", () => {
expect(resolved.thinkingOptionId).toBe("low");
});
it("resolves provider only from allowed provider map", () => {
it("clears an invalid provider instead of falling back to the first allowed provider", () => {
const resolved = __private__.resolveFormState(
undefined,
{ provider: "codex" },
@@ -399,7 +430,121 @@ describe("useAgentFormState", () => {
claudeProviderMap,
);
expect(resolved.provider).toBe("claude");
expect(resolved.provider).toBeNull();
});
it("preserves a user-selected provider and model while that provider is loading during refresh", () => {
const loadingEntries: ProviderSnapshotEntry[] = [
{
provider: "codex",
status: "loading",
label: TEST_CODEX_DEFINITION.label,
description: TEST_CODEX_DEFINITION.description,
defaultModeId: TEST_CODEX_DEFINITION.defaultModeId,
modes: TEST_CODEX_DEFINITION.modes,
},
{
provider: "claude",
status: "ready",
label: TEST_CLAUDE_DEFINITION.label,
description: TEST_CLAUDE_DEFINITION.description,
defaultModeId: TEST_CLAUDE_DEFINITION.defaultModeId,
modes: TEST_CLAUDE_DEFINITION.modes,
models: [{ provider: "claude", id: "default", label: "Default", isDefault: true }],
},
];
const providerDefinitions = buildProviderDefinitions(loadingEntries);
const resolvableProviderMap = __private__.buildProviderDefinitionMapForStatuses({
snapshotEntries: loadingEntries,
providerDefinitions,
statuses: new Set<ProviderSnapshotEntry["status"]>(["ready", "loading"]),
});
const resolved = __private__.resolveFormState(
undefined,
{},
null,
{
serverId: false,
provider: true,
modeId: true,
model: true,
thinkingOptionId: true,
workingDir: false,
},
{
serverId: null,
provider: "codex",
modeId: "full-access",
model: "gpt-5.3-codex",
thinkingOptionId: "xhigh",
workingDir: "",
},
new Set<string>(),
resolvableProviderMap,
);
expect(resolved.provider).toBe("codex");
expect(resolved.modeId).toBe("full-access");
expect(resolved.model).toBe("gpt-5.3-codex");
expect(resolved.thinkingOptionId).toBe("xhigh");
});
it("clears a user-selected provider when the refreshed snapshot marks it unavailable", () => {
const unavailableEntries: ProviderSnapshotEntry[] = [
{
provider: "codex",
status: "unavailable",
label: TEST_CODEX_DEFINITION.label,
description: TEST_CODEX_DEFINITION.description,
defaultModeId: TEST_CODEX_DEFINITION.defaultModeId,
modes: TEST_CODEX_DEFINITION.modes,
},
{
provider: "claude",
status: "ready",
label: TEST_CLAUDE_DEFINITION.label,
description: TEST_CLAUDE_DEFINITION.description,
defaultModeId: TEST_CLAUDE_DEFINITION.defaultModeId,
modes: TEST_CLAUDE_DEFINITION.modes,
models: [{ provider: "claude", id: "default", label: "Default", isDefault: true }],
},
];
const providerDefinitions = buildProviderDefinitions(unavailableEntries);
const resolvableProviderMap = __private__.buildProviderDefinitionMapForStatuses({
snapshotEntries: unavailableEntries,
providerDefinitions,
statuses: new Set<ProviderSnapshotEntry["status"]>(["ready", "loading"]),
});
const resolved = __private__.resolveFormState(
undefined,
{},
null,
{
serverId: false,
provider: true,
modeId: false,
model: false,
thinkingOptionId: false,
workingDir: false,
},
{
serverId: null,
provider: "codex",
modeId: "full-access",
model: "gpt-5.3-codex",
thinkingOptionId: "xhigh",
workingDir: "",
},
new Set<string>(),
resolvableProviderMap,
);
expect(resolved.provider).toBeNull();
expect(resolved.modeId).toBe("");
expect(resolved.model).toBe("");
expect(resolved.thinkingOptionId).toBe("");
});
it("does not force fallback provider when allowed provider map is empty", () => {
+95 -42
View File
@@ -48,7 +48,7 @@ const INITIAL_USER_MODIFIED: UserModifiedFields = {
// Internal form state
interface FormState {
serverId: string | null;
provider: AgentProvider;
provider: AgentProvider | null;
modeId: string;
model: string;
thinkingOptionId: string;
@@ -68,7 +68,7 @@ export type UseAgentFormStateResult = {
selectedServerId: string | null;
setSelectedServerId: (value: string | null) => void;
setSelectedServerIdFromUser: (value: string | null) => void;
selectedProvider: AgentProvider;
selectedProvider: AgentProvider | null;
setProviderFromUser: (provider: AgentProvider) => void;
selectedMode: string;
setModeFromUser: (modeId: string) => void;
@@ -97,9 +97,6 @@ export type UseAgentFormStateResult = {
persistFormPreferences: () => Promise<void>;
};
const DEFAULT_PROVIDER: AgentProvider = "claude";
const DEFAULT_MODE_FOR_DEFAULT_PROVIDER = "default";
function normalizeSelectedModelId(modelId: string | null | undefined): string {
const normalized = typeof modelId === "string" ? modelId.trim() : "";
if (!normalized) {
@@ -177,8 +174,6 @@ function resolveFormState(
): FormState {
// Start with current state - we only update non-user-modified fields
const result = { ...currentState };
const fallbackProvider = allowedProviderMap.keys().next().value as AgentProvider | undefined;
// 1. Resolve provider first (other fields depend on it)
if (!userModified.provider) {
if (initialValues?.provider && allowedProviderMap.has(initialValues.provider)) {
@@ -188,22 +183,33 @@ function resolveFormState(
allowedProviderMap.has(preferences.provider as AgentProvider)
) {
result.provider = preferences.provider as AgentProvider;
} else if (!allowedProviderMap.has(result.provider) && fallbackProvider) {
result.provider = fallbackProvider;
} else if (
result.provider &&
allowedProviderMap.size > 0 &&
!allowedProviderMap.has(result.provider)
) {
result.provider = null;
}
// else keep current (initialized to DEFAULT_PROVIDER)
} else if (!allowedProviderMap.has(result.provider) && fallbackProvider) {
result.provider = fallbackProvider;
} else if (
result.provider &&
allowedProviderMap.size > 0 &&
!allowedProviderMap.has(result.provider)
) {
result.provider = null;
}
const providerDef = allowedProviderMap.get(result.provider);
const providerPrefs = preferences?.providerPreferences?.[result.provider];
const providerDef = result.provider ? allowedProviderMap.get(result.provider) : undefined;
const providerPrefs = result.provider
? preferences?.providerPreferences?.[result.provider]
: undefined;
// 2. Resolve modeId (depends on provider)
if (!userModified.modeId) {
const validModeIds = providerDef?.modes.map((m) => m.id) ?? [];
if (
if (!result.provider) {
result.modeId = "";
} else if (
typeof initialValues?.modeId === "string" &&
initialValues.modeId.length > 0 &&
validModeIds.includes(initialValues.modeId)
@@ -223,7 +229,9 @@ function resolveFormState(
const preferredModel = normalizeSelectedModelId(providerPrefs?.model);
const defaultModelId = resolveDefaultModelId(availableModels);
if (initialModel) {
if (!result.provider) {
result.model = "";
} else if (initialModel) {
if (!availableModels || isValidModel(initialModel)) {
result.model = initialModel;
} else {
@@ -246,7 +254,9 @@ function resolveFormState(
? initialValues.thinkingOptionId.trim()
: "";
if (!userModified.thinkingOptionId) {
if (!result.provider) {
result.thinkingOptionId = "";
} else if (!userModified.thinkingOptionId) {
const effectiveModelId = result.model.trim();
const preferredThinking = effectiveModelId
? (providerPrefs?.thinkingByModel?.[effectiveModelId]?.trim() ?? "")
@@ -262,7 +272,7 @@ function resolveFormState(
}
// Validate thinking option once model metadata is available.
if (availableModels) {
if (result.provider && availableModels) {
result.thinkingOptionId = resolveThinkingOptionId({
availableModels,
modelId: result.model,
@@ -312,6 +322,37 @@ function combineInitialValues(
return initialValues;
}
const RESOLVABLE_PROVIDER_STATUSES = new Set<ProviderSnapshotEntry["status"]>(["ready", "loading"]);
const SELECTABLE_PROVIDER_STATUSES = new Set<ProviderSnapshotEntry["status"]>(["ready"]);
function buildProviderDefinitionMap(
providerDefinitions: AgentProviderDefinition[],
): Map<AgentProvider, AgentProviderDefinition> {
return new Map<AgentProvider, AgentProviderDefinition>(
providerDefinitions.map((definition) => [definition.id, definition]),
);
}
function buildProviderDefinitionMapForStatuses(args: {
snapshotEntries: ProviderSnapshotEntry[] | undefined;
providerDefinitions: AgentProviderDefinition[];
statuses: ReadonlySet<ProviderSnapshotEntry["status"]>;
}): Map<AgentProvider, AgentProviderDefinition> {
if (!args.snapshotEntries?.length) {
return buildProviderDefinitionMap(args.providerDefinitions);
}
const matchingProviders = new Set(
args.snapshotEntries
.filter((entry) => args.statuses.has(entry.status))
.map((entry) => entry.provider),
);
return buildProviderDefinitionMap(
args.providerDefinitions.filter((definition) => matchingProviders.has(definition.id)),
);
}
export function useAgentFormState(options: UseAgentFormStateOptions = {}): UseAgentFormStateResult {
const {
initialServerId = null,
@@ -335,8 +376,8 @@ export function useAgentFormState(options: UseAgentFormStateOptions = {}): UseAg
// Form state
const [formState, setFormState] = useState<FormState>(() => ({
serverId: initialServerId,
provider: DEFAULT_PROVIDER,
modeId: DEFAULT_MODE_FOR_DEFAULT_PROVIDER,
provider: null,
modeId: "",
model: "",
thinkingOptionId: "",
workingDir: "",
@@ -371,25 +412,25 @@ export function useAgentFormState(options: UseAgentFormStateOptions = {}): UseAg
[snapshotEntries],
);
const snapshotProviderDefinitionMap = useMemo(
() =>
new Map<AgentProvider, AgentProviderDefinition>(
snapshotProviderDefinitions.map((definition) => [definition.id, definition]),
),
() => buildProviderDefinitionMap(snapshotProviderDefinitions),
[snapshotProviderDefinitions],
);
const snapshotResolvableProviderDefinitionMap = useMemo(
() =>
buildProviderDefinitionMapForStatuses({
snapshotEntries,
providerDefinitions: snapshotProviderDefinitions,
statuses: RESOLVABLE_PROVIDER_STATUSES,
}),
[snapshotEntries, snapshotProviderDefinitions],
);
const snapshotSelectableProviderDefinitionMap = useMemo(() => {
if (!snapshotEntries?.length) {
return snapshotProviderDefinitionMap;
}
const readyProviders = new Set(
snapshotEntries.filter((entry) => entry.status === "ready").map((entry) => entry.provider),
);
return new Map<AgentProvider, AgentProviderDefinition>(
snapshotProviderDefinitions
.filter((definition) => readyProviders.has(definition.id))
.map((definition) => [definition.id, definition]),
);
}, [snapshotEntries, snapshotProviderDefinitionMap, snapshotProviderDefinitions]);
return buildProviderDefinitionMapForStatuses({
snapshotEntries,
providerDefinitions: snapshotProviderDefinitions,
statuses: SELECTABLE_PROVIDER_STATUSES,
});
}, [snapshotEntries, snapshotProviderDefinitions]);
const snapshotAllProviderModels = useMemo(() => {
const map = new Map<string, AgentModelDefinition[]>();
for (const entry of snapshotEntries ?? []) {
@@ -398,13 +439,16 @@ export function useAgentFormState(options: UseAgentFormStateOptions = {}): UseAg
return map;
}, [snapshotEntries]);
const snapshotSelectedEntry = useMemo(
() => (snapshotEntries ?? []).find((entry) => entry.provider === formState.provider) ?? null,
() =>
formState.provider
? ((snapshotEntries ?? []).find((entry) => entry.provider === formState.provider) ?? null)
: null,
[formState.provider, snapshotEntries],
);
const snapshotSelectedProviderModels = snapshotSelectedEntry?.models ?? null;
const snapshotSelectedProviderModes =
snapshotSelectedEntry?.modes ??
snapshotProviderDefinitionMap.get(formState.provider)?.modes ??
(formState.provider ? snapshotProviderDefinitionMap.get(formState.provider)?.modes : []) ??
[];
const providerDefinitions = snapshotProviderDefinitions;
const providerDefinitionMap = snapshotProviderDefinitionMap;
@@ -437,7 +481,7 @@ export function useAgentFormState(options: UseAgentFormStateOptions = {}): UseAg
userModified,
formStateRef.current,
validServerIds,
selectableProviderDefinitionMap,
snapshotResolvableProviderDefinitionMap,
);
// Only update if something changed
@@ -462,7 +506,7 @@ export function useAgentFormState(options: UseAgentFormStateOptions = {}): UseAg
availableModels,
userModified,
validServerIds,
selectableProviderDefinitionMap,
snapshotResolvableProviderDefinitionMap,
]);
// Auto-select the first online host when:
@@ -629,12 +673,17 @@ export function useAgentFormState(options: UseAgentFormStateOptions = {}): UseAg
}, [refetchSnapshotIfStale]);
const persistFormPreferences = useCallback(async () => {
if (!formState.provider) {
return;
}
const provider = formState.provider;
const resolvedModel = resolveEffectiveModel(availableModels, formState.model);
const modelId = resolvedModel?.id ?? formState.model;
await updatePreferences((current) =>
mergeProviderPreferences({
preferences: current,
provider: formState.provider,
provider,
updates: {
model: modelId || undefined,
mode: formState.modeId || undefined,
@@ -657,7 +706,9 @@ export function useAgentFormState(options: UseAgentFormStateOptions = {}): UseAg
updatePreferences,
]);
const agentDefinition = providerDefinitionMap.get(formState.provider);
const agentDefinition = formState.provider
? providerDefinitionMap.get(formState.provider)
: undefined;
const effectiveModel = resolveEffectiveModel(availableModels, formState.model);
const resolvedModelId = effectiveModel?.id ?? formState.model;
const availableThinkingOptions = effectiveModel?.thinkingOptions ?? [];
@@ -738,6 +789,8 @@ export function useAgentFormState(options: UseAgentFormStateOptions = {}): UseAg
export type CreateAgentInitialValues = FormInitialValues;
export const __private__ = {
buildProviderDefinitionMap,
buildProviderDefinitionMapForStatuses,
combineInitialValues,
resolveDefaultModel,
resolveFormState,
@@ -10,6 +10,7 @@ import {
import { useDraftAgentFeatures } from "@/hooks/use-draft-agent-features";
import { useDraftStore } from "@/stores/draft-store";
import type { AgentModelDefinition } from "@server/server/agent/agent-sdk-types";
import type { AgentProvider } from "@server/server/agent/agent-sdk-types";
type AttachmentUpdater =
| ComposerAttachment[]
@@ -113,7 +114,7 @@ function resolveEffectiveComposerThinkingOptionId(input: {
}
function buildDraftComposerCommandConfig(input: {
provider: DraftAgentStatusBarProps["selectedProvider"];
provider: AgentProvider | null;
cwd: string;
modeOptions: DraftAgentStatusBarProps["modeOptions"];
selectedMode: string;
@@ -122,7 +123,7 @@ function buildDraftComposerCommandConfig(input: {
featureValues?: Record<string, unknown>;
}): DraftCommandConfig | undefined {
const cwd = input.cwd.trim();
if (!cwd) {
if (!input.provider || !cwd) {
return undefined;
}
@@ -16,7 +16,7 @@ type DraftFeatureConfig = Pick<
export function useDraftAgentFeatures(input: {
serverId: string | null | undefined;
provider: AgentProvider;
provider: AgentProvider | null;
cwd: string | null | undefined;
modeId: string | null | undefined;
modelId: string | null | undefined;
@@ -28,30 +28,31 @@ export function useDraftAgentFeatures(input: {
const isConnected = useHostRuntimeIsConnected(serverId ?? "");
const { preferences, updatePreferences } = useFormPreferences();
const normalizedCwd = cwd?.trim() || "";
const normalizedProvider = provider ?? null;
const persistedFeatureValues = useMemo(
() => preferences.providerPreferences?.[provider]?.featureValues ?? {},
() => (provider ? (preferences.providerPreferences?.[provider]?.featureValues ?? {}) : {}),
[preferences.providerPreferences, provider],
);
const draftConfig = useMemo<DraftFeatureConfig | null>(() => {
if (!normalizedCwd) {
if (!normalizedProvider || !normalizedCwd) {
return null;
}
return {
provider,
provider: normalizedProvider,
cwd: normalizedCwd,
...(modeId ? { modeId } : {}),
...(modelId ? { model: modelId } : {}),
...(thinkingOptionId ? { thinkingOptionId } : {}),
};
}, [modeId, modelId, normalizedCwd, provider, thinkingOptionId]);
}, [modeId, modelId, normalizedCwd, normalizedProvider, thinkingOptionId]);
const featuresQuery = useQuery({
queryKey: [
"providerFeatures",
serverId ?? null,
provider,
normalizedProvider,
normalizedCwd || null,
modeId ?? null,
modelId ?? null,
@@ -106,6 +107,9 @@ export function useDraftAgentFeatures(input: {
return { ...current, [featureId]: value };
});
if (!provider) {
return;
}
void updatePreferences((current) =>
mergeProviderPreferences({
preferences: current,
@@ -0,0 +1,150 @@
/**
* @vitest-environment jsdom
*/
import React from "react";
import { act, renderHook, waitFor } from "@testing-library/react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { afterEach, describe, expect, it, vi } from "vitest";
import type { DaemonClient } from "@server/client/daemon-client";
import { useSessionStore } from "@/stores/session-store";
import {
providersSnapshotQueryKey,
shouldApplyProvidersSnapshotUpdate,
useProvidersSnapshot,
} from "./use-providers-snapshot";
const { mockClient, mockRuntime } = vi.hoisted(() => {
const mockClient = {
getProvidersSnapshot: vi.fn(),
refreshProvidersSnapshot: vi.fn(),
on: vi.fn(() => () => {}),
};
return {
mockClient,
mockRuntime: {
client: mockClient,
isConnected: true,
},
};
});
vi.mock("@/runtime/host-runtime", () => ({
useHostRuntimeClient: () => mockRuntime.client,
useHostRuntimeIsConnected: () => mockRuntime.isConnected,
}));
const serverId = "server-1";
function createQueryClient(): QueryClient {
return new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
});
}
function enableProvidersSnapshot(): void {
act(() => {
useSessionStore.getState().initializeSession(serverId, mockClient as unknown as DaemonClient);
useSessionStore.getState().updateSessionServerInfo(serverId, {
serverId,
hostname: "localhost",
version: "test",
features: { providersSnapshot: true },
} as never);
});
}
function renderProvidersSnapshotHook(cwd?: string | null) {
const queryClient = createQueryClient();
const wrapper = ({ children }: { children: React.ReactNode }) =>
React.createElement(QueryClientProvider, { client: queryClient }, children);
return renderHook(() => useProvidersSnapshot(serverId, cwd), { wrapper });
}
afterEach(() => {
act(() => {
useSessionStore.getState().clearSession(serverId);
});
vi.clearAllMocks();
});
describe("providers snapshot hook cache scope", () => {
it("uses no cwd in the settings query key", () => {
expect(providersSnapshotQueryKey(serverId)).toEqual(["providersSnapshot", serverId, null]);
});
it("accepts concrete home update events for settings snapshots", () => {
expect(shouldApplyProvidersSnapshotUpdate(undefined, "/Users/alex")).toBe(true);
expect(shouldApplyProvidersSnapshotUpdate(null, "/home/alex")).toBe(true);
});
it("keeps workspace snapshot updates scoped to matching cwd keys", () => {
expect(shouldApplyProvidersSnapshotUpdate("/Users/alex/project", "/Users/alex/project")).toBe(
true,
);
expect(
shouldApplyProvidersSnapshotUpdate("/Users/alex/project-a", "/Users/alex/project-b"),
).toBe(false);
expect(shouldApplyProvidersSnapshotUpdate("/Users/alex/project", "/Users/alex")).toBe(false);
});
it("sends no cwd for settings snapshot loads and refreshes", async () => {
enableProvidersSnapshot();
mockClient.getProvidersSnapshot.mockResolvedValue({
entries: [],
generatedAt: "2026-01-01T00:00:00.000Z",
requestId: "settings",
});
mockClient.refreshProvidersSnapshot.mockResolvedValue({
acknowledged: true,
requestId: "settings-refresh",
});
const { result } = renderProvidersSnapshotHook();
await waitFor(() => {
expect(mockClient.getProvidersSnapshot).toHaveBeenCalledWith({});
});
await act(async () => {
await result.current.refresh(["codex"]);
});
expect(mockClient.refreshProvidersSnapshot).toHaveBeenCalledWith({ providers: ["codex"] });
expect(mockClient.getProvidersSnapshot).toHaveBeenLastCalledWith({});
});
it("sends cwd for workspace snapshot loads and refreshes", async () => {
enableProvidersSnapshot();
mockClient.getProvidersSnapshot.mockResolvedValue({
entries: [],
generatedAt: "2026-01-01T00:00:00.000Z",
requestId: "workspace",
});
mockClient.refreshProvidersSnapshot.mockResolvedValue({
acknowledged: true,
requestId: "workspace-refresh",
});
const { result } = renderProvidersSnapshotHook("/repo");
await waitFor(() => {
expect(mockClient.getProvidersSnapshot).toHaveBeenCalledWith({ cwd: "/repo" });
});
await act(async () => {
await result.current.refresh(["codex"]);
});
expect(mockClient.refreshProvidersSnapshot).toHaveBeenCalledWith({
cwd: "/repo",
providers: ["codex"],
});
expect(mockClient.getProvidersSnapshot).toHaveBeenLastCalledWith({ cwd: "/repo" });
});
});
@@ -6,7 +6,7 @@ import { useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-
import { useSessionStore } from "@/stores/session-store";
import { queryClient as singletonQueryClient } from "@/query/query-client";
function normalizeProvidersSnapshotCwdKey(cwd?: string | null): string | null {
export function normalizeProvidersSnapshotCwdKey(cwd?: string | null): string | null {
const trimmed = cwd?.trim();
if (!trimmed) {
return null;
@@ -15,10 +15,33 @@ function normalizeProvidersSnapshotCwdKey(cwd?: string | null): string | null {
return trimmed.replace(/^\/(?:Users|home)\/[^/]+/, "~");
}
export function shouldApplyProvidersSnapshotUpdate(
currentCwd?: string | null,
messageCwd?: string | null,
): boolean {
const currentCwdKey = normalizeProvidersSnapshotCwdKey(currentCwd);
const messageCwdKey = normalizeProvidersSnapshotCwdKey(messageCwd);
return messageCwdKey === currentCwdKey || (currentCwdKey === null && messageCwdKey === "~");
}
export function providersSnapshotQueryKey(serverId: string | null, cwd?: string | null) {
return ["providersSnapshot", serverId, normalizeProvidersSnapshotCwdKey(cwd)] as const;
}
function providersSnapshotRequest(cwd?: string): { cwd?: string } {
return cwd ? { cwd } : {};
}
function refreshProvidersSnapshotRequest(
cwd: string | undefined,
providers: AgentProvider[] | undefined,
): { cwd?: string; providers?: AgentProvider[] } {
return {
...providersSnapshotRequest(cwd),
...(providers ? { providers } : {}),
};
}
interface UseProvidersSnapshotResult {
entries: ProviderSnapshotEntry[] | undefined;
isLoading: boolean;
@@ -62,7 +85,7 @@ export function useProvidersSnapshot(
if (!client) {
throw new Error("Host is not connected");
}
return client.getProvidersSnapshot({ cwd: normalizedCwd });
return client.getProvidersSnapshot(providersSnapshotRequest(normalizedCwd));
},
});
@@ -71,7 +94,9 @@ export function useProvidersSnapshot(
if (!client) {
return;
}
await client.refreshProvidersSnapshot({ cwd: normalizedCwd, providers });
await client.refreshProvidersSnapshot(
refreshProvidersSnapshotRequest(normalizedCwd, providers),
);
},
});
const { mutateAsync: refreshSnapshot, isPending: isRefreshing } = refreshMutation;
@@ -85,8 +110,7 @@ export function useProvidersSnapshot(
if (message.type !== "providers_snapshot_update") {
return;
}
const messageCwdKey = normalizeProvidersSnapshotCwdKey(message.payload.cwd);
if (messageCwdKey !== normalizedCwdKey) {
if (!shouldApplyProvidersSnapshotUpdate(normalizedCwd, message.payload.cwd)) {
return;
}
queryClient.setQueryData(queryKey, {
@@ -99,6 +123,7 @@ export function useProvidersSnapshot(
client,
enabled,
isConnected,
normalizedCwd,
normalizedCwdKey,
queryClient,
queryKey,
@@ -108,9 +133,14 @@ export function useProvidersSnapshot(
const refresh = useCallback(
async (providers?: AgentProvider[]) => {
if (!client) {
return;
}
await refreshSnapshot(providers);
const snapshot = await client.getProvidersSnapshot(providersSnapshotRequest(normalizedCwd));
queryClient.setQueryData(queryKey, snapshot);
},
[refreshSnapshot],
[client, normalizedCwd, queryClient, queryKey, refreshSnapshot],
);
const refetchIfStale = useCallback(() => {
@@ -139,6 +169,6 @@ export function prefetchProvidersSnapshot(
void singletonQueryClient.prefetchQuery({
queryKey,
staleTime: 60_000,
queryFn: () => client.getProvidersSnapshot({ cwd: normalizedCwd }),
queryFn: () => client.getProvidersSnapshot(providersSnapshotRequest(normalizedCwd)),
});
}