import type { ProviderAuthAuthorization } from "@opencode-ai/sdk/v2/client" import { Button } from "@opencode-ai/ui/button" import { useDialog } from "@opencode-ai/ui/context/dialog" import { Dialog } from "@opencode-ai/ui/dialog" import { Icon } from "@opencode-ai/ui/icon" import { IconButton } from "@opencode-ai/ui/icon-button" import type { IconName } from "@opencode-ai/ui/icons/provider" import { List, type ListRef } from "@opencode-ai/ui/list" import { ProviderIcon } from "@opencode-ai/ui/provider-icon" import { Spinner } from "@opencode-ai/ui/spinner" import { TextField } from "@opencode-ai/ui/text-field" import { showToast } from "@opencode-ai/ui/toast" import { createMemo, Match, onCleanup, onMount, Switch } from "solid-js" import { createStore, produce } from "solid-js/store" import { Link } from "@/components/link" import { useLanguage } from "@/context/language" import { useGlobalSDK } from "@/context/global-sdk" import { useGlobalSync } from "@/context/global-sync" import { usePlatform } from "@/context/platform" import { DialogSelectModel } from "./dialog-select-model" import { DialogSelectProvider } from "./dialog-select-provider" export function DialogConnectProvider(props: { provider: string }) { const dialog = useDialog() const globalSync = useGlobalSync() const globalSDK = useGlobalSDK() const platform = usePlatform() const language = useLanguage() const alive = { value: true } const timer = { current: undefined as ReturnType | undefined } onCleanup(() => { alive.value = false if (timer.current === undefined) return clearTimeout(timer.current) timer.current = undefined }) const provider = createMemo(() => globalSync.data.provider.all.find((x) => x.id === props.provider)!) const methods = createMemo( () => globalSync.data.provider_auth[props.provider] ?? [ { type: "api", label: language.t("provider.connect.method.apiKey"), }, ], ) const [store, setStore] = createStore({ methodIndex: undefined as undefined | number, authorization: undefined as undefined | ProviderAuthAuthorization, state: "pending" as undefined | "pending" | "complete" | "error", error: undefined as string | undefined, }) type Action = | { type: "method.select"; index: number } | { type: "method.reset" } | { type: "auth.pending" } | { type: "auth.complete"; authorization: ProviderAuthAuthorization } | { type: "auth.error"; error: string } function dispatch(action: Action) { setStore( produce((draft) => { if (action.type === "method.select") { draft.methodIndex = action.index draft.authorization = undefined draft.state = undefined draft.error = undefined return } if (action.type === "method.reset") { draft.methodIndex = undefined draft.authorization = undefined draft.state = undefined draft.error = undefined return } if (action.type === "auth.pending") { draft.state = "pending" draft.error = undefined return } if (action.type === "auth.complete") { draft.state = "complete" draft.authorization = action.authorization draft.error = undefined return } draft.state = "error" draft.error = action.error }), ) } const method = createMemo(() => (store.methodIndex !== undefined ? methods().at(store.methodIndex!) : undefined)) const methodLabel = (value?: { type?: string; label?: string }) => { if (!value) return "" if (value.type === "api") return language.t("provider.connect.method.apiKey") return value.label ?? "" } function formatError(value: unknown, fallback: string): string { if (value && typeof value === "object" && "data" in value) { const data = (value as { data?: { message?: unknown } }).data if (typeof data?.message === "string" && data.message) return data.message } if (value && typeof value === "object" && "error" in value) { const nested = formatError((value as { error?: unknown }).error, "") if (nested) return nested } if (value && typeof value === "object" && "message" in value) { const message = (value as { message?: unknown }).message if (typeof message === "string" && message) return message } if (value instanceof Error && value.message) return value.message if (typeof value === "string" && value) return value return fallback } async function selectMethod(index: number) { if (timer.current !== undefined) { clearTimeout(timer.current) timer.current = undefined } const method = methods()[index] dispatch({ type: "method.select", index }) if (method.type === "oauth") { dispatch({ type: "auth.pending" }) const start = Date.now() await globalSDK.client.provider.oauth .authorize( { providerID: props.provider, method: index, }, { throwOnError: true }, ) .then((x) => { if (!alive.value) return const elapsed = Date.now() - start const delay = 1000 - elapsed if (delay > 0) { if (timer.current !== undefined) clearTimeout(timer.current) timer.current = setTimeout(() => { timer.current = undefined if (!alive.value) return dispatch({ type: "auth.complete", authorization: x.data! }) }, delay) return } dispatch({ type: "auth.complete", authorization: x.data! }) }) .catch((e) => { if (!alive.value) return dispatch({ type: "auth.error", error: formatError(e, language.t("common.requestFailed")) }) }) } } let listRef: ListRef | undefined function handleKey(e: KeyboardEvent) { if (e.key === "Enter" && e.target instanceof HTMLInputElement) { return } if (e.key === "Escape") return listRef?.onKeyDown(e) } onMount(() => { if (methods().length === 1) { selectMethod(0) } }) async function complete() { await globalSDK.client.global.dispose() dialog.close() showToast({ variant: "success", icon: "circle-check", title: language.t("provider.connect.toast.connected.title", { provider: provider().name }), description: language.t("provider.connect.toast.connected.description", { provider: provider().name }), }) } function goBack() { if (methods().length === 1) { dialog.show(() => ) return } if (store.authorization) { dispatch({ type: "method.reset" }) return } if (store.methodIndex !== undefined) { dispatch({ type: "method.reset" }) return } dialog.show(() => ) } function MethodSelection() { return ( <>
{language.t("provider.connect.selectMethod", { provider: provider().name })}
{ listRef = ref }} items={methods} key={(m) => m?.label} onSelect={async (selected, index) => { if (!selected) return selectMethod(index) }} > {(i) => (
{methodLabel(i)}
)}
) } function ApiAuthView() { const [formStore, setFormStore] = createStore({ value: "", error: undefined as string | undefined, }) async function handleSubmit(e: SubmitEvent) { e.preventDefault() const form = e.currentTarget as HTMLFormElement const formData = new FormData(form) const apiKey = formData.get("apiKey") as string if (!apiKey?.trim()) { setFormStore("error", language.t("provider.connect.apiKey.required")) return } setFormStore("error", undefined) await globalSDK.client.auth.set({ providerID: props.provider, auth: { type: "api", key: apiKey, }, }) await complete() } return (
{language.t("provider.connect.opencodeZen.line1")}
{language.t("provider.connect.opencodeZen.line2")}
{language.t("provider.connect.opencodeZen.visit.prefix")} {language.t("provider.connect.opencodeZen.visit.link")} {language.t("provider.connect.opencodeZen.visit.suffix")}
{language.t("provider.connect.apiKey.description", { provider: provider().name })}
setFormStore("value", v)} validationState={formStore.error ? "invalid" : undefined} error={formStore.error} />
) } function OAuthCodeView() { const [formStore, setFormStore] = createStore({ value: "", error: undefined as string | undefined, }) onMount(() => { if (store.authorization?.method === "code" && store.authorization?.url) { platform.openLink(store.authorization.url) } }) async function handleSubmit(e: SubmitEvent) { e.preventDefault() const form = e.currentTarget as HTMLFormElement const formData = new FormData(form) const code = formData.get("code") as string if (!code?.trim()) { setFormStore("error", language.t("provider.connect.oauth.code.required")) return } setFormStore("error", undefined) const result = await globalSDK.client.provider.oauth .callback({ providerID: props.provider, method: store.methodIndex, code, }) .then((value) => (value.error ? { ok: false as const, error: value.error } : { ok: true as const })) .catch((error) => ({ ok: false as const, error })) if (result.ok) { await complete() return } setFormStore("error", formatError(result.error, language.t("provider.connect.oauth.code.invalid"))) } return (
{language.t("provider.connect.oauth.code.visit.prefix")} {language.t("provider.connect.oauth.code.visit.link")} {language.t("provider.connect.oauth.code.visit.suffix", { provider: provider().name })}
setFormStore("value", v)} validationState={formStore.error ? "invalid" : undefined} error={formStore.error} />
) } function OAuthAutoView() { const code = createMemo(() => { const instructions = store.authorization?.instructions if (instructions?.includes(":")) { return instructions.split(":")[1]?.trim() } return instructions }) onMount(() => { void (async () => { if (store.authorization?.url) { platform.openLink(store.authorization.url) } const result = await globalSDK.client.provider.oauth .callback({ providerID: props.provider, method: store.methodIndex, }) .then((value) => (value.error ? { ok: false as const, error: value.error } : { ok: true as const })) .catch((error) => ({ ok: false as const, error })) if (!alive.value) return if (!result.ok) { const message = formatError(result.error, language.t("common.requestFailed")) dispatch({ type: "auth.error", error: message }) return } await complete() })() }) return (
{language.t("provider.connect.oauth.auto.visit.prefix")} {language.t("provider.connect.oauth.auto.visit.link")} {language.t("provider.connect.oauth.auto.visit.suffix", { provider: provider().name })}
{language.t("provider.connect.status.waiting")}
) } return ( } >
{language.t("provider.connect.title.anthropicProMax")} {language.t("provider.connect.title", { provider: provider().name })}
{language.t("provider.connect.status.inProgress")}
{language.t("provider.connect.status.failed", { error: store.error ?? "" })}
) }