cloudaxe-opencode/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx

222 lines
6.1 KiB
TypeScript

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 { useTheme } from "../context/theme"
import { TextAttributes } from "@opentui/core"
import type { ProviderAuthAuthorization } from "@opencode-ai/sdk"
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,
map((provider) => ({
title: provider.name,
value: provider.id,
footer: {
opencode: "Recommended",
anthropic: "Claude Max or API key",
}[provider.id],
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({
path: {
id: provider.id,
},
body: {
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} />)
}
},
})),
sortBy((x) => PROVIDER_PRIORITY[x.value] ?? 99),
)
})
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({
path: {
id: props.providerID,
},
body: {
method: props.index,
},
})
if (result.error) {
dialog.clear()
return
}
await sdk.client.instance.dispose()
await sync.bootstrap()
dialog.replace(() => <DialogModel />)
})
return (
<box paddingLeft={2} paddingRight={2} gap={1} paddingBottom={1}>
<box flexDirection="row" justifyContent="space-between">
<text attributes={TextAttributes.BOLD}>{props.title}</text>
<text fg={theme.textMuted}>esc</text>
</box>
<box gap={1}>
<text fg={theme.primary}>{props.authorization.url}</text>
<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({
path: {
id: props.providerID,
},
body: {
method: props.index,
code: value,
},
})
if (!error) {
await sdk.client.instance.dispose()
await sync.bootstrap()
dialog.replace(() => <DialogModel />)
return
}
setError(true)
}}
description={() => (
<box gap={1}>
<text fg={theme.textMuted}>{props.authorization.instructions}</text>
<text fg={theme.primary}>{props.authorization.url}</text>
<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()
return (
<DialogPrompt
title={props.title}
placeholder="API key"
onConfirm={async (value) => {
if (!value) return
sdk.client.auth.set({
path: {
id: props.providerID,
},
body: {
type: "api",
key: value,
},
})
await sdk.client.instance.dispose()
await sync.bootstrap()
dialog.replace(() => <DialogModel />)
}}
/>
)
}