|
|
@@ -1,14 +1,153 @@
|
|
|
-import { Component } from "solid-js"
|
|
|
+import { Button } from "@opencode-ai/ui/button"
|
|
|
+import { useDialog } from "@opencode-ai/ui/context/dialog"
|
|
|
+import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
|
|
+import { Tag } from "@opencode-ai/ui/tag"
|
|
|
+import { showToast } from "@opencode-ai/ui/toast"
|
|
|
+import type { IconName } from "@opencode-ai/ui/icons/provider"
|
|
|
+import { popularProviders, useProviders } from "@/hooks/use-providers"
|
|
|
+import { createMemo, type Component, For, Show } from "solid-js"
|
|
|
import { useLanguage } from "@/context/language"
|
|
|
+import { useGlobalSDK } from "@/context/global-sdk"
|
|
|
+import { DialogConnectProvider } from "./dialog-connect-provider"
|
|
|
+import { DialogSelectProvider } from "./dialog-select-provider"
|
|
|
+
|
|
|
+type ProviderSource = "env" | "api" | "config" | "custom"
|
|
|
+type ProviderMeta = { source?: ProviderSource }
|
|
|
|
|
|
export const SettingsProviders: Component = () => {
|
|
|
+ const dialog = useDialog()
|
|
|
const language = useLanguage()
|
|
|
+ const globalSDK = useGlobalSDK()
|
|
|
+ const providers = useProviders()
|
|
|
+
|
|
|
+ const connected = createMemo(() => providers.connected())
|
|
|
+ const popular = createMemo(() => {
|
|
|
+ const items = providers.popular().slice()
|
|
|
+ items.sort((a, b) => popularProviders.indexOf(a.id) - popularProviders.indexOf(b.id))
|
|
|
+ return items
|
|
|
+ })
|
|
|
+
|
|
|
+ const source = (item: unknown) => (item as ProviderMeta).source
|
|
|
+
|
|
|
+ const disconnect = async (providerID: string, name: string) => {
|
|
|
+ await globalSDK.client.auth
|
|
|
+ .remove({ providerID })
|
|
|
+ .then(async () => {
|
|
|
+ await globalSDK.client.global.dispose()
|
|
|
+ showToast({
|
|
|
+ variant: "success",
|
|
|
+ icon: "circle-check",
|
|
|
+ title: language.t("provider.disconnect.toast.disconnected.title", { provider: name }),
|
|
|
+ description: language.t("provider.disconnect.toast.disconnected.description", { provider: name }),
|
|
|
+ })
|
|
|
+ })
|
|
|
+ .catch((err: unknown) => {
|
|
|
+ const message = err instanceof Error ? err.message : String(err)
|
|
|
+ showToast({ title: language.t("common.requestFailed"), description: message })
|
|
|
+ })
|
|
|
+ }
|
|
|
|
|
|
return (
|
|
|
- <div class="flex flex-col h-full overflow-y-auto">
|
|
|
- <div class="flex flex-col gap-6 p-6 max-w-[600px]">
|
|
|
- <h2 class="text-16-medium text-text-strong">{language.t("settings.providers.title")}</h2>
|
|
|
- <p class="text-14-regular text-text-weak">{language.t("settings.providers.description")}</p>
|
|
|
+ <div class="flex flex-col h-full overflow-y-auto no-scrollbar" style={{ padding: "0 40px 40px 40px" }}>
|
|
|
+ <div
|
|
|
+ class="sticky top-0 z-10"
|
|
|
+ style={{
|
|
|
+ background:
|
|
|
+ "linear-gradient(to bottom, var(--surface-raised-stronger-non-alpha) calc(100% - 24px), transparent)",
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ <div class="flex flex-col gap-4 pt-6 pb-6 max-w-[720px]">
|
|
|
+ <div class="flex items-center justify-between gap-4">
|
|
|
+ <h2 class="text-16-medium text-text-strong">{language.t("settings.providers.title")}</h2>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="flex flex-col gap-8 max-w-[720px]">
|
|
|
+ <div class="flex flex-col gap-1">
|
|
|
+ <h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.providers.section.connected")}</h3>
|
|
|
+ <div class="bg-surface-raised-base px-4 rounded-lg">
|
|
|
+ <Show
|
|
|
+ when={connected().length > 0}
|
|
|
+ fallback={
|
|
|
+ <div class="py-4 text-14-regular text-text-weak">
|
|
|
+ {language.t("settings.providers.connected.empty")}
|
|
|
+ </div>
|
|
|
+ }
|
|
|
+ >
|
|
|
+ <For each={connected()}>
|
|
|
+ {(item) => (
|
|
|
+ <div class="flex items-center justify-between gap-4 py-3 border-b border-border-weak-base last:border-none">
|
|
|
+ <div class="flex items-center gap-3 min-w-0">
|
|
|
+ <ProviderIcon id={item.id as IconName} class="size-5 shrink-0 icon-strong-base" />
|
|
|
+ <span class="text-14-regular text-text-strong truncate">{item.name}</span>
|
|
|
+ <Show when={source(item) === "env"}>
|
|
|
+ <Tag>{language.t("settings.providers.tag.environment")}</Tag>
|
|
|
+ </Show>
|
|
|
+ <Show when={source(item) === "api"}>
|
|
|
+ <Tag>{language.t("provider.connect.method.apiKey")}</Tag>
|
|
|
+ </Show>
|
|
|
+ </div>
|
|
|
+ <Show when={source(item) === "api"}>
|
|
|
+ <Button size="small" variant="ghost" onClick={() => void disconnect(item.id, item.name)}>
|
|
|
+ {language.t("common.disconnect")}
|
|
|
+ </Button>
|
|
|
+ </Show>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ </For>
|
|
|
+ </Show>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="flex flex-col gap-1">
|
|
|
+ <h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.providers.section.popular")}</h3>
|
|
|
+ <div class="bg-surface-raised-base px-4 rounded-lg">
|
|
|
+ <For each={popular()}>
|
|
|
+ {(item) => (
|
|
|
+ <div class="flex items-center justify-between gap-4 py-3 border-b border-border-weak-base last:border-none">
|
|
|
+ <div class="flex items-center gap-x-3 min-w-0">
|
|
|
+ <ProviderIcon id={item.id as IconName} class="size-5 shrink-0 icon-strong-base" />
|
|
|
+ <span class="text-14-regular text-text-strong">{item.name}</span>
|
|
|
+ <Show when={item.id === "opencode"}>
|
|
|
+ <Tag>{language.t("dialog.provider.tag.recommended")}</Tag>
|
|
|
+ </Show>
|
|
|
+ <Show when={item.id === "anthropic"}>
|
|
|
+ <div class="text-14-regular text-text-weak">{language.t("dialog.provider.anthropic.note")}</div>
|
|
|
+ </Show>
|
|
|
+ <Show when={item.id === "openai"}>
|
|
|
+ <div class="text-14-regular text-text-weak">{language.t("dialog.provider.openai.note")}</div>
|
|
|
+ </Show>
|
|
|
+ <Show when={item.id.startsWith("github-copilot")}>
|
|
|
+ <div class="text-14-regular text-text-weak">{language.t("dialog.provider.copilot.note")}</div>
|
|
|
+ </Show>
|
|
|
+ </div>
|
|
|
+ <Button
|
|
|
+ size="small"
|
|
|
+ variant="secondary"
|
|
|
+ icon="plus-small"
|
|
|
+ onClick={() => {
|
|
|
+ dialog.show(() => <DialogConnectProvider provider={item.id} />)
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ {language.t("common.connect")}
|
|
|
+ </Button>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ </For>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <Button
|
|
|
+ variant="ghost"
|
|
|
+ class="w-full justify-start px-[11px] py-3.5 gap-4.5 text-14-medium"
|
|
|
+ icon="dot-grid"
|
|
|
+ onClick={() => {
|
|
|
+ dialog.show(() => <DialogSelectProvider />)
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ {language.t("dialog.provider.viewAll")}
|
|
|
+ </Button>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</div>
|
|
|
)
|