import { type Message, type Agent, type Session, type Part, type Config, type Path, type File, type FileNode, type Project, type FileDiff, type Todo, type SessionStatus, type ProviderListResponse, type ProviderAuthResponse, type Command, createOpencodeClient, } from "@opencode-ai/sdk/v2/client" import { createStore, produce, reconcile } from "solid-js/store" import { Binary } from "@opencode-ai/util/binary" import { createSimpleContext } from "@opencode-ai/ui/context" import { useGlobalSDK } from "./global-sdk" import { onMount } from "solid-js" type State = { ready: boolean agent: Agent[] command: Command[] project: string provider: ProviderListResponse config: Config path: Path session: Session[] session_status: { [sessionID: string]: SessionStatus } session_diff: { [sessionID: string]: FileDiff[] } todo: { [sessionID: string]: Todo[] } limit: number message: { [sessionID: string]: Message[] } part: { [messageID: string]: Part[] } node: FileNode[] changes: File[] } export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimpleContext({ name: "GlobalSync", init: () => { const globalSDK = useGlobalSDK() const [globalStore, setGlobalStore] = createStore<{ ready: boolean path: Path project: Project[] provider: ProviderListResponse provider_auth: ProviderAuthResponse children: Record }>({ ready: false, path: { state: "", config: "", worktree: "", directory: "", home: "" }, project: [], provider: { all: [], connected: [], default: {} }, provider_auth: {}, children: {}, }) const children: Record>> = {} function child(directory: string) { if (!children[directory]) { setGlobalStore("children", directory, { project: "", provider: { all: [], connected: [], default: {} }, config: {}, path: { state: "", config: "", worktree: "", directory: "", home: "" }, ready: false, agent: [], command: [], session: [], session_status: {}, session_diff: {}, todo: {}, limit: 5, message: {}, part: {}, node: [], changes: [], }) children[directory] = createStore(globalStore.children[directory]) bootstrapInstance(directory) } return children[directory] } async function loadSessions(directory: string) { const [store, setStore] = child(directory) globalSDK.client.session.list({ directory }).then((x) => { const fourHoursAgo = Date.now() - 4 * 60 * 60 * 1000 const nonArchived = (x.data ?? []) .slice() .filter((s) => !s.time.archived) .sort((a, b) => a.id.localeCompare(b.id)) // Include sessions up to the limit, plus any updated in the last hour const sessions = nonArchived.filter((s, i) => { if (i < store.limit) return true const updated = new Date(s.time.updated).getTime() return updated > fourHoursAgo }) setStore("session", sessions) }) } async function bootstrapInstance(directory: string) { const [, setStore] = child(directory) const sdk = createOpencodeClient({ baseUrl: globalSDK.url, directory, }) const load = { project: () => sdk.project.current().then((x) => setStore("project", x.data!.id)), provider: () => sdk.provider.list().then((x) => setStore("provider", x.data!)), path: () => sdk.path.get().then((x) => setStore("path", x.data!)), agent: () => sdk.app.agents().then((x) => setStore("agent", x.data ?? [])), command: () => sdk.command.list().then((x) => setStore("command", x.data ?? [])), session: () => loadSessions(directory), status: () => sdk.session.status().then((x) => setStore("session_status", x.data!)), config: () => sdk.config.get().then((x) => setStore("config", x.data!)), changes: () => sdk.file.status().then((x) => setStore("changes", x.data!)), node: () => sdk.file.list({ path: "/" }).then((x) => setStore("node", x.data!)), } await Promise.all(Object.values(load).map((p) => p())).then(() => setStore("ready", true)) } globalSDK.event.listen((e) => { const directory = e.name const event = e.details if (directory === "global") { switch (event?.type) { case "global.disposed": { bootstrap() break } case "project.updated": { const result = Binary.search(globalStore.project, event.properties.id, (s) => s.id) if (result.found) { setGlobalStore("project", result.index, reconcile(event.properties)) return } setGlobalStore( "project", produce((draft) => { draft.splice(result.index, 0, event.properties) }), ) break } } return } const [store, setStore] = child(directory) switch (event.type) { case "server.instance.disposed": { bootstrapInstance(directory) break } case "session.updated": { const result = Binary.search(store.session, event.properties.info.id, (s) => s.id) if (event.properties.info.time.archived) { if (result.found) { setStore( "session", produce((draft) => { draft.splice(result.index, 1) }), ) } break } if (result.found) { setStore("session", result.index, reconcile(event.properties.info)) break } setStore( "session", produce((draft) => { draft.splice(result.index, 0, event.properties.info) }), ) break } case "session.diff": setStore("session_diff", event.properties.sessionID, event.properties.diff) break case "todo.updated": setStore("todo", event.properties.sessionID, event.properties.todos) break case "session.status": { setStore("session_status", event.properties.sessionID, event.properties.status) break } case "message.updated": { const messages = store.message[event.properties.info.sessionID] if (!messages) { setStore("message", event.properties.info.sessionID, [event.properties.info]) break } const result = Binary.search(messages, event.properties.info.id, (m) => m.id) if (result.found) { setStore("message", event.properties.info.sessionID, result.index, reconcile(event.properties.info)) break } setStore( "message", event.properties.info.sessionID, produce((draft) => { draft.splice(result.index, 0, event.properties.info) }), ) break } case "message.removed": { const messages = store.message[event.properties.sessionID] if (!messages) break const result = Binary.search(messages, event.properties.messageID, (m) => m.id) if (result.found) { setStore( "message", event.properties.sessionID, produce((draft) => { draft.splice(result.index, 1) }), ) } break } case "message.part.updated": { const part = event.properties.part const parts = store.part[part.messageID] if (!parts) { setStore("part", part.messageID, [part]) break } const result = Binary.search(parts, part.id, (p) => p.id) if (result.found) { setStore("part", part.messageID, result.index, reconcile(part)) break } setStore( "part", part.messageID, produce((draft) => { draft.splice(result.index, 0, part) }), ) break } case "message.part.removed": { const parts = store.part[event.properties.messageID] if (!parts) break const result = Binary.search(parts, event.properties.partID, (p) => p.id) if (result.found) { setStore( "part", event.properties.messageID, produce((draft) => { draft.splice(result.index, 1) }), ) } break } } }) async function bootstrap() { return Promise.all([ globalSDK.client.path.get().then((x) => { setGlobalStore("path", x.data!) }), globalSDK.client.project.list().then(async (x) => { setGlobalStore( "project", x.data!.filter((p) => !p.worktree.includes("opencode-test")).sort((a, b) => a.id.localeCompare(b.id)), ) }), globalSDK.client.provider.list().then((x) => { setGlobalStore("provider", x.data ?? {}) }), globalSDK.client.provider.auth().then((x) => { setGlobalStore("provider_auth", x.data ?? {}) }), ]).then(() => setGlobalStore("ready", true)) } onMount(() => { bootstrap() }) return { data: globalStore, get ready() { return globalStore.ready }, child, bootstrap, project: { loadSessions, }, } }, })