|
@@ -1,7 +1,7 @@
|
|
|
-import { createEffect, createMemo, For, Match, ParentProps, Show, Switch, type JSX } from "solid-js"
|
|
|
|
|
|
|
+import { createEffect, createMemo, For, Match, onCleanup, onMount, ParentProps, Show, Switch, type JSX } from "solid-js"
|
|
|
import { DateTime } from "luxon"
|
|
import { DateTime } from "luxon"
|
|
|
import { A, useNavigate, useParams } from "@solidjs/router"
|
|
import { A, useNavigate, useParams } from "@solidjs/router"
|
|
|
-import { useLayout } from "@/context/layout"
|
|
|
|
|
|
|
+import { useLayout, getAvatarColors } from "@/context/layout"
|
|
|
import { useGlobalSync } from "@/context/global-sync"
|
|
import { useGlobalSync } from "@/context/global-sync"
|
|
|
import { base64Decode, base64Encode } from "@opencode-ai/util/encode"
|
|
import { base64Decode, base64Encode } from "@opencode-ai/util/encode"
|
|
|
import { Mark } from "@opencode-ai/ui/logo"
|
|
import { Mark } from "@opencode-ai/ui/logo"
|
|
@@ -17,9 +17,9 @@ import { DiffChanges } from "@opencode-ai/ui/diff-changes"
|
|
|
import { getFilename } from "@opencode-ai/util/path"
|
|
import { getFilename } from "@opencode-ai/util/path"
|
|
|
import { Select } from "@opencode-ai/ui/select"
|
|
import { Select } from "@opencode-ai/ui/select"
|
|
|
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
|
|
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
|
|
|
-import { Session, Project, ProviderAuthMethod } from "@opencode-ai/sdk/v2/client"
|
|
|
|
|
|
|
+import { Session, Project, ProviderAuthMethod, ProviderAuthAuthorization } from "@opencode-ai/sdk/v2/client"
|
|
|
import { usePlatform } from "@/context/platform"
|
|
import { usePlatform } from "@/context/platform"
|
|
|
-import { createStore } from "solid-js/store"
|
|
|
|
|
|
|
+import { createStore, produce } from "solid-js/store"
|
|
|
import {
|
|
import {
|
|
|
DragDropProvider,
|
|
DragDropProvider,
|
|
|
DragDropSensors,
|
|
DragDropSensors,
|
|
@@ -36,9 +36,12 @@ import { IconName } from "@opencode-ai/ui/icons/provider"
|
|
|
import { popularProviders, useProviders } from "@/hooks/use-providers"
|
|
import { popularProviders, useProviders } from "@/hooks/use-providers"
|
|
|
import { Dialog } from "@opencode-ai/ui/dialog"
|
|
import { Dialog } from "@opencode-ai/ui/dialog"
|
|
|
import { iife } from "@opencode-ai/util/iife"
|
|
import { iife } from "@opencode-ai/util/iife"
|
|
|
|
|
+import { Link } from "@/components/link"
|
|
|
import { List, ListRef } from "@opencode-ai/ui/list"
|
|
import { List, ListRef } from "@opencode-ai/ui/list"
|
|
|
-import { Input } from "@opencode-ai/ui/input"
|
|
|
|
|
|
|
+import { TextField } from "@opencode-ai/ui/text-field"
|
|
|
|
|
+import { showToast, Toast } from "@opencode-ai/ui/toast"
|
|
|
import { useGlobalSDK } from "@/context/global-sdk"
|
|
import { useGlobalSDK } from "@/context/global-sdk"
|
|
|
|
|
+import { Spinner } from "@opencode-ai/ui/spinner"
|
|
|
|
|
|
|
|
export default function Layout(props: ParentProps) {
|
|
export default function Layout(props: ParentProps) {
|
|
|
const [store, setStore] = createStore({
|
|
const [store, setStore] = createStore({
|
|
@@ -177,7 +180,7 @@ export default function Layout(props: ParentProps) {
|
|
|
<Avatar
|
|
<Avatar
|
|
|
fallback={name()}
|
|
fallback={name()}
|
|
|
src={props.project.icon?.url}
|
|
src={props.project.icon?.url}
|
|
|
- background={props.project.icon?.color ?? "var(--surface-info-base)"}
|
|
|
|
|
|
|
+ {...getAvatarColors(props.project.icon?.color)}
|
|
|
class="size-full"
|
|
class="size-full"
|
|
|
/>
|
|
/>
|
|
|
</div>
|
|
</div>
|
|
@@ -197,7 +200,7 @@ export default function Layout(props: ParentProps) {
|
|
|
<Avatar
|
|
<Avatar
|
|
|
fallback={name()}
|
|
fallback={name()}
|
|
|
src={props.project.icon?.url}
|
|
src={props.project.icon?.url}
|
|
|
- background={props.project.icon?.color ?? "var(--surface-info-base)"}
|
|
|
|
|
|
|
+ {...getAvatarColors(props.project.icon?.color)}
|
|
|
class="size-full"
|
|
class="size-full"
|
|
|
/>
|
|
/>
|
|
|
</div>
|
|
</div>
|
|
@@ -228,7 +231,7 @@ export default function Layout(props: ParentProps) {
|
|
|
<Avatar
|
|
<Avatar
|
|
|
fallback={name()}
|
|
fallback={name()}
|
|
|
src={props.project.icon?.url}
|
|
src={props.project.icon?.url}
|
|
|
- background={props.project.icon?.color ?? "var(--surface-info-base)"}
|
|
|
|
|
|
|
+ {...getAvatarColors(props.project.icon?.color)}
|
|
|
class="size-full group-hover/session:hidden"
|
|
class="size-full group-hover/session:hidden"
|
|
|
/>
|
|
/>
|
|
|
<Icon
|
|
<Icon
|
|
@@ -487,7 +490,7 @@ export default function Layout(props: ParentProps) {
|
|
|
</div>
|
|
</div>
|
|
|
<div class="flex flex-col gap-1.5 self-stretch items-start shrink-0 px-2 py-3">
|
|
<div class="flex flex-col gap-1.5 self-stretch items-start shrink-0 px-2 py-3">
|
|
|
<Switch>
|
|
<Switch>
|
|
|
- <Match when={!providers().paid().length && layout.sidebar.opened()}>
|
|
|
|
|
|
|
+ <Match when={!providers.paid().length && layout.sidebar.opened()}>
|
|
|
<div class="rounded-md bg-background-stronger shadow-xs-border-base">
|
|
<div class="rounded-md bg-background-stronger shadow-xs-border-base">
|
|
|
<div class="p-3 flex flex-col gap-2">
|
|
<div class="p-3 flex flex-col gap-2">
|
|
|
<div class="text-12-medium text-text-strong">Getting started</div>
|
|
<div class="text-12-medium text-text-strong">Getting started</div>
|
|
@@ -533,17 +536,17 @@ export default function Layout(props: ParentProps) {
|
|
|
</Button>
|
|
</Button>
|
|
|
</Tooltip>
|
|
</Tooltip>
|
|
|
</Show>
|
|
</Show>
|
|
|
- <Tooltip placement="right" value="Settings" inactive={layout.sidebar.opened()}>
|
|
|
|
|
- <Button
|
|
|
|
|
- disabled
|
|
|
|
|
- class="flex w-full text-left justify-start text-12-medium text-text-base stroke-[1.5px] rounded-lg px-2"
|
|
|
|
|
- variant="ghost"
|
|
|
|
|
- size="large"
|
|
|
|
|
- icon="settings-gear"
|
|
|
|
|
- >
|
|
|
|
|
- <Show when={layout.sidebar.opened()}>Settings</Show>
|
|
|
|
|
- </Button>
|
|
|
|
|
- </Tooltip>
|
|
|
|
|
|
|
+ {/* <Tooltip placement="right" value="Settings" inactive={layout.sidebar.opened()}> */}
|
|
|
|
|
+ {/* <Button */}
|
|
|
|
|
+ {/* disabled */}
|
|
|
|
|
+ {/* class="flex w-full text-left justify-start text-12-medium text-text-base stroke-[1.5px] rounded-lg px-2" */}
|
|
|
|
|
+ {/* variant="ghost" */}
|
|
|
|
|
+ {/* size="large" */}
|
|
|
|
|
+ {/* icon="settings-gear" */}
|
|
|
|
|
+ {/* > */}
|
|
|
|
|
+ {/* <Show when={layout.sidebar.opened()}>Settings</Show> */}
|
|
|
|
|
+ {/* </Button> */}
|
|
|
|
|
+ {/* </Tooltip> */}
|
|
|
<Tooltip placement="right" value="Share feedback" inactive={layout.sidebar.opened()}>
|
|
<Tooltip placement="right" value="Share feedback" inactive={layout.sidebar.opened()}>
|
|
|
<Button
|
|
<Button
|
|
|
as={"a"}
|
|
as={"a"}
|
|
@@ -567,7 +570,7 @@ export default function Layout(props: ParentProps) {
|
|
|
placeholder="Search providers"
|
|
placeholder="Search providers"
|
|
|
activeIcon="plus-small"
|
|
activeIcon="plus-small"
|
|
|
key={(x) => x?.id}
|
|
key={(x) => x?.id}
|
|
|
- items={providers().all}
|
|
|
|
|
|
|
+ items={providers.all}
|
|
|
filterKeys={["id", "name"]}
|
|
filterKeys={["id", "name"]}
|
|
|
groupBy={(x) => (popularProviders.includes(x.id) ? "Popular" : "Other")}
|
|
groupBy={(x) => (popularProviders.includes(x.id) ? "Popular" : "Other")}
|
|
|
sortBy={(a, b) => {
|
|
sortBy={(a, b) => {
|
|
@@ -617,27 +620,102 @@ export default function Layout(props: ParentProps) {
|
|
|
</Show>
|
|
</Show>
|
|
|
<Show when={layout.dialog.opened() === "connect"}>
|
|
<Show when={layout.dialog.opened() === "connect"}>
|
|
|
{iife(() => {
|
|
{iife(() => {
|
|
|
|
|
+ const providerID = createMemo(() => layout.connect.provider()!)
|
|
|
|
|
+ const provider = createMemo(() => globalSync.data.provider.all.find((x) => x.id === providerID())!)
|
|
|
|
|
+ const methods = createMemo(
|
|
|
|
|
+ () =>
|
|
|
|
|
+ globalSync.data.provider_auth[providerID()] ?? [
|
|
|
|
|
+ {
|
|
|
|
|
+ type: "api",
|
|
|
|
|
+ label: "API key",
|
|
|
|
|
+ },
|
|
|
|
|
+ ],
|
|
|
|
|
+ )
|
|
|
const [store, setStore] = createStore({
|
|
const [store, setStore] = createStore({
|
|
|
method: undefined as undefined | ProviderAuthMethod,
|
|
method: undefined as undefined | ProviderAuthMethod,
|
|
|
|
|
+ authorization: undefined as undefined | ProviderAuthAuthorization,
|
|
|
|
|
+ state: "pending" as undefined | "pending" | "complete" | "error",
|
|
|
|
|
+ error: undefined as string | undefined,
|
|
|
})
|
|
})
|
|
|
- const providerID = layout.connect.provider()!
|
|
|
|
|
- const provider = globalSync.data.provider.all.find((x) => x.id === providerID)!
|
|
|
|
|
- const methods = globalSync.data.provider_auth[providerID] ?? [
|
|
|
|
|
- {
|
|
|
|
|
- type: "api",
|
|
|
|
|
- label: "API key",
|
|
|
|
|
- },
|
|
|
|
|
- ]
|
|
|
|
|
- if (methods.length === 1) {
|
|
|
|
|
- setStore("method", methods[0])
|
|
|
|
|
|
|
+
|
|
|
|
|
+ const methodIndex = createMemo(() => methods().findIndex((x) => x.label === store.method?.label))
|
|
|
|
|
+
|
|
|
|
|
+ async function selectMethod(index: number) {
|
|
|
|
|
+ const method = methods()[index]
|
|
|
|
|
+ setStore(
|
|
|
|
|
+ produce((draft) => {
|
|
|
|
|
+ draft.method = method
|
|
|
|
|
+ draft.authorization = undefined
|
|
|
|
|
+ draft.state = undefined
|
|
|
|
|
+ draft.error = undefined
|
|
|
|
|
+ }),
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ if (method.type === "oauth") {
|
|
|
|
|
+ setStore("state", "pending")
|
|
|
|
|
+ const start = Date.now()
|
|
|
|
|
+ await globalSDK.client.provider.oauth
|
|
|
|
|
+ .authorize(
|
|
|
|
|
+ {
|
|
|
|
|
+ providerID: providerID(),
|
|
|
|
|
+ method: index,
|
|
|
|
|
+ },
|
|
|
|
|
+ { throwOnError: true },
|
|
|
|
|
+ )
|
|
|
|
|
+ .then((x) => {
|
|
|
|
|
+ const elapsed = Date.now() - start
|
|
|
|
|
+ const delay = 1000 - elapsed
|
|
|
|
|
+
|
|
|
|
|
+ if (delay > 0) {
|
|
|
|
|
+ setTimeout(() => {
|
|
|
|
|
+ setStore("state", "complete")
|
|
|
|
|
+ setStore("authorization", x.data!)
|
|
|
|
|
+ }, delay)
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ setStore("state", "complete")
|
|
|
|
|
+ setStore("authorization", x.data!)
|
|
|
|
|
+ })
|
|
|
|
|
+ .catch((e) => {
|
|
|
|
|
+ setStore("state", "error")
|
|
|
|
|
+ setStore("error", String(e))
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
let listRef: ListRef | undefined
|
|
let listRef: ListRef | undefined
|
|
|
- const handleKey = (e: KeyboardEvent) => {
|
|
|
|
|
|
|
+ function handleKey(e: KeyboardEvent) {
|
|
|
|
|
+ if (e.key === "Enter" && e.target instanceof HTMLInputElement) {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
if (e.key === "Escape") return
|
|
if (e.key === "Escape") return
|
|
|
listRef?.onKeyDown(e)
|
|
listRef?.onKeyDown(e)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ onMount(() => {
|
|
|
|
|
+ if (methods().length === 1) {
|
|
|
|
|
+ selectMethod(0)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ document.addEventListener("keydown", handleKey)
|
|
|
|
|
+ onCleanup(() => {
|
|
|
|
|
+ document.removeEventListener("keydown", handleKey)
|
|
|
|
|
+ })
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ async function complete() {
|
|
|
|
|
+ await globalSDK.client.global.dispose()
|
|
|
|
|
+ setTimeout(() => {
|
|
|
|
|
+ showToast({
|
|
|
|
|
+ variant: "success",
|
|
|
|
|
+ icon: "circle-check",
|
|
|
|
|
+ title: `${provider().name} connected`,
|
|
|
|
|
+ description: `${provider().name} models are now available to use.`,
|
|
|
|
|
+ })
|
|
|
|
|
+ layout.connect.complete()
|
|
|
|
|
+ }, 500)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
return (
|
|
return (
|
|
|
<Dialog
|
|
<Dialog
|
|
|
modal
|
|
modal
|
|
@@ -657,7 +735,16 @@ export default function Layout(props: ParentProps) {
|
|
|
icon="arrow-left"
|
|
icon="arrow-left"
|
|
|
variant="ghost"
|
|
variant="ghost"
|
|
|
onClick={() => {
|
|
onClick={() => {
|
|
|
- if (store.method && methods.length > 1) {
|
|
|
|
|
|
|
+ if (methods().length === 1) {
|
|
|
|
|
+ layout.dialog.open("provider")
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ if (store.authorization) {
|
|
|
|
|
+ setStore("authorization", undefined)
|
|
|
|
|
+ setStore("method", undefined)
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ if (store.method) {
|
|
|
setStore("method", undefined)
|
|
setStore("method", undefined)
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
@@ -670,145 +757,256 @@ export default function Layout(props: ParentProps) {
|
|
|
<Dialog.Body>
|
|
<Dialog.Body>
|
|
|
<div class="flex flex-col gap-6 px-2.5 pb-3">
|
|
<div class="flex flex-col gap-6 px-2.5 pb-3">
|
|
|
<div class="px-2.5 flex gap-4 items-center">
|
|
<div class="px-2.5 flex gap-4 items-center">
|
|
|
- <ProviderIcon id={providerID as IconName} class="size-5 shrink-0 icon-strong-base" />
|
|
|
|
|
- <div class="text-16-medium text-text-strong">Connect {provider.name}</div>
|
|
|
|
|
|
|
+ <ProviderIcon id={providerID() as IconName} class="size-5 shrink-0 icon-strong-base" />
|
|
|
|
|
+ <div class="text-16-medium text-text-strong">
|
|
|
|
|
+ <Switch>
|
|
|
|
|
+ <Match
|
|
|
|
|
+ when={providerID() === "anthropic" && store.method?.label?.toLowerCase().includes("max")}
|
|
|
|
|
+ >
|
|
|
|
|
+ Login with Claude Pro/Max
|
|
|
|
|
+ </Match>
|
|
|
|
|
+ <Match when={true}>Connect {provider().name}</Match>
|
|
|
|
|
+ </Switch>
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
- <Show when={store.method === undefined}>
|
|
|
|
|
- <div class="px-2.5 text-14-regular text-text-base">Select login method for {provider.name}.</div>
|
|
|
|
|
- <div class="">
|
|
|
|
|
- <Input hidden type="text" class="opacity-0 size-0" autofocus onKeyDown={handleKey} />
|
|
|
|
|
- <List
|
|
|
|
|
- ref={(ref) => (listRef = ref)}
|
|
|
|
|
- items={methods}
|
|
|
|
|
- key={(m) => m?.label}
|
|
|
|
|
- onSelect={(method) => {
|
|
|
|
|
- if (!method) return
|
|
|
|
|
- setStore("method", method)
|
|
|
|
|
-
|
|
|
|
|
- if (method.type === "oauth") {
|
|
|
|
|
- // const result = await sdk.client.provider.oauth.authorize({
|
|
|
|
|
- // providerID: provider.id,
|
|
|
|
|
- // method: index,
|
|
|
|
|
- // })
|
|
|
|
|
- // if (result.data?.method === "code") {
|
|
|
|
|
- // dialog.replace(() => (
|
|
|
|
|
- // <CodeMethod
|
|
|
|
|
- // providerID={provider.id}
|
|
|
|
|
- // title={method.label}
|
|
|
|
|
- // index={index}
|
|
|
|
|
- // authorization={result.data!}
|
|
|
|
|
- // />
|
|
|
|
|
- // ))
|
|
|
|
|
- // }
|
|
|
|
|
- // if (result.data?.method === "auto") {
|
|
|
|
|
- // dialog.replace(() => (
|
|
|
|
|
- // <AutoMethod
|
|
|
|
|
- // providerID={provider.id}
|
|
|
|
|
- // title={method.label}
|
|
|
|
|
- // index={index}
|
|
|
|
|
- // authorization={result.data!}
|
|
|
|
|
- // />
|
|
|
|
|
- // ))
|
|
|
|
|
- // }
|
|
|
|
|
- }
|
|
|
|
|
- if (method.type === "api") {
|
|
|
|
|
- // return dialog.replace(() => <ApiMethod providerID={provider.id} title={method.label} />)
|
|
|
|
|
- }
|
|
|
|
|
- }}
|
|
|
|
|
- >
|
|
|
|
|
- {(i) => (
|
|
|
|
|
- <div class="w-full flex items-center gap-x-2.5">
|
|
|
|
|
- {/* TODO: add checkmark thing */}
|
|
|
|
|
- <span>{i.label}</span>
|
|
|
|
|
|
|
+ <div class="px-2.5 pb-10 flex flex-col gap-6">
|
|
|
|
|
+ <Switch>
|
|
|
|
|
+ <Match when={store.method === undefined}>
|
|
|
|
|
+ <div class="text-14-regular text-text-base">Select login method for {provider().name}.</div>
|
|
|
|
|
+ <div class="">
|
|
|
|
|
+ <List
|
|
|
|
|
+ ref={(ref) => (listRef = ref)}
|
|
|
|
|
+ items={methods}
|
|
|
|
|
+ key={(m) => m?.label}
|
|
|
|
|
+ onSelect={async (method, index) => {
|
|
|
|
|
+ if (!method) return
|
|
|
|
|
+ selectMethod(index)
|
|
|
|
|
+ }}
|
|
|
|
|
+ >
|
|
|
|
|
+ {(i) => (
|
|
|
|
|
+ <div class="w-full flex items-center gap-x-4">
|
|
|
|
|
+ <div class="w-4 h-2 rounded-[1px] bg-input-base shadow-xs-border-base flex items-center justify-center">
|
|
|
|
|
+ <div
|
|
|
|
|
+ class="w-2.5 h-0.5 bg-icon-strong-base hidden"
|
|
|
|
|
+ data-slot="list-item-extra-icon"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <span>{i.label}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </List>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </Match>
|
|
|
|
|
+ <Match when={store.state === "pending"}>
|
|
|
|
|
+ <div class="text-14-regular text-text-base">
|
|
|
|
|
+ <div class="flex items-center gap-x-4">
|
|
|
|
|
+ <Spinner />
|
|
|
|
|
+ <span>Authorization in progress...</span>
|
|
|
</div>
|
|
</div>
|
|
|
- )}
|
|
|
|
|
- </List>
|
|
|
|
|
- </div>
|
|
|
|
|
- </Show>
|
|
|
|
|
- <Show when={store.method?.type === "api"}>
|
|
|
|
|
- {iife(() => {
|
|
|
|
|
- const [formStore, setFormStore] = createStore({
|
|
|
|
|
- value: "",
|
|
|
|
|
- error: undefined as string | undefined,
|
|
|
|
|
- })
|
|
|
|
|
-
|
|
|
|
|
- async function handleSubmit(e: SubmitEvent) {
|
|
|
|
|
- e.preventDefault()
|
|
|
|
|
-
|
|
|
|
|
- const form = e.currentTarget as HTMLFormElement
|
|
|
|
|
- const formData = new FormData(form)
|
|
|
|
|
- const apiKey = formData.get("apiKey") as string
|
|
|
|
|
-
|
|
|
|
|
- if (!apiKey?.trim()) {
|
|
|
|
|
- setFormStore("error", "API key is required")
|
|
|
|
|
- return
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- setFormStore("error", undefined)
|
|
|
|
|
- await globalSDK.client.auth.set({
|
|
|
|
|
- providerID,
|
|
|
|
|
- auth: {
|
|
|
|
|
- type: "api",
|
|
|
|
|
- key: apiKey,
|
|
|
|
|
- },
|
|
|
|
|
- })
|
|
|
|
|
- await globalSDK.client.global.dispose()
|
|
|
|
|
- layout.connect.complete()
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </Match>
|
|
|
|
|
+ <Match when={store.state === "error"}>
|
|
|
|
|
+ <div class="text-14-regular text-text-base">
|
|
|
|
|
+ <div class="flex items-center gap-x-4">
|
|
|
|
|
+ <Icon name="circle-ban-sign" class="text-icon-critical-base" />
|
|
|
|
|
+ <span>Authorization failed: {store.error}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </Match>
|
|
|
|
|
+ <Match when={store.method?.type === "api"}>
|
|
|
|
|
+ {iife(() => {
|
|
|
|
|
+ const [formStore, setFormStore] = createStore({
|
|
|
|
|
+ value: "",
|
|
|
|
|
+ error: undefined as string | undefined,
|
|
|
|
|
+ })
|
|
|
|
|
|
|
|
- return (
|
|
|
|
|
- <div class="px-2.5 pb-10 flex flex-col gap-6">
|
|
|
|
|
- <Switch>
|
|
|
|
|
- <Match when={provider.id === "opencode"}>
|
|
|
|
|
- <div class="flex flex-col gap-4">
|
|
|
|
|
- <div class="text-14-regular text-text-base">
|
|
|
|
|
- OpenCode Zen gives you access to a curated set of reliable optimized models for
|
|
|
|
|
- coding agents.
|
|
|
|
|
- </div>
|
|
|
|
|
- <div class="text-14-regular text-text-base">
|
|
|
|
|
- With a single API key you’ll get access to models such as Claude, GPT, Gemini, GLM
|
|
|
|
|
- and more.
|
|
|
|
|
|
|
+ async function handleSubmit(e: SubmitEvent) {
|
|
|
|
|
+ e.preventDefault()
|
|
|
|
|
+
|
|
|
|
|
+ const form = e.currentTarget as HTMLFormElement
|
|
|
|
|
+ const formData = new FormData(form)
|
|
|
|
|
+ const apiKey = formData.get("apiKey") as string
|
|
|
|
|
+
|
|
|
|
|
+ if (!apiKey?.trim()) {
|
|
|
|
|
+ setFormStore("error", "API key is required")
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ setFormStore("error", undefined)
|
|
|
|
|
+ await globalSDK.client.auth.set({
|
|
|
|
|
+ providerID: providerID(),
|
|
|
|
|
+ auth: {
|
|
|
|
|
+ type: "api",
|
|
|
|
|
+ key: apiKey,
|
|
|
|
|
+ },
|
|
|
|
|
+ })
|
|
|
|
|
+ await complete()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return (
|
|
|
|
|
+ <div class="flex flex-col gap-6">
|
|
|
|
|
+ <Switch>
|
|
|
|
|
+ <Match when={provider().id === "opencode"}>
|
|
|
|
|
+ <div class="flex flex-col gap-4">
|
|
|
|
|
+ <div class="text-14-regular text-text-base">
|
|
|
|
|
+ OpenCode Zen gives you access to a curated set of reliable optimized models for
|
|
|
|
|
+ coding agents.
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="text-14-regular text-text-base">
|
|
|
|
|
+ With a single API key you’ll get access to models such as Claude, GPT, Gemini,
|
|
|
|
|
+ GLM and more.
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="text-14-regular text-text-base">
|
|
|
|
|
+ Visit{" "}
|
|
|
|
|
+ <Link href="https://opencode.ai/zen" tabIndex={-1}>
|
|
|
|
|
+ opencode.ai/zen
|
|
|
|
|
+ </Link>{" "}
|
|
|
|
|
+ to collect your API key.
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </Match>
|
|
|
|
|
+ <Match when={true}>
|
|
|
|
|
+ <div class="text-14-regular text-text-base">
|
|
|
|
|
+ Enter your {provider().name} API key to connect your account and use{" "}
|
|
|
|
|
+ {provider().name} models in OpenCode.
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </Match>
|
|
|
|
|
+ </Switch>
|
|
|
|
|
+ <form onSubmit={handleSubmit} class="flex flex-col items-start gap-4">
|
|
|
|
|
+ <TextField
|
|
|
|
|
+ autofocus
|
|
|
|
|
+ type="text"
|
|
|
|
|
+ label={`${provider().name} API key`}
|
|
|
|
|
+ placeholder="API key"
|
|
|
|
|
+ name="apiKey"
|
|
|
|
|
+ value={formStore.value}
|
|
|
|
|
+ onChange={setFormStore.bind(null, "value")}
|
|
|
|
|
+ validationState={formStore.error ? "invalid" : undefined}
|
|
|
|
|
+ error={formStore.error}
|
|
|
|
|
+ />
|
|
|
|
|
+ <Button class="w-auto" type="submit" size="large" variant="primary">
|
|
|
|
|
+ Submit
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ </form>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )
|
|
|
|
|
+ })}
|
|
|
|
|
+ </Match>
|
|
|
|
|
+ <Match when={store.method?.type === "oauth"}>
|
|
|
|
|
+ <Switch>
|
|
|
|
|
+ <Match when={store.authorization?.method === "code"}>
|
|
|
|
|
+ {iife(() => {
|
|
|
|
|
+ const [formStore, setFormStore] = createStore({
|
|
|
|
|
+ value: "",
|
|
|
|
|
+ error: undefined as string | undefined,
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ onMount(() => {
|
|
|
|
|
+ if (store.authorization?.method === "code" && store.authorization?.url) {
|
|
|
|
|
+ platform.openLink(store.authorization.url)
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ async function handleSubmit(e: SubmitEvent) {
|
|
|
|
|
+ e.preventDefault()
|
|
|
|
|
+
|
|
|
|
|
+ const form = e.currentTarget as HTMLFormElement
|
|
|
|
|
+ const formData = new FormData(form)
|
|
|
|
|
+ const code = formData.get("code") as string
|
|
|
|
|
+
|
|
|
|
|
+ if (!code?.trim()) {
|
|
|
|
|
+ setFormStore("error", "Authorization code is required")
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ setFormStore("error", undefined)
|
|
|
|
|
+ const { error } = await globalSDK.client.provider.oauth.callback({
|
|
|
|
|
+ providerID: providerID(),
|
|
|
|
|
+ method: methodIndex(),
|
|
|
|
|
+ code,
|
|
|
|
|
+ })
|
|
|
|
|
+ if (!error) {
|
|
|
|
|
+ await complete()
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ setFormStore("error", "Invalid authorization code")
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return (
|
|
|
|
|
+ <div class="flex flex-col gap-6">
|
|
|
|
|
+ <div class="text-14-regular text-text-base">
|
|
|
|
|
+ Visit <Link href={store.authorization!.url}>this link</Link> to collect your
|
|
|
|
|
+ authorization code to connect your account and use {provider().name} models in
|
|
|
|
|
+ OpenCode.
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <form onSubmit={handleSubmit} class="flex flex-col items-start gap-4">
|
|
|
|
|
+ <TextField
|
|
|
|
|
+ autofocus
|
|
|
|
|
+ type="text"
|
|
|
|
|
+ label={`${store.method?.label} authorization code`}
|
|
|
|
|
+ placeholder="Authorization code"
|
|
|
|
|
+ name="code"
|
|
|
|
|
+ value={formStore.value}
|
|
|
|
|
+ onChange={setFormStore.bind(null, "value")}
|
|
|
|
|
+ validationState={formStore.error ? "invalid" : undefined}
|
|
|
|
|
+ error={formStore.error}
|
|
|
|
|
+ />
|
|
|
|
|
+ <Button class="w-auto" type="submit" size="large" variant="primary">
|
|
|
|
|
+ Submit
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ </form>
|
|
|
</div>
|
|
</div>
|
|
|
- <div class="text-14-regular text-text-base">
|
|
|
|
|
- Visit{" "}
|
|
|
|
|
- <button
|
|
|
|
|
- tabIndex={-1}
|
|
|
|
|
- class="text-text-strong underline"
|
|
|
|
|
- onClick={() => platform.openLink("https://opencode.ai/zen")}
|
|
|
|
|
- >
|
|
|
|
|
- opencode.ai/zen
|
|
|
|
|
- </button>{" "}
|
|
|
|
|
- to collect your API key.
|
|
|
|
|
|
|
+ )
|
|
|
|
|
+ })}
|
|
|
|
|
+ </Match>
|
|
|
|
|
+ <Match when={store.authorization?.method === "auto"}>
|
|
|
|
|
+ {iife(() => {
|
|
|
|
|
+ const code = createMemo(() => {
|
|
|
|
|
+ const instructions = store.authorization?.instructions
|
|
|
|
|
+ if (instructions?.includes(":")) {
|
|
|
|
|
+ return instructions?.split(":")[1]?.trim()
|
|
|
|
|
+ }
|
|
|
|
|
+ return instructions
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ onMount(async () => {
|
|
|
|
|
+ const result = await globalSDK.client.provider.oauth.callback({
|
|
|
|
|
+ providerID: providerID(),
|
|
|
|
|
+ method: methodIndex(),
|
|
|
|
|
+ })
|
|
|
|
|
+ if (result.error) {
|
|
|
|
|
+ // TODO: show error
|
|
|
|
|
+ layout.dialog.close("connect")
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ await complete()
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ return (
|
|
|
|
|
+ <div class="flex flex-col gap-6">
|
|
|
|
|
+ <div class="text-14-regular text-text-base">
|
|
|
|
|
+ Visit <Link href={store.authorization!.url}>this link</Link> and enter the code
|
|
|
|
|
+ below to connect your account and use {provider().name} models in OpenCode.
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <TextField
|
|
|
|
|
+ label="Confirmation code"
|
|
|
|
|
+ class="font-mono"
|
|
|
|
|
+ value={code()}
|
|
|
|
|
+ readOnly
|
|
|
|
|
+ copyable
|
|
|
|
|
+ />
|
|
|
|
|
+ <div class="text-14-regular text-text-base flex items-center gap-4">
|
|
|
|
|
+ <Spinner />
|
|
|
|
|
+ <span>Waiting for authorization...</span>
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
- </div>
|
|
|
|
|
- </Match>
|
|
|
|
|
- <Match when={true}>
|
|
|
|
|
- <div class="text-14-regular text-text-base">
|
|
|
|
|
- Enter your {provider.name} API key to connect your account and use {provider.name}{" "}
|
|
|
|
|
- models in OpenCode.
|
|
|
|
|
- </div>
|
|
|
|
|
- </Match>
|
|
|
|
|
- </Switch>
|
|
|
|
|
- <form onSubmit={handleSubmit} class="flex flex-col items-start gap-4">
|
|
|
|
|
- <Input
|
|
|
|
|
- autofocus
|
|
|
|
|
- type="text"
|
|
|
|
|
- label={`${provider.name} API key`}
|
|
|
|
|
- placeholder="API key"
|
|
|
|
|
- name="apiKey"
|
|
|
|
|
- value={formStore.value}
|
|
|
|
|
- onChange={setFormStore.bind(null, "value")}
|
|
|
|
|
- validationState={formStore.error ? "invalid" : undefined}
|
|
|
|
|
- error={formStore.error}
|
|
|
|
|
- />
|
|
|
|
|
- <Button class="w-auto" type="submit" size="large" variant="primary">
|
|
|
|
|
- Submit
|
|
|
|
|
- </Button>
|
|
|
|
|
- </form>
|
|
|
|
|
- </div>
|
|
|
|
|
- )
|
|
|
|
|
- })}
|
|
|
|
|
- </Show>
|
|
|
|
|
|
|
+ )
|
|
|
|
|
+ })}
|
|
|
|
|
+ </Match>
|
|
|
|
|
+ </Switch>
|
|
|
|
|
+ </Match>
|
|
|
|
|
+ </Switch>
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
</Dialog.Body>
|
|
</Dialog.Body>
|
|
|
</Dialog>
|
|
</Dialog>
|
|
@@ -816,6 +1014,7 @@ export default function Layout(props: ParentProps) {
|
|
|
})}
|
|
})}
|
|
|
</Show>
|
|
</Show>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+ <Toast.Region />
|
|
|
</div>
|
|
</div>
|
|
|
)
|
|
)
|
|
|
}
|
|
}
|