import type { ProviderAuthAuthorization, ProviderAuthMethod } 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 { 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 { createEffect, createMemo, createResource, Match, onCleanup, onMount, Switch } from "solid-js" import { createStore, produce } from "solid-js/store" import { Link } from "@/components/link" import { useGlobalSDK } from "@/context/global-sdk" import { useGlobalSync } from "@/context/global-sync" import { useLanguage } from "@/context/language" import { useProviders } from "@/hooks/use-providers" export function DialogConnectProvider(props: { provider: string }) { const dialog = useDialog() const globalSync = useGlobalSync() const globalSDK = useGlobalSDK() const language = useLanguage() const providers = useProviders() const all = () => { void import("./dialog-select-provider").then((x) => { dialog.show(() => ) }) } 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( () => providers.all().find((x) => x.id === props.provider) ?? globalSync.data.provider.all.find((x) => x.id === props.provider)!, ) const fallback = createMemo(() => [ { type: "api" as const, label: language.t("provider.connect.method.apiKey"), }, ]) const [auth] = createResource( () => props.provider, async () => { const cached = globalSync.data.provider_auth[props.provider] if (cached) return cached const res = await globalSDK.client.provider.auth() if (!alive.value) return fallback() globalSync.set("provider_auth", res.data ?? {}) return res.data?.[props.provider] ?? fallback() }, ) const loading = createMemo(() => auth.loading && !globalSync.data.provider_auth[props.provider]) const methods = createMemo(() => auth.latest ?? globalSync.data.provider_auth[props.provider] ?? fallback()) const [store, setStore] = createStore({ methodIndex: undefined as undefined | number, authorization: undefined as undefined | ProviderAuthAuthorization, state: "pending" as undefined | "pending" | "complete" | "error" | "prompt", error: undefined as string | undefined, }) type Action = | { type: "method.select"; index: number } | { type: "method.reset" } | { type: "auth.prompt" } | { 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.prompt") { draft.state = "prompt" 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, inputs?: Record) { if (timer.current !== undefined) { clearTimeout(timer.current) timer.current = undefined } const method = methods()[index] dispatch({ type: "method.select", index }) if (method.type === "oauth") { if (method.prompts?.length && !inputs) { dispatch({ type: "auth.prompt" }) return } dispatch({ type: "auth.pending" }) const start = Date.now() await globalSDK.client.provider.oauth .authorize( { providerID: props.provider, method: index, inputs, }, { 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")) }) }) } } function OAuthPromptsView() { const [formStore, setFormStore] = createStore({ value: {} as Record, index: 0, }) const prompts = createMemo>(() => { const value = method() if (value?.type !== "oauth") return [] return value.prompts ?? [] }) const matches = (prompt: NonNullable[number]>, value: Record) => { if (!prompt.when) return true const actual = value[prompt.when.key] if (actual === undefined) return false return prompt.when.op === "eq" ? actual === prompt.when.value : actual !== prompt.when.value } const current = createMemo(() => { const all = prompts() const index = all.findIndex((prompt, index) => index >= formStore.index && matches(prompt, formStore.value)) if (index === -1) return return { index, prompt: all[index], } }) const valid = createMemo(() => { const item = current() if (!item || item.prompt.type !== "text") return false const value = formStore.value[item.prompt.key] ?? "" return value.trim().length > 0 }) async function next(index: number, value: Record) { if (store.methodIndex === undefined) return const next = prompts().findIndex((prompt, i) => i > index && matches(prompt, value)) if (next !== -1) { setFormStore("index", next) return } await selectMethod(store.methodIndex, value) } async function handleSubmit(e: SubmitEvent) { e.preventDefault() const item = current() if (!item || item.prompt.type !== "text") return if (!valid()) return await next(item.index, formStore.value) } const item = () => current() const text = createMemo(() => { const prompt = item()?.prompt if (!prompt || prompt.type !== "text") return return prompt }) const select = createMemo(() => { const prompt = item()?.prompt if (!prompt || prompt.type !== "select") return return prompt }) return ( { const prompt = text() if (!prompt) return setFormStore("value", prompt.key, value) }} /> {language.t("common.continue")} {select()?.message} x.value} current={select()?.options.find((x) => x.value === formStore.value[select()!.key])} onSelect={(value) => { if (!value) return const prompt = select() if (!prompt) return const nextValue = { ...formStore.value, [prompt.key]: value.value, } setFormStore("value", prompt.key, value.value) void next(item()!.index, nextValue) }} > {(option) => ( {option.label} {option.hint} )} ) } 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) } let auto = false createEffect(() => { if (auto) return if (loading()) return if (methods().length === 1) { auto = true void 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) { all() return } if (store.authorization) { dispatch({ type: "method.reset" }) return } if (store.methodIndex !== undefined) { dispatch({ type: "method.reset" }) return } all() } 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 void 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} /> {language.t("common.continue")} ) } function OAuthCodeView() { 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 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} /> {language.t("common.continue")} ) } function OAuthAutoView() { const code = createMemo(() => { const instructions = store.authorization?.instructions if (instructions?.includes(":")) { return instructions.split(":")[1]?.trim() } return instructions }) onMount(() => { void (async () => { 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.inProgress")} {language.t("provider.connect.status.failed", { error: store.error ?? "" })} ) }