mirror of
https://github.com/sst/opencode.git
synced 2025-12-23 10:11:41 +00:00
sync
This commit is contained in:
parent
ec6122a575
commit
ffe42bdc8a
5 changed files with 115 additions and 78 deletions
|
|
@ -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>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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") {
|
||||
|
|
|
|||
|
|
@ -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()),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue