Переглянути джерело

Apply PR #23792: refactor(app): load sync state through TanStack Query

opencode-agent[bot] 2 днів тому
батько
коміт
5350878423

+ 9 - 1
packages/app/src/app.tsx

@@ -83,7 +83,15 @@ declare global {
 }
 
 function QueryProvider(props: ParentProps) {
-  const client = new QueryClient()
+  const client = new QueryClient({
+    defaultOptions: {
+      queries: {
+        refetchOnReconnect: false,
+        refetchOnMount: false,
+        refetchOnWindowFocus: false,
+      },
+    },
+  })
   return <QueryClientProvider client={client}>{props.children}</QueryClientProvider>
 }
 

+ 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 - 45
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",
@@ -162,14 +163,6 @@ export function StatusPopoverBody(props: { shown: Accessor<boolean> }) {
   const dialog = useDialog()
   const language = useLanguage()
   const navigate = useNavigate()
-  const sdk = useSDK()
-
-  const [load, setLoad] = createStore({
-    lspDone: false,
-    lspLoading: false,
-    mcpDone: false,
-    mcpLoading: false,
-  })
 
   const fail = (err: unknown) => {
     showToast({
@@ -181,40 +174,6 @@ export function StatusPopoverBody(props: { shown: Accessor<boolean> }) {
 
   createEffect(() => {
     if (!props.shown()) return
-
-    if (!sync.data.mcp_ready && !load.mcpDone && !load.mcpLoading) {
-      setLoad("mcpLoading", true)
-      void sdk.client.mcp
-        .status()
-        .then((result) => {
-          sync.set("mcp", result.data ?? {})
-          sync.set("mcp_ready", true)
-        })
-        .catch((err) => {
-          setLoad("mcpDone", true)
-          fail(err)
-        })
-        .finally(() => {
-          setLoad("mcpLoading", false)
-        })
-    }
-
-    if (!sync.data.lsp_ready && !load.lspDone && !load.lspLoading) {
-      setLoad("lspLoading", true)
-      void sdk.client.lsp
-        .status()
-        .then((result) => {
-          sync.set("lsp", result.data ?? [])
-          sync.set("lsp_ready", true)
-        })
-        .catch((err) => {
-          setLoad("lspDone", true)
-          fail(err)
-        })
-        .finally(() => {
-          setLoad("lspLoading", false)
-        })
-    }
   })
 
   let dialogRun = 0

+ 94 - 73
packages/app/src/context/global-sync.tsx

@@ -10,15 +10,22 @@ 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 { bootstrapDirectory, bootstrapGlobal, clearProviderRev } from "./global-sync/bootstrap"
+import {
+  bootstrapDirectory,
+  bootstrapGlobal,
+  clearProviderRev,
+  loadGlobalConfigQuery,
+  loadPathQuery,
+  loadProjectsQuery,
+  loadProvidersQuery,
+} from "./global-sync/bootstrap"
 import { createChildStoreManager } from "./global-sync/child-store"
 import { applyDirectoryEvent, applyGlobalEvent, cleanupDroppedSessionCaches } from "./global-sync/event-reducer"
-import { createRefreshQueue } from "./global-sync/queue"
 import { clearSessionPrefetchDirectory } from "./global-sync/session-prefetch"
 import { estimateRootSessionTotal, loadRootSessionsWithFallback } from "./global-sync/session-load"
 import { trimSessions } from "./global-sync/session-trim"
@@ -26,7 +33,7 @@ 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, useQueryClient } from "@tanstack/solid-query"
+import { queryOptions, skipToken, useMutation, useQueries, useQuery, useQueryClient } from "@tanstack/solid-query"
 
 type GlobalStore = {
   ready: boolean
@@ -45,6 +52,18 @@ type GlobalStore = {
 export const loadSessionsQuery = (directory: string) =>
   queryOptions<null>({ queryKey: [directory, "loadSessions"], queryFn: skipToken })
 
+export const loadMcpQuery = (directory: string, sdk?: OpencodeClient) =>
+  queryOptions({
+    queryKey: [directory, "mcp"],
+    queryFn: sdk ? () => sdk.mcp.status().then((r) => r.data ?? {}) : skipToken,
+  })
+
+export const loadLspQuery = (directory: string, sdk?: OpencodeClient) =>
+  queryOptions({
+    queryKey: [directory, "lsp"],
+    queryFn: sdk ? () => sdk.lsp.status().then((r) => r.data ?? []) : skipToken,
+  })
+
 function createGlobalSync() {
   const globalSDK = useGlobalSDK()
   const language = useLanguage()
@@ -61,15 +80,34 @@ function createGlobalSync() {
     createStore({ value: [] as Project[] }),
   )
 
+  const [configQuery, providerQuery, pathQuery] = useQueries(() => ({
+    queries: [loadGlobalConfigQuery(), loadProvidersQuery(null), loadPathQuery(null), loadProjectsQuery()],
+  }))
+
   const [globalStore, setGlobalStore] = createStore<GlobalStore>({
-    ready: false,
-    path: { state: "", config: "", worktree: "", directory: "", home: "" },
+    get ready() {
+      return bootstrap.isPending
+    },
     project: projectCache.value,
     session_todo: {},
-    provider: { all: [], connected: [], default: {} },
     provider_auth: {},
-    config: {},
-    reload: undefined,
+    get path() {
+      const EMPTY = { state: "", config: "", worktree: "", directory: "", home: "" }
+      if (pathQuery.isLoading) return EMPTY
+      return pathQuery.data ?? EMPTY
+    },
+    get provider() {
+      const EMPTY = { all: [], connected: [], default: {} }
+      if (providerQuery.isLoading) return EMPTY
+      return providerQuery.data ?? EMPTY
+    },
+    get config() {
+      if (configQuery.isLoading) return {}
+      return configQuery.data ?? {}
+    },
+    get reload() {
+      return updateConfigMutation.isPending ? "pending" : undefined
+    },
   })
   const queryClient = useQueryClient()
 
@@ -109,6 +147,22 @@ function createGlobalSync() {
     return (setGlobalStore as (...args: unknown[]) => unknown)(...input)
   }) as typeof setGlobalStore
 
+  const bootstrap = useQuery(() => ({
+    queryKey: ["bootstrap"],
+    queryFn: async () => {
+      await bootstrapGlobal({
+        globalSDK: globalSDK.client,
+        requestFailedTitle: language.t("common.requestFailed"),
+        translate: language.t,
+        formatMoreCount: (count) => language.t("common.moreCountSuffix", { count }),
+        setGlobalStore: setBootStore,
+        queryClient,
+      })
+      bootedAt = Date.now()
+      return bootedAt
+    },
+  }))
+
   const set = ((...input: unknown[]) => {
     if (input[0] === "project" && (Array.isArray(input[1]) || typeof input[1] === "function")) {
       setProjects(input[1] as Project[] | ((draft: Project[]) => Project[]))
@@ -143,11 +197,22 @@ function createGlobalSync() {
 
   const paused = () => untrack(() => globalStore.reload) !== undefined
 
-  const queue = createRefreshQueue({
-    paused,
-    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,
@@ -157,26 +222,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
@@ -314,14 +369,14 @@ function createGlobalSync() {
         project: globalStore.project,
         refresh: () => {
           if (recent) return
-          queue.refresh()
+          bootstrap.refetch()
         },
         setGlobalProject: setProjects,
       })
       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
@@ -336,47 +391,25 @@ function createGlobalSync() {
       directory,
       store,
       setStore,
-      push: queue.push,
+      push: () => {}, //  queue.push,
       setSessionTodo,
       vcsCache: children.vcsCache.get(directory),
       loadLsp: () => {
-        void sdkFor(directory)
-          .lsp.status()
-          .then((x) => {
-            setStore("lsp", x.data ?? [])
-            setStore("lsp_ready", true)
-          })
+        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)
     }
   })
 
-  async function bootstrap() {
-    bootingRoot = true
-    try {
-      await bootstrapGlobal({
-        globalSDK: globalSDK.client,
-        requestFailedTitle: language.t("common.requestFailed"),
-        translate: language.t,
-        formatMoreCount: (count) => language.t("common.moreCountSuffix", { count }),
-        setGlobalStore: setBootStore,
-        queryClient,
-      })
-      bootedAt = Date.now()
-    } finally {
-      bootingRoot = false
-    }
-  }
-
   onMount(() => {
     if (typeof requestAnimationFrame === "function") {
       eventFrame = requestAnimationFrame(() => {
@@ -392,7 +425,6 @@ function createGlobalSync() {
         void globalSDK.event.start()
       }, 0)
     }
-    void bootstrap()
   })
 
   const projectApi = {
@@ -405,21 +437,10 @@ function createGlobalSync() {
     },
   }
 
-  const updateConfig = async (config: Config) => {
-    setGlobalStore("reload", "pending")
-    return globalSDK.client.global.config
-      .update({ config })
-      .then(bootstrap)
-      .then(() => {
-        queue.refresh()
-        setGlobalStore("reload", undefined)
-        queue.refresh()
-      })
-      .catch((error) => {
-        setGlobalStore("reload", undefined)
-        throw error
-      })
-  }
+  const updateConfigMutation = useMutation(() => ({
+    mutationFn: (config: Config) => globalSDK.client.global.config.update({ config }),
+    // onSuccess: () => bootstrap.refetch(),
+  }))
 
   return {
     data: globalStore,
@@ -432,8 +453,8 @@ function createGlobalSync() {
     },
     child: children.child,
     peek: children.peek,
-    bootstrap,
-    updateConfig,
+    // bootstrap,
+    updateConfig: updateConfigMutation.mutateAsync,
     project: projectApi,
     todo: {
       set: setSessionTodo,

+ 100 - 107
packages/app/src/context/global-sync/bootstrap.ts

@@ -19,6 +19,7 @@ import type { State, VcsCache } from "./types"
 import { cmp, normalizeAgentList, normalizeProviderList } from "./utils"
 import { formatServerError } from "@/utils/server-errors"
 import { QueryClient, queryOptions, skipToken } from "@tanstack/solid-query"
+import { loadMcpQuery } from "../global-sync"
 
 type GlobalStore = {
   ready: boolean
@@ -66,6 +67,62 @@ function runAll(list: Array<() => Promise<unknown>>) {
   return Promise.allSettled(list.map((item) => item()))
 }
 
+function showErrors(input: {
+  errors: unknown[]
+  title: string
+  translate: (key: string, vars?: Record<string, string | number>) => string
+  formatMoreCount: (count: number) => string
+}) {
+  if (input.errors.length === 0) return
+  const message = formatServerError(input.errors[0], input.translate)
+  const more = input.errors.length > 1 ? input.formatMoreCount(input.errors.length - 1) : ""
+  showToast({
+    variant: "error",
+    title: input.title,
+    description: message + more,
+  })
+}
+
+export const loadGlobalConfigQuery = (
+  sdk?: OpencodeClient,
+  transform?: (x: Awaited<ReturnType<OpencodeClient["global"]["config"]["get"]>>) => void,
+) =>
+  queryOptions({
+    queryKey: ["config"],
+    queryFn: sdk
+      ? () =>
+          retry(() =>
+            sdk.global.config.get().then((x) => {
+              transform?.(x)
+              return x.data!
+            }),
+          )
+      : skipToken,
+  })
+
+export const loadProjectsQuery = (
+  sdk?: OpencodeClient,
+  transform?: (x: Awaited<ReturnType<OpencodeClient["project"]["list"]>>["data"]) => void,
+) =>
+  queryOptions({
+    queryKey: ["project"],
+    queryFn: sdk
+      ? () =>
+          retry(() =>
+            sdk.project
+              .list()
+              .then((x) => {
+                return (x.data ?? [])
+                  .filter((p) => !!p?.id)
+                  .filter((p) => !!p.worktree && !p.worktree.includes("opencode-test"))
+                  .slice()
+                  .sort((a, b) => cmp(a.id, b.id))
+              })
+              .then(transform),
+          )
+      : skipToken,
+  })
+
 export async function bootstrapGlobal(input: {
   globalSDK: OpencodeClient
   requestFailedTitle: string
@@ -74,61 +131,21 @@ export async function bootstrapGlobal(input: {
   setGlobalStore: SetStoreFunction<GlobalStore>
   queryClient: QueryClient
 }) {
-  const fast = [
-    () =>
-      retry(() =>
-        input.globalSDK.global.config.get().then((x) => {
-          input.setGlobalStore("config", x.data!)
-        }),
-      ),
-  ]
-
   const slow = [
+    () => input.queryClient.fetchQuery(loadGlobalConfigQuery(input.globalSDK)),
+    () => input.queryClient.fetchQuery(loadProvidersQuery(null, input.globalSDK)),
+    () => input.queryClient.fetchQuery(loadPathQuery(null, input.globalSDK)),
     () =>
-      input.queryClient.fetchQuery({
-        ...loadProvidersQuery(null),
-        queryFn: () =>
-          retry(() =>
-            input.globalSDK.provider.list().then((x) => {
-              input.setGlobalStore("provider", normalizeProviderList(x.data!))
-              return null
-            }),
-          ),
-      }),
-    () =>
-      retry(() =>
-        input.globalSDK.path.get().then((x) => {
-          input.setGlobalStore("path", x.data!)
-        }),
-      ),
-    () =>
-      retry(() =>
-        input.globalSDK.project.list().then((x) => {
-          const projects = (x.data ?? [])
-            .filter((p) => !!p?.id)
-            .filter((p) => !!p.worktree && !p.worktree.includes("opencode-test"))
-            .slice()
-            .sort((a, b) => cmp(a.id, b.id))
-          input.setGlobalStore("project", projects)
-        }),
+      input.queryClient.fetchQuery(
+        loadProjectsQuery(input.globalSDK, (data) => input.setGlobalStore("project", data ?? [])),
       ),
   ]
-  await runAll(fast)
-  // showErrors({
-  //   errors: errors(await runAll(fast)),
-  //   title: input.requestFailedTitle,
-  //   translate: input.translate,
-  //   formatMoreCount: input.formatMoreCount,
-  // })
-  await waitForPaint()
-  await runAll(slow)
-  // showErrors({
-  //   errors: errors(),
-  //   title: input.requestFailedTitle,
-  //   translate: input.translate,
-  //   formatMoreCount: input.formatMoreCount,
-  // })
-  input.setGlobalStore("ready", true)
+  showErrors({
+    errors: errors(await runAll(slow)),
+    title: input.requestFailedTitle,
+    translate: input.translate,
+    formatMoreCount: input.formatMoreCount,
+  })
 }
 
 function groupBySession<T extends { id: string; sessionID: string }>(input: T[]) {
@@ -179,26 +196,28 @@ function warmSessions(input: {
   ).then(() => undefined)
 }
 
-export const loadProvidersQuery = (directory: string | null) =>
-  queryOptions<null>({ queryKey: [directory, "providers"], queryFn: skipToken })
+export const loadProvidersQuery = (directory: string | null, sdk?: OpencodeClient) =>
+  queryOptions({
+    queryKey: [directory, "providers"],
+    queryFn: sdk ? () => retry(() => sdk.provider.list().then((x) => normalizeProviderList(x.data!))) : skipToken,
+  })
 
 export const loadAgentsQuery = (
   directory: string | null,
   sdk?: OpencodeClient,
   transform?: (x: Awaited<ReturnType<OpencodeClient["app"]["agents"]>>) => void,
 ) =>
-  queryOptions<null>({
+  queryOptions({
     queryKey: [directory, "agents"],
-    queryFn:
-      sdk && transform
-        ? () =>
-            retry(() =>
-              sdk.app
-                .agents()
-                .then(transform)
-                .then(() => null),
-            )
-        : skipToken,
+    queryFn: sdk
+      ? () =>
+          retry(() =>
+            sdk.app.agents().then((x) => {
+              transform?.(x)
+              return x.data!
+            }),
+          )
+      : skipToken,
   })
 
 export const loadPathQuery = (
@@ -208,16 +227,15 @@ export const loadPathQuery = (
 ) =>
   queryOptions<Path>({
     queryKey: [directory, "path"],
-    queryFn:
-      sdk && transform
-        ? () =>
-            retry(() =>
-              sdk.path.get().then(async (x) => {
-                transform(x)
-                return x.data!
-              }),
-            )
-        : skipToken,
+    queryFn: sdk
+      ? () =>
+          retry(() =>
+            sdk.path.get().then(async (x) => {
+              transform?.(x)
+              return x.data!
+            }),
+          )
+      : skipToken,
   })
 
 export async function bootstrapDirectory(input: {
@@ -247,13 +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)
   }
-  if (loading || input.store.provider.all.length === 0) {
-    input.setStore("provider_ready", false)
-  }
-  input.setStore("mcp_ready", false)
-  input.setStore("mcp", {})
-  input.setStore("lsp_ready", false)
-  input.setStore("lsp", [])
   if (loading) input.setStore("status", "partial")
 
   const rev = (providerRev.get(input.directory) ?? 0) + 1
@@ -339,33 +350,15 @@ export async function bootstrapDirectory(input: {
           }),
         ),
       () => Promise.resolve(input.loadSessions(input.directory)),
+      () => input.queryClient.fetchQuery(loadMcpQuery(input.directory, input.sdk)),
       () =>
-        retry(() =>
-          input.sdk.mcp.status().then((x) => {
-            input.setStore("mcp", x.data!)
-            input.setStore("mcp_ready", true)
-          }),
-        ),
-      () =>
-        input.queryClient.ensureQueryData({
-          ...loadProvidersQuery(input.directory),
-          queryFn: () =>
-            retry(() => input.sdk.provider.list())
-              .then((x) => {
-                if (providerRev.get(input.directory) !== rev) return
-                input.setStore("provider", normalizeProviderList(x.data!))
-                input.setStore("provider_ready", true)
-              })
-              .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),
-                })
-              })
-              .then(() => null),
+        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>)[]
 

+ 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) => {

+ 31 - 9
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,
@@ -14,8 +14,9 @@ import {
   type VcsCache,
 } from "./types"
 import { canDisposeDirectory, pickDirectoriesToEvict } from "./eviction"
-import { useQuery } from "@tanstack/solid-query"
-import { loadPathQuery } from "./bootstrap"
+import { useQueries } from "@tanstack/solid-query"
+import { loadPathQuery, loadProvidersQuery } from "./bootstrap"
+import { loadLspQuery, loadMcpQuery } from "../global-sync"
 
 export function createChildStoreManager(input: {
   owner: Owner
@@ -24,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>()
@@ -156,15 +158,27 @@ 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 = useQuery(() => loadPathQuery(directory))
+          const [pathQuery, mcpQuery, lspQuery, providerQuery] = useQueries(() => ({
+            queries: [
+              loadPathQuery(directory, sdk),
+              loadMcpQuery(directory, sdk),
+              loadLspQuery(directory, sdk),
+              loadProvidersQuery(directory, sdk),
+            ],
+          }))
+
           const child = createStore<State>({
             project: "",
             projectMeta: initialMeta,
             icon: initialIcon,
-            provider_ready: false,
+            get provider_ready() {
+              return providerQuery.isLoading
+            },
             provider: { all: [], connected: [], default: {} },
             config: {},
             get path() {
@@ -182,10 +196,18 @@ export function createChildStoreManager(input: {
             todo: {},
             permission: {},
             question: {},
-            mcp_ready: false,
-            mcp: {},
-            lsp_ready: false,
-            lsp: [],
+            get mcp_ready() {
+              return mcpQuery.isLoading
+            },
+            get mcp() {
+              return mcpQuery.isLoading ? {} : (mcpQuery.data ?? {})
+            },
+            get lsp_ready() {
+              return lspQuery.isLoading
+            },
+            get lsp() {
+              return lspQuery.isLoading ? [] : (lspQuery.data ?? [])
+            },
             vcs: vcsStore.value,
             limit: 5,
             message: {},

+ 1 - 0
packages/opencode/src/plugin/codex.ts

@@ -374,6 +374,7 @@ export async function CodexAuthPlugin(input: PluginInput): Promise<Hooks> {
           "gpt-5.3-codex",
           "gpt-5.4",
           "gpt-5.4-mini",
+          "gpt-5.5",
         ])
         for (const [modelId, model] of Object.entries(provider.models)) {
           if (modelId.includes("codex")) continue