From ad5614bbb91004855608fc98f3d0e75033d52ccf Mon Sep 17 00:00:00 2001
From: Adam <2363879+adamdotdevin@users.noreply.github.com>
Date: Sun, 14 Dec 2025 20:33:18 -0600
Subject: wip(desktop): progress
---
.../src/components/dialog-connect-provider.tsx | 383 +++++++++++++++++++++
packages/desktop/src/components/dialog-connect.tsx | 383 ---------------------
.../src/components/dialog-select-model-unpaid.tsx | 4 +-
.../src/components/dialog-select-provider.tsx | 4 +-
4 files changed, 387 insertions(+), 387 deletions(-)
create mode 100644 packages/desktop/src/components/dialog-connect-provider.tsx
delete mode 100644 packages/desktop/src/components/dialog-connect.tsx
diff --git a/packages/desktop/src/components/dialog-connect-provider.tsx b/packages/desktop/src/components/dialog-connect-provider.tsx
new file mode 100644
index 000000000..4660e1398
--- /dev/null
+++ b/packages/desktop/src/components/dialog-connect-provider.tsx
@@ -0,0 +1,383 @@
+import { createMemo, Match, onCleanup, onMount, Switch } from "solid-js"
+import { createStore, produce } from "solid-js/store"
+import { useDialog } from "@opencode-ai/ui/context/dialog"
+import { useGlobalSync } from "@/context/global-sync"
+import { useGlobalSDK } from "@/context/global-sdk"
+import { usePlatform } from "@/context/platform"
+import { ProviderAuthAuthorization } from "@opencode-ai/sdk/v2/client"
+import { Dialog } from "@opencode-ai/ui/dialog"
+import { List, ListRef } from "@opencode-ai/ui/list"
+import { Button } from "@opencode-ai/ui/button"
+import { IconButton } from "@opencode-ai/ui/icon-button"
+import { TextField } from "@opencode-ai/ui/text-field"
+import { Spinner } from "@opencode-ai/ui/spinner"
+import { Icon } from "@opencode-ai/ui/icon"
+import { showToast } from "@opencode-ai/ui/toast"
+import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
+import { IconName } from "@opencode-ai/ui/icons/provider"
+import { iife } from "@opencode-ai/util/iife"
+import { Link } from "@/components/link"
+import { DialogSelectProvider } from "./dialog-select-provider"
+import { DialogSelectModel } from "./dialog-select-model"
+
+export function DialogConnectProvider(props: { provider: string }) {
+ const dialog = useDialog()
+ const globalSync = useGlobalSync()
+ const globalSDK = useGlobalSDK()
+ const platform = usePlatform()
+ const provider = createMemo(() => globalSync.data.provider.all.find((x) => x.id === props.provider)!)
+ const methods = createMemo(
+ () =>
+ globalSync.data.provider_auth[props.provider] ?? [
+ {
+ type: "api",
+ label: "API key",
+ },
+ ],
+ )
+ const [store, setStore] = createStore({
+ methodIndex: undefined as undefined | number,
+ authorization: undefined as undefined | ProviderAuthAuthorization,
+ state: "pending" as undefined | "pending" | "complete" | "error",
+ error: undefined as string | undefined,
+ })
+
+ const method = createMemo(() => (store.methodIndex !== undefined ? methods().at(store.methodIndex!) : undefined))
+
+ async function selectMethod(index: number) {
+ const method = methods()[index]
+ setStore(
+ produce((draft) => {
+ draft.methodIndex = index
+ 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: props.provider,
+ method: index,
+ },
+ { throwOnError: true },
+ )
+ .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))
+ })
+ }
+ }
+
+ let listRef: ListRef | undefined
+ function handleKey(e: KeyboardEvent) {
+ if (e.key === "Enter" && e.target instanceof HTMLInputElement) {
+ return
+ }
+ if (e.key === "Escape") return
+ listRef?.onKeyDown(e)
+ }
+
+ onMount(() => {
+ if (methods().length === 1) {
+ selectMethod(0)
+ }
+ document.addEventListener("keydown", handleKey)
+ onCleanup(() => {
+ document.removeEventListener("keydown", handleKey)
+ })
+ })
+
+ async function complete() {
+ 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.`,
+ })
+ dialog.replace(() => )
+ }, 1000)
+ }
+
+ function goBack() {
+ if (methods().length === 1) {
+ dialog.replace(() => )
+ return
+ }
+ if (store.authorization) {
+ setStore("authorization", undefined)
+ setStore("methodIndex", undefined)
+ return
+ }
+ if (store.methodIndex) {
+ setStore("methodIndex", undefined)
+ return
+ }
+ dialog.replace(() => )
+ }
+
+ return (
+ }>
+
+
+
+
+
+
+ Login with Claude Pro/Max
+
+ Connect {provider().name}
+
+
+
+
+
+
+ Select login method for {provider().name}.
+
+
(listRef = ref)}
+ items={methods}
+ key={(m) => m?.label}
+ onSelect={async (method, index) => {
+ if (!method) return
+ selectMethod(index)
+ }}
+ >
+ {(i) => (
+
+ )}
+
+
+
+
+
+
+
+ Authorization in progress...
+
+
+
+
+
+
+
+ Authorization failed: {store.error}
+
+
+
+
+ {iife(() => {
+ const [formStore, setFormStore] = createStore({
+ value: "",
+ error: undefined as string | undefined,
+ })
+
+ 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
+
+ if (!apiKey?.trim()) {
+ setFormStore("error", "API key is required")
+ return
+ }
+
+ setFormStore("error", undefined)
+ await globalSDK.client.auth.set({
+ providerID: props.provider,
+ auth: {
+ type: "api",
+ key: apiKey,
+ },
+ })
+ await complete()
+ }
+
+ return (
+
+
+
+
+
+ OpenCode Zen gives you access to a curated set of reliable optimized models for coding
+ agents.
+
+
+ With a single API key you'll get access to models such as Claude, GPT, Gemini, GLM and more.
+
+
+ Visit{" "}
+
+ opencode.ai/zen
+ {" "}
+ to collect your API key.
+
+
+
+
+
+ Enter your {provider().name} API key to connect your account and use {provider().name} models
+ in OpenCode.
+
+
+
+
+
+ )
+ })}
+
+
+
+
+ {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: props.provider,
+ method: store.methodIndex,
+ code,
+ })
+ if (!error) {
+ await complete()
+ 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.
+
+
+
+ )
+ })}
+
+
+ {iife(() => {
+ const code = createMemo(() => {
+ const instructions = store.authorization?.instructions
+ if (instructions?.includes(":")) {
+ return instructions?.split(":")[1]?.trim()
+ }
+ return instructions
+ })
+
+ onMount(async () => {
+ const result = await globalSDK.client.provider.oauth.callback({
+ providerID: props.provider,
+ method: store.methodIndex,
+ })
+ if (result.error) {
+ // TODO: show error
+ dialog.clear()
+ return
+ }
+ await complete()
+ })
+
+ return (
+
+
+ Visit this link and enter the code below to
+ connect your account and use {provider().name} models in OpenCode.
+
+
+
+
+ Waiting for authorization...
+
+
+ )
+ })}
+
+
+
+
+
+
+
+ )
+}
diff --git a/packages/desktop/src/components/dialog-connect.tsx b/packages/desktop/src/components/dialog-connect.tsx
deleted file mode 100644
index f61221f72..000000000
--- a/packages/desktop/src/components/dialog-connect.tsx
+++ /dev/null
@@ -1,383 +0,0 @@
-import { createMemo, Match, onCleanup, onMount, Switch } from "solid-js"
-import { createStore, produce } from "solid-js/store"
-import { useDialog } from "@opencode-ai/ui/context/dialog"
-import { useGlobalSync } from "@/context/global-sync"
-import { useGlobalSDK } from "@/context/global-sdk"
-import { usePlatform } from "@/context/platform"
-import { ProviderAuthAuthorization } from "@opencode-ai/sdk/v2/client"
-import { Dialog } from "@opencode-ai/ui/dialog"
-import { List, ListRef } from "@opencode-ai/ui/list"
-import { Button } from "@opencode-ai/ui/button"
-import { IconButton } from "@opencode-ai/ui/icon-button"
-import { TextField } from "@opencode-ai/ui/text-field"
-import { Spinner } from "@opencode-ai/ui/spinner"
-import { Icon } from "@opencode-ai/ui/icon"
-import { showToast } from "@opencode-ai/ui/toast"
-import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
-import { IconName } from "@opencode-ai/ui/icons/provider"
-import { iife } from "@opencode-ai/util/iife"
-import { Link } from "@/components/link"
-import { DialogSelectProvider } from "./dialog-select-provider"
-import { DialogSelectModel } from "./dialog-select-model"
-
-export function DialogConnect(props: { provider: string }) {
- const dialog = useDialog()
- const globalSync = useGlobalSync()
- const globalSDK = useGlobalSDK()
- const platform = usePlatform()
- const provider = createMemo(() => globalSync.data.provider.all.find((x) => x.id === props.provider)!)
- const methods = createMemo(
- () =>
- globalSync.data.provider_auth[props.provider] ?? [
- {
- type: "api",
- label: "API key",
- },
- ],
- )
- const [store, setStore] = createStore({
- methodIndex: undefined as undefined | number,
- authorization: undefined as undefined | ProviderAuthAuthorization,
- state: "pending" as undefined | "pending" | "complete" | "error",
- error: undefined as string | undefined,
- })
-
- const method = createMemo(() => (store.methodIndex !== undefined ? methods().at(store.methodIndex!) : undefined))
-
- async function selectMethod(index: number) {
- const method = methods()[index]
- setStore(
- produce((draft) => {
- draft.methodIndex = index
- 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: props.provider,
- method: index,
- },
- { throwOnError: true },
- )
- .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))
- })
- }
- }
-
- let listRef: ListRef | undefined
- function handleKey(e: KeyboardEvent) {
- if (e.key === "Enter" && e.target instanceof HTMLInputElement) {
- return
- }
- if (e.key === "Escape") return
- listRef?.onKeyDown(e)
- }
-
- onMount(() => {
- if (methods().length === 1) {
- selectMethod(0)
- }
- document.addEventListener("keydown", handleKey)
- onCleanup(() => {
- document.removeEventListener("keydown", handleKey)
- })
- })
-
- async function complete() {
- 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.`,
- })
- dialog.replace(() => )
- }, 1000)
- }
-
- function goBack() {
- if (methods().length === 1) {
- dialog.replace(() => )
- return
- }
- if (store.authorization) {
- setStore("authorization", undefined)
- setStore("methodIndex", undefined)
- return
- }
- if (store.methodIndex) {
- setStore("methodIndex", undefined)
- return
- }
- dialog.replace(() => )
- }
-
- return (
- }>
-
-
-
-
-
-
- Login with Claude Pro/Max
-
- Connect {provider().name}
-
-
-
-
-
-
- Select login method for {provider().name}.
-
-
(listRef = ref)}
- items={methods}
- key={(m) => m?.label}
- onSelect={async (method, index) => {
- if (!method) return
- selectMethod(index)
- }}
- >
- {(i) => (
-
- )}
-
-
-
-
-
-
-
- Authorization in progress...
-
-
-
-
-
-
-
- Authorization failed: {store.error}
-
-
-
-
- {iife(() => {
- const [formStore, setFormStore] = createStore({
- value: "",
- error: undefined as string | undefined,
- })
-
- 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
-
- if (!apiKey?.trim()) {
- setFormStore("error", "API key is required")
- return
- }
-
- setFormStore("error", undefined)
- await globalSDK.client.auth.set({
- providerID: props.provider,
- auth: {
- type: "api",
- key: apiKey,
- },
- })
- await complete()
- }
-
- return (
-
-
-
-
-
- OpenCode Zen gives you access to a curated set of reliable optimized models for coding
- agents.
-
-
- With a single API key you'll get access to models such as Claude, GPT, Gemini, GLM and more.
-
-
- Visit{" "}
-
- opencode.ai/zen
- {" "}
- to collect your API key.
-
-
-
-
-
- Enter your {provider().name} API key to connect your account and use {provider().name} models
- in OpenCode.
-
-
-
-
-
- )
- })}
-
-
-
-
- {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: props.provider,
- method: store.methodIndex,
- code,
- })
- if (!error) {
- await complete()
- 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.
-
-
-
- )
- })}
-
-
- {iife(() => {
- const code = createMemo(() => {
- const instructions = store.authorization?.instructions
- if (instructions?.includes(":")) {
- return instructions?.split(":")[1]?.trim()
- }
- return instructions
- })
-
- onMount(async () => {
- const result = await globalSDK.client.provider.oauth.callback({
- providerID: props.provider,
- method: store.methodIndex,
- })
- if (result.error) {
- // TODO: show error
- dialog.clear()
- return
- }
- await complete()
- })
-
- return (
-
-
- Visit this link and enter the code below to
- connect your account and use {provider().name} models in OpenCode.
-
-
-
-
- Waiting for authorization...
-
-
- )
- })}
-
-
-
-
-
-
-
- )
-}
diff --git a/packages/desktop/src/components/dialog-select-model-unpaid.tsx b/packages/desktop/src/components/dialog-select-model-unpaid.tsx
index 1c9e9cc75..7cdb24915 100644
--- a/packages/desktop/src/components/dialog-select-model-unpaid.tsx
+++ b/packages/desktop/src/components/dialog-select-model-unpaid.tsx
@@ -9,7 +9,7 @@ import { List, ListRef } from "@opencode-ai/ui/list"
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
import { IconName } from "@opencode-ai/ui/icons/provider"
import { DialogSelectProvider } from "./dialog-select-provider"
-import { DialogConnect } from "./dialog-connect"
+import { DialogConnectProvider } from "./dialog-connect-provider"
export const DialogSelectModelUnpaid: Component = () => {
const local = useLocal()
@@ -75,7 +75,7 @@ export const DialogSelectModelUnpaid: Component = () => {
}}
onSelect={(x) => {
if (!x) return
- dialog.replace(() => )
+ dialog.replace(() => )
}}
>
{(i) => (
diff --git a/packages/desktop/src/components/dialog-select-provider.tsx b/packages/desktop/src/components/dialog-select-provider.tsx
index 292d5fccb..8da10b1d5 100644
--- a/packages/desktop/src/components/dialog-select-provider.tsx
+++ b/packages/desktop/src/components/dialog-select-provider.tsx
@@ -6,7 +6,7 @@ import { List } from "@opencode-ai/ui/list"
import { Tag } from "@opencode-ai/ui/tag"
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
import { IconName } from "@opencode-ai/ui/icons/provider"
-import { DialogConnect } from "./dialog-connect"
+import { DialogConnectProvider } from "./dialog-connect-provider"
export const DialogSelectProvider: Component = () => {
const dialog = useDialog()
@@ -34,7 +34,7 @@ export const DialogSelectProvider: Component = () => {
}}
onSelect={(x) => {
if (!x) return
- dialog.replace(() => )
+ dialog.replace(() => )
}}
>
{(i) => (
--
cgit v1.2.3