diff --git a/packages/app/src/app.tsx b/packages/app/src/app.tsx
index 18c6fef30a..bf8138fcde 100644
--- a/packages/app/src/app.tsx
+++ b/packages/app/src/app.tsx
@@ -82,7 +82,15 @@ declare global {
}
function QueryProvider(props: ParentProps) {
- const client = new QueryClient()
+ const client = new QueryClient({
+ defaultOptions: {
+ queries: {
+ refetchOnReconnect: false,
+ refetchOnMount: false,
+ refetchOnWindowFocus: false,
+ },
+ },
+ })
return {props.children}
}
diff --git a/packages/app/src/components/dialog-select-mcp.tsx b/packages/app/src/components/dialog-select-mcp.tsx
index 98f262ce5a..9bb36d32d8 100644
--- a/packages/app/src/components/dialog-select-mcp.tsx
+++ b/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)
diff --git a/packages/app/src/components/status-popover-body.tsx b/packages/app/src/components/status-popover-body.tsx
index 0f6a1c1355..952e3eac64 100644
--- a/packages/app/src/components/status-popover-body.tsx
+++ b/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 }) {
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 }) {
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
diff --git a/packages/app/src/context/global-sync.tsx b/packages/app/src/context/global-sync.tsx
index 86496bad50..2c80f31b19 100644
--- a/packages/app/src/context/global-sync.tsx
+++ b/packages/app/src/context/global-sync.tsx
@@ -14,17 +14,25 @@ import { createStore, produce, reconcile } from "solid-js/store"
import { useLanguage } from "@/context/language"
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"
import type { ProjectMeta } from "./global-sync/types"
import { SESSION_RECENT_LIMIT } from "./global-sync/types"
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"
+import { createRefreshQueue } from "./global-sync/queue"
type GlobalStore = {
ready: boolean
@@ -43,6 +51,18 @@ type GlobalStore = {
export const loadSessionsQuery = (directory: string) =>
queryOptions({ 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()
@@ -54,15 +74,34 @@ function createGlobalSync() {
const sessionLoads = new Map>()
const sessionMeta = new Map()
+ const [configQuery, providerQuery, pathQuery] = useQueries(() => ({
+ queries: [loadGlobalConfigQuery(), loadProvidersQuery(null), loadPathQuery(null), loadProjectsQuery()],
+ }))
+
const [globalStore, setGlobalStore] = createStore({
- ready: false,
- path: { state: "", config: "", worktree: "", directory: "", home: "" },
+ get ready() {
+ return bootstrap.isPending
+ },
project: [],
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()
@@ -88,6 +127,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[]))
@@ -114,10 +169,21 @@ function createGlobalSync() {
const queue = createRefreshQueue({
paused,
- bootstrap,
+ 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,
isBooting: (directory) => booting.has(directory),
@@ -133,19 +199,9 @@ function createGlobalSync() {
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
@@ -264,26 +320,13 @@ function createGlobalSync() {
const event = e.details
const recent = bootingRoot || Date.now() - bootedAt < 1500
- if (event.type === "session.error") {
- const error = event.properties.error
- if (error?.name !== "MessageAbortedError") {
- console.error("[global-sync] session error", {
- scope: directory === "global" ? "global" : "workspace",
- directory: directory === "global" ? undefined : directory,
- project: directory === "global" ? undefined : getFilename(directory),
- sessionID: event.properties.sessionID,
- error,
- })
- }
- }
-
if (directory === "global") {
applyGlobalEvent({
event,
project: globalStore.project,
refresh: () => {
if (recent) return
- queue.refresh()
+ bootstrap.refetch()
},
setGlobalProject: setProjects,
})
@@ -309,12 +352,7 @@ function createGlobalSync() {
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)))
},
})
})
@@ -329,23 +367,6 @@ function createGlobalSync() {
}
})
- 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(() => {
@@ -361,7 +382,6 @@ function createGlobalSync() {
void globalSDK.event.start()
}, 0)
}
- void bootstrap()
})
const projectApi = {
@@ -374,21 +394,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,
@@ -401,8 +410,8 @@ function createGlobalSync() {
},
child: children.child,
peek: children.peek,
- bootstrap,
- updateConfig,
+ // bootstrap,
+ updateConfig: updateConfigMutation.mutateAsync,
project: projectApi,
todo: {
set: setSessionTodo,
diff --git a/packages/app/src/context/global-sync/bootstrap.ts b/packages/app/src/context/global-sync/bootstrap.ts
index a83030fad2..451531835d 100644
--- a/packages/app/src/context/global-sync/bootstrap.ts
+++ b/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>) {
return Promise.allSettled(list.map((item) => item()))
}
+function showErrors(input: {
+ errors: unknown[]
+ title: string
+ translate: (key: string, vars?: Record) => 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>) => 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>["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,53 +131,15 @@ export async function bootstrapGlobal(input: {
setGlobalStore: SetStoreFunction
queryClient: QueryClient
}) {
- const fast = [
- () =>
- retry(() =>
- input.globalSDK.global.config.get().then((x) => {
- input.setGlobalStore("config", reconcile(x.data!, { merge: false }))
- }),
- ),
- ]
-
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(),
@@ -128,7 +147,6 @@ export async function bootstrapGlobal(input: {
// translate: input.translate,
// formatMoreCount: input.formatMoreCount,
// })
- input.setGlobalStore("ready", true)
}
function groupBySession(input: T[]) {
@@ -179,26 +197,28 @@ function warmSessions(input: {
).then(() => undefined)
}
-export const loadProvidersQuery = (directory: string | null) =>
- queryOptions({ 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>) => void,
) =>
- queryOptions({
+ 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 +228,15 @@ export const loadPathQuery = (
) =>
queryOptions({
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 +266,6 @@ export async function bootstrapDirectory(input: {
if (Object.keys(input.store.config).length === 0 && Object.keys(input.global.config).length > 0) {
input.setStore("config", reconcile(input.global.config, { merge: false }))
}
- 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
@@ -340,33 +352,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)[]
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 eee763f16d..24b4a46500 100644
--- a/packages/app/src/context/global-sync/child-store.test.ts
+++ b/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) => {
diff --git a/packages/app/src/context/global-sync/child-store.ts b/packages/app/src/context/global-sync/child-store.ts
index f3b613a7f2..d3b82894a4 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 { 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
+ getSdk: (directory: string) => OpencodeClient
}) {
const children: Record, SetStoreFunction]> = {}
const vcsCache = new Map()
@@ -156,14 +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({
project: "",
- projectMeta: undefined,
+ projectMeta: initialMeta,
icon: initialIcon,
- provider_ready: false,
+ get provider_ready() {
+ return providerQuery.isLoading
+ },
provider: { all: [], connected: [], default: {} },
config: {},
get path() {
@@ -181,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: {},
@@ -207,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)