diff --git a/packages/app/src/components/provider-connection-controller.ts b/packages/app/src/components/provider-connection-controller.ts index 271ea03209..ebb64b7dae 100644 --- a/packages/app/src/components/provider-connection-controller.ts +++ b/packages/app/src/components/provider-connection-controller.ts @@ -119,9 +119,11 @@ export function createProviderConnectionController(options: { const finish = async () => { cancelPolling() const directory = options.directory() - await queryClient - .refetchQueries(serverSync.queryOptions.providers(directory ? pathKey(directory) : null)) - .catch(() => undefined) + const key = directory ? pathKey(directory) : null + await Promise.all([ + queryClient.refetchQueries(serverSync.queryOptions.providers(key)).catch(() => undefined), + queryClient.refetchQueries(serverSync.queryOptions.integrations(key)).catch(() => undefined), + ]) if (polling.disposed) return options.onComplete() } diff --git a/packages/app/src/components/settings-v2/providers.tsx b/packages/app/src/components/settings-v2/providers.tsx index 6e02686ada..3e3390b9a3 100644 --- a/packages/app/src/components/settings-v2/providers.tsx +++ b/packages/app/src/components/settings-v2/providers.tsx @@ -4,6 +4,7 @@ import { useDialog } from "@opencode-ai/ui/context/dialog" import { ProviderIcon } from "@opencode-ai/ui/provider-icon" import { showToast } from "@/utils/toast" import { popularProviders, useProviders } from "@/hooks/use-providers" +import { useIntegrations } from "@/hooks/use-integrations" import { createMemo, type Component, For, Show } from "solid-js" import { useLanguage } from "@/context/language" import { useServerSDK } from "@/context/server-sdk" @@ -40,7 +41,9 @@ export const SettingsProvidersV2: Component<{ const serverSdk = useServerSDK() const serverSync = useServerSync() const providers = useProviders(() => props.directory) + const integrations = useIntegrations(() => props.directory) const providerConnect = useProviderConnectController({ onBack: props.onBack }) + const integration = (providerID: string) => integrations.list().find((item) => item.id === providerID) const connect = (provider?: string) => { providerConnect.select(provider) @@ -73,7 +76,14 @@ export const SettingsProvidersV2: Component<{ return items }) + // Connection state comes from the integration list like the TUI: credential + // connections mean an API key or OAuth grant, env connections mean detected + // environment variables, and a connectionless integration is config-provided. const source = (item: ProviderItem): ProviderSource | undefined => { + const current = integration(item.id) + if (current?.connections.some((connection) => connection.type === "credential")) return "api" + if (current?.connections.some((connection) => connection.type === "env")) return "env" + if (current) return "config" if (!("source" in item)) return const value = item.source if (value === "env" || value === "api" || value === "config" || value === "custom") return value @@ -92,7 +102,11 @@ export const SettingsProvidersV2: Component<{ return language.t("settings.providers.tag.other") } - const canDisconnect = (item: ProviderItem) => source(item) !== "env" && !isConfigCustom(item.id) + const canDisconnect = (item: ProviderItem) => { + const current = integration(item.id) + if (current) return current.connections.some((connection) => connection.type === "credential") + return source(item) !== "env" && !isConfigCustom(item.id) + } const note = (id: string) => PROVIDER_NOTES.find((item) => item.match(id))?.key diff --git a/packages/app/src/hooks/use-providers.ts b/packages/app/src/hooks/use-providers.ts index 21af866d0f..4cd905dcd7 100644 --- a/packages/app/src/hooks/use-providers.ts +++ b/packages/app/src/hooks/use-providers.ts @@ -2,6 +2,7 @@ import { useQueryOptions } from "@/context/server-sync" import { Iterable, pipe } from "effect" import { type Accessor } from "solid-js" import { emptyProviderCatalog } from "./provider-catalog" +import { useIntegrations } from "./use-integrations" import { useQuery } from "@tanstack/solid-query" import { pathKey } from "@/utils/path-key" @@ -23,6 +24,7 @@ export function useProviders(directory: Accessor) { const dir = directory() return queryOpts.providers(dir ? pathKey(dir) : null) }) + const integrations = useIntegrations(directory) const providers = () => (!providersQuery.isSuccess ? emptyProviderCatalog : providersQuery.data) @@ -30,13 +32,22 @@ export function useProviders(directory: Accessor) { ready: () => providersQuery.isSuccess, all: () => providers().all, default: () => providers().default, - popular: () => - pipe( + // V2 servers list only available providers, so the connectable catalog + // comes from the integration list, with the provider catalog as fallback. + popular: () => { + const catalog = integrations + .list() + .filter((integration) => popularProviderSet.has(integration.id)) + .map((integration) => ({ id: integration.id, name: integration.name })) + const seen = new Set(catalog.map((integration) => integration.id)) + return pipe( providers().all, Iterable.map(([, p]) => p), - Iterable.filter((p) => popularProviderSet.has(p.id)), - (v) => Array.from(v), - ), + Iterable.filter((p) => popularProviderSet.has(p.id) && !seen.has(p.id)), + Iterable.map((p) => ({ id: p.id, name: p.name })), + (v) => [...catalog, ...v], + ) + }, connected: () => { const connected = new Set(providers().connected) return pipe(