diff --git a/packages/app/src/components/dialog-select-mcp.tsx b/packages/app/src/components/dialog-select-mcp.tsx index 576ec8fec4b7..228af409c57d 100644 --- a/packages/app/src/components/dialog-select-mcp.tsx +++ b/packages/app/src/components/dialog-select-mcp.tsx @@ -6,7 +6,7 @@ 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" const statusLabels = { connected: "mcp.status.connected", @@ -20,6 +20,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 +33,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({ queryKey: queryOptions.mcp(sync.directory).queryKey }), })) const enabledCount = createMemo(() => items().filter((i) => i.status === "connected").length) diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx index 2417fa98e25a..baec1f45e53b 100644 --- a/packages/app/src/components/prompt-input.tsx +++ b/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,7 @@ 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" interface PromptInputProps { class?: string @@ -103,7 +102,7 @@ const NON_EMPTY_TEXT = /[^\s\u200B]/ export const PromptInput: Component = (props) => { const sdk = useSDK() - const globalSDK = useGlobalSDK() + const queryOptions = useQueryOptions() const sync = useSync() const local = useLocal() @@ -1255,11 +1254,7 @@ export const PromptInput: Component = (props) => { } const [agentsQuery, globalProvidersQuery, providersQuery] = useQueries(() => ({ - queries: [ - loadAgentsQuery(sdk.directory, sdk.client), - loadProvidersQuery(null, globalSDK.client), - loadProvidersQuery(sdk.directory, sdk.client), - ], + queries: [queryOptions.agents(sdk.directory), queryOptions.providers(null), queryOptions.providers(sdk.directory)], })) const agentsLoading = () => agentsQuery.isLoading diff --git a/packages/app/src/components/status-popover-body.tsx b/packages/app/src/components/status-popover-body.tsx index bbac56278428..436ac293e3e2 100644 --- a/packages/app/src/components/status-popover-body.tsx +++ b/packages/app/src/components/status-popover-body.tsx @@ -15,7 +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 { mcpQueryKey } from "@/context/global-sync" +import { useQueryOptions } from "@/context/global-sync" const pollMs = 10_000 @@ -139,13 +139,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({ queryKey: queryOptions.mcp(sync.directory).queryKey }), onError: (err) => { showToast({ variant: "error", diff --git a/packages/app/src/context/global-sync.tsx b/packages/app/src/context/global-sync.tsx index 31c90463d893..93b9de4c3025 100644 --- a/packages/app/src/context/global-sync.tsx +++ b/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" @@ -48,21 +50,15 @@ 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 ?? []), }) @@ -77,12 +73,33 @@ function createGlobalSync() { const sessionLoads = new Map>() const sessionMeta = new Map() + 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 = { + globalConfig: () => loadGlobalConfigQuery(globalSDK.client), + projects: () => loadProjectsQuery(globalSDK.client), + providers: (directory: string | null) => + loadProvidersQuery(directory, directory === null ? globalSDK.client : sdkFor(directory)), + path: (directory: string | null) => + loadPathQuery(directory, directory === null ? globalSDK.client : sdkFor(directory)), + agents: (directory: string) => loadAgentsQuery(directory, sdkFor(directory)), + mcp: (directory: string) => loadMcpQuery(directory, sdkFor(directory)), + lsp: (directory: string) => loadLspQuery(directory, sdkFor(directory)), + sessions: (directory: string) => ({ queryKey: [directory, "loadSessions"] as const }), + } + 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({ @@ -181,18 +198,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 +214,7 @@ function createGlobalSync() { clearSessionPrefetchDirectory(key) }, translate: language.t, - getSdk: sdkFor, + queryOptions: queryOptionsApi, global: { provider: globalStore.provider, }, @@ -239,7 +244,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 +373,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 +431,7 @@ function createGlobalSync() { }, child: children.child, peek: children.peek, + queryOptions: queryOptionsApi, // bootstrap, updateConfig: updateConfigMutation.mutateAsync, project: projectApi, @@ -447,3 +453,7 @@ export function useGlobalSync() { if (!context) throw new Error("useGlobalSync must be used within GlobalSyncProvider") return context } + +export function useQueryOptions() { + return useGlobalSync().queryOptions +} diff --git a/packages/app/src/context/global-sync/child-store.test.ts b/packages/app/src/context/global-sync/child-store.test.ts index 30dda86919b6..5e0bb1c007e5 100644 --- a/packages/app/src/context/global-sync/child-store.test.ts +++ b/packages/app/src/context/global-sync/child-store.test.ts @@ -22,7 +22,12 @@ describe("createChildStoreManager", () => { onBootstrap() {}, onDispose() {}, translate: (key) => key, - getSdk: () => null!, + queryOptions: { + lsp: () => null!, + mcp: () => null!, + path: () => null!, + providers: () => null!, + }, global: { provider: null! }, }) diff --git a/packages/app/src/context/global-sync/child-store.ts b/packages/app/src/context/global-sync/child-store.ts index 0138310cdccd..9941fba878ec 100644 --- a/packages/app/src/context/global-sync/child-store.ts +++ b/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, @@ -26,7 +26,12 @@ export function createChildStoreManager(input: { onBootstrap: (directory: string) => void onDispose: (directory: string) => void translate: (key: string, vars?: Record) => string - getSdk: (directory: string) => OpencodeClient + queryOptions: { + lsp: (directory: string) => ReturnType + mcp: (directory: string) => ReturnType + path: (directory: string) => ReturnType + providers: (directory: string) => ReturnType + } global: { provider: ProviderListResponse } @@ -171,17 +176,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), ], })) diff --git a/packages/app/src/pages/layout/sidebar-workspace.tsx b/packages/app/src/pages/layout/sidebar-workspace.tsx index 9b80adac29c4..6b0d38940c10 100644 --- a/packages/app/src/pages/layout/sidebar-workspace.tsx +++ b/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(() => ({ queryKey: queryOptions.sessions(props.directory).queryKey })) 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(() => ({ queryKey: queryOptions.sessions(props.project.worktree).queryKey })) const hasMore = createMemo(() => workspace().store.sessionTotal > count()) const loading = () => fetching() > 0 && count() === 0 const loadMore = async () => {