sync
Some checks are pending
format / format (push) Waiting to run
test / test (push) Waiting to run
Update Nix Hashes / update (push) Waiting to run

This commit is contained in:
Dax Raad 2025-11-20 23:50:01 -05:00
parent ec6122a575
commit ffe42bdc8a
5 changed files with 115 additions and 78 deletions

View file

@ -1,12 +1,13 @@
import { createMemo } from "solid-js"
import { createMemo, onMount } from "solid-js"
import { useSync } from "@tui/context/sync"
import { map, pipe, sortBy } from "remeda"
import { DialogSelect } from "@tui/ui/dialog-select"
import { Dialog, useDialog } from "@tui/ui/dialog"
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, ProviderAuthMethod } from "@opencode-ai/sdk"
const PROVIDER_PRIORITY: Record<string, number> = {
opencode: 0,
@ -73,59 +74,15 @@ export function createDialogProviderOptions() {
},
})
if (result.data?.method === "code") {
while (true) {
const url = result.data.url
const code = await DialogPrompt.show(dialog, "Login with " + method.label, {
placeholder: "Authorization code",
description: () => (
<box gap={1}>
<text fg={theme.textMuted}>Visit the url to collect your authorization code</text>
<text fg={theme.primary}>{url}</text>
</box>
),
})
if (!code) break
const { error } = await sdk.client.provider.oauth.callback({
path: {
id: provider.id,
},
body: {
method: index,
code,
},
})
if (!error) {
await sdk.client.instance.dispose()
await sync.bootstrap()
return
}
}
dialog.replace(() => (
<CodeMethod providerID={provider.id} title={method.label} index={index} authorization={result.data!} />
))
}
if (result.data?.method === "auto") {
const { instructions, url } = result.data
dialog.replace(() => (
<PendingDialog
title={method.label}
description={() => (
<box gap={1}>
<text fg={theme.primary}>{url}</text>
<text fg={theme.textMuted}>{instructions}</text>
</box>
)}
/>
<AutoMethod providerID={provider.id} title={method.label} index={index} authorization={result.data!} />
))
await sdk.client.provider.oauth.callback({
path: {
id: provider.id,
},
body: {
method: index,
},
})
dialog.clear()
await sdk.client.instance.dispose()
await sync.bootstrap()
}
}
},
@ -138,24 +95,91 @@ export function createDialogProviderOptions() {
export function DialogProvider() {
const options = createDialogProviderOptions()
return <DialogSelect title="Connect a provider" options={options()} />
}
interface PendingDialogProps {
interface AutoMethodProps {
index: number
providerID: string
title: string
description: () => JSX.Element
authorization: ProviderAuthAuthorization
}
function PendingDialog(props: PendingDialogProps) {
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.clear()
})
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}>{props.description}</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()
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()
return
}
}}
description={() => (
<box gap={1}>
<text fg={theme.textMuted}>{props.authorization.instructions}</text>
<text fg={theme.primary}>{props.authorization.url}</text>
</box>
)}
/>
)
}

View file

@ -13,6 +13,7 @@ import type {
FormatterStatus,
SessionStatus,
ProviderListResponse,
ProviderAuthMethod,
} from "@opencode-ai/sdk"
import { createStore, produce, reconcile } from "solid-js/store"
import { useSDK } from "@tui/context/sdk"
@ -30,7 +31,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 }[]>
provider_auth: Record<string, ProviderAuthMethod[]>
agent: Agent[]
command: Command[]
permission: {

View file

@ -18,10 +18,14 @@ export namespace ProviderAuth {
return { methods, pending: {} as Record<string, AuthOuathResult> }
})
export const Method = z.object({
type: z.union([z.literal("oauth"), z.literal("api")]),
label: z.string(),
})
export const Method = z
.object({
type: z.union([z.literal("oauth"), z.literal("api")]),
label: z.string(),
})
.meta({
ref: "ProviderAuthMethod",
})
export type Method = z.infer<typeof Method>
export async function methods() {
@ -36,19 +40,23 @@ export namespace ProviderAuth {
)
}
export const AuthorizeResult = z.object({
url: z.string(),
method: z.union([z.literal("auto"), z.literal("code")]),
instructions: z.string(),
})
export type AuthorizeResult = z.infer<typeof AuthorizeResult>
export const Authorization = z
.object({
url: z.string(),
method: z.union([z.literal("auto"), z.literal("code")]),
instructions: z.string(),
})
.meta({
ref: "ProviderAuthAuthorization",
})
export type Authorization = z.infer<typeof Authorization>
export const authorize = fn(
z.object({
providerID: z.string(),
method: z.number(),
}),
async (input): Promise<AuthorizeResult | undefined> => {
async (input): Promise<Authorization | undefined> => {
const auth = await state().then((s) => s.methods[input.providerID])
const method = auth.methods[input.method]
if (method.type === "oauth") {

View file

@ -8,7 +8,7 @@ import { proxy } from "hono/proxy"
import { Session } from "../session"
import z from "zod"
import { Provider } from "../provider/provider"
import { map, mapValues, pipe, values } from "remeda"
import { mapValues } from "remeda"
import { NamedError } from "../util/error"
import { ModelsDev } from "../provider/models"
import { Ripgrep } from "../file/ripgrep"
@ -1247,7 +1247,7 @@ export namespace Server {
description: "Authorization URL and method",
content: {
"application/json": {
schema: resolver(ProviderAuth.AuthorizeResult.optional()),
schema: resolver(ProviderAuth.Authorization.optional()),
},
},
},

View file

@ -1331,6 +1331,17 @@ export type Provider = {
}
}
export type ProviderAuthMethod = {
type: "oauth" | "api"
label: string
}
export type ProviderAuthAuthorization = {
url: string
method: "auto" | "code"
instructions: string
}
export type Symbol = {
name: string
kind: number
@ -2538,10 +2549,7 @@ export type ProviderAuthResponses = {
* Provider auth methods
*/
200: {
[key: string]: Array<{
type: "oauth" | "api"
label: string
}>
[key: string]: Array<ProviderAuthMethod>
}
}
@ -2579,11 +2587,7 @@ export type ProviderOauthAuthorizeResponses = {
/**
* Authorization URL and method
*/
200: {
url: string
method: "auto" | "code"
instructions: string
}
200: ProviderAuthAuthorization
}
export type ProviderOauthAuthorizeResponse = ProviderOauthAuthorizeResponses[keyof ProviderOauthAuthorizeResponses]