| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365 |
- import {
- type Config,
- type Path,
- type Project,
- type ProviderAuthResponse,
- type ProviderListResponse,
- createOpencodeClient,
- } from "@opencode-ai/sdk/v2/client"
- import { createStore, produce, reconcile } from "solid-js/store"
- import { useGlobalSDK } from "./global-sdk"
- import type { InitError } from "../pages/error"
- import {
- createContext,
- createEffect,
- untrack,
- getOwner,
- useContext,
- onCleanup,
- onMount,
- type ParentProps,
- Switch,
- Match,
- } from "solid-js"
- import { showToast } from "@opencode-ai/ui/toast"
- import { getFilename } from "@opencode-ai/util/path"
- import { usePlatform } from "./platform"
- import { useLanguage } from "@/context/language"
- import { Persist, persisted } from "@/utils/persist"
- import { createRefreshQueue } from "./global-sync/queue"
- import { createChildStoreManager } from "./global-sync/child-store"
- import { trimSessions } from "./global-sync/session-trim"
- import { estimateRootSessionTotal, loadRootSessionsWithFallback } from "./global-sync/session-load"
- import { applyDirectoryEvent, applyGlobalEvent } from "./global-sync/event-reducer"
- import { bootstrapDirectory, bootstrapGlobal } from "./global-sync/bootstrap"
- import { sanitizeProject } from "./global-sync/utils"
- import type { ProjectMeta } from "./global-sync/types"
- import { SESSION_RECENT_LIMIT } from "./global-sync/types"
- type GlobalStore = {
- ready: boolean
- error?: InitError
- path: Path
- project: Project[]
- provider: ProviderListResponse
- provider_auth: ProviderAuthResponse
- config: Config
- reload: undefined | "pending" | "complete"
- }
- function createGlobalSync() {
- const globalSDK = useGlobalSDK()
- const platform = usePlatform()
- const language = useLanguage()
- const owner = getOwner()
- if (!owner) throw new Error("GlobalSync must be created within owner")
- const stats = {
- evictions: 0,
- loadSessionsFallback: 0,
- }
- const sdkCache = new Map<string, ReturnType<typeof createOpencodeClient>>()
- const booting = new Map<string, Promise<void>>()
- const sessionLoads = new Map<string, Promise<void>>()
- const sessionMeta = new Map<string, { limit: number }>()
- const [projectCache, setProjectCache, , projectCacheReady] = persisted(
- Persist.global("globalSync.project", ["globalSync.project.v1"]),
- createStore({ value: [] as Project[] }),
- )
- const [globalStore, setGlobalStore] = createStore<GlobalStore>({
- ready: false,
- path: { state: "", config: "", worktree: "", directory: "", home: "" },
- project: projectCache.value,
- provider: { all: [], connected: [], default: {} },
- provider_auth: {},
- config: {},
- reload: undefined,
- })
- const updateStats = (activeDirectoryStores: number) => {
- if (!import.meta.env.DEV) return
- ;(
- globalThis as {
- __OPENCODE_GLOBAL_SYNC_STATS?: {
- activeDirectoryStores: number
- evictions: number
- loadSessionsFullFetchFallback: number
- }
- }
- ).__OPENCODE_GLOBAL_SYNC_STATS = {
- activeDirectoryStores,
- evictions: stats.evictions,
- loadSessionsFullFetchFallback: stats.loadSessionsFallback,
- }
- }
- const paused = () => untrack(() => globalStore.reload) !== undefined
- const queue = createRefreshQueue({
- paused,
- bootstrap,
- bootstrapInstance,
- })
- const children = createChildStoreManager({
- owner,
- markStats: updateStats,
- incrementEvictions: () => {
- stats.evictions += 1
- updateStats(Object.keys(children.children).length)
- },
- 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)
- },
- })
- const sdkFor = (directory: string) => {
- const cached = sdkCache.get(directory)
- if (cached) return cached
- const sdk = createOpencodeClient({
- baseUrl: globalSDK.url,
- fetch: platform.fetch,
- directory,
- throwOnError: true,
- })
- sdkCache.set(directory, sdk)
- return sdk
- }
- createEffect(() => {
- if (!projectCacheReady()) return
- if (globalStore.project.length !== 0) return
- const cached = projectCache.value
- if (cached.length === 0) return
- setGlobalStore("project", cached)
- })
- createEffect(() => {
- if (!projectCacheReady()) return
- const projects = globalStore.project
- if (projects.length === 0) {
- const cachedLength = untrack(() => projectCache.value.length)
- if (cachedLength !== 0) return
- }
- setProjectCache("value", projects.map(sanitizeProject))
- })
- createEffect(() => {
- if (globalStore.reload !== "complete") return
- setGlobalStore("reload", undefined)
- queue.refresh()
- })
- async function loadSessions(directory: string) {
- const pending = sessionLoads.get(directory)
- if (pending) return pending
- children.pin(directory)
- const [store, setStore] = children.child(directory, { bootstrap: false })
- const meta = sessionMeta.get(directory)
- 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" }))
- }
- children.unpin(directory)
- return
- }
- const limit = Math.max(store.limit + SESSION_RECENT_LIMIT, SESSION_RECENT_LIMIT)
- const promise = loadRootSessionsWithFallback({
- directory,
- limit,
- list: (query) => globalSDK.client.session.list(query),
- onFallback: () => {
- stats.loadSessionsFallback += 1
- updateStats(Object.keys(children.children).length)
- },
- })
- .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 })
- setStore(
- "sessionTotal",
- estimateRootSessionTotal({ count: nonArchived.length, limit: x.limit, limited: x.limited }),
- )
- setStore("session", reconcile(sessions, { key: "id" }))
- sessionMeta.set(directory, { limit })
- })
- .catch((err) => {
- console.error("Failed to load sessions", err)
- const project = getFilename(directory)
- showToast({ title: language.t("toast.session.listFailed.title", { project }), description: err.message })
- })
- sessionLoads.set(directory, promise)
- promise.finally(() => {
- sessionLoads.delete(directory)
- children.unpin(directory)
- })
- return promise
- }
- async function bootstrapInstance(directory: string) {
- if (!directory) return
- const pending = booting.get(directory)
- if (pending) return pending
- children.pin(directory)
- const promise = (async () => {
- const child = children.ensureChild(directory)
- const cache = children.vcsCache.get(directory)
- if (!cache) return
- const sdk = sdkFor(directory)
- await bootstrapDirectory({
- directory,
- sdk,
- store: child[0],
- setStore: child[1],
- vcsCache: cache,
- loadSessions,
- })
- })()
- booting.set(directory, promise)
- promise.finally(() => {
- booting.delete(directory)
- children.unpin(directory)
- })
- return promise
- }
- const unsub = globalSDK.event.listen((e) => {
- const directory = e.name
- const event = e.details
- if (directory === "global") {
- applyGlobalEvent({
- event,
- project: globalStore.project,
- refresh: queue.refresh,
- setGlobalProject(next) {
- if (typeof next === "function") {
- setGlobalStore("project", produce(next))
- return
- }
- setGlobalStore("project", next)
- },
- })
- return
- }
- const existing = children.children[directory]
- if (!existing) return
- children.mark(directory)
- const [store, setStore] = existing
- applyDirectoryEvent({
- event,
- directory,
- store,
- setStore,
- push: queue.push,
- vcsCache: children.vcsCache.get(directory),
- loadLsp: () => {
- sdkFor(directory)
- .lsp.status()
- .then((x) => setStore("lsp", x.data ?? []))
- },
- })
- })
- onCleanup(unsub)
- onCleanup(() => {
- queue.dispose()
- })
- onCleanup(() => {
- for (const directory of Object.keys(children.children)) {
- children.disposeDirectory(directory)
- }
- })
- async function bootstrap() {
- await bootstrapGlobal({
- globalSDK: globalSDK.client,
- connectErrorTitle: language.t("dialog.server.add.error"),
- connectErrorDescription: language.t("error.globalSync.connectFailed", { url: globalSDK.url }),
- requestFailedTitle: language.t("common.requestFailed"),
- setGlobalStore,
- })
- }
- onMount(() => {
- void bootstrap()
- })
- function projectMeta(directory: string, patch: ProjectMeta) {
- children.projectMeta(directory, patch)
- }
- function projectIcon(directory: string, value: string | undefined) {
- children.projectIcon(directory, value)
- }
- return {
- data: globalStore,
- set: setGlobalStore,
- get ready() {
- return globalStore.ready
- },
- get error() {
- return globalStore.error
- },
- child: children.child,
- bootstrap,
- updateConfig: (config: Config) => {
- setGlobalStore("reload", "pending")
- return globalSDK.client.global.config.update({ config }).finally(() => {
- setTimeout(() => {
- setGlobalStore("reload", "complete")
- }, 1000)
- })
- },
- project: {
- loadSessions,
- meta: projectMeta,
- icon: projectIcon,
- },
- }
- }
- const GlobalSyncContext = createContext<ReturnType<typeof createGlobalSync>>()
- export function GlobalSyncProvider(props: ParentProps) {
- const value = createGlobalSync()
- return (
- <Switch>
- <Match when={value.ready}>
- <GlobalSyncContext.Provider value={value}>{props.children}</GlobalSyncContext.Provider>
- </Match>
- </Switch>
- )
- }
- export function useGlobalSync() {
- const context = useContext(GlobalSyncContext)
- if (!context) throw new Error("useGlobalSync must be used within GlobalSyncProvider")
- return context
- }
- export { canDisposeDirectory, pickDirectoriesToEvict } from "./global-sync/eviction"
- export { estimateRootSessionTotal, loadRootSessionsWithFallback } from "./global-sync/session-load"
|