refresh onboarding when provider catalog loads (#9051)
Signed-off-by: tulsi <tulsi@block.xyz>
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import { act, renderHook } from "@testing-library/react";
|
import { act, renderHook, waitFor } from "@testing-library/react";
|
||||||
import type { ProviderInventoryEntryDto } from "@aaif/goose-sdk";
|
import type { ProviderInventoryEntryDto } from "@aaif/goose-sdk";
|
||||||
import { beforeEach, describe, expect, it } from "vitest";
|
import { beforeEach, describe, expect, it } from "vitest";
|
||||||
import { useAgentStore } from "@/features/agents/stores/agentStore";
|
import { useAgentStore } from "@/features/agents/stores/agentStore";
|
||||||
@@ -90,6 +90,35 @@ describe("useOnboardingGate", () => {
|
|||||||
expect(result.current.readiness.reason).toBe("ready");
|
expect(result.current.readiness.reason).toBe("ready");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("recomputes model readiness when the provider catalog loads after inventory", async () => {
|
||||||
|
useProviderCatalogStore.getState().reset();
|
||||||
|
writeCompletedOnboarding("anthropic", "claude-sonnet-4-5");
|
||||||
|
useProviderInventoryStore.getState().setEntries([providerEntry({})]);
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useOnboardingGate(true));
|
||||||
|
|
||||||
|
expect(result.current.shouldShowOnboarding).toBe(true);
|
||||||
|
expect(result.current.readiness.reason).toBe("missing_provider");
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
useProviderCatalogStore.getState().setEntries([
|
||||||
|
{
|
||||||
|
id: "anthropic",
|
||||||
|
displayName: "Anthropic",
|
||||||
|
category: "model",
|
||||||
|
description: "",
|
||||||
|
setupMethod: "single_api_key",
|
||||||
|
group: "default",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.current.shouldShowOnboarding).toBe(false);
|
||||||
|
expect(result.current.readiness.providerId).toBe("anthropic");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("skips onboarding when completion and the selected Goose model are usable", () => {
|
it("skips onboarding when completion and the selected Goose model are usable", () => {
|
||||||
writeCompletedOnboarding("anthropic", "claude-sonnet-4-5");
|
writeCompletedOnboarding("anthropic", "claude-sonnet-4-5");
|
||||||
setStoredModelPreference("goose", {
|
setStoredModelPreference("goose", {
|
||||||
|
|||||||
@@ -2,10 +2,11 @@ import { useCallback, useMemo, useState } from "react";
|
|||||||
import type { ProviderInventoryEntryDto } from "@aaif/goose-sdk";
|
import type { ProviderInventoryEntryDto } from "@aaif/goose-sdk";
|
||||||
import { useAgentStore } from "@/features/agents/stores/agentStore";
|
import { useAgentStore } from "@/features/agents/stores/agentStore";
|
||||||
import {
|
import {
|
||||||
getModelProviders,
|
getModelProvidersFromEntries,
|
||||||
resolveAgentProviderCatalogIdStrict,
|
resolveAgentProviderCatalogIdStrictFromEntries,
|
||||||
} from "@/features/providers/providerCatalog";
|
} from "@/features/providers/providerCatalog";
|
||||||
import { useProviderInventory } from "@/features/providers/hooks/useProviderInventory";
|
import { useProviderInventory } from "@/features/providers/hooks/useProviderInventory";
|
||||||
|
import { useProviderCatalogStore } from "@/features/providers/stores/providerCatalogStore";
|
||||||
import { useDistroStore } from "@/features/settings/stores/distroStore";
|
import { useDistroStore } from "@/features/settings/stores/distroStore";
|
||||||
import { filterModelProvidersForDistro } from "@/features/providers/distroProviderConstraints";
|
import { filterModelProvidersForDistro } from "@/features/providers/distroProviderConstraints";
|
||||||
import { getStoredModelPreference } from "@/features/chat/lib/modelPreferences";
|
import { getStoredModelPreference } from "@/features/chat/lib/modelPreferences";
|
||||||
@@ -51,6 +52,7 @@ export function useOnboardingGate(startupReady: boolean) {
|
|||||||
const selectedProvider = useAgentStore((state) => state.selectedProvider);
|
const selectedProvider = useAgentStore((state) => state.selectedProvider);
|
||||||
const { entries, configuredModelProviderEntries, getModelsForAgent } =
|
const { entries, configuredModelProviderEntries, getModelsForAgent } =
|
||||||
useProviderInventory();
|
useProviderInventory();
|
||||||
|
const catalogEntries = useProviderCatalogStore((state) => state.entries);
|
||||||
const distro = useDistroStore((state) => state.manifest);
|
const distro = useDistroStore((state) => state.manifest);
|
||||||
const [completion, setCompletion] = useState<OnboardingCompletion | null>(
|
const [completion, setCompletion] = useState<OnboardingCompletion | null>(
|
||||||
readCompletion,
|
readCompletion,
|
||||||
@@ -59,17 +61,24 @@ export function useOnboardingGate(startupReady: boolean) {
|
|||||||
const modelProviderIds = useMemo(
|
const modelProviderIds = useMemo(
|
||||||
() =>
|
() =>
|
||||||
new Set(
|
new Set(
|
||||||
filterModelProvidersForDistro(getModelProviders(), distro).map(
|
filterModelProvidersForDistro(
|
||||||
(provider) => provider.id,
|
getModelProvidersFromEntries(catalogEntries),
|
||||||
),
|
distro,
|
||||||
|
).map((provider) => provider.id),
|
||||||
),
|
),
|
||||||
[distro],
|
[catalogEntries, distro],
|
||||||
|
);
|
||||||
|
|
||||||
|
const selectedAgentId = useMemo(
|
||||||
|
() =>
|
||||||
|
resolveAgentProviderCatalogIdStrictFromEntries(
|
||||||
|
catalogEntries,
|
||||||
|
selectedProvider,
|
||||||
|
) ?? "goose",
|
||||||
|
[catalogEntries, selectedProvider],
|
||||||
);
|
);
|
||||||
|
|
||||||
const readiness = useMemo<OnboardingReadiness>(() => {
|
const readiness = useMemo<OnboardingReadiness>(() => {
|
||||||
const selectedAgentId =
|
|
||||||
resolveAgentProviderCatalogIdStrict(selectedProvider) ?? "goose";
|
|
||||||
|
|
||||||
if (selectedAgentId !== "goose") {
|
if (selectedAgentId !== "goose") {
|
||||||
const models = getModelsForAgent(selectedAgentId);
|
const models = getModelsForAgent(selectedAgentId);
|
||||||
const entry = entries.get(selectedAgentId);
|
const entry = entries.get(selectedAgentId);
|
||||||
@@ -136,7 +145,7 @@ export function useOnboardingGate(startupReady: boolean) {
|
|||||||
entries,
|
entries,
|
||||||
getModelsForAgent,
|
getModelsForAgent,
|
||||||
modelProviderIds,
|
modelProviderIds,
|
||||||
selectedProvider,
|
selectedAgentId,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const completeOnboarding = useCallback(
|
const completeOnboarding = useCallback(
|
||||||
|
|||||||
@@ -158,6 +158,39 @@ describe("useOnboardingProviderStep", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("updates provider setup rows when the catalog loads after initial render", async () => {
|
||||||
|
useProviderCatalogStore.getState().reset();
|
||||||
|
|
||||||
|
const { result } = renderProviderStep(
|
||||||
|
readyReadiness({
|
||||||
|
isUsable: false,
|
||||||
|
providerId: null,
|
||||||
|
reason: "not_completed",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.current.modelProviders).toHaveLength(0);
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
useProviderCatalogStore.getState().setEntries([
|
||||||
|
{
|
||||||
|
id: "anthropic",
|
||||||
|
displayName: "Anthropic",
|
||||||
|
category: "model",
|
||||||
|
description: "",
|
||||||
|
setupMethod: "single_api_key",
|
||||||
|
group: "default",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(
|
||||||
|
result.current.modelProviders.map((provider) => provider.id),
|
||||||
|
).toEqual(["anthropic"]),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it("continues with a current ACP agent without writing Goose model defaults", async () => {
|
it("continues with a current ACP agent without writing Goose model defaults", async () => {
|
||||||
const { result, onReady, onSelectedSetup } = renderProviderStep(
|
const { result, onReady, onSelectedSetup } = renderProviderStep(
|
||||||
readyReadiness({
|
readyReadiness({
|
||||||
|
|||||||
@@ -3,11 +3,12 @@ import type { ProviderInventoryEntryDto } from "@aaif/goose-sdk";
|
|||||||
import { useAgentStore } from "@/features/agents/stores/agentStore";
|
import { useAgentStore } from "@/features/agents/stores/agentStore";
|
||||||
import { setStoredModelPreference } from "@/features/chat/lib/modelPreferences";
|
import { setStoredModelPreference } from "@/features/chat/lib/modelPreferences";
|
||||||
import {
|
import {
|
||||||
getAgentProviders,
|
getAgentProvidersFromEntries,
|
||||||
getModelProviders,
|
getModelProvidersFromEntries,
|
||||||
} from "@/features/providers/providerCatalog";
|
} from "@/features/providers/providerCatalog";
|
||||||
import { filterModelProvidersForDistro } from "@/features/providers/distroProviderConstraints";
|
import { filterModelProvidersForDistro } from "@/features/providers/distroProviderConstraints";
|
||||||
import { useCredentials } from "@/features/providers/hooks/useCredentials";
|
import { useCredentials } from "@/features/providers/hooks/useCredentials";
|
||||||
|
import { useProviderCatalogStore } from "@/features/providers/stores/providerCatalogStore";
|
||||||
import { useProviderInventoryStore } from "@/features/providers/stores/providerInventoryStore";
|
import { useProviderInventoryStore } from "@/features/providers/stores/providerInventoryStore";
|
||||||
import { useDistroStore } from "@/features/settings/stores/distroStore";
|
import { useDistroStore } from "@/features/settings/stores/distroStore";
|
||||||
import { saveDefaults } from "../api/onboarding";
|
import { saveDefaults } from "../api/onboarding";
|
||||||
@@ -42,6 +43,7 @@ export function useOnboardingProviderStep({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const inventoryEntries = useProviderInventoryStore((state) => state.entries);
|
const inventoryEntries = useProviderInventoryStore((state) => state.entries);
|
||||||
|
const catalogEntries = useProviderCatalogStore((state) => state.entries);
|
||||||
const agentStore = useAgentStore();
|
const agentStore = useAgentStore();
|
||||||
const distro = useDistroStore((state) => state.manifest);
|
const distro = useDistroStore((state) => state.manifest);
|
||||||
|
|
||||||
@@ -57,8 +59,16 @@ export function useOnboardingProviderStep({
|
|||||||
completeNativeSetup,
|
completeNativeSetup,
|
||||||
} = useCredentials();
|
} = useCredentials();
|
||||||
|
|
||||||
|
const agentProviders = useMemo(
|
||||||
|
() => getAgentProvidersFromEntries(catalogEntries),
|
||||||
|
[catalogEntries],
|
||||||
|
);
|
||||||
|
|
||||||
const modelProviders = useMemo(() => {
|
const modelProviders = useMemo(() => {
|
||||||
const all = filterModelProvidersForDistro(getModelProviders(), distro);
|
const all = filterModelProvidersForDistro(
|
||||||
|
getModelProvidersFromEntries(catalogEntries),
|
||||||
|
distro,
|
||||||
|
);
|
||||||
return [...all].sort((a, b) => {
|
return [...all].sort((a, b) => {
|
||||||
const aIndex = PROMOTED_MODEL_ORDER.indexOf(a.id);
|
const aIndex = PROMOTED_MODEL_ORDER.indexOf(a.id);
|
||||||
const bIndex = PROMOTED_MODEL_ORDER.indexOf(b.id);
|
const bIndex = PROMOTED_MODEL_ORDER.indexOf(b.id);
|
||||||
@@ -67,7 +77,7 @@ export function useOnboardingProviderStep({
|
|||||||
}
|
}
|
||||||
return a.displayName.localeCompare(b.displayName);
|
return a.displayName.localeCompare(b.displayName);
|
||||||
});
|
});
|
||||||
}, [distro]);
|
}, [catalogEntries, distro]);
|
||||||
|
|
||||||
const visibleModelProviders = modelProviders.filter(
|
const visibleModelProviders = modelProviders.filter(
|
||||||
(provider) =>
|
(provider) =>
|
||||||
@@ -93,12 +103,10 @@ export function useOnboardingProviderStep({
|
|||||||
(entry) =>
|
(entry) =>
|
||||||
entry.configured &&
|
entry.configured &&
|
||||||
entry.providerId !== "goose" &&
|
entry.providerId !== "goose" &&
|
||||||
getAgentProviders().some(
|
agentProviders.some((provider) => provider.id === entry.providerId) &&
|
||||||
(provider) => provider.id === entry.providerId,
|
|
||||||
) &&
|
|
||||||
entry.models.length > 0,
|
entry.models.length > 0,
|
||||||
),
|
),
|
||||||
[inventoryEntries],
|
[agentProviders, inventoryEntries],
|
||||||
);
|
);
|
||||||
|
|
||||||
const usableDefaultEntries = useMemo<UsableDefaultEntry[]>(
|
const usableDefaultEntries = useMemo<UsableDefaultEntry[]>(
|
||||||
@@ -167,7 +175,7 @@ export function useOnboardingProviderStep({
|
|||||||
modelId: readiness.modelId,
|
modelId: readiness.modelId,
|
||||||
modelName: readiness.modelName,
|
modelName: readiness.modelName,
|
||||||
};
|
};
|
||||||
const isAgentProvider = getAgentProviders().some(
|
const isAgentProvider = agentProviders.some(
|
||||||
(provider) => provider.id === readiness.providerId,
|
(provider) => provider.id === readiness.providerId,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -57,7 +57,7 @@
|
|||||||
"defaultFallback": "Ready on Home",
|
"defaultFallback": "Ready on Home",
|
||||||
"defaultDescription": "This is what Goose will use when you start a new chat.",
|
"defaultDescription": "This is what Goose will use when you start a new chat.",
|
||||||
"extensionsTitle": "Extensions",
|
"extensionsTitle": "Extensions",
|
||||||
"extensionsDescription": "Connected tools are available in Settings. Imported Claude Desktop tools stay off until you enable them.",
|
"extensionsDescription": "Goose can use connected tools in chats when needed. Manage imported tools from Extensions.",
|
||||||
"skillsTitle": "Skills",
|
"skillsTitle": "Skills",
|
||||||
"skillsDescription": "Personal skills Goose can see now. Project skills appear when you open a project."
|
"skillsDescription": "Personal skills Goose can see now. Project skills appear when you open a project."
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -57,7 +57,7 @@
|
|||||||
"defaultFallback": "Listo en Inicio",
|
"defaultFallback": "Listo en Inicio",
|
||||||
"defaultDescription": "Esto es lo que Goose usará cuando empieces un chat nuevo.",
|
"defaultDescription": "Esto es lo que Goose usará cuando empieces un chat nuevo.",
|
||||||
"extensionsTitle": "Extensiones",
|
"extensionsTitle": "Extensiones",
|
||||||
"extensionsDescription": "Las herramientas conectadas están disponibles en Configuración. Las herramientas importadas de Claude Desktop quedan desactivadas hasta que las habilites.",
|
"extensionsDescription": "Goose puede usar herramientas conectadas en los chats cuando las necesite. Administra las herramientas importadas desde Extensiones.",
|
||||||
"skillsTitle": "Habilidades",
|
"skillsTitle": "Habilidades",
|
||||||
"skillsDescription": "Habilidades personales que Goose puede ver ahora. Las habilidades de proyecto aparecen cuando abres un proyecto."
|
"skillsDescription": "Habilidades personales que Goose puede ver ahora. Las habilidades de proyecto aparecen cuando abres un proyecto."
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -93,6 +93,48 @@ export function buildInitScript(options?: {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
const PROVIDER_SETUP_CATALOG = [
|
||||||
|
{
|
||||||
|
providerId: "claude",
|
||||||
|
name: "Claude",
|
||||||
|
category: "model",
|
||||||
|
description: "Claude provider",
|
||||||
|
setupMethod: "single_api_key",
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: "ANTHROPIC_API_KEY",
|
||||||
|
label: "API key",
|
||||||
|
secret: true,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
group: "default",
|
||||||
|
showOnlyWhenInstalled: false,
|
||||||
|
supportsInstall: false,
|
||||||
|
supportsAuth: false,
|
||||||
|
supportsAuthStatus: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
providerId: "openai",
|
||||||
|
name: "OpenAI",
|
||||||
|
category: "model",
|
||||||
|
description: "OpenAI provider",
|
||||||
|
setupMethod: "single_api_key",
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: "OPENAI_API_KEY",
|
||||||
|
label: "API key",
|
||||||
|
secret: true,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
group: "default",
|
||||||
|
showOnlyWhenInstalled: false,
|
||||||
|
supportsInstall: false,
|
||||||
|
supportsAuth: false,
|
||||||
|
supportsAuthStatus: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
"goose:onboarding:v1",
|
"goose:onboarding:v1",
|
||||||
@@ -243,7 +285,7 @@ export function buildInitScript(options?: {
|
|||||||
case "_goose/providers/list":
|
case "_goose/providers/list":
|
||||||
return jsonRpcResult(message.id, { entries: PROVIDER_INVENTORY });
|
return jsonRpcResult(message.id, { entries: PROVIDER_INVENTORY });
|
||||||
case "_goose/providers/setup/catalog/list":
|
case "_goose/providers/setup/catalog/list":
|
||||||
return jsonRpcResult(message.id, { providers: [] });
|
return jsonRpcResult(message.id, { providers: PROVIDER_SETUP_CATALOG });
|
||||||
case "_goose/providers/inventory/refresh":
|
case "_goose/providers/inventory/refresh":
|
||||||
return jsonRpcResult(message.id, { started: [], skipped: [] });
|
return jsonRpcResult(message.id, { started: [], skipped: [] });
|
||||||
case "_goose/defaults/read":
|
case "_goose/defaults/read":
|
||||||
|
|||||||
Reference in New Issue
Block a user