diff options
| -rw-r--r-- | packages/desktop/src/components/link.tsx | 17 | ||||
| -rw-r--r-- | packages/desktop/src/pages/layout.tsx | 118 |
2 files changed, 121 insertions, 14 deletions
diff --git a/packages/desktop/src/components/link.tsx b/packages/desktop/src/components/link.tsx new file mode 100644 index 000000000..e13c31330 --- /dev/null +++ b/packages/desktop/src/components/link.tsx @@ -0,0 +1,17 @@ +import { ComponentProps, splitProps } from "solid-js" +import { usePlatform } from "@/context/platform" + +export interface LinkProps extends ComponentProps<"button"> { + href: string +} + +export function Link(props: LinkProps) { + const platform = usePlatform() + const [local, rest] = splitProps(props, ["href", "children"]) + + return ( + <button class="text-text-strong underline" onClick={() => platform.openLink(local.href)} {...rest}> + {local.children} + </button> + ) +} diff --git a/packages/desktop/src/pages/layout.tsx b/packages/desktop/src/pages/layout.tsx index 4a3fa766b..3086ff2fd 100644 --- a/packages/desktop/src/pages/layout.tsx +++ b/packages/desktop/src/pages/layout.tsx @@ -36,6 +36,7 @@ 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 { Link } from "@/components/link" import { List, ListRef } from "@opencode-ai/ui/list" import { Input } from "@opencode-ai/ui/input" import { showToast, Toast } from "@opencode-ai/ui/toast" @@ -637,6 +638,8 @@ export default function Layout(props: ParentProps) { error: undefined as string | undefined, }) + const methodIndex = createMemo(() => methods().findIndex((x) => x.label === store.method?.label)) + async function selectMethod(index: number) { const method = methods()[index] setStore( @@ -652,10 +655,13 @@ export default function Layout(props: ParentProps) { setStore("state", "pending") const start = Date.now() await globalSDK.client.provider.oauth - .authorize({ - providerID: providerID(), - method: index, - }) + .authorize( + { + providerID: providerID(), + method: index, + }, + { throwOnError: true }, + ) .then((x) => { const elapsed = Date.now() - start const delay = 1000 - elapsed @@ -731,7 +737,16 @@ export default function Layout(props: ParentProps) { <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 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 class="px-2.5 pb-10 flex flex-col gap-6"> <Switch> @@ -756,7 +771,6 @@ export default function Layout(props: ParentProps) { data-slot="list-item-extra-icon" /> </div> - {/* TODO: add checkmark thing */} <span>{i.label}</span> </div> )} @@ -833,13 +847,9 @@ export default function Layout(props: ParentProps) { </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")} - > + <Link href="https://opencode.ai/zen" tabIndex={-1}> opencode.ai/zen - </button>{" "} + </Link>{" "} to collect your API key. </div> </div> @@ -873,8 +883,88 @@ export default function Layout(props: ParentProps) { </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> + <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 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 + } + 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"> + <Input + 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> + ) + })} + </Match> + <Match when={store.authorization?.method === "auto"}> + <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> + </div> + </Match> </Switch> </Match> </Switch> |
