dialog-provider.tsx 6.6 KB

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