| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225 |
- import { createMemo, createSignal, onMount, Show } from "solid-js"
- import { useSync } from "@tui/context/sync"
- import { map, pipe, sortBy } from "remeda"
- import { DialogSelect } from "@tui/ui/dialog-select"
- import { useDialog } from "@tui/ui/dialog"
- import { useSDK } from "../context/sdk"
- import { DialogPrompt } from "../ui/dialog-prompt"
- import { Link } from "../ui/link"
- import { useTheme } from "../context/theme"
- import { TextAttributes } from "@opentui/core"
- import type { ProviderAuthAuthorization } from "@opencode-ai/sdk/v2"
- import { DialogModel } from "./dialog-model"
- const PROVIDER_PRIORITY: Record<string, number> = {
- opencode: 0,
- anthropic: 1,
- "github-copilot": 2,
- openai: 3,
- google: 4,
- openrouter: 5,
- }
- export function createDialogProviderOptions() {
- const sync = useSync()
- const dialog = useDialog()
- const sdk = useSDK()
- const options = createMemo(() => {
- return pipe(
- sync.data.provider_next.all,
- sortBy((x) => PROVIDER_PRIORITY[x.id] ?? 99),
- map((provider) => ({
- title: provider.name,
- value: provider.id,
- description: {
- opencode: "(Recommended)",
- anthropic: "(Claude Max or API key)",
- }[provider.id],
- category: provider.id in PROVIDER_PRIORITY ? "Popular" : "Other",
- async onSelect() {
- const methods = sync.data.provider_auth[provider.id] ?? [
- {
- type: "api",
- label: "API key",
- },
- ]
- let index: number | null = 0
- if (methods.length > 1) {
- index = await new Promise<number | null>((resolve) => {
- dialog.replace(
- () => (
- <DialogSelect
- title="Select auth method"
- options={methods.map((x, index) => ({
- title: x.label,
- value: index,
- }))}
- onSelect={(option) => resolve(option.value)}
- />
- ),
- () => resolve(null),
- )
- })
- }
- if (index == null) return
- const method = methods[index]
- if (method.type === "oauth") {
- const result = await sdk.client.provider.oauth.authorize({
- providerID: provider.id,
- method: index,
- })
- if (result.data?.method === "code") {
- dialog.replace(() => (
- <CodeMethod providerID={provider.id} title={method.label} index={index} authorization={result.data!} />
- ))
- }
- if (result.data?.method === "auto") {
- dialog.replace(() => (
- <AutoMethod providerID={provider.id} title={method.label} index={index} authorization={result.data!} />
- ))
- }
- }
- if (method.type === "api") {
- return dialog.replace(() => <ApiMethod providerID={provider.id} title={method.label} />)
- }
- },
- })),
- )
- })
- return options
- }
- export function DialogProvider() {
- const options = createDialogProviderOptions()
- return <DialogSelect title="Connect a provider" options={options()} />
- }
- interface AutoMethodProps {
- index: number
- providerID: string
- title: string
- authorization: ProviderAuthAuthorization
- }
- function AutoMethod(props: AutoMethodProps) {
- const { theme } = useTheme()
- const sdk = useSDK()
- const dialog = useDialog()
- const sync = useSync()
- onMount(async () => {
- const result = await sdk.client.provider.oauth.callback({
- providerID: props.providerID,
- method: props.index,
- })
- if (result.error) {
- dialog.clear()
- return
- }
- await sdk.client.instance.dispose()
- await sync.bootstrap()
- dialog.replace(() => <DialogModel providerID={props.providerID} />)
- })
- return (
- <box paddingLeft={2} paddingRight={2} gap={1} paddingBottom={1}>
- <box flexDirection="row" justifyContent="space-between">
- <text attributes={TextAttributes.BOLD} fg={theme.text}>
- {props.title}
- </text>
- <text fg={theme.textMuted}>esc</text>
- </box>
- <box gap={1}>
- <Link href={props.authorization.url} fg={theme.primary} />
- <text fg={theme.textMuted}>{props.authorization.instructions}</text>
- </box>
- <text fg={theme.textMuted}>Waiting for authorization...</text>
- </box>
- )
- }
- interface CodeMethodProps {
- index: number
- title: string
- providerID: string
- authorization: ProviderAuthAuthorization
- }
- function CodeMethod(props: CodeMethodProps) {
- const { theme } = useTheme()
- const sdk = useSDK()
- const sync = useSync()
- const dialog = useDialog()
- const [error, setError] = createSignal(false)
- return (
- <DialogPrompt
- title={props.title}
- placeholder="Authorization code"
- onConfirm={async (value) => {
- const { error } = await sdk.client.provider.oauth.callback({
- providerID: props.providerID,
- method: props.index,
- code: value,
- })
- if (!error) {
- await sdk.client.instance.dispose()
- await sync.bootstrap()
- dialog.replace(() => <DialogModel providerID={props.providerID} />)
- return
- }
- setError(true)
- }}
- description={() => (
- <box gap={1}>
- <text fg={theme.textMuted}>{props.authorization.instructions}</text>
- <Link href={props.authorization.url} fg={theme.primary} />
- <Show when={error()}>
- <text fg={theme.error}>Invalid code</text>
- </Show>
- </box>
- )}
- />
- )
- }
- interface ApiMethodProps {
- providerID: string
- title: string
- }
- function ApiMethod(props: ApiMethodProps) {
- const dialog = useDialog()
- const sdk = useSDK()
- const sync = useSync()
- const { theme } = useTheme()
- return (
- <DialogPrompt
- title={props.title}
- placeholder="API key"
- description={
- props.providerID === "opencode" ? (
- <box gap={1}>
- <text fg={theme.textMuted}>
- OpenCode Zen gives you access to all the best coding models at the cheapest prices with a single API key.
- </text>
- <text fg={theme.text}>
- Go to <span style={{ fg: theme.primary }}>https://opencode.ai/zen</span> to get a key
- </text>
- </box>
- ) : undefined
- }
- onConfirm={async (value) => {
- if (!value) return
- sdk.client.auth.set({
- providerID: props.providerID,
- auth: {
- type: "api",
- key: value,
- },
- })
- await sdk.client.instance.dispose()
- await sync.bootstrap()
- dialog.replace(() => <DialogModel providerID={props.providerID} />)
- }}
- />
- )
- }
|