From 067770740e460fcea18f44f796ea7608630da7bb Mon Sep 17 00:00:00 2001 From: tulsi Date: Tue, 12 May 2026 13:42:19 -0700 Subject: [PATCH] refresh onboarding when provider catalog loads (#9051) Signed-off-by: tulsi --- .../hooks/useOnboardingGate.test.tsx | 31 ++++++++++++- .../onboarding/hooks/useOnboardingGate.ts | 29 +++++++----- .../hooks/useOnboardingProviderStep.test.tsx | 33 ++++++++++++++ .../hooks/useOnboardingProviderStep.ts | 26 +++++++---- .../shared/i18n/locales/en/onboarding.json | 2 +- .../shared/i18n/locales/es/onboarding.json | 2 +- ui/goose2/tests/e2e/fixtures/tauri-mock.ts | 44 ++++++++++++++++++- 7 files changed, 144 insertions(+), 23 deletions(-) diff --git a/ui/goose2/src/features/onboarding/hooks/useOnboardingGate.test.tsx b/ui/goose2/src/features/onboarding/hooks/useOnboardingGate.test.tsx index b8df7ab658..59d48c1f2f 100644 --- a/ui/goose2/src/features/onboarding/hooks/useOnboardingGate.test.tsx +++ b/ui/goose2/src/features/onboarding/hooks/useOnboardingGate.test.tsx @@ -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", { diff --git a/ui/goose2/src/features/onboarding/hooks/useOnboardingGate.ts b/ui/goose2/src/features/onboarding/hooks/useOnboardingGate.ts index 21bdffa4d1..416708e99e 100644 --- a/ui/goose2/src/features/onboarding/hooks/useOnboardingGate.ts +++ b/ui/goose2/src/features/onboarding/hooks/useOnboardingGate.ts @@ -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( 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(() => { - 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( diff --git a/ui/goose2/src/features/onboarding/hooks/useOnboardingProviderStep.test.tsx b/ui/goose2/src/features/onboarding/hooks/useOnboardingProviderStep.test.tsx index ae38b558ff..d4a0d3faf1 100644 --- a/ui/goose2/src/features/onboarding/hooks/useOnboardingProviderStep.test.tsx +++ b/ui/goose2/src/features/onboarding/hooks/useOnboardingProviderStep.test.tsx @@ -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({ diff --git a/ui/goose2/src/features/onboarding/hooks/useOnboardingProviderStep.ts b/ui/goose2/src/features/onboarding/hooks/useOnboardingProviderStep.ts index 9623a8aaf4..78a436369f 100644 --- a/ui/goose2/src/features/onboarding/hooks/useOnboardingProviderStep.ts +++ b/ui/goose2/src/features/onboarding/hooks/useOnboardingProviderStep.ts @@ -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( @@ -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, ); diff --git a/ui/goose2/src/shared/i18n/locales/en/onboarding.json b/ui/goose2/src/shared/i18n/locales/en/onboarding.json index 7f4b2feff9..cc3cdf6928 100644 --- a/ui/goose2/src/shared/i18n/locales/en/onboarding.json +++ b/ui/goose2/src/shared/i18n/locales/en/onboarding.json @@ -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." }, diff --git a/ui/goose2/src/shared/i18n/locales/es/onboarding.json b/ui/goose2/src/shared/i18n/locales/es/onboarding.json index 980f2c1697..a0cff076d8 100644 --- a/ui/goose2/src/shared/i18n/locales/es/onboarding.json +++ b/ui/goose2/src/shared/i18n/locales/es/onboarding.json @@ -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." }, diff --git a/ui/goose2/tests/e2e/fixtures/tauri-mock.ts b/ui/goose2/tests/e2e/fixtures/tauri-mock.ts index 6ca637d8cf..ac8702e2d4 100644 --- a/ui/goose2/tests/e2e/fixtures/tauri-mock.ts +++ b/ui/goose2/tests/e2e/fixtures/tauri-mock.ts @@ -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":