Buckets:
| import { createMemo, createSignal, onMount, Show } from "solid-js" | |
| import { useSync } from "../context/sync" | |
| import { map, pipe, sortBy } from "remeda" | |
| import { DialogSelect } from "../ui/dialog-select" | |
| import { useDialog } from "../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, ProviderAuthMethod } from "@opencode-ai/sdk/v2" | |
| import { DialogModel } from "./dialog-model" | |
| import { useToast } from "../ui/toast" | |
| import { isConsoleManagedProvider } from "../util/provider-origin" | |
| import { useConnected } from "./use-connected" | |
| import { useBindings } from "../keymap" | |
| import { useClipboard } from "../context/clipboard" | |
| const PROVIDER_PRIORITY: Record<string, number> = { | |
| opencode: 0, | |
| "opencode-go": 1, | |
| openai: 2, | |
| "github-copilot": 3, | |
| anthropic: 4, | |
| google: 5, | |
| } | |
| const CUSTOM_PROVIDER_OPTION_VALUE = "__opencode_custom_provider__" | |
| const CUSTOM_PROVIDER_ID = /^[a-z0-9][a-z0-9-_]*$/ | |
| type ProviderOptionBase = { | |
| title: string | |
| value: string | |
| description?: string | |
| category: string | |
| } | |
| type ProviderOption = | |
| | (ProviderOptionBase & { | |
| type: "provider" | |
| providerID: string | |
| }) | |
| | (ProviderOptionBase & { | |
| type: "custom" | |
| }) | |
| export function providerOptions(list: { id: string; name: string }[]): ProviderOption[] { | |
| return [ | |
| ...pipe( | |
| list, | |
| sortBy( | |
| (x) => PROVIDER_PRIORITY[x.id] ?? 99, | |
| (x) => x.name.toLowerCase(), | |
| (x) => x.id, | |
| ), | |
| map((provider) => ({ | |
| type: "provider" as const, | |
| title: provider.name, | |
| value: provider.id, | |
| providerID: provider.id, | |
| description: { | |
| opencode: "(Recommended)", | |
| anthropic: "(API key)", | |
| openai: "(ChatGPT Plus/Pro or API key)", | |
| "opencode-go": "Low cost subscription for everyone", | |
| }[provider.id], | |
| category: provider.id in PROVIDER_PRIORITY ? "Popular" : "Providers", | |
| })), | |
| ), | |
| { | |
| type: "custom", | |
| title: "Other", | |
| value: CUSTOM_PROVIDER_OPTION_VALUE, | |
| description: "Custom provider", | |
| category: "Providers", | |
| }, | |
| ] | |
| } | |
| export function normalizeCustomProviderID(value: string) { | |
| const providerID = value.trim().replace(/^@ai-sdk\//, "") | |
| if (!CUSTOM_PROVIDER_ID.test(providerID)) return | |
| return providerID | |
| } | |
| export function createDialogProviderOptions() { | |
| const sync = useSync() | |
| const dialog = useDialog() | |
| const sdk = useSDK() | |
| const toast = useToast() | |
| const { theme } = useTheme() | |
| const onboarded = useConnected() | |
| async function promptCustomProviderID(): Promise<string | undefined> { | |
| const value = await DialogPrompt.show(dialog, "Other", { | |
| placeholder: "Provider id", | |
| description: () => ( | |
| <text fg={theme.textMuted}> | |
| This only stores a credential. Configure the provider in opencode.json to use it. | |
| </text> | |
| ), | |
| }) | |
| if (value === null) return | |
| const providerID = normalizeCustomProviderID(value) | |
| if (providerID) return providerID | |
| toast.show({ | |
| variant: "error", | |
| message: | |
| "Provider ids must start with a lowercase letter or number and only use lowercase letters, numbers, hyphens, and underscores", | |
| }) | |
| return promptCustomProviderID() | |
| } | |
| const options = createMemo(() => { | |
| return pipe( | |
| providerOptions(sync.data.provider_next.all), | |
| map((provider) => { | |
| if (provider.type === "custom") { | |
| return { | |
| title: provider.title, | |
| value: provider.value, | |
| description: provider.description, | |
| category: provider.category, | |
| async onSelect() { | |
| const providerID = await promptCustomProviderID() | |
| if (!providerID) return | |
| return dialog.replace(() => <ApiMethod providerID={providerID} title="API key" custom />) | |
| }, | |
| } | |
| } | |
| const providerID = provider.providerID | |
| const consoleManaged = isConsoleManagedProvider(sync.data.console_state.consoleManagedProviders, providerID) | |
| const connected = sync.data.provider_next.connected.includes(providerID) | |
| return { | |
| title: provider.title, | |
| value: provider.value, | |
| description: provider.description, | |
| footer: consoleManaged ? sync.data.console_state.activeOrgName : undefined, | |
| category: provider.category, | |
| gutter: connected && onboarded() ? () => <text fg={theme.success}>✓</text> : undefined, | |
| async onSelect() { | |
| if (consoleManaged) return | |
| const methods = sync.data.provider_auth[providerID] ?? [ | |
| { | |
| 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") { | |
| let inputs: Record<string, string> | undefined | |
| if (method.prompts?.length) { | |
| const value = await PromptsMethod({ | |
| dialog, | |
| prompts: method.prompts, | |
| }) | |
| if (!value) return | |
| inputs = value | |
| } | |
| const result = await sdk.client.provider.oauth.authorize({ | |
| providerID, | |
| method: index, | |
| inputs, | |
| }) | |
| if (result.error) { | |
| toast.show({ | |
| variant: "error", | |
| message: JSON.stringify(result.error), | |
| }) | |
| dialog.clear() | |
| return | |
| } | |
| if (result.data?.method === "code") { | |
| dialog.replace(() => ( | |
| <CodeMethod providerID={providerID} title={method.label} index={index} authorization={result.data!} /> | |
| )) | |
| } | |
| if (result.data?.method === "auto") { | |
| dialog.replace(() => ( | |
| <AutoMethod providerID={providerID} title={method.label} index={index} authorization={result.data!} /> | |
| )) | |
| } | |
| } | |
| if (method.type === "api") { | |
| let metadata: Record<string, string> | undefined | |
| if (method.prompts?.length) { | |
| const value = await PromptsMethod({ dialog, prompts: method.prompts }) | |
| if (!value) return | |
| metadata = value | |
| } | |
| return dialog.replace(() => ( | |
| <ApiMethod providerID={providerID} title={method.label} metadata={metadata} /> | |
| )) | |
| } | |
| }, | |
| } | |
| }), | |
| ) | |
| }) | |
| 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() | |
| const toast = useToast() | |
| const clipboard = useClipboard() | |
| useBindings(() => ({ | |
| bindings: [ | |
| { | |
| key: "c", | |
| desc: "Copy provider code", | |
| group: "Dialog", | |
| cmd: () => { | |
| const code = | |
| props.authorization.instructions.match(/[A-Z0-9]{4}-[A-Z0-9]{4,5}/)?.[0] ?? props.authorization.url | |
| clipboard | |
| .write?.(code) | |
| .then(() => toast.show({ message: "Copied to clipboard", variant: "info" })) | |
| .catch(toast.error) | |
| }, | |
| }, | |
| ], | |
| })) | |
| onMount(async () => { | |
| const result = await sdk.client.provider.oauth.callback({ | |
| providerID: props.providerID, | |
| method: props.index, | |
| }) | |
| if (result.error) { | |
| toast.show({ | |
| variant: "error", | |
| message: | |
| "name" in result.error && result.error.name === "ProviderAuthOauthCallbackFailed" | |
| ? "OAuth authorization failed. Try /connect again." | |
| : JSON.stringify(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} onMouseUp={() => dialog.clear()}> | |
| 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> | |
| <text fg={theme.text}> | |
| c <span style={{ fg: theme.textMuted }}>copy</span> | |
| </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 | |
| metadata?: Record<string, string> | |
| custom?: boolean | |
| } | |
| function ApiMethod(props: ApiMethodProps) { | |
| const dialog = useDialog() | |
| const sdk = useSDK() | |
| const sync = useSync() | |
| const toast = useToast() | |
| const { theme } = useTheme() | |
| return ( | |
| <DialogPrompt | |
| title={props.title} | |
| placeholder="API key" | |
| description={ | |
| { | |
| 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> | |
| ), | |
| "opencode-go": ( | |
| <box gap={1}> | |
| <text fg={theme.textMuted}> | |
| OpenCode Go is a $10 per month subscription that provides reliable access to popular open coding models | |
| with generous usage limits. | |
| </text> | |
| <text fg={theme.text}> | |
| Go to <span style={{ fg: theme.primary }}>https://opencode.ai/go</span> and enable OpenCode Go | |
| </text> | |
| </box> | |
| ), | |
| }[props.providerID] ?? undefined | |
| } | |
| onConfirm={async (value) => { | |
| if (!value) return | |
| await sdk.client.auth.set({ | |
| providerID: props.providerID, | |
| auth: { | |
| type: "api", | |
| key: value, | |
| ...(props.metadata ? { metadata: props.metadata } : {}), | |
| }, | |
| }) | |
| await sdk.client.instance.dispose() | |
| await sync.bootstrap() | |
| if (props.custom && !sync.data.provider_next.all.some((provider) => provider.id === props.providerID)) { | |
| toast.show({ | |
| variant: "info", | |
| message: `Saved credential for ${props.providerID}. Configure it in opencode.json to use it.`, | |
| }) | |
| dialog.clear() | |
| return | |
| } | |
| dialog.replace(() => <DialogModel providerID={props.providerID} />) | |
| }} | |
| /> | |
| ) | |
| } | |
| interface PromptsMethodProps { | |
| dialog: ReturnType<typeof useDialog> | |
| prompts: NonNullable<ProviderAuthMethod["prompts"]>[number][] | |
| } | |
| async function PromptsMethod(props: PromptsMethodProps) { | |
| const inputs: Record<string, string> = {} | |
| for (const prompt of props.prompts) { | |
| if (prompt.when) { | |
| const value = inputs[prompt.when.key] | |
| if (value === undefined) continue | |
| const matches = prompt.when.op === "eq" ? value === prompt.when.value : value !== prompt.when.value | |
| if (!matches) continue | |
| } | |
| if (prompt.type === "select") { | |
| const value = await new Promise<string | null>((resolve) => { | |
| props.dialog.replace( | |
| () => ( | |
| <DialogSelect | |
| title={prompt.message} | |
| options={prompt.options.map((x) => ({ | |
| title: x.label, | |
| value: x.value, | |
| description: x.hint, | |
| }))} | |
| onSelect={(option) => resolve(option.value)} | |
| /> | |
| ), | |
| () => resolve(null), | |
| ) | |
| }) | |
| if (value === null) return null | |
| inputs[prompt.key] = value | |
| continue | |
| } | |
| const value = await new Promise<string | null>((resolve) => { | |
| props.dialog.replace( | |
| () => ( | |
| <DialogPrompt title={prompt.message} placeholder={prompt.placeholder} onConfirm={(value) => resolve(value)} /> | |
| ), | |
| () => resolve(null), | |
| ) | |
| }) | |
| if (value === null) return null | |
| inputs[prompt.key] = value | |
| } | |
| return inputs | |
| } | |
Xet Storage Details
- Size:
- 14.8 kB
- Xet hash:
- f72de64b4cf0d067e6ca961d2cb33e3306a4ab3f7925260cbd1fac791bbea335
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.