summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--packages/desktop/src/components/link.tsx17
-rw-r--r--packages/desktop/src/pages/layout.tsx118
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>