refresh onboarding when provider catalog loads (#9051)

Signed-off-by: tulsi <tulsi@block.xyz>
This commit is contained in:
tulsi
2026-05-12 13:42:19 -07:00
committed by GitHub
parent 9ff0825b1d
commit 067770740e
7 changed files with 144 additions and 23 deletions
@@ -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 { beforeEach, describe, expect, it } from "vitest";
import { useAgentStore } from "@/features/agents/stores/agentStore";
@@ -90,6 +90,35 @@ describe("useOnboardingGate", () => {
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", () => {
writeCompletedOnboarding("anthropic", "claude-sonnet-4-5");
setStoredModelPreference("goose", {
@@ -2,10 +2,11 @@ import { useCallback, useMemo, useState } from "react";
import type { ProviderInventoryEntryDto } from "@aaif/goose-sdk";
import { useAgentStore } from "@/features/agents/stores/agentStore";
import {
getModelProviders,
resolveAgentProviderCatalogIdStrict,
getModelProvidersFromEntries,
resolveAgentProviderCatalogIdStrictFromEntries,
} from "@/features/providers/providerCatalog";
import { useProviderInventory } from "@/features/providers/hooks/useProviderInventory";
import { useProviderCatalogStore } from "@/features/providers/stores/providerCatalogStore";
import { useDistroStore } from "@/features/settings/stores/distroStore";
import { filterModelProvidersForDistro } from "@/features/providers/distroProviderConstraints";
import { getStoredModelPreference } from "@/features/chat/lib/modelPreferences";
@@ -51,6 +52,7 @@ export function useOnboardingGate(startupReady: boolean) {
const selectedProvider = useAgentStore((state) => state.selectedProvider);
const { entries, configuredModelProviderEntries, getModelsForAgent } =
useProviderInventory();
const catalogEntries = useProviderCatalogStore((state) => state.entries);
const distro = useDistroStore((state) => state.manifest);
const [completion, setCompletion] = useState<OnboardingCompletion | null>(
readCompletion,
@@ -59,17 +61,24 @@ export function useOnboardingGate(startupReady: boolean) {
const modelProviderIds = useMemo(
() =>
new Set(
filterModelProvidersForDistro(getModelProviders(), distro).map(
(provider) => provider.id,
),
filterModelProvidersForDistro(
getModelProvidersFromEntries(catalogEntries),
distro,
).map((provider) => provider.id),
),
[distro],
[catalogEntries, distro],
);
const selectedAgentId = useMemo(
() =>
resolveAgentProviderCatalogIdStrictFromEntries(
catalogEntries,
selectedProvider,
) ?? "goose",
[catalogEntries, selectedProvider],
);
const readiness = useMemo<OnboardingReadiness>(() => {
const selectedAgentId =
resolveAgentProviderCatalogIdStrict(selectedProvider) ?? "goose";
if (selectedAgentId !== "goose") {
const models = getModelsForAgent(selectedAgentId);
const entry = entries.get(selectedAgentId);
@@ -136,7 +145,7 @@ export function useOnboardingGate(startupReady: boolean) {
entries,
getModelsForAgent,
modelProviderIds,
selectedProvider,
selectedAgentId,
]);
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 () => {
const { result, onReady, onSelectedSetup } = renderProviderStep(
readyReadiness({
@@ -3,11 +3,12 @@ import type { ProviderInventoryEntryDto } from "@aaif/goose-sdk";
import { useAgentStore } from "@/features/agents/stores/agentStore";
import { setStoredModelPreference } from "@/features/chat/lib/modelPreferences";
import {
getAgentProviders,
getModelProviders,
getAgentProvidersFromEntries,
getModelProvidersFromEntries,
} from "@/features/providers/providerCatalog";
import { filterModelProvidersForDistro } from "@/features/providers/distroProviderConstraints";
import { useCredentials } from "@/features/providers/hooks/useCredentials";
import { useProviderCatalogStore } from "@/features/providers/stores/providerCatalogStore";
import { useProviderInventoryStore } from "@/features/providers/stores/providerInventoryStore";
import { useDistroStore } from "@/features/settings/stores/distroStore";
import { saveDefaults } from "../api/onboarding";
@@ -42,6 +43,7 @@ export function useOnboardingProviderStep({
);
const inventoryEntries = useProviderInventoryStore((state) => state.entries);
const catalogEntries = useProviderCatalogStore((state) => state.entries);
const agentStore = useAgentStore();
const distro = useDistroStore((state) => state.manifest);
@@ -57,8 +59,16 @@ export function useOnboardingProviderStep({
completeNativeSetup,
} = useCredentials();
const agentProviders = useMemo(
() => getAgentProvidersFromEntries(catalogEntries),
[catalogEntries],
);
const modelProviders = useMemo(() => {
const all = filterModelProvidersForDistro(getModelProviders(), distro);
const all = filterModelProvidersForDistro(
getModelProvidersFromEntries(catalogEntries),
distro,
);
return [...all].sort((a, b) => {
const aIndex = PROMOTED_MODEL_ORDER.indexOf(a.id);
const bIndex = PROMOTED_MODEL_ORDER.indexOf(b.id);
@@ -67,7 +77,7 @@ export function useOnboardingProviderStep({
}
return a.displayName.localeCompare(b.displayName);
});
}, [distro]);
}, [catalogEntries, distro]);
const visibleModelProviders = modelProviders.filter(
(provider) =>
@@ -93,12 +103,10 @@ export function useOnboardingProviderStep({
(entry) =>
entry.configured &&
entry.providerId !== "goose" &&
getAgentProviders().some(
(provider) => provider.id === entry.providerId,
) &&
agentProviders.some((provider) => provider.id === entry.providerId) &&
entry.models.length > 0,
),
[inventoryEntries],
[agentProviders, inventoryEntries],
);
const usableDefaultEntries = useMemo<UsableDefaultEntry[]>(
@@ -167,7 +175,7 @@ export function useOnboardingProviderStep({
modelId: readiness.modelId,
modelName: readiness.modelName,
};
const isAgentProvider = getAgentProviders().some(
const isAgentProvider = agentProviders.some(
(provider) => provider.id === readiness.providerId,
);
@@ -57,7 +57,7 @@
"defaultFallback": "Ready on Home",
"defaultDescription": "This is what Goose will use when you start a new chat.",
"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",
"skillsDescription": "Personal skills Goose can see now. Project skills appear when you open a project."
},
@@ -57,7 +57,7 @@
"defaultFallback": "Listo en Inicio",
"defaultDescription": "Esto es lo que Goose usará cuando empieces un chat nuevo.",
"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",
"skillsDescription": "Habilidades personales que Goose puede ver ahora. Las habilidades de proyecto aparecen cuando abres un proyecto."
},
+43 -1
View File
@@ -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(
"goose:onboarding:v1",
@@ -243,7 +285,7 @@ export function buildInitScript(options?: {
case "_goose/providers/list":
return jsonRpcResult(message.id, { entries: PROVIDER_INVENTORY });
case "_goose/providers/setup/catalog/list":
return jsonRpcResult(message.id, { providers: [] });
return jsonRpcResult(message.id, { providers: PROVIDER_SETUP_CATALOG });
case "_goose/providers/inventory/refresh":
return jsonRpcResult(message.id, { started: [], skipped: [] });
case "_goose/defaults/read":