diff options
Diffstat (limited to 'packages/desktop/src')
| -rw-r--r-- | packages/desktop/src/pages/layout.tsx | 352 |
1 files changed, 203 insertions, 149 deletions
diff --git a/packages/desktop/src/pages/layout.tsx b/packages/desktop/src/pages/layout.tsx index e9f10e3a2..4a3fa766b 100644 --- a/packages/desktop/src/pages/layout.tsx +++ b/packages/desktop/src/pages/layout.tsx @@ -1,4 +1,4 @@ -import { createEffect, createMemo, For, Match, ParentProps, Show, Switch, type JSX } from "solid-js" +import { createEffect, createMemo, For, Match, onMount, ParentProps, Show, Switch, type JSX } from "solid-js" import { DateTime } from "luxon" import { A, useNavigate, useParams } from "@solidjs/router" import { useLayout } from "@/context/layout" @@ -17,9 +17,9 @@ 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, ProviderAuthMethod } from "@opencode-ai/sdk/v2/client" +import { Session, Project, ProviderAuthMethod, ProviderAuthAuthorization } from "@opencode-ai/sdk/v2/client" import { usePlatform } from "@/context/platform" -import { createStore } from "solid-js/store" +import { createStore, produce } from "solid-js/store" import { DragDropProvider, DragDropSensors, @@ -40,6 +40,7 @@ import { List, ListRef } from "@opencode-ai/ui/list" import { Input } from "@opencode-ai/ui/input" import { showToast, Toast } from "@opencode-ai/ui/toast" import { useGlobalSDK } from "@/context/global-sdk" +import { Spinner } from "@opencode-ai/ui/spinner" export default function Layout(props: ParentProps) { const [store, setStore] = createStore({ @@ -618,9 +619,6 @@ export default function Layout(props: ParentProps) { </Show> <Show when={layout.dialog.opened() === "connect"}> {iife(() => { - const [store, setStore] = createStore({ - method: undefined as undefined | ProviderAuthMethod, - }) const providerID = createMemo(() => layout.connect.provider()!) const provider = createMemo(() => globalSync.data.provider.all.find((x) => x.id === providerID())!) const methods = createMemo( @@ -632,12 +630,61 @@ export default function Layout(props: ParentProps) { }, ], ) - if (methods().length === 1) { - setStore("method", methods()[0]) + const [store, setStore] = createStore({ + method: undefined as undefined | ProviderAuthMethod, + authorization: undefined as undefined | ProviderAuthAuthorization, + state: "pending" as undefined | "pending" | "complete" | "error", + error: undefined as string | undefined, + }) + + 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, + }) + .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)) + }) + } } + onMount(() => { + if (methods().length === 1) { + selectMethod(0) + } + }) + let listRef: ListRef | undefined - const handleKey = (e: KeyboardEvent) => { + function handleKey(e: KeyboardEvent) { if (e.key === "Escape") return listRef?.onKeyDown(e) } @@ -661,7 +708,16 @@ export default function Layout(props: ParentProps) { icon="arrow-left" variant="ghost" 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) return } @@ -677,154 +733,152 @@ export default function Layout(props: ParentProps) { <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> - <Switch> - <Match 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> - </Match> - <Match when={store.method?.type === "api"}> - {iife(() => { - const [formStore, setFormStore] = createStore({ - value: "", - error: undefined as string | undefined, - }) + <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=""> + <Input hidden type="text" class="opacity-0 size-0" autofocus onKeyDown={handleKey} /> + <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> + {/* TODO: add checkmark thing */} + <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> + </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, + }) - async function handleSubmit(e: SubmitEvent) { - e.preventDefault() + 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 + 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 - } + 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 globalSDK.client.global.dispose() - setTimeout(() => { - showToast({ - variant: "success", - icon: "circle-check", - title: `${provider().name} connected`, - description: `${provider().name} models are now available to use.`, + setFormStore("error", undefined) + await globalSDK.client.auth.set({ + providerID: providerID(), + auth: { + type: "api", + key: apiKey, + }, }) - layout.connect.complete() - }, 500) - } + 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 ( - <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. + 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{" "} + <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"> - With a single API key you’ll get access to models such as Claude, GPT, Gemini, GLM - and more. + Enter your {provider().name} API key to connect your account and use{" "} + {provider().name} models in OpenCode. </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> - ) - })} - </Match> - </Switch> + </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> + ) + })} + </Match> + <Match when={store.method?.type === "oauth"}> + <Switch> + <Match when={store.authorization?.method === "code"}>Code {store.authorization?.url}</Match> + <Match when={store.authorization?.method === "auto"}>Auto {store.authorization?.url}</Match> + </Switch> + </Match> + </Switch> + </div> </div> </Dialog.Body> </Dialog> |
