-
+
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..2a24a845c 100644
--- a/packages/desktop/src/context/global-sync.tsx
+++ b/packages/desktop/src/context/global-sync.tsx
@@ -1,22 +1,25 @@
-import type {
- Message,
- Agent,
- Session,
- Part,
- Config,
- Path,
- File,
- FileNode,
- Project,
- FileDiff,
- Todo,
- SessionStatus,
- ProviderListResponse,
-} from "@opencode-ai/sdk/v2"
+import {
+ type Message,
+ type Agent,
+ type Session,
+ type Part,
+ type Config,
+ type Path,
+ type File,
+ type FileNode,
+ type Project,
+ type FileDiff,
+ type Todo,
+ type SessionStatus,
+ type ProviderListResponse,
+ type ProviderAuthResponse,
+ createOpencodeClient,
+} from "@opencode-ai/sdk/v2/client"
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
@@ -49,19 +52,48 @@ type State = {
export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimpleContext({
name: "GlobalSync",
init: () => {
- const sdk = useGlobalSDK()
+ const globalSDK = useGlobalSDK()
const [globalStore, setGlobalStore] = createStore<{
ready: boolean
project: Project[]
provider: ProviderListResponse
+ provider_auth: ProviderAuthResponse
children: Record
}>({
ready: false,
project: [],
provider: { all: [], connected: [], default: {} },
+ provider_auth: {},
children: {},
})
+ async function bootstrapInstance(directory: string) {
+ const [store, setStore] = child(directory)
+ const sdk = createOpencodeClient({
+ baseUrl: globalSDK.url,
+ directory,
+ })
+ const load = {
+ project: () => sdk.project.current().then((x) => setStore("project", x.data!.id)),
+ provider: () => sdk.provider.list().then((x) => setStore("provider", x.data!)),
+ path: () => sdk.path.get().then((x) => setStore("path", x.data!)),
+ agent: () => sdk.app.agents().then((x) => setStore("agent", x.data ?? [])),
+ session: () =>
+ sdk.session.list().then((x) => {
+ const sessions = (x.data ?? [])
+ .slice()
+ .sort((a, b) => a.id.localeCompare(b.id))
+ .slice(0, store.limit)
+ setStore("session", sessions)
+ }),
+ status: () => sdk.session.status().then((x) => setStore("session_status", x.data!)),
+ config: () => sdk.config.get().then((x) => setStore("config", x.data!)),
+ changes: () => sdk.file.status().then((x) => setStore("changes", x.data!)),
+ node: () => sdk.file.list({ path: "/" }).then((x) => setStore("node", x.data!)),
+ }
+ await Promise.all(Object.values(load).map((p) => p())).then(() => setStore("ready", true))
+ }
+
const children: Record>> = {}
function child(directory: string) {
if (!children[directory]) {
@@ -83,16 +115,21 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple
changes: [],
})
children[directory] = createStore(globalStore.children[directory])
+ bootstrapInstance(directory)
}
return children[directory]
}
- sdk.event.listen((e) => {
+ globalSDK.event.listen((e) => {
const directory = e.name
const event = e.details
if (directory === "global") {
switch (event.type) {
+ case "global.disposed": {
+ bootstrap()
+ break
+ }
case "project.updated": {
const result = Binary.search(globalStore.project, event.properties.id, (s) => s.id)
if (result.found) {
@@ -113,6 +150,10 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple
const [store, setStore] = child(directory)
switch (event.type) {
+ case "server.instance.disposed": {
+ bootstrapInstance(directory)
+ break
+ }
case "session.updated": {
const result = Binary.search(store.session, event.properties.info.id, (s) => s.id)
if (result.found) {
@@ -181,19 +222,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([
+ globalSDK.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)),
+ )
+ }),
+ globalSDK.client.provider.list().then((x) => {
+ setGlobalStore("provider", x.data ?? {})
+ }),
+ globalSDK.client.provider.auth().then((x) => {
+ setGlobalStore("provider_auth", x.data ?? {})
+ }),
+ ]).then(() => setGlobalStore("ready", true))
+ }
+
+ onMount(() => {
+ bootstrap()
+ })
return {
data: globalStore,
@@ -201,6 +251,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..24ba55a53 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 { createStore, produce } from "solid-js/store"
+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,13 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
},
)
const [ephemeral, setEphemeral] = createStore({
+ connect: {
+ provider: undefined as undefined | string,
+ state: undefined as undefined | "pending" | "complete" | "error",
+ error: undefined as undefined | string,
+ },
dialog: {
- open: undefined as undefined | "provider" | "model",
+ open: undefined as undefined | Dialog,
},
})
const usedColors = new Set()
@@ -169,14 +176,47 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
},
dialog: {
opened: createMemo(() => ephemeral.dialog?.open),
- open(dialog: "provider" | "model") {
+ open(dialog: Dialog) {
setEphemeral("dialog", "open", dialog)
+ if (dialog !== "connect") {
+ setEphemeral("connect", {})
+ }
},
- close(dialog: "provider" | "model") {
+ close(dialog: Dialog) {
if (ephemeral.dialog?.open === dialog) {
setEphemeral("dialog", "open", undefined)
+ setEphemeral("connect", {})
}
},
+ connect(provider: string) {
+ batch(() => {
+ setEphemeral("dialog", "open", "connect")
+ setEphemeral("connect", { provider, state: "pending" })
+ })
+ },
+ },
+ connect: {
+ provider: createMemo(() => ephemeral.connect.provider),
+ state: createMemo(() => ephemeral.connect.state),
+ complete() {
+ setEphemeral(
+ produce((state) => {
+ state.dialog.open = "model"
+ state.connect.state = "complete"
+ }),
+ )
+ },
+ error(message: string) {
+ setEphemeral(
+ produce((state) => {
+ state.connect.state = "error"
+ state.connect.error = message
+ }),
+ )
+ },
+ clear() {
+ setEphemeral("connect", {})
+ },
},
}
},
diff --git a/packages/desktop/src/context/local.tsx b/packages/desktop/src/context/local.tsx
index 74d3ac364..d8dfa732a 100644
--- a/packages/desktop/src/context/local.tsx
+++ b/packages/desktop/src/context/local.tsx
@@ -6,6 +6,7 @@ import { createSimpleContext } from "@opencode-ai/ui/context"
import { useSDK } from "./sdk"
import { useSync } from "./sync"
import { base64Encode } from "@opencode-ai/util/encode"
+import { useProviders } from "@/hooks/use-providers"
export type LocalFile = FileNode &
Partial<{
@@ -37,10 +38,17 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
init: () => {
const sdk = useSDK()
const sync = useSync()
+ const providers = useProviders()
function isModelValid(model: ModelKey) {
- const provider = sync.data.provider?.all.find((x) => x.id === model.providerID)
- return !!provider?.models[model.modelID] && sync.data.provider?.connected.includes(model.providerID)
+ const provider = providers().all.find((x) => x.id === model.providerID)
+ return (
+ !!provider?.models[model.modelID] &&
+ providers()
+ .connected()
+ .map((p) => p.id)
+ .includes(model.providerID)
+ )
}
function getFirstValidModel(...modelFns: (() => ModelKey | undefined)[]) {
@@ -115,8 +123,8 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
})
const list = createMemo(() =>
- sync.data.provider.all
- .filter((p) => sync.data.provider.connected.includes(p.id))
+ providers()
+ .connected()
.flatMap((p) =>
Object.values(p.models).map((m) => ({
...m,
@@ -145,11 +153,11 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
}
}
- for (const p of sync.data.provider.connected) {
- if (p in sync.data.provider.default) {
+ for (const p of providers().connected()) {
+ if (p.id in providers().default) {
return {
- providerID: p,
- modelID: sync.data.provider.default[p],
+ providerID: p.id,
+ modelID: providers().default[p.id],
}
}
}
diff --git a/packages/desktop/src/context/sync.tsx b/packages/desktop/src/context/sync.tsx
index 1a11cd599..85758c5b6 100644
--- a/packages/desktop/src/context/sync.tsx
+++ b/packages/desktop/src/context/sync.tsx
@@ -11,28 +11,6 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
const globalSync = useGlobalSync()
const sdk = useSDK()
const [store, setStore] = globalSync.child(sdk.directory)
-
- const load = {
- project: () => sdk.client.project.current().then((x) => setStore("project", x.data!.id)),
- provider: () => sdk.client.provider.list().then((x) => setStore("provider", x.data!)),
- path: () => sdk.client.path.get().then((x) => setStore("path", x.data!)),
- agent: () => sdk.client.app.agents().then((x) => setStore("agent", x.data ?? [])),
- session: () =>
- sdk.client.session.list().then((x) => {
- const sessions = (x.data ?? [])
- .slice()
- .sort((a, b) => a.id.localeCompare(b.id))
- .slice(0, store.limit)
- setStore("session", sessions)
- }),
- status: () => sdk.client.session.status().then((x) => setStore("session_status", x.data!)),
- config: () => sdk.client.config.get().then((x) => setStore("config", x.data!)),
- changes: () => sdk.client.file.status().then((x) => setStore("changes", x.data!)),
- node: () => sdk.client.file.list({ path: "/" }).then((x) => setStore("node", x.data!)),
- }
-
- Promise.all(Object.values(load).map((p) => p())).then(() => setStore("ready", true))
-
const absolute = (path: string) => (store.path.directory + "/" + path).replace("//", "/")
return {
@@ -78,11 +56,16 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
},
fetch: async (count = 10) => {
setStore("limit", (x) => x + count)
- await load.session()
+ await sdk.client.session.list().then((x) => {
+ const sessions = (x.data ?? [])
+ .slice()
+ .sort((a, b) => a.id.localeCompare(b.id))
+ .slice(0, store.limit)
+ setStore("session", sessions)
+ })
},
more: createMemo(() => store.session.length >= store.limit),
},
- load,
absolute,
get directory() {
return store.path.directory
diff --git a/packages/desktop/src/hooks/use-providers.ts b/packages/desktop/src/hooks/use-providers.ts
index c3fcc7898..04ef855d4 100644
--- a/packages/desktop/src/hooks/use-providers.ts
+++ b/packages/desktop/src/hooks/use-providers.ts
@@ -16,16 +16,14 @@ export function useProviders() {
}
return globalSync.data.provider
})
- const connected = createMemo(() =>
- providers().all.filter(
- (p) => providers().connected.includes(p.id) && Object.values(p.models).find((m) => m.cost?.input),
- ),
- )
+ const connected = createMemo(() => providers().all.filter((p) => providers().connected.includes(p.id)))
+ const paid = createMemo(() => connected().filter((p) => Object.values(p.models).find((m) => m.cost?.input)))
const popular = createMemo(() => providers().all.filter((p) => popularProviders.includes(p.id)))
return createMemo(() => ({
all: providers().all,
default: providers().default,
popular,
connected,
+ paid,
}))
}
diff --git a/packages/desktop/src/pages/layout.tsx b/packages/desktop/src/pages/layout.tsx
index 10d4cbfda..39917c420 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()
@@ -434,7 +440,7 @@ export default function Layout(props: ParentProps) {