tui onboarding

This commit is contained in:
Dax Raad 2025-11-19 23:57:48 -05:00
parent 13ffccec1f
commit 913cf0dc5e
7 changed files with 116 additions and 18 deletions

View file

@ -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<string, number> = {
@ -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<number | null>((resolve) => {
dialog.replace(
() => (
<DialogSelect
title="Select auth method"
options={methods.map((x, index) => ({
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),

View file

@ -30,6 +30,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
provider: Provider[]
provider_default: Record<string, string>
provider_next: ProviderListResponse
provider_auth: Record<string, { type: string; label: string }[]>
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")
})

View file

@ -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<typeof Method>
export async function methods() {
const s = await state()
return mapValues(s, (x) =>
x.methods.map(
(y): Method => ({
type: y.type,
label: y.label,
}),
),
)
}
}

View file

@ -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"

View file

@ -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({

View file

@ -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<ThrowOnError extends boolean = false>(options?: Options<ProviderAuthData, ThrowOnError>) {
return (options?.client ?? this._client).get<ProviderAuthResponses, unknown, ThrowOnError>({
url: "/provider/auth",
...options,
})
}
}
class Find extends _HeyApiClient {

View file

@ -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