| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206 |
- 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, string | number>) => string
- formatMoreCount: (count: number) => string
- setGlobalStore: SetStoreFunction<GlobalStore>
- }) {
- 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<T extends { id: string; sessionID: string }>(input: T[]) {
- return input.reduce<Record<string, T[]>>((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<State>
- setStore: SetStoreFunction<State>
- vcsCache: VcsCache
- loadSessions: (directory: string) => Promise<void> | void
- translate: (key: string, vars?: Record<string, string | number>) => 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")
- })
- }
|