dialog-provider.tsx 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225
  1. import { createMemo, createSignal, onMount, Show } from "solid-js"
  2. import { useSync } from "@tui/context/sync"
  3. import { map, pipe, sortBy } from "remeda"
  4. import { DialogSelect } from "@tui/ui/dialog-select"
  5. import { useDialog } from "@tui/ui/dialog"
  6. import { useSDK } from "../context/sdk"
  7. import { DialogPrompt } from "../ui/dialog-prompt"
  8. import { Link } from "../ui/link"
  9. import { useTheme } from "../context/theme"
  10. import { TextAttributes } from "@opentui/core"
  11. import type { ProviderAuthAuthorization } from "@opencode-ai/sdk/v2"
  12. import { DialogModel } from "./dialog-model"
  13. const PROVIDER_PRIORITY: Record<string, number> = {
  14. opencode: 0,
  15. anthropic: 1,
  16. "github-copilot": 2,
  17. openai: 3,
  18. google: 4,
  19. openrouter: 5,
  20. }
  21. export function createDialogProviderOptions() {
  22. const sync = useSync()
  23. const dialog = useDialog()
  24. const sdk = useSDK()
  25. const options = createMemo(() => {
  26. return pipe(
  27. sync.data.provider_next.all,
  28. sortBy((x) => PROVIDER_PRIORITY[x.id] ?? 99),
  29. map((provider) => ({
  30. title: provider.name,
  31. value: provider.id,
  32. description: {
  33. opencode: "(Recommended)",
  34. anthropic: "(Claude Max or API key)",
  35. }[provider.id],
  36. category: provider.id in PROVIDER_PRIORITY ? "Popular" : "Other",
  37. async onSelect() {
  38. const methods = sync.data.provider_auth[provider.id] ?? [
  39. {
  40. type: "api",
  41. label: "API key",
  42. },
  43. ]
  44. let index: number | null = 0
  45. if (methods.length > 1) {
  46. index = await new Promise<number | null>((resolve) => {
  47. dialog.replace(
  48. () => (
  49. <DialogSelect
  50. title="Select auth method"
  51. options={methods.map((x, index) => ({
  52. title: x.label,
  53. value: index,
  54. }))}
  55. onSelect={(option) => resolve(option.value)}
  56. />
  57. ),
  58. () => resolve(null),
  59. )
  60. })
  61. }
  62. if (index == null) return
  63. const method = methods[index]
  64. if (method.type === "oauth") {
  65. const result = await sdk.client.provider.oauth.authorize({
  66. providerID: provider.id,
  67. method: index,
  68. })
  69. if (result.data?.method === "code") {
  70. dialog.replace(() => (
  71. <CodeMethod providerID={provider.id} title={method.label} index={index} authorization={result.data!} />
  72. ))
  73. }
  74. if (result.data?.method === "auto") {
  75. dialog.replace(() => (
  76. <AutoMethod providerID={provider.id} title={method.label} index={index} authorization={result.data!} />
  77. ))
  78. }
  79. }
  80. if (method.type === "api") {
  81. return dialog.replace(() => <ApiMethod providerID={provider.id} title={method.label} />)
  82. }
  83. },
  84. })),
  85. )
  86. })
  87. return options
  88. }
  89. export function DialogProvider() {
  90. const options = createDialogProviderOptions()
  91. return <DialogSelect title="Connect a provider" options={options()} />
  92. }
  93. interface AutoMethodProps {
  94. index: number
  95. providerID: string
  96. title: string
  97. authorization: ProviderAuthAuthorization
  98. }
  99. function AutoMethod(props: AutoMethodProps) {
  100. const { theme } = useTheme()
  101. const sdk = useSDK()
  102. const dialog = useDialog()
  103. const sync = useSync()
  104. onMount(async () => {
  105. const result = await sdk.client.provider.oauth.callback({
  106. providerID: props.providerID,
  107. method: props.index,
  108. })
  109. if (result.error) {
  110. dialog.clear()
  111. return
  112. }
  113. await sdk.client.instance.dispose()
  114. await sync.bootstrap()
  115. dialog.replace(() => <DialogModel providerID={props.providerID} />)
  116. })
  117. return (
  118. <box paddingLeft={2} paddingRight={2} gap={1} paddingBottom={1}>
  119. <box flexDirection="row" justifyContent="space-between">
  120. <text attributes={TextAttributes.BOLD} fg={theme.text}>
  121. {props.title}
  122. </text>
  123. <text fg={theme.textMuted}>esc</text>
  124. </box>
  125. <box gap={1}>
  126. <Link href={props.authorization.url} fg={theme.primary} />
  127. <text fg={theme.textMuted}>{props.authorization.instructions}</text>
  128. </box>
  129. <text fg={theme.textMuted}>Waiting for authorization...</text>
  130. </box>
  131. )
  132. }
  133. interface CodeMethodProps {
  134. index: number
  135. title: string
  136. providerID: string
  137. authorization: ProviderAuthAuthorization
  138. }
  139. function CodeMethod(props: CodeMethodProps) {
  140. const { theme } = useTheme()
  141. const sdk = useSDK()
  142. const sync = useSync()
  143. const dialog = useDialog()
  144. const [error, setError] = createSignal(false)
  145. return (
  146. <DialogPrompt
  147. title={props.title}
  148. placeholder="Authorization code"
  149. onConfirm={async (value) => {
  150. const { error } = await sdk.client.provider.oauth.callback({
  151. providerID: props.providerID,
  152. method: props.index,
  153. code: value,
  154. })
  155. if (!error) {
  156. await sdk.client.instance.dispose()
  157. await sync.bootstrap()
  158. dialog.replace(() => <DialogModel providerID={props.providerID} />)
  159. return
  160. }
  161. setError(true)
  162. }}
  163. description={() => (
  164. <box gap={1}>
  165. <text fg={theme.textMuted}>{props.authorization.instructions}</text>
  166. <Link href={props.authorization.url} fg={theme.primary} />
  167. <Show when={error()}>
  168. <text fg={theme.error}>Invalid code</text>
  169. </Show>
  170. </box>
  171. )}
  172. />
  173. )
  174. }
  175. interface ApiMethodProps {
  176. providerID: string
  177. title: string
  178. }
  179. function ApiMethod(props: ApiMethodProps) {
  180. const dialog = useDialog()
  181. const sdk = useSDK()
  182. const sync = useSync()
  183. const { theme } = useTheme()
  184. return (
  185. <DialogPrompt
  186. title={props.title}
  187. placeholder="API key"
  188. description={
  189. props.providerID === "opencode" ? (
  190. <box gap={1}>
  191. <text fg={theme.textMuted}>
  192. OpenCode Zen gives you access to all the best coding models at the cheapest prices with a single API key.
  193. </text>
  194. <text fg={theme.text}>
  195. Go to <span style={{ fg: theme.primary }}>https://opencode.ai/zen</span> to get a key
  196. </text>
  197. </box>
  198. ) : undefined
  199. }
  200. onConfirm={async (value) => {
  201. if (!value) return
  202. sdk.client.auth.set({
  203. providerID: props.providerID,
  204. auth: {
  205. type: "api",
  206. key: value,
  207. },
  208. })
  209. await sdk.client.instance.dispose()
  210. await sync.bootstrap()
  211. dialog.replace(() => <DialogModel providerID={props.providerID} />)
  212. }}
  213. />
  214. )
  215. }