import type { Config, OpencodeClient, Path, Project, ProviderAuthResponse, ProviderListResponse, Todo, } from "@opencode-ai/sdk/v2/client" import { showToast } from "@opencode-ai/ui/toast" import { getFilename } from "@opencode-ai/core/util/path" import { batch, createContext, getOwner, onCleanup, onMount, type ParentProps, untrack, useContext } from "solid-js" 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, 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 { 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, useMutation, useQueries, useQuery, useQueryClient } from "@tanstack/solid-query" import { createRefreshQueue } from "./global-sync/queue" import { directoryKey } from "./global-sync/utils" type GlobalStore = { ready: boolean error?: InitError path: Path project: Project[] session_todo: { [sessionID: string]: Todo[] } provider: ProviderListResponse provider_auth: ProviderAuthResponse config: Config reload: undefined | "pending" | "complete" } 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() const owner = getOwner() if (!owner) throw new Error("GlobalSync must be created within owner") const sdkCache = new Map() const booting = new Map>() const sessionLoads = new Map>() const sessionMeta = new Map() const [configQuery, providerQuery, pathQuery] = useQueries(() => ({ queries: [loadGlobalConfigQuery(), loadProvidersQuery(null), loadPathQuery(null), loadProjectsQuery()], })) const [globalStore, setGlobalStore] = createStore({ get ready() { return bootstrap.isPending }, project: [], session_todo: {}, 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: [], 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() let bootedAt = 0 let bootingRoot = false let eventFrame: number | undefined let eventTimer: ReturnType | 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: ["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[])) return input[1] } return (setGlobalStore as (...args: unknown[]) => unknown)(...input) }) as typeof setGlobalStore const setSessionTodo = (sessionID: string, todos: Todo[] | undefined) => { if (!sessionID) return if (!todos) { setGlobalStore( "session_todo", produce((draft) => { delete draft[sessionID] }), ) return } setGlobalStore("session_todo", sessionID, reconcile(todos, { key: "id" })) } const paused = () => untrack(() => globalStore.reload) !== undefined const queue = createRefreshQueue({ paused, key: directoryKey, bootstrap: () => queryClient.fetchQuery({ queryKey: ["bootstrap"] }), bootstrapInstance, }) const sdkFor = (directory: string) => { const key = directoryKey(directory) const cached = sdkCache.get(key) if (cached) return cached const sdk = globalSDK.createClient({ directory, throwOnError: true, }) sdkCache.set(key, sdk) return sdk } const children = createChildStoreManager({ owner, isBooting: (directory) => booting.has(directory), isLoadingSessions: (directory) => sessionLoads.has(directory), onBootstrap: (directory) => { void bootstrapInstance(directory) }, onDispose: (directory) => { const key = directoryKey(directory) queue.clear(key) sessionMeta.delete(key) sdkCache.delete(key) clearProviderRev(key) clearSessionPrefetchDirectory(key) }, translate: language.t, getSdk: sdkFor, global: { provider: globalStore.provider, }, }) async function loadSessions(directory: string) { const key = directoryKey(directory) const pending = sessionLoads.get(key) if (pending) return pending children.pin(key) const [store, setStore] = children.child(directory, { bootstrap: false }) const meta = sessionMeta.get(key) if (meta && meta.limit >= store.limit) { const next = trimSessions(store.session, { limit: store.limit, permission: store.permission, }) if (next.length !== store.session.length) { setStore("session", reconcile(next, { key: "id" })) cleanupDroppedSessionCaches(store, setStore, next, setSessionTodo) } children.unpin(key) return } const limit = Math.max(store.limit + SESSION_RECENT_LIMIT, SESSION_RECENT_LIMIT) const promise = queryClient .fetchQuery({ ...loadSessionsQuery(key), queryFn: () => loadRootSessionsWithFallback({ directory, limit, list: (query) => globalSDK.client.session.list(query), }) .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 = store.limit const childSessions = store.session.filter((s) => !!s.parentID) const sessions = trimSessions([...nonArchived, ...childSessions], { limit, permission: store.permission, }) batch(() => { setStore( "sessionTotal", estimateRootSessionTotal({ count: nonArchived.length, limit: x.limit, limited: x.limited, }), ) setStore("session", reconcile(sessions, { key: "id" })) cleanupDroppedSessionCaches(store, setStore, sessions, setSessionTodo) }) 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, global: { config: globalStore.config, path: globalStore.path, project: globalStore.project, provider: globalStore.provider, }, sdk, store: child[0], setStore: child[1], vcsCache: cache, loadSessions, translate: language.t, queryClient, }) }) booting.set(key, promise) void promise.finally(() => { booting.delete(key) children.unpin(key) }) return promise } const unsub = globalSDK.event.listen((e) => { const directory = e.name const key = directoryKey(directory) const event = e.details const recent = bootingRoot || Date.now() - bootedAt < 1500 if (directory === "global") { applyGlobalEvent({ event, project: globalStore.project, refresh: () => { if (recent) return bootstrap.refetch() }, setGlobalProject: setProjects, }) if (event.type === "server.connected" || event.type === "global.disposed") { if (recent) return for (const directory of Object.keys(children.children)) { queue.push(directory) } } return } const existing = children.children[key] if (!existing) return children.mark(key) const [store, setStore] = existing applyDirectoryEvent({ event, directory, store, setStore, push: queue.push, setSessionTodo, vcsCache: children.vcsCache.get(key), loadLsp: () => { void queryClient.fetchQuery(loadLspQuery(key, sdkFor(directory))) }, }) }) 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 globalSDK.event.start() }, 0) }) } else { eventTimer = setTimeout(() => { eventTimer = undefined void globalSDK.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) => globalSDK.client.global.config.update({ config }), onSuccess: () => bootstrap.refetch(), })) return { data: globalStore, set, get ready() { return globalStore.ready }, get error() { return globalStore.error }, child: children.child, peek: children.peek, // bootstrap, updateConfig: updateConfigMutation.mutateAsync, project: projectApi, todo: { set: setSessionTodo, }, } } const GlobalSyncContext = createContext>() export function GlobalSyncProvider(props: ParentProps) { const value = createGlobalSync() return {props.children} } export function useGlobalSync() { const context = useContext(GlobalSyncContext) if (!context) throw new Error("useGlobalSync must be used within GlobalSyncProvider") return context }