From 913cf0dc5e15e2738127ca4e176130be246b04da Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Wed, 19 Nov 2025 23:57:48 -0500 Subject: [PATCH] tui onboarding --- .../cli/cmd/tui/component/dialog-provider.tsx | 39 +++++++++++-------- .../opencode/src/cli/cmd/tui/context/sync.tsx | 3 ++ packages/opencode/src/provider/auth.ts | 34 ++++++++++++++++ packages/opencode/src/provider/provider.ts | 2 - packages/opencode/src/server/server.ts | 21 ++++++++++ packages/sdk/js/src/gen/sdk.gen.ts | 12 ++++++ packages/sdk/js/src/gen/types.gen.ts | 23 +++++++++++ 7 files changed, 116 insertions(+), 18 deletions(-) create mode 100644 packages/opencode/src/provider/auth.ts diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx index 97784353d..a36219e78 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx @@ -2,8 +2,7 @@ import { createMemo } 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 { DialogPrompt } from "../ui/dialog-prompt" +import { Dialog, useDialog } from "@tui/ui/dialog" import { useSDK } from "../context/sdk" const PROVIDER_PRIORITY: Record = { @@ -33,20 +32,28 @@ export function createDialogProviderOptions() { anthropic: "Claude Max or API key", }[provider.id], async onSelect() { - const key = await DialogPrompt.show(dialog, "Enter API key") - if (!key) return - await sdk.client.auth.set({ - path: { - id: provider.id, - }, - body: { - type: "api", - key, - }, - }) - await sdk.client.instance.dispose() - await sync.bootstrap() - dialog.clear() + const methods = sync.data.provider_auth[provider.id] + let method = methods[0]?.type ?? "api" + if (methods.length > 1) { + const index = await new Promise((resolve) => { + dialog.replace( + () => ( + ({ + title: x.label, + value: index, + category: "Method", + }))} + onSelect={(option) => resolve(option.value)} + /> + ), + () => resolve(null), + ) + }) + if (!index) return + method = methods[index].type + } }, })), sortBy((x) => PROVIDER_PRIORITY[x.value] ?? 99), diff --git a/packages/opencode/src/cli/cmd/tui/context/sync.tsx b/packages/opencode/src/cli/cmd/tui/context/sync.tsx index 7474a8939..d5bdadbfc 100644 --- a/packages/opencode/src/cli/cmd/tui/context/sync.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/sync.tsx @@ -30,6 +30,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ provider: Provider[] provider_default: Record provider_next: ProviderListResponse + provider_auth: Record agent: Agent[] command: Command[] permission: { @@ -63,6 +64,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ default: {}, connected: [], }, + provider_auth: {}, config: {}, status: "loading", agent: [], @@ -271,6 +273,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ sdk.client.mcp.status().then((x) => setStore("mcp", x.data!)), sdk.client.formatter.status().then((x) => setStore("formatter", x.data!)), sdk.client.session.status().then((x) => setStore("session_status", x.data!)), + sdk.client.provider.auth().then((x) => setStore("provider_auth", x.data ?? {})), ]).then(() => { setStore("status", "complete") }) diff --git a/packages/opencode/src/provider/auth.ts b/packages/opencode/src/provider/auth.ts new file mode 100644 index 000000000..b07eed2c5 --- /dev/null +++ b/packages/opencode/src/provider/auth.ts @@ -0,0 +1,34 @@ +import { Instance } from "@/project/instance" +import { Plugin } from "../plugin" +import { map, filter, pipe, fromEntries, mapValues } from "remeda" +import z from "zod" + +export namespace ProviderAuth { + const state = Instance.state(async () => { + const result = pipe( + await Plugin.list(), + filter((x) => x.auth?.provider !== undefined), + map((x) => [x.auth!.provider, x.auth!] as const), + fromEntries(), + ) + return result + }) + + export const Method = z.object({ + type: z.union([z.literal("oauth"), z.literal("api")]), + label: z.string(), + }) + export type Method = z.infer + + export async function methods() { + const s = await state() + return mapValues(s, (x) => + x.methods.map( + (y): Method => ({ + type: y.type, + label: y.label, + }), + ), + ) + } +} diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index 6a1754429..228c855aa 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -1,5 +1,4 @@ import z from "zod" -import path from "path" import { Config } from "../config/config" import { mergeDeep, sortBy } from "remeda" import { NoSuchModelError, type LanguageModel, type Provider as SDK } from "ai" @@ -10,7 +9,6 @@ import { ModelsDev } from "./models" import { NamedError } from "../util/error" import { Auth } from "../auth" import { Instance } from "../project/instance" -import { Global } from "../global" import { Flag } from "../flag/flag" import { iife } from "@/util/iife" diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts index ed8d1de9b..8c0cfbf46 100644 --- a/packages/opencode/src/server/server.ts +++ b/packages/opencode/src/server/server.ts @@ -23,6 +23,7 @@ import { Instance } from "../project/instance" import { Agent } from "../agent/agent" import { Auth } from "../auth" import { Command } from "../command" +import { ProviderAuth } from "../provider/auth" import { Global } from "../global" import { ProjectRoute } from "./project" import { ToolRegistry } from "../tool/registry" @@ -1216,6 +1217,26 @@ export namespace Server { }) }, ) + .get( + "/provider/auth", + describeRoute({ + description: "Get provider authentication methods", + operationId: "provider.auth", + responses: { + 200: { + description: "Provider auth methods", + content: { + "application/json": { + schema: resolver(z.record(z.string(), z.array(ProviderAuth.Method))), + }, + }, + }, + }, + }), + async (c) => { + return c.json(await ProviderAuth.methods()) + }, + ) .get( "/find", describeRoute({ diff --git a/packages/sdk/js/src/gen/sdk.gen.ts b/packages/sdk/js/src/gen/sdk.gen.ts index 650271e64..d4d099073 100644 --- a/packages/sdk/js/src/gen/sdk.gen.ts +++ b/packages/sdk/js/src/gen/sdk.gen.ts @@ -96,6 +96,8 @@ import type { ConfigProvidersResponses, ProviderListData, ProviderListResponses, + ProviderAuthData, + ProviderAuthResponses, FindTextData, FindTextResponses, FindFilesData, @@ -580,6 +582,16 @@ class Provider extends _HeyApiClient { ...options, }) } + + /** + * Get provider authentication methods + */ + public auth(options?: Options) { + return (options?.client ?? this._client).get({ + url: "/provider/auth", + ...options, + }) + } } class Find extends _HeyApiClient { diff --git a/packages/sdk/js/src/gen/types.gen.ts b/packages/sdk/js/src/gen/types.gen.ts index 103470fa4..b158da9f9 100644 --- a/packages/sdk/js/src/gen/types.gen.ts +++ b/packages/sdk/js/src/gen/types.gen.ts @@ -2524,6 +2524,29 @@ export type ProviderListResponses = { export type ProviderListResponse = ProviderListResponses[keyof ProviderListResponses] +export type ProviderAuthData = { + body?: never + path?: never + query?: { + directory?: string + } + url: "/provider/auth" +} + +export type ProviderAuthResponses = { + /** + * Provider auth methods + */ + 200: { + [key: string]: Array<{ + type: "oauth" | "api" + label: string + }> + } +} + +export type ProviderAuthResponse = ProviderAuthResponses[keyof ProviderAuthResponses] + export type FindTextData = { body?: never path?: never