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) => ( +
+
+ + {i.label} +
+ )} + +
+ + +
+
+ + 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) => ( -
-
- - {i.label} -
- )} - -
- - -
-
- - 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