|
|
@@ -17,7 +17,7 @@ import { DiffChanges } from "@opencode-ai/ui/diff-changes"
|
|
|
import { getFilename } from "@opencode-ai/util/path"
|
|
|
import { Select } from "@opencode-ai/ui/select"
|
|
|
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
|
|
|
-import { Session, Project } from "@opencode-ai/sdk/v2/client"
|
|
|
+import { Session, Project, ProviderAuthMethod } from "@opencode-ai/sdk/v2/client"
|
|
|
import { usePlatform } from "@/context/platform"
|
|
|
import { createStore } from "solid-js/store"
|
|
|
import {
|
|
|
@@ -34,6 +34,11 @@ import { SelectDialog } from "@opencode-ai/ui/select-dialog"
|
|
|
import { Tag } from "@opencode-ai/ui/tag"
|
|
|
import { IconName } from "@opencode-ai/ui/icons/provider"
|
|
|
import { popularProviders, useProviders } from "@/hooks/use-providers"
|
|
|
+import { Dialog } from "@opencode-ai/ui/dialog"
|
|
|
+import { iife } from "@opencode-ai/util/iife"
|
|
|
+import { List, ListRef } from "@opencode-ai/ui/list"
|
|
|
+import { Input } from "@opencode-ai/ui/input"
|
|
|
+import { useGlobalSDK } from "@/context/global-sdk"
|
|
|
|
|
|
export default function Layout(props: ParentProps) {
|
|
|
const [store, setStore] = createStore({
|
|
|
@@ -42,6 +47,7 @@ export default function Layout(props: ParentProps) {
|
|
|
})
|
|
|
|
|
|
const params = useParams()
|
|
|
+ const globalSDK = useGlobalSDK()
|
|
|
const globalSync = useGlobalSync()
|
|
|
const layout = useLayout()
|
|
|
const platform = usePlatform()
|
|
|
@@ -434,7 +440,7 @@ export default function Layout(props: ParentProps) {
|
|
|
<Button
|
|
|
variant="ghost"
|
|
|
size="large"
|
|
|
- class="group/sidebar-toggle shrink-0 w-full text-left justify-start rounded-lg"
|
|
|
+ class="group/sidebar-toggle shrink-0 w-full text-left justify-start rounded-lg px-2"
|
|
|
onClick={layout.sidebar.toggle}
|
|
|
>
|
|
|
<div class="relative -ml-px flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
|
|
|
@@ -481,7 +487,7 @@ export default function Layout(props: ParentProps) {
|
|
|
</div>
|
|
|
<div class="flex flex-col gap-1.5 self-stretch items-start shrink-0 px-2 py-3">
|
|
|
<Switch>
|
|
|
- <Match when={!providers().connected().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="p-3 flex flex-col gap-2">
|
|
|
<div class="text-12-medium text-text-strong">Getting started</div>
|
|
|
@@ -562,7 +568,6 @@ export default function Layout(props: ParentProps) {
|
|
|
activeIcon="plus-small"
|
|
|
key={(x) => x?.id}
|
|
|
items={providers().all}
|
|
|
- // current={local.model.current()}
|
|
|
filterKeys={["id", "name"]}
|
|
|
groupBy={(x) => (popularProviders.includes(x.id) ? "Popular" : "Other")}
|
|
|
sortBy={(a, b) => {
|
|
|
@@ -575,7 +580,10 @@ export default function Layout(props: ParentProps) {
|
|
|
if (b.category === "Popular" && a.category !== "Popular") return 1
|
|
|
return 0
|
|
|
}}
|
|
|
- // onSelect={(x) => }
|
|
|
+ onSelect={(x) => {
|
|
|
+ if (!x) return
|
|
|
+ layout.dialog.connect(x.id)
|
|
|
+ }}
|
|
|
onOpenChange={(open) => {
|
|
|
if (open) {
|
|
|
layout.dialog.open("provider")
|
|
|
@@ -607,6 +615,206 @@ export default function Layout(props: ParentProps) {
|
|
|
)}
|
|
|
</SelectDialog>
|
|
|
</Show>
|
|
|
+ <Show when={layout.dialog.opened() === "connect"}>
|
|
|
+ {iife(() => {
|
|
|
+ const [store, setStore] = createStore({
|
|
|
+ method: undefined as undefined | ProviderAuthMethod,
|
|
|
+ })
|
|
|
+ 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])
|
|
|
+ }
|
|
|
+
|
|
|
+ let listRef: ListRef | undefined
|
|
|
+ const handleKey = (e: KeyboardEvent) => {
|
|
|
+ if (e.key === "Escape") return
|
|
|
+ listRef?.onKeyDown(e)
|
|
|
+ }
|
|
|
+
|
|
|
+ return (
|
|
|
+ <Dialog
|
|
|
+ modal
|
|
|
+ defaultOpen
|
|
|
+ onOpenChange={(open) => {
|
|
|
+ if (open) {
|
|
|
+ layout.dialog.open("connect")
|
|
|
+ } else {
|
|
|
+ layout.dialog.close("connect")
|
|
|
+ }
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ <Dialog.Header class="px-4.5">
|
|
|
+ <Dialog.Title class="flex items-center">
|
|
|
+ <IconButton
|
|
|
+ tabIndex={-1}
|
|
|
+ icon="arrow-left"
|
|
|
+ variant="ghost"
|
|
|
+ onClick={() => {
|
|
|
+ if (store.method && methods.length > 1) {
|
|
|
+ setStore("method", undefined)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ layout.dialog.open("provider")
|
|
|
+ }}
|
|
|
+ />
|
|
|
+ </Dialog.Title>
|
|
|
+ <Dialog.CloseButton tabIndex={-1} />
|
|
|
+ </Dialog.Header>
|
|
|
+ <Dialog.Body>
|
|
|
+ <div class="flex flex-col gap-6 px-2.5 pb-3">
|
|
|
+ <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>
|
|
|
+ </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>
|
|
|
+ )}
|
|
|
+ </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()
|
|
|
+ }
|
|
|
+
|
|
|
+ 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.
|
|
|
+ </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.
|
|
|
+ </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>
|
|
|
+ </div>
|
|
|
+ </Dialog.Body>
|
|
|
+ </Dialog>
|
|
|
+ )
|
|
|
+ })}
|
|
|
+ </Show>
|
|
|
</div>
|
|
|
</div>
|
|
|
)
|