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 (
+
+ )
+}
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) {
-
Connect {provider().name}
+
+
+
+ Login with Claude Pro/Max
+
+ Connect {provider().name}
+
+
@@ -756,7 +771,6 @@ export default function Layout(props: ParentProps) {
data-slot="list-item-extra-icon"
/>
- {/* TODO: add checkmark thing */}
{i.label}
)}
@@ -833,13 +847,9 @@ export default function Layout(props: ParentProps) {
Visit{" "}
- {" "}
+ {" "}
to collect your API key.
@@ -873,8 +883,88 @@ export default function Layout(props: ParentProps) {
- Code {store.authorization?.url}
- Auto {store.authorization?.url}
+
+ {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 (
+
+
+ Visit this link to collect your
+ authorization code to connect your account and use {provider().name} models in
+ OpenCode.
+
+
+
+ )
+ })}
+
+
+
+
+ Visit this link and enter the code below
+ to connect your account and use {provider().name} models in OpenCode.
+
+
+