import type { Config, OpencodeClient, Path, PermissionRequest, Project, ProviderAuthResponse, ProviderListResponse, QuestionRequest, Todo, } from "@opencode-ai/sdk/v2/client" import { showToast } from "@opencode-ai/ui/toast" import { getFilename } from "@opencode-ai/util/path" import { retry } from "@opencode-ai/util/retry" import { batch } from "solid-js" import { reconcile, type SetStoreFunction, type Store } from "solid-js/store" import type { State, VcsCache } from "./types" import { cmp, normalizeProviderList } from "./utils" import { formatServerError } from "@/utils/server-errors" type GlobalStore = { ready: boolean path: Path project: Project[] session_todo: { [sessionID: string]: Todo[] } provider: ProviderListResponse provider_auth: ProviderAuthResponse config: Config reload: undefined | "pending" | "complete" } export async function bootstrapGlobal(input: { globalSDK: OpencodeClient connectErrorTitle: string connectErrorDescription: string requestFailedTitle: string translate: (key: string, vars?: Record) => string formatMoreCount: (count: number) => string setGlobalStore: SetStoreFunction }) { const health = await input.globalSDK.global .health() .then((x) => x.data) .catch(() => undefined) if (!health?.healthy) { showToast({ variant: "error", title: input.connectErrorTitle, description: input.connectErrorDescription, }) input.setGlobalStore("ready", true) return } const tasks = [ retry(() => input.globalSDK.path.get().then((x) => { input.setGlobalStore("path", x.data!) }), ), retry(() => input.globalSDK.global.config.get().then((x) => { input.setGlobalStore("config", x.data!) }), ), retry(() => input.globalSDK.project.list().then((x) => { const projects = (x.data ?? []) .filter((p) => !!p?.id) .filter((p) => !!p.worktree && !p.worktree.includes("opencode-test")) .slice() .sort((a, b) => cmp(a.id, b.id)) input.setGlobalStore("project", projects) }), ), retry(() => input.globalSDK.provider.list().then((x) => { input.setGlobalStore("provider", normalizeProviderList(x.data!)) }), ), retry(() => input.globalSDK.provider.auth().then((x) => { input.setGlobalStore("provider_auth", x.data ?? {}) }), ), ] const results = await Promise.allSettled(tasks) const errors = results.filter((r): r is PromiseRejectedResult => r.status === "rejected").map((r) => r.reason) if (errors.length) { const message = formatServerError(errors[0], input.translate) const more = errors.length > 1 ? input.formatMoreCount(errors.length - 1) : "" showToast({ variant: "error", title: input.requestFailedTitle, description: message + more, }) } input.setGlobalStore("ready", true) } function groupBySession(input: T[]) { return input.reduce>((acc, item) => { if (!item?.id || !item.sessionID) return acc const list = acc[item.sessionID] if (list) list.push(item) if (!list) acc[item.sessionID] = [item] return acc }, {}) } export async function bootstrapDirectory(input: { directory: string sdk: OpencodeClient store: Store setStore: SetStoreFunction vcsCache: VcsCache loadSessions: (directory: string) => Promise | void translate: (key: string, vars?: Record) => string }) { if (input.store.status !== "complete") input.setStore("status", "loading") const blockingRequests = { project: () => input.sdk.project.current().then((x) => input.setStore("project", x.data!.id)), provider: () => input.sdk.provider.list().then((x) => { input.setStore("provider", normalizeProviderList(x.data!)) }), agent: () => input.sdk.app.agents().then((x) => input.setStore("agent", x.data ?? [])), config: () => input.sdk.config.get().then((x) => input.setStore("config", x.data!)), } try { await Promise.all(Object.values(blockingRequests).map((p) => retry(p))) } catch (err) { console.error("Failed to bootstrap instance", err) const project = getFilename(input.directory) showToast({ variant: "error", title: input.translate("toast.project.reloadFailed.title", { project }), description: formatServerError(err, input.translate), }) input.setStore("status", "partial") return } if (input.store.status !== "complete") input.setStore("status", "partial") Promise.all([ input.sdk.path.get().then((x) => input.setStore("path", x.data!)), input.sdk.command.list().then((x) => input.setStore("command", x.data ?? [])), input.sdk.session.status().then((x) => input.setStore("session_status", x.data!)), input.loadSessions(input.directory), input.sdk.mcp.status().then((x) => input.setStore("mcp", x.data!)), input.sdk.lsp.status().then((x) => input.setStore("lsp", x.data!)), input.sdk.vcs.get().then((x) => { const next = x.data ?? input.store.vcs input.setStore("vcs", next) if (next?.branch) input.vcsCache.setStore("value", next) }), input.sdk.permission.list().then((x) => { const grouped = groupBySession( (x.data ?? []).filter((perm): perm is PermissionRequest => !!perm?.id && !!perm.sessionID), ) batch(() => { for (const sessionID of Object.keys(input.store.permission)) { if (grouped[sessionID]) continue input.setStore("permission", sessionID, []) } for (const [sessionID, permissions] of Object.entries(grouped)) { input.setStore( "permission", sessionID, reconcile( permissions.filter((p) => !!p?.id).sort((a, b) => cmp(a.id, b.id)), { key: "id" }, ), ) } }) }), input.sdk.question.list().then((x) => { const grouped = groupBySession((x.data ?? []).filter((q): q is QuestionRequest => !!q?.id && !!q.sessionID)) batch(() => { for (const sessionID of Object.keys(input.store.question)) { if (grouped[sessionID]) continue input.setStore("question", sessionID, []) } for (const [sessionID, questions] of Object.entries(grouped)) { input.setStore( "question", sessionID, reconcile( questions.filter((q) => !!q?.id).sort((a, b) => cmp(a.id, b.id)), { key: "id" }, ), ) } }) }), ]).then(() => { input.setStore("status", "complete") }) }