From 3bb546c94d6bb295bfeafdafbb9d34b7cc462560 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Wed, 10 Dec 2025 21:16:50 -0600 Subject: wip(desktop): progress --- packages/desktop/src/components/prompt-input.tsx | 97 ++++++----- packages/desktop/src/context/global-sync.tsx | 44 +++-- packages/desktop/src/context/layout.tsx | 22 ++- packages/desktop/src/context/sync.tsx | 23 ++- packages/desktop/src/pages/layout.tsx | 213 ++++++++++++++++++++++- 5 files changed, 331 insertions(+), 68 deletions(-) (limited to 'packages/desktop/src') diff --git a/packages/desktop/src/components/prompt-input.tsx b/packages/desktop/src/components/prompt-input.tsx index 22f2c1642..41af8644b 100644 --- a/packages/desktop/src/components/prompt-input.tsx +++ b/packages/desktop/src/components/prompt-input.tsx @@ -579,54 +579,61 @@ export const PromptInput: Component = (props) => {
-
+
Add more models from popular providers
- x?.id} - items={providers().popular()} - activeIcon="plus-small" - sortBy={(a, b) => { - if (popularProviders.includes(a.id) && popularProviders.includes(b.id)) - return popularProviders.indexOf(a.id) - popularProviders.indexOf(b.id) - return a.name.localeCompare(b.name) - }} - onSelect={(x) => { - layout.dialog.close("model") - }} - > - {(i) => ( -
- - {i.name} - - Recommended - - -
- Connect with Claude Pro/Max or API key -
-
-
- )} -
- +
+ x?.id} + items={providers().popular()} + activeIcon="plus-small" + sortBy={(a, b) => { + if (popularProviders.includes(a.id) && popularProviders.includes(b.id)) + return popularProviders.indexOf(a.id) - popularProviders.indexOf(b.id) + return a.name.localeCompare(b.name) + }} + onSelect={(x) => { + if (!x) return + layout.dialog.connect(x.id) + }} + > + {(i) => ( +
+ + {i.name} + + Recommended + + +
+ Connect with Claude Pro/Max or API key +
+
+
+ )} +
+ +
diff --git a/packages/desktop/src/context/global-sync.tsx b/packages/desktop/src/context/global-sync.tsx index 3a6062fb8..09dfb3a83 100644 --- a/packages/desktop/src/context/global-sync.tsx +++ b/packages/desktop/src/context/global-sync.tsx @@ -12,11 +12,13 @@ import type { Todo, SessionStatus, ProviderListResponse, + ProviderAuthResponse, } from "@opencode-ai/sdk/v2" import { createStore, produce, reconcile } from "solid-js/store" import { Binary } from "@opencode-ai/util/binary" import { createSimpleContext } from "@opencode-ai/ui/context" import { useGlobalSDK } from "./global-sdk" +import { onMount } from "solid-js" type State = { ready: boolean @@ -54,11 +56,13 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple ready: boolean project: Project[] provider: ProviderListResponse + provider_auth: ProviderAuthResponse children: Record }>({ ready: false, project: [], provider: { all: [], connected: [], default: {} }, + provider_auth: {}, children: {}, }) @@ -113,6 +117,10 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple const [store, setStore] = child(directory) switch (event.type) { + // case "server.instance.disposed": { + // bootstrap() + // break + // } case "session.updated": { const result = Binary.search(store.session, event.properties.info.id, (s) => s.id) if (result.found) { @@ -181,19 +189,28 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple } }) - Promise.all([ - sdk.client.project.list().then(async (x) => { - setGlobalStore( - "project", - x - .data!.filter((p) => !p.worktree.includes("opencode-test") && p.vcs) - .sort((a, b) => a.id.localeCompare(b.id)), - ) - }), - sdk.client.provider.list().then((x) => { - setGlobalStore("provider", x.data ?? {}) - }), - ]).then(() => setGlobalStore("ready", true)) + async function bootstrap() { + return Promise.all([ + sdk.client.project.list().then(async (x) => { + setGlobalStore( + "project", + x + .data!.filter((p) => !p.worktree.includes("opencode-test") && p.vcs) + .sort((a, b) => a.id.localeCompare(b.id)), + ) + }), + sdk.client.provider.list().then((x) => { + setGlobalStore("provider", x.data ?? {}) + }), + sdk.client.provider.auth().then((x) => { + setGlobalStore("provider_auth", x.data ?? {}) + }), + ]).then(() => setGlobalStore("ready", true)) + } + + onMount(() => { + bootstrap() + }) return { data: globalStore, @@ -201,6 +218,7 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple return globalStore.ready }, child, + bootstrap, } }, }) diff --git a/packages/desktop/src/context/layout.tsx b/packages/desktop/src/context/layout.tsx index 5530ad28f..d00e101b8 100644 --- a/packages/desktop/src/context/layout.tsx +++ b/packages/desktop/src/context/layout.tsx @@ -1,5 +1,5 @@ import { createStore } from "solid-js/store" -import { createMemo, onMount } from "solid-js" +import { batch, createMemo, onMount } from "solid-js" import { createSimpleContext } from "@opencode-ai/ui/context" import { makePersisted } from "@solid-primitives/storage" import { useGlobalSync } from "./global-sync" @@ -19,6 +19,8 @@ const PASTEL_COLORS = [ "#C1E1C1", // pastel mint ] +type Dialog = "provider" | "model" | "connect" + export const { use: useLayout, provider: LayoutProvider } = createSimpleContext({ name: "Layout", init: () => { @@ -44,8 +46,11 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( }, ) const [ephemeral, setEphemeral] = createStore({ + connect: { + provider: undefined as undefined | string, + }, dialog: { - open: undefined as undefined | "provider" | "model", + open: undefined as undefined | Dialog, }, }) const usedColors = new Set() @@ -169,14 +174,23 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( }, dialog: { opened: createMemo(() => ephemeral.dialog?.open), - open(dialog: "provider" | "model") { + open(dialog: Dialog) { setEphemeral("dialog", "open", dialog) }, - close(dialog: "provider" | "model") { + close(dialog: Dialog) { if (ephemeral.dialog?.open === dialog) { setEphemeral("dialog", "open", undefined) } }, + connect(provider: string) { + batch(() => { + setEphemeral("dialog", "open", "connect") + setEphemeral("connect", "provider", provider) + }) + }, + }, + connect: { + provider: createMemo(() => ephemeral.connect.provider), }, } }, diff --git a/packages/desktop/src/context/sync.tsx b/packages/desktop/src/context/sync.tsx index 1a11cd599..d64fcce2a 100644 --- a/packages/desktop/src/context/sync.tsx +++ b/packages/desktop/src/context/sync.tsx @@ -1,5 +1,5 @@ import { produce } from "solid-js/store" -import { createMemo } from "solid-js" +import { createMemo, onMount } from "solid-js" import { Binary } from "@opencode-ai/util/binary" import { createSimpleContext } from "@opencode-ai/ui/context" import { useGlobalSync } from "./global-sync" @@ -31,7 +31,24 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ node: () => sdk.client.file.list({ path: "/" }).then((x) => setStore("node", x.data!)), } - Promise.all(Object.values(load).map((p) => p())).then(() => setStore("ready", true)) + async function bootstrap() { + return Promise.all(Object.values(load).map((p) => p())).then(() => setStore("ready", true)) + } + + onMount(() => { + bootstrap() + }) + + sdk.event.listen((e) => { + if (e.name !== sdk.directory) return + const event = e.details + switch (event.type) { + case "server.instance.disposed": { + bootstrap() + break + } + } + }) const absolute = (path: string) => (store.path.directory + "/" + path).replace("//", "/") @@ -82,7 +99,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ }, more: createMemo(() => store.session.length >= store.limit), }, - load, + bootstrap, absolute, get directory() { return store.path.directory diff --git a/packages/desktop/src/pages/layout.tsx b/packages/desktop/src/pages/layout.tsx index 10d4cbfda..0ba6c0a2d 100644 --- a/packages/desktop/src/pages/layout.tsx +++ b/packages/desktop/src/pages/layout.tsx @@ -17,7 +17,7 @@ import { DiffChanges } from "@opencode-ai/ui/diff-changes" import { getFilename } from "@opencode-ai/util/path" import { Select } from "@opencode-ai/ui/select" import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu" -import { Session, Project } from "@opencode-ai/sdk/v2/client" +import { Session, Project, ProviderAuthMethod } from "@opencode-ai/sdk/v2/client" import { usePlatform } from "@/context/platform" import { createStore } from "solid-js/store" import { @@ -34,6 +34,11 @@ import { SelectDialog } from "@opencode-ai/ui/select-dialog" import { Tag } from "@opencode-ai/ui/tag" 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 { List, ListRef } from "@opencode-ai/ui/list" +import { Input } from "@opencode-ai/ui/input" +import { useGlobalSDK } from "@/context/global-sdk" export default function Layout(props: ParentProps) { const [store, setStore] = createStore({ @@ -42,6 +47,7 @@ export default function Layout(props: ParentProps) { }) const params = useParams() + const globalSDK = useGlobalSDK() const globalSync = useGlobalSync() const layout = useLayout() const platform = usePlatform() @@ -562,7 +568,6 @@ export default function Layout(props: ParentProps) { activeIcon="plus-small" key={(x) => x?.id} items={providers().all} - // current={local.model.current()} filterKeys={["id", "name"]} groupBy={(x) => (popularProviders.includes(x.id) ? "Popular" : "Other")} sortBy={(a, b) => { @@ -575,7 +580,10 @@ export default function Layout(props: ParentProps) { if (b.category === "Popular" && a.category !== "Popular") return 1 return 0 }} - // onSelect={(x) => } + onSelect={(x) => { + if (!x) return + layout.dialog.connect(x.id) + }} onOpenChange={(open) => { if (open) { layout.dialog.open("provider") @@ -607,6 +615,205 @@ export default function Layout(props: ParentProps) { )} + + {iife(() => { + const [store, setStore] = createStore({ + method: undefined as undefined | ProviderAuthMethod, + }) + const providerID = layout.connect.provider()! + const provider = globalSync.data.provider.all.find((x) => x.id === providerID)! + const methods = globalSync.data.provider_auth[providerID] ?? [ + { + type: "api", + label: "API key", + }, + ] + if (methods.length === 1) { + setStore("method", methods[0]) + } + + let listRef: ListRef | undefined + const handleKey = (e: KeyboardEvent) => { + if (e.key === "Escape") return + listRef?.onKeyDown(e) + } + + return ( + { + if (open) { + layout.dialog.open("connect") + } else { + layout.dialog.close("connect") + } + }} + > + + + { + if (store.method && methods.length > 1) { + setStore("method", undefined) + return + } + layout.dialog.open("provider") + }} + /> + + + + +
+
+ +
Connect {provider.name}
+
+ +
Select login method for {provider.name}.
+
+ + (listRef = ref)} + items={methods} + key={(m) => m?.label} + onSelect={(method) => { + if (!method) return + setStore("method", method) + + if (method.type === "oauth") { + // const result = await sdk.client.provider.oauth.authorize({ + // providerID: provider.id, + // method: index, + // }) + // if (result.data?.method === "code") { + // dialog.replace(() => ( + // + // )) + // } + // if (result.data?.method === "auto") { + // dialog.replace(() => ( + // + // )) + // } + } + if (method.type === "api") { + // return dialog.replace(() => ) + } + }} + > + {(i) => ( +
+ {/* TODO: add checkmark thing */} + {i.label} +
+ )} +
+
+
+ + {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) + globalSDK.client.auth.set({ + providerID, + auth: { + type: "api", + key: apiKey, + }, + }) + await globalSDK.client.instance.dispose() + } + + 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{" "} + {" "} + to collect your API key. +
+
+
+ +
+ Enter your {provider.name} API key to connect your account and use {provider.name}{" "} + models in OpenCode. +
+
+
+
+ + +
+
+ ) + })} +
+
+
+
+ ) + })} +
) -- cgit v1.2.3