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/context/layout.tsx | 22 ++++++++++++++++++----
 1 file changed, 18 insertions(+), 4 deletions(-)

(limited to 'packages/desktop/src/context/layout.tsx')

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<string>()
@@ -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),
       },
     }
   },
-- 
cgit v1.2.3


From 1980113ee4305844803f866aef05d742f7cffd47 Mon Sep 17 00:00:00 2001
From: Adam <2363879+adamdotdevin@users.noreply.github.com>
Date: Thu, 11 Dec 2025 03:13:08 -0600
Subject: wip(desktop): progress

---
 packages/desktop/src/context/global-sync.tsx | 75 ++++++++++++++++++++--------
 packages/desktop/src/context/layout.tsx      | 32 +++++++++++-
 packages/desktop/src/context/sync.tsx        | 50 +++----------------
 packages/desktop/src/pages/layout.tsx        |  5 +-
 packages/ui/src/components/list.tsx          | 11 ++--
 5 files changed, 101 insertions(+), 72 deletions(-)

(limited to 'packages/desktop/src/context/layout.tsx')

diff --git a/packages/desktop/src/context/global-sync.tsx b/packages/desktop/src/context/global-sync.tsx
index 770275a5c..2a24a845c 100644
--- a/packages/desktop/src/context/global-sync.tsx
+++ b/packages/desktop/src/context/global-sync.tsx
@@ -1,19 +1,20 @@
-import type {
-  Message,
-  Agent,
-  Session,
-  Part,
-  Config,
-  Path,
-  File,
-  FileNode,
-  Project,
-  FileDiff,
-  Todo,
-  SessionStatus,
-  ProviderListResponse,
-  ProviderAuthResponse,
-} 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"
@@ -51,7 +52,7 @@ 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[]
@@ -66,6 +67,33 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple
       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<string, ReturnType<typeof createStore<State>>> = {}
     function child(directory: string) {
       if (!children[directory]) {
@@ -87,11 +115,12 @@ 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
 
@@ -121,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) {
@@ -191,7 +224,7 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple
 
     async function bootstrap() {
       return Promise.all([
-        sdk.client.project.list().then(async (x) => {
+        globalSDK.client.project.list().then(async (x) => {
           setGlobalStore(
             "project",
             x
@@ -199,10 +232,10 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple
               .sort((a, b) => a.id.localeCompare(b.id)),
           )
         }),
-        sdk.client.provider.list().then((x) => {
+        globalSDK.client.provider.list().then((x) => {
           setGlobalStore("provider", x.data ?? {})
         }),
-        sdk.client.provider.auth().then((x) => {
+        globalSDK.client.provider.auth().then((x) => {
           setGlobalStore("provider_auth", x.data ?? {})
         }),
       ]).then(() => setGlobalStore("ready", true))
diff --git a/packages/desktop/src/context/layout.tsx b/packages/desktop/src/context/layout.tsx
index d00e101b8..d4a8875f7 100644
--- a/packages/desktop/src/context/layout.tsx
+++ b/packages/desktop/src/context/layout.tsx
@@ -1,4 +1,4 @@
-import { createStore } from "solid-js/store"
+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"
@@ -48,6 +48,8 @@ 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 | Dialog,
@@ -176,21 +178,47 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
         opened: createMemo(() => ephemeral.dialog?.open),
         open(dialog: Dialog) {
           setEphemeral("dialog", "open", dialog)
+          if (dialog !== "connect") {
+            setEphemeral("connect", {})
+          }
         },
         close(dialog: Dialog) {
           if (ephemeral.dialog?.open === dialog) {
             setEphemeral("dialog", "open", undefined)
+            if (dialog === "connect") {
+              setEphemeral("connect", {})
+            }
           }
         },
         connect(provider: string) {
           batch(() => {
             setEphemeral("dialog", "open", "connect")
-            setEphemeral("connect", "provider", provider)
+            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/sync.tsx b/packages/desktop/src/context/sync.tsx
index 9c3abd731..85758c5b6 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, onMount } from "solid-js"
+import { createMemo } from "solid-js"
 import { Binary } from "@opencode-ai/util/binary"
 import { createSimpleContext } from "@opencode-ai/ui/context"
 import { useGlobalSync } from "./global-sync"
@@ -11,45 +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!)),
-    }
-
-    async function bootstrap() {
-      return Promise.all(Object.values(load).map((p) => p())).then(() => setStore("ready", true))
-    }
-
-    onMount(() => {
-      bootstrap()
-    })
-
-    sdk.event.listen((e) => {
-      const event = e.details
-      console.log(event)
-      switch (event.type) {
-        case "server.instance.disposed": {
-          bootstrap()
-          break
-        }
-      }
-    })
-
     const absolute = (path: string) => (store.path.directory + "/" + path).replace("//", "/")
 
     return {
@@ -95,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),
       },
-      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 0bc6e9e09..5f0b26f6b 100644
--- a/packages/desktop/src/pages/layout.tsx
+++ b/packages/desktop/src/pages/layout.tsx
@@ -440,7 +440,7 @@ export default function Layout(props: ParentProps) {
               <Button
                 variant="ghost"
                 size="large"
-                class="group/sidebar-toggle shrink-0 w-full text-left justify-start rounded-lg"
+                class="group/sidebar-toggle shrink-0 w-full text-left justify-start rounded-lg px-2"
                 onClick={layout.sidebar.toggle}
               >
                 <div class="relative -ml-px flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
@@ -615,7 +615,7 @@ export default function Layout(props: ParentProps) {
             )}
           </SelectDialog>
         </Show>
-        <Show when={layout.dialog?.opened() === "connect"}>
+        <Show when={layout.dialog.opened() === "connect"}>
           {iife(() => {
             const [store, setStore] = createStore({
               method: undefined as undefined | ProviderAuthMethod,
@@ -753,6 +753,7 @@ export default function Layout(props: ParentProps) {
                             },
                           })
                           await globalSDK.client.global.dispose()
+                          layout.connect.complete()
                         }
 
                         return (
diff --git a/packages/ui/src/components/list.tsx b/packages/ui/src/components/list.tsx
index a7f2db9ef..013767e60 100644
--- a/packages/ui/src/components/list.tsx
+++ b/packages/ui/src/components/list.tsx
@@ -65,8 +65,8 @@ export function List<T>(props: ListProps<T> & { ref?: (ref: ListRef) => void })
     element?.scrollIntoView({ block: "nearest", behavior: "smooth" })
   })
 
-  const handleSelect = (item: T | undefined) => {
-    props.onSelect?.(item)
+  const handleSelect = (item: T | undefined, index: number) => {
+    props.onSelect?.(item, index)
   }
 
   const handleKey = (e: KeyboardEvent) => {
@@ -75,11 +75,12 @@ export function List<T>(props: ListProps<T> & { ref?: (ref: ListRef) => void })
 
     const all = flat()
     const selected = all.find((x) => props.key(x) === active())
+    const index = selected ? all.indexOf(selected) : -1
     props.onKeyEvent?.(e, selected)
 
     if (e.key === "Enter") {
       e.preventDefault()
-      if (selected) handleSelect(selected)
+      if (selected) handleSelect(selected, index)
     } else {
       onKeyDown(e)
     }
@@ -110,13 +111,13 @@ export function List<T>(props: ListProps<T> & { ref?: (ref: ListRef) => void })
               </Show>
               <div data-slot="list-items">
                 <For each={group.items}>
-                  {(item) => (
+                  {(item, i) => (
                     <button
                       data-slot="list-item"
                       data-key={props.key(item)}
                       data-active={props.key(item) === active()}
                       data-selected={item === props.current}
-                      onClick={() => handleSelect(item)}
+                      onClick={() => handleSelect(item, i())}
                       onMouseMove={() => {
                         setStore("mouseActive", true)
                         setActive(props.key(item))
-- 
cgit v1.2.3


From f9f78122d0f2e1d750d5c6a35011e1b092c6957c Mon Sep 17 00:00:00 2001
From: Adam <2363879+adamdotdevin@users.noreply.github.com>
Date: Thu, 11 Dec 2025 03:33:50 -0600
Subject: wip(desktop): progress

---
 packages/desktop/src/context/layout.tsx     |  4 +---
 packages/desktop/src/context/local.tsx      | 24 ++++++++++++++++--------
 packages/desktop/src/hooks/use-providers.ts |  8 +++-----
 packages/desktop/src/pages/layout.tsx       |  2 +-
 4 files changed, 21 insertions(+), 17 deletions(-)

(limited to 'packages/desktop/src/context/layout.tsx')

diff --git a/packages/desktop/src/context/layout.tsx b/packages/desktop/src/context/layout.tsx
index d4a8875f7..24ba55a53 100644
--- a/packages/desktop/src/context/layout.tsx
+++ b/packages/desktop/src/context/layout.tsx
@@ -185,9 +185,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
         close(dialog: Dialog) {
           if (ephemeral.dialog?.open === dialog) {
             setEphemeral("dialog", "open", undefined)
-            if (dialog === "connect") {
-              setEphemeral("connect", {})
-            }
+            setEphemeral("connect", {})
           }
         },
         connect(provider: string) {
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/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 5f0b26f6b..39917c420 100644
--- a/packages/desktop/src/pages/layout.tsx
+++ b/packages/desktop/src/pages/layout.tsx
@@ -487,7 +487,7 @@ export default function Layout(props: ParentProps) {
           </div>
           <div class="flex flex-col gap-1.5 self-stretch items-start shrink-0 px-2 py-3">
             <Switch>
-              <Match when={!providers().connected().length && layout.sidebar.opened()}>
+              <Match when={!providers().paid().length && layout.sidebar.opened()}>
                 <div class="rounded-md bg-background-stronger shadow-xs-border-base">
                   <div class="p-3 flex flex-col gap-2">
                     <div class="text-12-medium text-text-strong">Getting started</div>
-- 
cgit v1.2.3

