openAgent
gitlabThe open source UNIVERSAL agent.
clone
https://gitlab.com/bitzzdev/openAgent.gitAIagentautomationopen-sourceuniversal-agent
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: stringvalue: stringdescription?: stringcategory: 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)) returnreturn 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) returnconst providerID = normalizeCustomProviderID(value)if (providerID) return providerIDtoast.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) returnreturn dialog.replace(() => <ApiMethod providerID={providerID} title="API key" custom />)},}}const providerID = provider.providerIDconst 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) returnconst methods = sync.data.provider_auth[providerID] ?? [{type: "api",label: "API key",},]let index: number | null = 0if (methods.length > 1) {index = await new Promise<number | null>((resolve) => {dialog.replace(() => (<DialogSelecttitle="Select auth method"options={methods.map((x, index) => ({title: x.label,value: index,}))}onSelect={(option) => resolve(option.value)}/>),() => resolve(null),)})}if (index == null) returnconst method = methods[index]if (method.type === "oauth") {let inputs: Record<string, string> | undefinedif (method.prompts?.length) {const value = await PromptsMethod({dialog,prompts: method.prompts,})if (!value) returninputs = 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> | undefinedif (method.prompts?.length) {const value = await PromptsMethod({ dialog, prompts: method.prompts })if (!value) returnmetadata = 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: numberproviderID: stringtitle: stringauthorization: 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.urlclipboard.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: numbertitle: stringproviderID: stringauthorization: ProviderAuthAuthorization}function CodeMethod(props: CodeMethodProps) {const { theme } = useTheme()const sdk = useSDK()const sync = useSync()const dialog = useDialog()const [error, setError] = createSignal(false)return (<DialogPrompttitle={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: stringtitle: stringmetadata?: 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 (<DialogPrompttitle={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 APIkey.</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 modelswith 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) returnawait 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) continueconst matches = prompt.when.op === "eq" ? value === prompt.when.value : value !== prompt.when.valueif (!matches) continue}if (prompt.type === "select") {const value = await new Promise<string | null>((resolve) => {props.dialog.replace(() => (<DialogSelecttitle={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 nullinputs[prompt.key] = valuecontinue}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 nullinputs[prompt.key] = value}return inputs}