settings-providers.tsx 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
  1. import { Button } from "@opencode-ai/ui/button"
  2. import { useDialog } from "@opencode-ai/ui/context/dialog"
  3. import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
  4. import { Tag } from "@opencode-ai/ui/tag"
  5. import { showToast } from "@opencode-ai/ui/toast"
  6. import type { IconName } from "@opencode-ai/ui/icons/provider"
  7. import { popularProviders, useProviders } from "@/hooks/use-providers"
  8. import { createMemo, type Component, For, Show } from "solid-js"
  9. import { useLanguage } from "@/context/language"
  10. import { useGlobalSDK } from "@/context/global-sdk"
  11. import { DialogConnectProvider } from "./dialog-connect-provider"
  12. import { DialogSelectProvider } from "./dialog-select-provider"
  13. type ProviderSource = "env" | "api" | "config" | "custom"
  14. type ProviderMeta = { source?: ProviderSource }
  15. export const SettingsProviders: Component = () => {
  16. const dialog = useDialog()
  17. const language = useLanguage()
  18. const globalSDK = useGlobalSDK()
  19. const providers = useProviders()
  20. const connected = createMemo(() => providers.connected())
  21. const popular = createMemo(() => {
  22. const connectedIDs = new Set(connected().map((p) => p.id))
  23. const items = providers
  24. .popular()
  25. .filter((p) => !connectedIDs.has(p.id))
  26. .slice()
  27. items.sort((a, b) => popularProviders.indexOf(a.id) - popularProviders.indexOf(b.id))
  28. return items
  29. })
  30. const source = (item: unknown) => (item as ProviderMeta).source
  31. const type = (item: unknown) => {
  32. const current = source(item)
  33. if (current === "env") return language.t("settings.providers.tag.environment")
  34. if (current === "api") return language.t("provider.connect.method.apiKey")
  35. if (current === "config") return language.t("settings.providers.tag.config")
  36. if (current === "custom") return language.t("settings.providers.tag.custom")
  37. return language.t("settings.providers.tag.other")
  38. }
  39. const canDisconnect = (item: unknown) => source(item) !== "env"
  40. const disconnect = async (providerID: string, name: string) => {
  41. await globalSDK.client.auth
  42. .remove({ providerID })
  43. .then(async () => {
  44. await globalSDK.client.global.dispose()
  45. showToast({
  46. variant: "success",
  47. icon: "circle-check",
  48. title: language.t("provider.disconnect.toast.disconnected.title", { provider: name }),
  49. description: language.t("provider.disconnect.toast.disconnected.description", { provider: name }),
  50. })
  51. })
  52. .catch((err: unknown) => {
  53. const message = err instanceof Error ? err.message : String(err)
  54. showToast({ title: language.t("common.requestFailed"), description: message })
  55. })
  56. }
  57. return (
  58. <div class="flex flex-col h-full overflow-y-auto no-scrollbar" style={{ padding: "0 40px 40px 40px" }}>
  59. <div class="sticky top-0 z-10 bg-[linear-gradient(to_bottom,var(--surface-raised-stronger-non-alpha)_calc(100%_-_24px),transparent)]">
  60. <div class="flex flex-col gap-1 pt-6 pb-8 max-w-[720px]">
  61. <h2 class="text-16-medium text-text-strong">{language.t("settings.providers.title")}</h2>
  62. </div>
  63. </div>
  64. <div class="flex flex-col gap-8 max-w-[720px]">
  65. <div class="flex flex-col gap-1">
  66. <h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.providers.section.connected")}</h3>
  67. <div class="bg-surface-raised-base px-4 rounded-lg">
  68. <Show
  69. when={connected().length > 0}
  70. fallback={
  71. <div class="py-4 text-14-regular text-text-weak">
  72. {language.t("settings.providers.connected.empty")}
  73. </div>
  74. }
  75. >
  76. <For each={connected()}>
  77. {(item) => (
  78. <div class="flex items-center justify-between gap-4 py-3 border-b border-border-weak-base last:border-none">
  79. <div class="flex items-center gap-3 min-w-0">
  80. <ProviderIcon id={item.id as IconName} class="size-5 shrink-0 icon-strong-base" />
  81. <span class="text-14-regular text-text-strong truncate">{item.name}</span>
  82. <Tag>{type(item)}</Tag>
  83. </div>
  84. <Show when={canDisconnect(item)}>
  85. <Button size="small" variant="ghost" onClick={() => void disconnect(item.id, item.name)}>
  86. {language.t("common.disconnect")}
  87. </Button>
  88. </Show>
  89. </div>
  90. )}
  91. </For>
  92. </Show>
  93. </div>
  94. </div>
  95. <div class="flex flex-col gap-1">
  96. <h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.providers.section.popular")}</h3>
  97. <div class="bg-surface-raised-base px-4 rounded-lg">
  98. <For each={popular()}>
  99. {(item) => (
  100. <div class="flex items-center justify-between gap-4 py-3 border-b border-border-weak-base last:border-none">
  101. <div class="flex items-center gap-x-3 min-w-0">
  102. <ProviderIcon id={item.id as IconName} class="size-5 shrink-0 icon-strong-base" />
  103. <span class="text-14-regular text-text-strong">{item.name}</span>
  104. <Show when={item.id === "opencode"}>
  105. <Tag>{language.t("dialog.provider.tag.recommended")}</Tag>
  106. </Show>
  107. <Show when={item.id === "anthropic"}>
  108. <div class="text-14-regular text-text-weak">{language.t("dialog.provider.anthropic.note")}</div>
  109. </Show>
  110. <Show when={item.id === "openai"}>
  111. <div class="text-14-regular text-text-weak">{language.t("dialog.provider.openai.note")}</div>
  112. </Show>
  113. <Show when={item.id.startsWith("github-copilot")}>
  114. <div class="text-14-regular text-text-weak">{language.t("dialog.provider.copilot.note")}</div>
  115. </Show>
  116. </div>
  117. <Button
  118. size="large"
  119. variant="secondary"
  120. icon="plus-small"
  121. onClick={() => {
  122. dialog.show(() => <DialogConnectProvider provider={item.id} />)
  123. }}
  124. >
  125. {language.t("common.connect")}
  126. </Button>
  127. </div>
  128. )}
  129. </For>
  130. </div>
  131. <Button
  132. variant="ghost"
  133. class="px-0 py-0 text-14-medium text-text-strong underline hover:bg-transparent active:bg-transparent"
  134. onClick={() => {
  135. dialog.show(() => <DialogSelectProvider />)
  136. }}
  137. >
  138. {language.t("dialog.provider.viewAll")}
  139. </Button>
  140. </div>
  141. </div>
  142. </div>
  143. )
  144. }