Adam 2 месяцев назад
Родитель
Сommit
634fd62b25
2 измененных файлов с 121 добавлено и 14 удалено
  1. 17 0
      packages/desktop/src/components/link.tsx
  2. 104 14
      packages/desktop/src/pages/layout.tsx

+ 17 - 0
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>
+  )
+}

+ 104 - 14
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>