Przeglądaj źródła

wip(desktop): progress

Adam 2 miesięcy temu
rodzic
commit
1980113ee4

+ 54 - 21
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))

+ 30 - 2
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", {})
+        },
       },
     }
   },

+ 8 - 42
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

+ 3 - 2
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 (

+ 6 - 5
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))