diff --git a/packages/app/src/components/dialog-select-mcp.tsx b/packages/app/src/components/dialog-select-mcp.tsx index 0f5aebc6d1..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 f2cdd1a6a4..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", diff --git a/packages/app/src/context/global-sync.tsx b/packages/app/src/context/global-sync.tsx index 6f6de45c07..136d8cb158 100644 --- a/packages/app/src/context/global-sync.tsx +++ b/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>() const sessionMeta = new Map() - 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 | 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,27 +167,10 @@ function createGlobalSync() { const paused = () => untrack(() => globalStore.reload) !== undefined - // const queue = createRefreshQueue({ - // paused, - // bootstrap: () => queryClient.fetchQuery({ queryKey: ["bootstrap"] }), - // bootstrapInstance, - // }) - - const children = createChildStoreManager({ - owner, - isBooting: (directory) => booting.has(directory), - isLoadingSessions: (directory) => sessionLoads.has(directory), - onBootstrap: (directory) => { - void bootstrapInstance(directory) - }, - onDispose: (directory) => { - // queue.clear(directory) - sessionMeta.delete(directory) - sdkCache.delete(directory) - clearProviderRev(directory) - clearSessionPrefetchDirectory(directory) - }, - translate: language.t, + const queue = createRefreshQueue({ + paused, + bootstrap: () => queryClient.fetchQuery({ queryKey: ["bootstrap"] }), + bootstrapInstance, }) const sdkFor = (directory: string) => { @@ -231,6 +184,24 @@ function createGlobalSync() { return sdk } + const children = createChildStoreManager({ + owner, + isBooting: (directory) => booting.has(directory), + isLoadingSessions: (directory) => sessionLoads.has(directory), + onBootstrap: (directory) => { + void bootstrapInstance(directory) + }, + onDispose: (directory) => { + queue.clear(directory) + sessionMeta.delete(directory) + sdkCache.delete(directory) + clearProviderRev(directory) + clearSessionPrefetchDirectory(directory) + }, + translate: language.t, + getSdk: sdkFor, + }) + 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 { diff --git a/packages/app/src/context/global-sync/bootstrap.ts b/packages/app/src/context/global-sync/bootstrap.ts index 68895baeca..aed7d232a8 100644 --- a/packages/app/src/context/global-sync/bootstrap.ts +++ b/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)[] await waitForPaint() 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 10704f35ab..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, @@ -25,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() @@ -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({ 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)