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 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."
}, },
+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( 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":