| import type { |
| Config, |
| OpencodeClient, |
| Path, |
| Project, |
| ProviderAuthResponse, |
| SessionStatus, |
| } from "@opencode-ai/sdk/v2/client" |
| import { showToast } from "@/utils/toast" |
| import { getFilename } from "@opencode-ai/core/util/path" |
| import { type Accessor, batch, createMemo, getOwner, onCleanup, onMount, untrack } from "solid-js" |
| import { createStore, produce, reconcile } from "solid-js/store" |
| import { useLanguage } from "@/context/language" |
| import type { InitError } from "../pages/error" |
| import { ServerSDK } from "./server-sdk" |
| import { |
| bootstrapDirectory, |
| bootstrapGlobal, |
| clearProviderRev, |
| loadAgentsQuery, |
| loadCommands, |
| loadGlobalConfigQuery, |
| loadPathQuery, |
| loadProjectsQuery, |
| loadProvidersQuery, |
| loadReferencesQuery, |
| } from "./global-sync/bootstrap" |
| import { createChildStoreManager } from "./global-sync/child-store" |
| import { applyDirectoryEvent, applyGlobalEvent } from "./global-sync/event-reducer" |
| import { estimateRootSessionTotal, loadRootSessions, loadRootSessionsV1 } 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, useMutation, useQueries, useQuery, useQueryClient } from "@tanstack/solid-query" |
| import type { SolidQueryOptions } from "@tanstack/solid-query" |
| import { createRefreshQueue } from "./global-sync/queue" |
| import { directoryKey } from "./global-sync/utils" |
| import { PathKey } from "@/utils/path-key" |
| import { createDirSyncContext } from "./directory-sync" |
| import { createSimpleContext } from "@opencode-ai/ui/context" |
| import { NormalizedProviderListResponse } from "@opencode-ai/session-ui/context" |
| import { createRefCountMap } from "@/utils/refcount" |
| import { useGlobal } from "./global" |
| import { ServerConnection, useServer } from "./server" |
| import { retry } from "@opencode-ai/core/util/retry" |
| import type { ServerScope } from "@/utils/server-scope" |
| import { createHomeSessionIndexCache } from "./global-sync/home-session-index" |
| import { persisted } from "@/utils/persist" |
| import type { ServerApi } from "@/utils/server" |
| import type { |
| McpListInput, |
| McpListOutput, |
| McpResource, |
| McpResourceCatalogInput, |
| McpResourceCatalogOutput, |
| McpServer, |
| SessionActiveOutput, |
| } from "@opencode-ai/client/promise" |
| import { toggleMcp } from "./global-sync/mcp" |
| import { createServerSession, type ServerSession } from "./server-session" |
|
|
| type GlobalStore = { |
| ready: boolean |
| error?: InitError |
| path: Path |
| project: Project[] |
| provider: NormalizedProviderListResponse |
| provider_auth: ProviderAuthResponse |
| config: Config |
| reload: undefined | "pending" | "complete" |
| } |
|
|
| type McpListApi = { |
| readonly list: (input?: McpListInput) => Promise<McpListOutput> |
| } |
|
|
| type McpResourceApi = { |
| readonly resource: { |
| readonly catalog: (input?: McpResourceCatalogInput) => Promise<McpResourceCatalogOutput> |
| } |
| } |
|
|
| type ApiQueryOptions<T, K extends readonly unknown[]> = SolidQueryOptions<T, Error, T, K> & { |
| initialData?: undefined |
| queryKey: K |
| } |
|
|
| type SessionActiveApi = { |
| readonly active: () => Promise<SessionActiveOutput> |
| } |
|
|
| export const loadMcpQuery = ( |
| scope: ServerScope, |
| directory: string, |
| api: McpListApi, |
| legacy?: OpencodeClient, |
| protocol?: Promise<"v1" | "v2">, |
| ): ApiQueryOptions<Record<string, McpServer["status"]>, readonly [ServerScope, string, "mcp"]> => |
| queryOptions< |
| Record<string, McpServer["status"]>, |
| Error, |
| Record<string, McpServer["status"]>, |
| readonly [ServerScope, string, "mcp"] |
| >({ |
| queryKey: [scope, directory, "mcp"] as const, |
| queryFn: async () => { |
| if ((await protocol) === "v1" && legacy) return (await legacy.mcp.status()).data ?? {} |
| return api |
| .list({ location: { directory } }) |
| .then((result) => Object.fromEntries(result.data.map((server) => [server.name, server.status]))) |
| }, |
| }) |
|
|
| export const loadMcpResourcesQuery = ( |
| scope: ServerScope, |
| directory: string, |
| api: McpResourceApi, |
| legacy?: OpencodeClient, |
| protocol?: Promise<"v1" | "v2">, |
| ): ApiQueryOptions<Record<string, McpResource>, readonly [ServerScope, string, "mcpResources"]> => |
| queryOptions< |
| Record<string, McpResource>, |
| Error, |
| Record<string, McpResource>, |
| readonly [ServerScope, string, "mcpResources"] |
| >({ |
| queryKey: [scope, directory, "mcpResources"] as const, |
| queryFn: async () => { |
| if ((await protocol) === "v1" && legacy) { |
| return Object.fromEntries( |
| Object.entries((await legacy.experimental.resource.list()).data ?? {}).map(([key, resource]) => [ |
| key, |
| { ...resource, server: resource.client }, |
| ]), |
| ) |
| } |
| return api.resource |
| .catalog({ location: { directory } }) |
| .then((result) => |
| Object.fromEntries(result.data.resources.map((resource) => [`${resource.server}:${resource.uri}`, resource])), |
| ) |
| }, |
| placeholderData: {}, |
| }) |
|
|
| export const loadLspQuery = (scope: ServerScope, directory: string, sdk: OpencodeClient) => |
| queryOptions({ |
| queryKey: [scope, directory, "lsp"] as const, |
| queryFn: () => sdk.lsp.status().then((r) => r.data ?? []), |
| }) |
|
|
| export const loadActiveSessionsQuery = ( |
| scope: ServerScope, |
| api: SessionActiveApi, |
| ): ApiQueryOptions<SessionActiveOutput, readonly [ServerScope, "activeSessions"]> => |
| queryOptions<SessionActiveOutput, Error, SessionActiveOutput, readonly [ServerScope, "activeSessions"]>({ |
| queryKey: [scope, "activeSessions"] as const, |
| queryFn: () => api.active(), |
| enabled: true, |
| staleTime: Number.POSITIVE_INFINITY, |
| gcTime: Number.POSITIVE_INFINITY, |
| refetchOnMount: false, |
| refetchOnReconnect: false, |
| refetchOnWindowFocus: false, |
| }) |
|
|
| export function seedActiveSessionStatuses( |
| session: Pick<ServerSession, "data" | "set">, |
| active: SessionActiveOutput | Record<string, SessionStatus>, |
| ) { |
| for (const sessionID of Object.keys(active)) { |
| if (session.data.session_status[sessionID] !== undefined) continue |
| const status = active[sessionID] |
| session.set("session_status", sessionID, status?.type === "running" ? { type: "busy" } : status) |
| } |
| } |
|
|
| function makeQueryOptionsApi( |
| scope: ServerScope, |
| serverSDK: () => OpencodeClient, |
| serverAPI: ServerApi, |
| sdkFor: (dir: PathKey) => OpencodeClient, |
| protocol: Promise<"v1" | "v2">, |
| ) { |
| return { |
| globalConfig: () => loadGlobalConfigQuery(scope, serverSDK(), protocol), |
| projects: () => loadProjectsQuery(scope, serverAPI.project), |
| providers: (directory: PathKey | null) => |
| loadProvidersQuery(scope, directory, serverAPI, directory ? sdkFor(directory) : serverSDK(), protocol), |
| path: (directory: PathKey | null) => |
| loadPathQuery(scope, directory, directory ? sdkFor(directory) : serverSDK(), protocol), |
| agents: (directory: PathKey) => loadAgentsQuery(scope, directory, serverAPI.agent, sdkFor(directory), protocol), |
| references: (directory: PathKey) => |
| loadReferencesQuery(scope, directory, serverAPI.reference, sdkFor(directory), protocol), |
| mcp: (directory: PathKey) => loadMcpQuery(scope, directory, serverAPI.mcp, sdkFor(directory), protocol), |
| mcpResources: (directory: PathKey) => |
| loadMcpResourcesQuery(scope, directory, serverAPI.mcp, sdkFor(directory), protocol), |
| lsp: (directory: PathKey) => loadLspQuery(scope, directory, sdkFor(directory)), |
| sessions: (directory: PathKey) => ({ queryKey: [scope, directory, "loadSessions"] as const }), |
| } |
| } |
| export type QueryOptionsApi = ReturnType<typeof makeQueryOptionsApi> |
|
|
| export function createServerSyncContextInner(serverSDK: ServerSDK) { |
| const language = useLanguage() |
| const owner = getOwner() |
| if (!owner) throw new Error("ServerSync must be created within owner") |
|
|
| const sdkCache = new Map<string, OpencodeClient>() |
| const booting = new Map<string, Promise<void>>() |
| 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 = serverSDK.createClient({ |
| directory, |
| throwOnError: true, |
| }) |
| sdkCache.set(key, sdk) |
| return sdk |
| } |
|
|
| const session = createServerSession(serverSDK.client, serverSDK.api.session, serverSDK.api.message, { |
| protocol: serverSDK.protocol, |
| }) |
| const queryOptionsApi = makeQueryOptionsApi( |
| serverSDK.scope, |
| () => serverSDK.client, |
| serverSDK.api, |
| sdkFor, |
| serverSDK.protocol, |
| ) |
|
|
| const [configQuery, providerQuery, pathQuery] = useQueries(() => ({ |
| queries: [queryOptionsApi.globalConfig(), queryOptionsApi.providers(null), queryOptionsApi.path(null)], |
| })) |
| const activeSessionsQuery = useQuery(() => |
| loadActiveSessionsQuery(serverSDK.scope, { |
| active: async () => { |
| if ((await serverSDK.protocol) === "v1") { |
| const statuses = (await serverSDK.client.session.status()).data ?? {} |
| seedActiveSessionStatuses(session, statuses) |
| for (const sessionID of Object.keys(statuses)) { |
| void session.resolve(sessionID).catch(() => undefined) |
| } |
| return Object.fromEntries( |
| Object.entries(statuses).flatMap(([sessionID, status]) => |
| status.type === "idle" ? [] : [[sessionID, { type: "running" as const }]], |
| ), |
| ) |
| } |
| const active = await serverSDK.api.session.active() |
| seedActiveSessionStatuses(session, active) |
| for (const sessionID of Object.keys(active)) { |
| void session.resolve(sessionID).catch(() => undefined) |
| } |
| return active |
| }, |
| }), |
| ) |
|
|
| const [globalStore, setGlobalStore] = createStore<GlobalStore>({ |
| get ready() { |
| return !bootstrap.isPending |
| }, |
| project: [], |
| provider_auth: {}, |
| get path() { |
| const EMPTY = { state: "", config: "", worktree: "", directory: "", home: "" } |
| if (pathQuery.isLoading) return EMPTY |
| return pathQuery.data ?? EMPTY |
| }, |
| get provider() { |
| const EMPTY = { all: new Map(), 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() |
| const homeSessions = createHomeSessionIndexCache(queryClient, ServerConnection.key(serverSDK.server)) |
| const refreshProviders = () => |
| queryClient.refetchQueries({ |
| predicate: (query) => query.queryKey[0] === serverSDK.scope && query.queryKey[2] === "providers", |
| }) |
|
|
| let bootedAt = 0 |
| let bootingRoot = false |
| let eventFrame: number | undefined |
| let eventTimer: ReturnType<typeof setTimeout> | undefined |
|
|
| onCleanup(() => { |
| if (eventFrame !== undefined) cancelAnimationFrame(eventFrame) |
| if (eventTimer !== undefined) clearTimeout(eventTimer) |
| }) |
|
|
| const setProjects = (next: Project[] | ((draft: Project[]) => Project[])) => { |
| setGlobalStore("project", next) |
| } |
|
|
| const setBootStore = ((...input: unknown[]) => { |
| if (input[0] === "project" && Array.isArray(input[1])) { |
| setProjects(input[1] as Project[]) |
| return input[1] |
| } |
| return (setGlobalStore as (...args: unknown[]) => unknown)(...input) |
| }) as typeof setGlobalStore |
|
|
| const bootstrap = useQuery(() => ({ |
| queryKey: [serverSDK.scope, "bootstrap"], |
| queryFn: async () => { |
| await bootstrapGlobal({ |
| serverSDK: serverSDK.client, |
| serverAPI: serverSDK.api, |
| protocol: serverSDK.protocol, |
| scope: serverSDK.scope, |
| 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[])) |
| return input[1] |
| } |
| return (setGlobalStore as (...args: unknown[]) => unknown)(...input) |
| }) as typeof setGlobalStore |
|
|
| const paused = () => untrack(() => globalStore.reload) !== undefined |
|
|
| const queue = createRefreshQueue({ |
| paused, |
| key: directoryKey, |
| bootstrap: () => queryClient.fetchQuery({ queryKey: [serverSDK.scope, "bootstrap"] }), |
| bootstrapInstance, |
| }) |
|
|
| const children = createChildStoreManager({ |
| owner, |
| scope: serverSDK.scope, |
| persist: persisted, |
| isBooting: (directory) => booting.has(directory), |
| isLoadingSessions: (directory) => sessionLoads.has(directory), |
| onBootstrap: (directory) => { |
| void bootstrapInstance(directory) |
| }, |
| onMcp: (directory, setStore) => { |
| void loadCommands(directory, serverSDK.api.command, sdkFor(directory), serverSDK.protocol) |
| .then((commands) => setStore("command", commands)) |
| .catch((err) => { |
| showToast({ |
| variant: "error", |
| title: language.t("toast.project.reloadFailed.title", { project: getFilename(directory) }), |
| description: formatServerError(err, language.t), |
| }) |
| }) |
| }, |
| onDispose: (directory) => { |
| const key = directoryKey(directory) |
| queue.clear(key) |
| sessionMeta.delete(key) |
| sdkCache.delete(key) |
| clearProviderRev(serverSDK.scope, key) |
| }, |
| translate: language.t, |
| queryOptions: queryOptionsApi, |
| global: { |
| provider: globalStore.provider, |
| }, |
| }) |
|
|
| async function loadSessions(directory: string, options?: { limit?: number }) { |
| const key = directoryKey(directory) |
| const pending = sessionLoads.get(key) |
| if (pending) { |
| await pending |
| return loadSessions(directory, options) |
| } |
|
|
| children.pin(key) |
| const [store, setStore] = children.child(directory, { bootstrap: false }) |
| const meta = sessionMeta.get(key) |
| const retainedLimit = Math.max(store.limit, options?.limit ?? 0, meta?.limit ?? 0) |
| if (meta && meta.limit >= retainedLimit) { |
| const next = trimSessions(store.session, { |
| limit: retainedLimit, |
| permission: session.data.permission, |
| }) |
| if (next.length !== store.session.length) { |
| setStore("session", reconcile(next, { key: "id" })) |
| } |
| children.unpin(key) |
| return |
| } |
|
|
| const limit = Math.max(retainedLimit + SESSION_RECENT_LIMIT, SESSION_RECENT_LIMIT) |
| const promise = queryClient |
| .fetchQuery({ |
| ...queryOptionsApi.sessions(key), |
| queryFn: () => |
| serverSDK.protocol |
| .then((protocol) => |
| protocol === "v1" |
| ? loadRootSessionsV1({ client: sdkFor(directory), directory, limit }) |
| : loadRootSessions({ api: serverSDK.api.session, directory, limit }), |
| ) |
| .then((x) => { |
| const nonArchived = (x.data ?? []) |
| .filter((s) => !!s?.id) |
| .filter((s) => !s.time?.archived) |
| .sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)) |
| const limit = Math.max(store.limit, options?.limit ?? 0, sessionMeta.get(key)?.limit ?? 0) |
| const childSessions = store.session.filter((s) => !!s.parentID) |
| const next = trimSessions([...nonArchived, ...childSessions], { |
| limit, |
| permission: session.data.permission, |
| }) |
| batch(() => { |
| next.forEach(session.remember) |
| setStore( |
| "sessionTotal", |
| estimateRootSessionTotal({ |
| count: nonArchived.length, |
| limit: x.limit, |
| limited: x.limited, |
| }), |
| ) |
| setStore("session", reconcile(next, { key: "id" })) |
| }) |
| sessionMeta.set(key, { limit }) |
| }) |
| .catch((err) => { |
| console.error("Failed to load sessions", err) |
| const project = getFilename(directory) |
| showToast({ |
| variant: "error", |
| title: language.t("toast.session.listFailed.title", { project }), |
| description: formatServerError(err, language.t), |
| }) |
| }) |
| .then(() => null), |
| }) |
| .then(() => {}) |
|
|
| sessionLoads.set(key, promise) |
| void promise.finally(() => { |
| sessionLoads.delete(key) |
| children.unpin(key) |
| }) |
| return promise |
| } |
|
|
| async function bootstrapInstance(directory: string) { |
| const key = directoryKey(directory) |
| if (!key) return |
| const pending = booting.get(key) |
| if (pending) return pending |
|
|
| children.pin(key) |
| const promise = Promise.resolve().then(async () => { |
| const child = children.ensureChild(directory) |
| const cache = children.vcsCache.get(key) |
| if (!cache) return |
| const sdk = sdkFor(directory) |
| await bootstrapDirectory({ |
| directory, |
| scope: serverSDK.scope, |
| mcp: children.mcp(key), |
| global: { |
| config: globalStore.config, |
| path: globalStore.path, |
| project: globalStore.project, |
| provider: globalStore.provider, |
| }, |
| sdk, |
| api: serverSDK.api, |
| store: child[0], |
| setStore: child[1], |
| vcsCache: cache, |
| loadSessions, |
| translate: language.t, |
| queryClient, |
| session, |
| protocol: serverSDK.protocol, |
| }) |
| }) |
|
|
| booting.set(key, promise) |
| void promise.finally(() => { |
| booting.delete(key) |
| children.unpin(key) |
| }) |
| return promise |
| } |
|
|
| const indexSession = (info: Parameters<typeof session.remember>[0]) => { |
| const key = directoryKey(info.directory) |
| const existing = children.children[key] |
| if (!existing) return |
| applyDirectoryEvent({ |
| event: { type: "session.created", properties: { info } }, |
| directory: key, |
| store: existing[0], |
| setStore: existing[1], |
| push: queue.push, |
| retainedLimit: sessionMeta.get(key)?.limit, |
| sessionContent: false, |
| permission: session.data.permission, |
| loadLsp() {}, |
| }) |
| } |
|
|
| const unsub = serverSDK.event.listen((e) => { |
| const directory = e.name |
| const key = directoryKey(directory) |
| const event = e.details |
| const eventType: string = event.type |
| const recent = bootingRoot || Date.now() - bootedAt < 1500 |
|
|
| if (event.current) session.applyV2(event.current) |
| session.apply(event) |
| if (event.type === "session.created" || event.type === "session.updated" || event.type === "session.deleted") { |
| homeSessions.apply(event) |
| } |
| homeSessions.refresh(event.type) |
| if (eventType === "integration.connection.updated") void refreshProviders() |
|
|
| if (directory === "global") { |
| if (eventType === "server.connected" && activeSessionsQuery.data === undefined && !activeSessionsQuery.isFetching) |
| void activeSessionsQuery.refetch() |
| applyGlobalEvent({ |
| event, |
| project: globalStore.project, |
| refresh: () => { |
| if (recent) return |
| bootstrap.refetch() |
| }, |
| setGlobalProject: setProjects, |
| }) |
| if ( |
| eventType === "config.updated" || |
| eventType === "catalog.updated" || |
| eventType === "agent.updated" || |
| eventType === "project.directories.updated" |
| ) |
| bootstrap.refetch() |
| if (eventType === "server.connected" || eventType === "global.disposed") { |
| if (recent) return |
| for (const directory of Object.keys(children.children)) { |
| if (!children.active(directory)) continue |
| queue.push(directory) |
| } |
| } |
| return |
| } |
|
|
| if (event.current?.type === "session.moved") { |
| const info = session.get(event.current.data.sessionID) |
| if (info) indexSession(info) |
| } |
| if (event.current?.type === "session.forked") |
| void session |
| .resolve(event.current.data.sessionID, { force: true }) |
| .then(indexSession) |
| .catch(() => {}) |
|
|
| const existing = children.children[key] |
| if (!existing) return |
| children.mark(key) |
| if ( |
| event.current?.type === "session.moved" || |
| |
| event.current?.type === "session.forked" || |
| eventType === "command.updated" || |
| eventType === "config.updated" || |
| eventType === "agent.updated" |
| ) |
| queue.push(key) |
| if (eventType === "mcp.status.changed") void queryClient.invalidateQueries(queryOptionsApi.mcp(key)) |
| if (eventType === "mcp.resources.changed") void queryClient.invalidateQueries(queryOptionsApi.mcpResources(key)) |
| const [store, setStore] = existing |
| applyDirectoryEvent({ |
| event, |
| directory, |
| store, |
| setStore, |
| push: (directory) => { |
| if (children.active(directory)) queue.push(directory) |
| }, |
| retainedLimit: sessionMeta.get(key)?.limit, |
| sessionContent: false, |
| permission: session.data.permission, |
| vcsCache: children.vcsCache.get(key), |
| loadLsp: () => { |
| if (!children.active(key)) return |
| void queryClient.fetchQuery(queryOptionsApi.lsp(key)) |
| }, |
| loadReferences: () => { |
| if (!children.active(key)) return |
| void queryClient.fetchQuery(queryOptionsApi.references(key)) |
| }, |
| }) |
| }) |
|
|
| onCleanup(unsub) |
| onCleanup(() => { |
| queue.dispose() |
| }) |
| onCleanup(() => { |
| for (const directory of Object.keys(children.children)) { |
| children.disposeDirectory(directoryKey(directory)) |
| } |
| }) |
|
|
| onMount(() => { |
| if (typeof requestAnimationFrame === "function") { |
| eventFrame = requestAnimationFrame(() => { |
| eventFrame = undefined |
| eventTimer = setTimeout(() => { |
| eventTimer = undefined |
| void serverSDK.event.start() |
| }, 0) |
| }) |
| } else { |
| eventTimer = setTimeout(() => { |
| eventTimer = undefined |
| void serverSDK.event.start() |
| }, 0) |
| } |
| }) |
|
|
| const projectApi = { |
| loadSessions, |
| meta(directory: string, patch: ProjectMeta) { |
| children.projectMeta(directory, patch) |
| }, |
| icon(directory: string, value: string | undefined) { |
| children.projectIcon(directory, value) |
| }, |
| } |
|
|
| const updateConfigMutation = useMutation(() => ({ |
| mutationFn: (config: Config) => serverSDK.client.global.config.update({ config }), |
| onSuccess: () => { |
| bootstrap.refetch() |
| |
| |
| queryClient.invalidateQueries({ queryKey: [serverSDK.scope, null, "providers"] }) |
| queryClient.invalidateQueries({ |
| predicate: (query) => query.queryKey[0] === serverSDK.scope && query.queryKey[2] === "providers", |
| }) |
| }, |
| })) |
|
|
| return { |
| data: globalStore, |
| set, |
| get ready() { |
| return globalStore.ready |
| }, |
| get error() { |
| return globalStore.error |
| }, |
| child: children.child, |
| peek: children.peek, |
| disableMcp: children.disableMcp, |
| queryOptions: queryOptionsApi, |
| refreshProviders, |
| |
| updateConfig: updateConfigMutation.mutateAsync, |
| project: projectApi, |
| session, |
| homeSessions, |
| mcp: { |
| toggle: async (directory: string, name: string) => { |
| const key = directoryKey(directory) |
| const sdk = sdkFor(key) |
| const status = children.child(key, { bootstrap: false })[0].mcp[name]?.status |
| if (!status) return |
| await toggleMcp({ |
| status, |
| connect: async () => { |
| if ((await serverSDK.protocol) === "v1") { |
| await sdk.mcp.connect({ name }) |
| return |
| } |
| await serverSDK.api.mcp.connect({ server: name, location: { directory: key } }) |
| }, |
| disconnect: async () => { |
| if ((await serverSDK.protocol) === "v1") { |
| await sdk.mcp.disconnect({ name }) |
| return |
| } |
| await serverSDK.api.mcp.disconnect({ server: name, location: { directory: key } }) |
| }, |
| authenticate: async () => { |
| await sdk.mcp.auth.authenticate({ name }) |
| }, |
| refresh: async () => { |
| await queryClient.refetchQueries(queryOptionsApi.mcp(key)) |
| await queryClient.refetchQueries(queryOptionsApi.mcpResources(key)) |
| }, |
| }) |
| }, |
| }, |
| } |
| } |
|
|
| export function createServerSyncContext(serverSDK: ServerSDK) { |
| const inner = createServerSyncContextInner(serverSDK) |
| return Object.assign(inner, { |
| ensureDirSyncContext: createRefCountMap( |
| (dir) => createDirSyncContext(dir, inner, serverSDK), |
| (dir) => inner.disableMcp(dir), |
| directoryKey, |
| ), |
| }) |
| } |
|
|
| export type ServerSync = ReturnType<typeof createServerSyncContext> |
|
|
| export const { use: useServerSync, provider: ServerSyncProvider } = createSimpleContext({ |
| name: "ServerSync", |
| |
| |
| init: (props: { server?: Accessor<ServerConnection.Any | undefined> }) => { |
| const global = useGlobal() |
| const language = useLanguage() |
| const server = useServer() |
|
|
| return createMemo<ServerSync>(() => { |
| const conn = props.server?.() ?? server.current |
| if (!conn) throw new Error(language.t("error.serverSDK.noServerAvailable")) |
| return global.ensureServerCtx(conn).sync |
| }) |
| }, |
| }) |
|
|
| export function useQueryOptions() { |
| const sync = useServerSync() |
| return createMemo(() => sync().queryOptions) |
| } |
|
|