Brendan Allan 3 дней назад
Родитель
Сommit
e8f56bace1

+ 7 - 55
packages/app/src/components/dialog-select-mcp.tsx

@@ -1,13 +1,12 @@
-import { useMutation } from "@tanstack/solid-query"
-import { Component, createEffect, createMemo, on, Show } from "solid-js"
-import { createStore } from "solid-js/store"
+import { useMutation, useQueryClient } from "@tanstack/solid-query"
+import { Component, createMemo, Show } from "solid-js"
 import { useSync } from "@/context/sync"
 import { useSDK } from "@/context/sdk"
 import { Dialog } from "@opencode-ai/ui/dialog"
 import { List } from "@opencode-ai/ui/list"
 import { Switch } from "@opencode-ai/ui/switch"
-import { showToast } from "@opencode-ai/ui/toast"
 import { useLanguage } from "@/context/language"
+import { loadMcpQuery } from "@/context/global-sync"
 
 const statusLabels = {
   connected: "mcp.status.connected",
@@ -20,48 +19,7 @@ export const DialogSelectMcp: Component = () => {
   const sync = useSync()
   const sdk = useSDK()
   const language = useLanguage()
-  const [state, setState] = createStore({
-    done: false,
-    loading: false,
-  })
-
-  createEffect(
-    on(
-      () => sync.data.mcp_ready,
-      (ready, prev) => {
-        if (!ready && prev) setState("done", false)
-      },
-      { defer: true },
-    ),
-  )
-
-  createEffect(() => {
-    if (state.done || state.loading) return
-    if (sync.data.mcp_ready) {
-      setState("done", true)
-      return
-    }
-
-    setState("loading", true)
-    void sdk.client.mcp
-      .status()
-      .then((result) => {
-        sync.set("mcp", result.data ?? {})
-        // sync.set("mcp_ready", true)
-        setState("done", true)
-      })
-      .catch((err) => {
-        setState("done", true)
-        showToast({
-          variant: "error",
-          title: language.t("common.requestFailed"),
-          description: err instanceof Error ? err.message : String(err),
-        })
-      })
-      .finally(() => {
-        setState("loading", false)
-      })
-  })
+  const queryClient = useQueryClient()
 
   const items = createMemo(() =>
     Object.entries(sync.data.mcp ?? {})
@@ -71,16 +29,10 @@ export const DialogSelectMcp: Component = () => {
 
   const toggle = useMutation(() => ({
     mutationFn: async (name: string) => {
-      const status = sync.data.mcp[name]
-      if (status?.status === "connected") {
-        await sdk.client.mcp.disconnect({ name })
-      } else {
-        await sdk.client.mcp.connect({ name })
-      }
-
-      const result = await sdk.client.mcp.status()
-      if (result.data) sync.set("mcp", result.data)
+      if (sync.data.mcp[name]?.status === "connected") await sdk.client.mcp.disconnect({ name })
+      else await sdk.client.mcp.connect({ name })
     },
+    onSuccess: () => queryClient.refetchQueries({ queryKey: loadMcpQuery(sync.directory).queryKey }),
   }))
 
   const enabledCount = createMemo(() => items().filter((i) => i.status === "connected").length)

+ 4 - 3
packages/app/src/components/status-popover-body.tsx

@@ -3,7 +3,7 @@ import { useDialog } from "@opencode-ai/ui/context/dialog"
 import { Icon } from "@opencode-ai/ui/icon"
 import { Switch } from "@opencode-ai/ui/switch"
 import { Tabs } from "@opencode-ai/ui/tabs"
-import { useMutation } from "@tanstack/solid-query"
+import { useMutation, useQueryClient } from "@tanstack/solid-query"
 import { showToast } from "@opencode-ai/ui/toast"
 import { useNavigate } from "@solidjs/router"
 import { type Accessor, createEffect, createMemo, For, type JSXElement, onCleanup, Show } from "solid-js"
@@ -15,6 +15,7 @@ import { useSDK } from "@/context/sdk"
 import { normalizeServerUrl, ServerConnection, useServer } from "@/context/server"
 import { useSync } from "@/context/sync"
 import { useCheckServerHealth, type ServerHealth } from "@/utils/server-health"
+import { loadMcpQuery } from "@/context/global-sync"
 
 const pollMs = 10_000
 
@@ -137,14 +138,14 @@ const useMcpToggleMutation = () => {
   const sync = useSync()
   const sdk = useSDK()
   const language = useLanguage()
+  const queryClient = useQueryClient()
 
   return useMutation(() => ({
     mutationFn: async (name: string) => {
       const status = sync.data.mcp[name]
       await (status?.status === "connected" ? sdk.client.mcp.disconnect({ name }) : sdk.client.mcp.connect({ name }))
-      const result = await sdk.client.mcp.status()
-      if (result.data) sync.set("mcp", result.data)
     },
+    onSuccess: () => queryClient.refetchQueries({ queryKey: loadMcpQuery(sync.directory).queryKey }),
     onError: (err) => {
       showToast({
         variant: "error",

+ 29 - 60
packages/app/src/context/global-sync.tsx

@@ -10,9 +10,8 @@ import type {
 import { showToast } from "@opencode-ai/ui/toast"
 import { getFilename } from "@opencode-ai/shared/util/path"
 import { batch, createContext, getOwner, onCleanup, onMount, type ParentProps, untrack, useContext } from "solid-js"
-import { createStore, produce, reconcile, unwrap } from "solid-js/store"
+import { createStore, produce, reconcile } from "solid-js/store"
 import { useLanguage } from "@/context/language"
-import { Persist, persisted } from "@/utils/persist"
 import type { InitError } from "../pages/error"
 import { useGlobalSDK } from "./global-sdk"
 import {
@@ -31,9 +30,9 @@ import { estimateRootSessionTotal, loadRootSessionsWithFallback } from "./global
 import { trimSessions } from "./global-sync/session-trim"
 import type { ProjectMeta } from "./global-sync/types"
 import { SESSION_RECENT_LIMIT } from "./global-sync/types"
-import { sanitizeProject } from "./global-sync/utils"
 import { formatServerError } from "@/utils/server-errors"
 import { queryOptions, skipToken, useMutation, useQueries, useQuery, useQueryClient } from "@tanstack/solid-query"
+import { createRefreshQueue } from "./global-sync/queue"
 
 type GlobalStore = {
   ready: boolean
@@ -61,7 +60,7 @@ export const loadMcpQuery = (directory: string, sdk?: OpencodeClient) =>
 export const loadLspQuery = (directory: string, sdk?: OpencodeClient) =>
   queryOptions({
     queryKey: [directory, "lsp"],
-    queryFn: sdk ? () => sdk.lsp.status().then((r) => r.data ?? {}) : skipToken,
+    queryFn: sdk ? () => sdk.lsp.status().then((r) => r.data ?? []) : skipToken,
   })
 
 function createGlobalSync() {
@@ -75,11 +74,6 @@ function createGlobalSync() {
   const sessionLoads = new Map<string, Promise<void>>()
   const sessionMeta = new Map<string, { limit: number }>()
 
-  const [projectCache, setProjectCache, projectInit] = persisted(
-    Persist.global("globalSync.project", ["globalSync.project.v1"]),
-    createStore({ value: [] as Project[] }),
-  )
-
   const [configQuery, providerQuery, pathQuery] = useQueries(() => ({
     queries: [loadGlobalConfigQuery(), loadProvidersQuery(null), loadPathQuery(null), loadProjectsQuery()],
   }))
@@ -88,7 +82,7 @@ function createGlobalSync() {
     get ready() {
       return bootstrap.isPending
     },
-    project: projectCache.value,
+    project: [],
     session_todo: {},
     provider_auth: {},
     get path() {
@@ -111,32 +105,18 @@ function createGlobalSync() {
   })
   const queryClient = useQueryClient()
 
-  let active = true
-  let projectWritten = false
   let bootedAt = 0
   let bootingRoot = false
   let eventFrame: number | undefined
   let eventTimer: ReturnType<typeof setTimeout> | undefined
 
-  onCleanup(() => {
-    active = false
-  })
   onCleanup(() => {
     if (eventFrame !== undefined) cancelAnimationFrame(eventFrame)
     if (eventTimer !== undefined) clearTimeout(eventTimer)
   })
 
-  const cacheProjects = () => {
-    setProjectCache(
-      "value",
-      untrack(() => globalStore.project.map(sanitizeProject)),
-    )
-  }
-
   const setProjects = (next: Project[] | ((draft: Project[]) => Project[])) => {
-    projectWritten = true
     setGlobalStore("project", next)
-    cacheProjects()
   }
 
   const setBootStore = ((...input: unknown[]) => {
@@ -171,16 +151,6 @@ function createGlobalSync() {
     return (setGlobalStore as (...args: unknown[]) => unknown)(...input)
   }) as typeof setGlobalStore
 
-  if (projectInit instanceof Promise) {
-    void projectInit.then(() => {
-      if (!active) return
-      if (projectWritten) return
-      const cached = projectCache.value
-      if (cached.length === 0) return
-      setGlobalStore("project", cached)
-    })
-  }
-
   const setSessionTodo = (sessionID: string, todos: Todo[] | undefined) => {
     if (!sessionID) return
     if (!todos) {
@@ -197,11 +167,22 @@ function createGlobalSync() {
 
   const paused = () => untrack(() => globalStore.reload) !== undefined
 
-  // const queue = createRefreshQueue({
-  //   paused,
-  //   bootstrap: () => queryClient.fetchQuery({ queryKey: ["bootstrap"] }),
-  //   bootstrapInstance,
-  // })
+  const queue = createRefreshQueue({
+    paused,
+    bootstrap: () => queryClient.fetchQuery({ queryKey: ["bootstrap"] }),
+    bootstrapInstance,
+  })
+
+  const sdkFor = (directory: string) => {
+    const cached = sdkCache.get(directory)
+    if (cached) return cached
+    const sdk = globalSDK.createClient({
+      directory,
+      throwOnError: true,
+    })
+    sdkCache.set(directory, sdk)
+    return sdk
+  }
 
   const children = createChildStoreManager({
     owner,
@@ -211,26 +192,16 @@ function createGlobalSync() {
       void bootstrapInstance(directory)
     },
     onDispose: (directory) => {
-      // queue.clear(directory)
+      queue.clear(directory)
       sessionMeta.delete(directory)
       sdkCache.delete(directory)
       clearProviderRev(directory)
       clearSessionPrefetchDirectory(directory)
     },
     translate: language.t,
+    getSdk: sdkFor,
   })
 
-  const sdkFor = (directory: string) => {
-    const cached = sdkCache.get(directory)
-    if (cached) return cached
-    const sdk = globalSDK.createClient({
-      directory,
-      throwOnError: true,
-    })
-    sdkCache.set(directory, sdk)
-    return sdk
-  }
-
   async function loadSessions(directory: string) {
     const pending = sessionLoads.get(directory)
     if (pending) return pending
@@ -362,7 +333,7 @@ function createGlobalSync() {
       if (event.type === "server.connected" || event.type === "global.disposed") {
         if (recent) return
         for (const directory of Object.keys(children.children)) {
-          // queue.push(directory)
+          queue.push(directory)
         }
       }
       return
@@ -377,21 +348,19 @@ function createGlobalSync() {
       directory,
       store,
       setStore,
-      push: () => {}, //  queue.push,
+      push: queue.push,
       setSessionTodo,
       vcsCache: children.vcsCache.get(directory),
       loadLsp: () => {
-        void queryClient.fetchQuery(loadLspQuery(directory, sdkFor(directory))).then((data) => {
-          setStore("lsp", data ?? [])
-        })
+        void queryClient.fetchQuery(loadLspQuery(directory, sdkFor(directory)))
       },
     })
   })
 
   onCleanup(unsub)
-  // onCleanup(() => {
-  //   queue.dispose()
-  // })
+  onCleanup(() => {
+    queue.dispose()
+  })
   onCleanup(() => {
     for (const directory of Object.keys(children.children)) {
       children.disposeDirectory(directory)
@@ -427,7 +396,7 @@ function createGlobalSync() {
 
   const updateConfigMutation = useMutation(() => ({
     mutationFn: (config: Config) => globalSDK.client.global.config.update({ config }),
-    // onSuccess: () => bootstrap.refetch(),
+    onSuccess: () => bootstrap.refetch(),
   }))
 
   return {

+ 8 - 14
packages/app/src/context/global-sync/bootstrap.ts

@@ -265,8 +265,6 @@ export async function bootstrapDirectory(input: {
   if (Object.keys(input.store.config).length === 0 && Object.keys(input.global.config).length > 0) {
     input.setStore("config", input.global.config)
   }
-  input.setStore("mcp", {})
-  input.setStore("lsp", [])
   if (loading) input.setStore("status", "partial")
 
   const rev = (providerRev.get(input.directory) ?? 0) + 1
@@ -354,18 +352,14 @@ export async function bootstrapDirectory(input: {
       () => Promise.resolve(input.loadSessions(input.directory)),
       () => input.queryClient.fetchQuery(loadMcpQuery(input.directory, input.sdk)),
       () =>
-        input.queryClient.ensureQueryData(
-          loadProvidersQuery(input.directory, input.sdk),
-          //     .catch((err) => {
-          //       if (providerRev.get(input.directory) !== rev) console.error("Failed to refresh provider list", err)
-          //       const project = getFilename(input.directory)
-          //       showToast({
-          //         variant: "error",
-          //         title: input.translate("toast.project.reloadFailed.title", { project }),
-          //         description: formatServerError(err, input.translate),
-          //       })
-          //     })
-        ),
+        input.queryClient.fetchQuery(loadProvidersQuery(input.directory, input.sdk)).catch((err) => {
+          const project = getFilename(input.directory)
+          showToast({
+            variant: "error",
+            title: input.translate("toast.project.reloadFailed.title", { project }),
+            description: formatServerError(err, input.translate),
+          })
+        }),
     ].filter(Boolean) as (() => Promise<any>)[]
 
     await waitForPaint()

+ 1 - 0
packages/app/src/context/global-sync/child-store.test.ts

@@ -22,6 +22,7 @@ describe("createChildStoreManager", () => {
       onBootstrap() {},
       onDispose() {},
       translate: (key) => key,
+      getSdk: () => null!,
     })
 
     Array.from({ length: 30 }, (_, index) => `/pinned-${index}`).forEach((directory) => {

+ 21 - 8
packages/app/src/context/global-sync/child-store.ts

@@ -1,7 +1,7 @@
 import { createRoot, getOwner, onCleanup, runWithOwner, type Owner } from "solid-js"
 import { createStore, type SetStoreFunction, type Store } from "solid-js/store"
 import { Persist, persisted } from "@/utils/persist"
-import type { VcsInfo } from "@opencode-ai/sdk/v2/client"
+import type { OpencodeClient, VcsInfo } from "@opencode-ai/sdk/v2/client"
 import {
   DIR_IDLE_TTL_MS,
   MAX_DIR_STORES,
@@ -25,6 +25,7 @@ export function createChildStoreManager(input: {
   onBootstrap: (directory: string) => void
   onDispose: (directory: string) => void
   translate: (key: string, vars?: Record<string, string | number>) => string
+  getSdk: (directory: string) => OpencodeClient
 }) {
   const children: Record<string, [Store<State>, SetStoreFunction<State>]> = {}
   const vcsCache = new Map<string, VcsCache>()
@@ -157,20 +158,23 @@ export function createChildStoreManager(input: {
 
       const init = () =>
         createRoot((dispose) => {
+          const sdk = input.getSdk(directory)
+
+          const initialMeta = meta[0].value
           const initialIcon = icon[0].value
 
           const [pathQuery, mcpQuery, lspQuery, providerQuery] = useQueries(() => ({
             queries: [
-              loadPathQuery(directory),
-              loadMcpQuery(directory),
-              loadLspQuery(directory),
-              loadProvidersQuery(directory),
+              loadPathQuery(directory, sdk),
+              loadMcpQuery(directory, sdk),
+              loadLspQuery(directory, sdk),
+              loadProvidersQuery(directory, sdk),
             ],
           }))
 
           const child = createStore<State>({
             project: "",
-            projectMeta: undefined,
+            projectMeta: initialMeta,
             icon: initialIcon,
             get provider_ready() {
               return providerQuery.isLoading
@@ -195,11 +199,15 @@ export function createChildStoreManager(input: {
             get mcp_ready() {
               return mcpQuery.isLoading
             },
-            mcp: {},
+            get mcp() {
+              return mcpQuery.isLoading ? {} : (mcpQuery.data ?? {})
+            },
             get lsp_ready() {
               return lspQuery.isLoading
             },
-            lsp: [],
+            get lsp() {
+              return lspQuery.isLoading ? [] : (lspQuery.data ?? [])
+            },
             vcs: vcsStore.value,
             limit: 5,
             message: {},
@@ -222,6 +230,11 @@ export function createChildStoreManager(input: {
             child[1]("vcs", (value) => value ?? cached)
           })
 
+          onPersistedInit(meta[2], () => {
+            if (child[0].projectMeta !== initialMeta) return
+            child[1]("projectMeta", meta[0].value)
+          })
+
           onPersistedInit(icon[2], () => {
             if (child[0].icon !== initialIcon) return
             child[1]("icon", icon[0].value)