Download packages/app/src/context/server-sync.tsx from SaylorTwift/opencode: direct link, hf CLI and curl.
- Browser
- Download file 26.1 kB
-
https://huggingface.co/SaylorTwift/opencode/resolve/main/packages/app/src/context/server-sync.tsx
- Command line
-
hf download hf://SaylorTwift/opencode/packages/app/src/context/server-sync.tsx
-
curl -L -o server-sync.tsx https://huggingface.co/SaylorTwift/opencode/resolve/main/packages/app/src/context/server-sync.tsx
26.1 kB
| 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.archived" || | |
| 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() | |
| // Invalidate all provider queries so newly configured custom providers | |
| // appear immediately in the available provider list across all directories. | |
| 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, | |
| // bootstrap, | |
| 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", | |
| // Returns an accessor so the resolved server can change reactively without | |
| // re-instantiating the subtree (mirrors useServerSDK). | |
| 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) | |
| } | |