Преглед изворни кода

refactor(app): centralize sync query options (#25941)

Co-authored-by: Brendan Allan <git@brendonovich.dev>
Co-authored-by: Brendan Allan <14191578+Brendonovich@users.noreply.github.com>
Luke Parker пре 3 месеци
родитељ
комит
caf1151cb5

+ 4 - 2
packages/app/src/components/dialog-select-mcp.tsx

@@ -6,7 +6,8 @@ import { Dialog } from "@opencode-ai/ui/dialog"
 import { List } from "@opencode-ai/ui/list"
 import { Switch } from "@opencode-ai/ui/switch"
 import { useLanguage } from "@/context/language"
-import { mcpQueryKey } from "@/context/global-sync"
+import { useQueryOptions } from "@/context/global-sync"
+import { pathKey } from "@/utils/path-key"
 
 const statusLabels = {
   connected: "mcp.status.connected",
@@ -20,6 +21,7 @@ export const DialogSelectMcp: Component = () => {
   const sdk = useSDK()
   const language = useLanguage()
   const queryClient = useQueryClient()
+  const queryOptions = useQueryOptions()
 
   const items = createMemo(() =>
     Object.entries(sync.data.mcp ?? {})
@@ -32,7 +34,7 @@ export const DialogSelectMcp: Component = () => {
       if (sync.data.mcp[name]?.status === "connected") await sdk.client.mcp.disconnect({ name })
       else await sdk.client.mcp.connect({ name })
     },
-    onSuccess: () => queryClient.refetchQueries({ queryKey: mcpQueryKey(sync.directory) }),
+    onSuccess: () => queryClient.refetchQueries(queryOptions.mcp(pathKey(sync.directory))),
   }))
 
   const enabledCount = createMemo(() => items().filter((i) => i.status === "connected").length)

+ 6 - 6
packages/app/src/components/prompt-input.tsx

@@ -16,7 +16,6 @@ import {
 } from "@/context/prompt"
 import { useLayout } from "@/context/layout"
 import { useSDK } from "@/context/sdk"
-import { useGlobalSDK } from "@/context/global-sdk"
 import { useSync } from "@/context/sync"
 import { useComments } from "@/context/comments"
 import { Button } from "@opencode-ai/ui/button"
@@ -56,7 +55,8 @@ import { PromptDragOverlay } from "./prompt-input/drag-overlay"
 import { promptPlaceholder } from "./prompt-input/placeholder"
 import { ImagePreview } from "@opencode-ai/ui/image-preview"
 import { useQueries } from "@tanstack/solid-query"
-import { loadAgentsQuery, loadProvidersQuery } from "@/context/global-sync/bootstrap"
+import { useQueryOptions } from "@/context/global-sync"
+import { pathKey } from "@/utils/path-key"
 
 interface PromptInputProps {
   class?: string
@@ -103,7 +103,7 @@ const NON_EMPTY_TEXT = /[^\s\u200B]/
 
 export const PromptInput: Component<PromptInputProps> = (props) => {
   const sdk = useSDK()
-  const globalSDK = useGlobalSDK()
+  const queryOptions = useQueryOptions()
 
   const sync = useSync()
   const local = useLocal()
@@ -1256,9 +1256,9 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
 
   const [agentsQuery, globalProvidersQuery, providersQuery] = useQueries(() => ({
     queries: [
-      loadAgentsQuery(sdk.directory, sdk.client),
-      loadProvidersQuery(null, globalSDK.client),
-      loadProvidersQuery(sdk.directory, sdk.client),
+      queryOptions.agents(pathKey(sdk.directory)),
+      queryOptions.providers(null),
+      queryOptions.providers(pathKey(sdk.directory)),
     ],
   }))
 

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

@@ -15,7 +15,8 @@ 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 { mcpQueryKey } from "@/context/global-sync"
+import { useQueryOptions } from "@/context/global-sync"
+import { pathKey } from "@/utils/path-key"
 
 const pollMs = 10_000
 
@@ -139,13 +140,14 @@ const useMcpToggleMutation = () => {
   const sdk = useSDK()
   const language = useLanguage()
   const queryClient = useQueryClient()
+  const queryOptions = useQueryOptions()
 
   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 }))
     },
-    onSuccess: () => queryClient.refetchQueries({ queryKey: mcpQueryKey(sync.directory) }),
+    onSuccess: () => queryClient.refetchQueries(queryOptions.mcp(pathKey(sync.directory))),
     onError: (err) => {
       showToast({
         variant: "error",

+ 43 - 28
packages/app/src/context/global-sync.tsx

@@ -18,8 +18,10 @@ import {
   bootstrapDirectory,
   bootstrapGlobal,
   clearProviderRev,
+  loadAgentsQuery,
   loadGlobalConfigQuery,
   loadPathQuery,
+  loadProjectsQuery,
   loadProvidersQuery,
 } from "./global-sync/bootstrap"
 import { createChildStoreManager } from "./global-sync/child-store"
@@ -33,6 +35,7 @@ import { formatServerError } from "@/utils/server-errors"
 import { queryOptions, useMutation, useQueries, useQuery, useQueryClient } from "@tanstack/solid-query"
 import { createRefreshQueue } from "./global-sync/queue"
 import { directoryKey } from "./global-sync/utils"
+import { PathKey } from "@/utils/path-key"
 
 type GlobalStore = {
   ready: boolean
@@ -48,24 +51,33 @@ type GlobalStore = {
   reload: undefined | "pending" | "complete"
 }
 
-export const loadSessionsQueryKey = (directory: string) => [directory, "loadSessions"] as const
-
-export const mcpQueryKey = (directory: string) => [directory, "mcp"] as const
-
 export const loadMcpQuery = (directory: string, sdk: OpencodeClient) =>
   queryOptions({
-    queryKey: mcpQueryKey(directory),
+    queryKey: [directory, "mcp"] as const,
     queryFn: () => sdk.mcp.status().then((r) => r.data ?? {}),
   })
 
-export const lspQueryKey = (directory: string) => [directory, "lsp"] as const
-
 export const loadLspQuery = (directory: string, sdk: OpencodeClient) =>
   queryOptions({
-    queryKey: lspQueryKey(directory),
+    queryKey: [directory, "lsp"] as const,
     queryFn: () => sdk.lsp.status().then((r) => r.data ?? []),
   })
 
+function makeQueryOptionsApi(globalSDK: () => OpencodeClient, sdkFor: (dir: PathKey) => OpencodeClient) {
+  return {
+    globalConfig: () => loadGlobalConfigQuery(globalSDK()),
+    projects: () => loadProjectsQuery(globalSDK()),
+    providers: (directory: PathKey | null) =>
+      loadProvidersQuery(directory, directory === null ? globalSDK() : sdkFor(directory)),
+    path: (directory: PathKey | null) => loadPathQuery(directory, directory === null ? globalSDK() : sdkFor(directory)),
+    agents: (directory: PathKey) => loadAgentsQuery(directory, sdkFor(directory)),
+    mcp: (directory: PathKey) => loadMcpQuery(directory, sdkFor(directory)),
+    lsp: (directory: PathKey) => loadLspQuery(directory, sdkFor(directory)),
+    sessions: (directory: PathKey) => ({ queryKey: [directory, "loadSessions"] as const }),
+  }
+}
+export type QueryOptionsApi = ReturnType<typeof makeQueryOptionsApi>
+
 function createGlobalSync() {
   const globalSDK = useGlobalSDK()
   const language = useLanguage()
@@ -77,12 +89,22 @@ function createGlobalSync() {
   const sessionLoads = new Map<string, Promise<void>>()
   const sessionMeta = new Map<string, { limit: number }>()
 
+  const sdkFor = (directory: string) => {
+    const key = directoryKey(directory)
+    const cached = sdkCache.get(key)
+    if (cached) return cached
+    const sdk = globalSDK.createClient({
+      directory,
+      throwOnError: true,
+    })
+    sdkCache.set(key, sdk)
+    return sdk
+  }
+
+  const queryOptionsApi = makeQueryOptionsApi(() => globalSDK.client, sdkFor)
+
   const [configQuery, providerQuery, pathQuery] = useQueries(() => ({
-    queries: [
-      loadGlobalConfigQuery(globalSDK.client),
-      loadProvidersQuery(null, globalSDK.client),
-      loadPathQuery(null, globalSDK.client),
-    ],
+    queries: [queryOptionsApi.globalConfig(), queryOptionsApi.providers(null), queryOptionsApi.path(null)],
   }))
 
   const [globalStore, setGlobalStore] = createStore<GlobalStore>({
@@ -181,18 +203,6 @@ function createGlobalSync() {
     bootstrapInstance,
   })
 
-  const sdkFor = (directory: string) => {
-    const key = directoryKey(directory)
-    const cached = sdkCache.get(key)
-    if (cached) return cached
-    const sdk = globalSDK.createClient({
-      directory,
-      throwOnError: true,
-    })
-    sdkCache.set(key, sdk)
-    return sdk
-  }
-
   const children = createChildStoreManager({
     owner,
     isBooting: (directory) => booting.has(directory),
@@ -209,7 +219,7 @@ function createGlobalSync() {
       clearSessionPrefetchDirectory(key)
     },
     translate: language.t,
-    getSdk: sdkFor,
+    queryOptions: queryOptionsApi,
     global: {
       provider: globalStore.provider,
     },
@@ -239,7 +249,7 @@ function createGlobalSync() {
     const limit = Math.max(store.limit + SESSION_RECENT_LIMIT, SESSION_RECENT_LIMIT)
     const promise = queryClient
       .fetchQuery({
-        queryKey: loadSessionsQueryKey(key),
+        ...queryOptionsApi.sessions(key),
         queryFn: () =>
           loadRootSessionsWithFallback({
             directory,
@@ -368,7 +378,7 @@ function createGlobalSync() {
       setSessionTodo,
       vcsCache: children.vcsCache.get(key),
       loadLsp: () => {
-        void queryClient.fetchQuery(loadLspQuery(key, sdkFor(directory)))
+        void queryClient.fetchQuery(queryOptionsApi.lsp(key))
       },
     })
   })
@@ -426,6 +436,7 @@ function createGlobalSync() {
     },
     child: children.child,
     peek: children.peek,
+    queryOptions: queryOptionsApi,
     // bootstrap,
     updateConfig: updateConfigMutation.mutateAsync,
     project: projectApi,
@@ -447,3 +458,7 @@ export function useGlobalSync() {
   if (!context) throw new Error("useGlobalSync must be used within GlobalSyncProvider")
   return context
 }
+
+export function useQueryOptions() {
+  return useGlobalSync().queryOptions
+}

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

@@ -22,7 +22,7 @@ describe("createChildStoreManager", () => {
       onBootstrap() {},
       onDispose() {},
       translate: (key) => key,
-      getSdk: () => null!,
+      queryOptions: {} as any,
       global: { provider: null! },
     })
 

+ 7 - 10
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 { OpencodeClient, ProviderListResponse, VcsInfo } from "@opencode-ai/sdk/v2/client"
+import type { ProviderListResponse, VcsInfo } from "@opencode-ai/sdk/v2/client"
 import {
   DIR_IDLE_TTL_MS,
   MAX_DIR_STORES,
@@ -15,8 +15,7 @@ import {
 } from "./types"
 import { canDisposeDirectory, pickDirectoriesToEvict } from "./eviction"
 import { useQueries } from "@tanstack/solid-query"
-import { loadPathQuery, loadProvidersQuery } from "./bootstrap"
-import { loadLspQuery, loadMcpQuery } from "../global-sync"
+import { QueryOptionsApi } from "../global-sync"
 import { directoryKey, type DirectoryKey } from "./utils"
 
 export function createChildStoreManager(input: {
@@ -26,7 +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
+  queryOptions: QueryOptionsApi
   global: {
     provider: ProviderListResponse
   }
@@ -171,17 +170,15 @@ 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(key, sdk),
-              loadMcpQuery(key, sdk),
-              loadLspQuery(key, sdk),
-              loadProvidersQuery(key, sdk),
+              input.queryOptions.path(key),
+              input.queryOptions.mcp(key),
+              input.queryOptions.lsp(key),
+              input.queryOptions.providers(key),
             ],
           }))
 

+ 5 - 3
packages/app/src/pages/layout/sidebar-workspace.tsx

@@ -14,7 +14,7 @@ import { Spinner } from "@opencode-ai/ui/spinner"
 import { Tooltip } from "@opencode-ai/ui/tooltip"
 import { type Session } from "@opencode-ai/sdk/v2/client"
 import { type LocalProject } from "@/context/layout"
-import { loadSessionsQueryKey, useGlobalSync } from "@/context/global-sync"
+import { useGlobalSync, useQueryOptions } from "@/context/global-sync"
 import { useLanguage } from "@/context/language"
 import { pathKey } from "@/utils/path-key"
 import { NewSessionItem, SessionItem, SessionSkeleton } from "./sidebar-items"
@@ -300,6 +300,7 @@ export const SortableWorkspace = (props: {
   const navigate = useNavigate()
   const params = useParams()
   const globalSync = useGlobalSync()
+  const queryOptions = useQueryOptions()
   const language = useLanguage()
   const sortable = createSortable(props.directory)
   const [workspaceStore, setWorkspaceStore] = globalSync.child(props.directory, { bootstrap: false })
@@ -320,7 +321,7 @@ export const SortableWorkspace = (props: {
   const boot = createMemo(() => open() || active())
   const count = createMemo(() => sessions()?.length ?? 0)
   const hasMore = createMemo(() => workspaceStore.sessionTotal > count())
-  const fetching = useIsFetching(() => ({ queryKey: loadSessionsQueryKey(props.directory) }))
+  const fetching = useIsFetching(() => queryOptions.sessions(pathKey(props.directory)))
   const busy = createMemo(() => props.ctx.isBusy(props.directory))
   const loading = () => fetching() > 0 && count() === 0
   const touch = createMediaQuery("(hover: none)")
@@ -446,6 +447,7 @@ export const LocalWorkspace = (props: {
   mobile?: boolean
 }): JSX.Element => {
   const globalSync = useGlobalSync()
+  const queryOptions = useQueryOptions()
   const language = useLanguage()
   const workspace = createMemo(() => {
     const [store, setStore] = globalSync.child(props.project.worktree)
@@ -454,7 +456,7 @@ export const LocalWorkspace = (props: {
   const slug = createMemo(() => base64Encode(props.project.worktree))
   const sessions = createMemo(() => sortedRootSessions(workspace().store, props.sortNow()))
   const count = createMemo(() => sessions()?.length ?? 0)
-  const fetching = useIsFetching(() => ({ queryKey: loadSessionsQueryKey(props.project.worktree) }))
+  const fetching = useIsFetching(() => queryOptions.sessions(pathKey(props.project.worktree)))
   const hasMore = createMemo(() => workspace().store.sessionTotal > count())
   const loading = () => fetching() > 0 && count() === 0
   const loadMore = async () => {