bootstrap.ts 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  1. import type {
  2. Config,
  3. OpencodeClient,
  4. Path,
  5. PermissionRequest,
  6. Project,
  7. ProviderAuthResponse,
  8. ProviderListResponse,
  9. QuestionRequest,
  10. Todo,
  11. } from "@opencode-ai/sdk/v2/client"
  12. import { showToast } from "@opencode-ai/ui/toast"
  13. import { getFilename } from "@opencode-ai/util/path"
  14. import { retry } from "@opencode-ai/util/retry"
  15. import { batch } from "solid-js"
  16. import { reconcile, type SetStoreFunction, type Store } from "solid-js/store"
  17. import type { State, VcsCache } from "./types"
  18. import { cmp, normalizeProviderList } from "./utils"
  19. import { formatServerError } from "@/utils/server-errors"
  20. type GlobalStore = {
  21. ready: boolean
  22. path: Path
  23. project: Project[]
  24. session_todo: {
  25. [sessionID: string]: Todo[]
  26. }
  27. provider: ProviderListResponse
  28. provider_auth: ProviderAuthResponse
  29. config: Config
  30. reload: undefined | "pending" | "complete"
  31. }
  32. export async function bootstrapGlobal(input: {
  33. globalSDK: OpencodeClient
  34. connectErrorTitle: string
  35. connectErrorDescription: string
  36. requestFailedTitle: string
  37. translate: (key: string, vars?: Record<string, string | number>) => string
  38. formatMoreCount: (count: number) => string
  39. setGlobalStore: SetStoreFunction<GlobalStore>
  40. }) {
  41. const health = await input.globalSDK.global
  42. .health()
  43. .then((x) => x.data)
  44. .catch(() => undefined)
  45. if (!health?.healthy) {
  46. showToast({
  47. variant: "error",
  48. title: input.connectErrorTitle,
  49. description: input.connectErrorDescription,
  50. })
  51. input.setGlobalStore("ready", true)
  52. return
  53. }
  54. const tasks = [
  55. retry(() =>
  56. input.globalSDK.path.get().then((x) => {
  57. input.setGlobalStore("path", x.data!)
  58. }),
  59. ),
  60. retry(() =>
  61. input.globalSDK.global.config.get().then((x) => {
  62. input.setGlobalStore("config", x.data!)
  63. }),
  64. ),
  65. retry(() =>
  66. input.globalSDK.project.list().then((x) => {
  67. const projects = (x.data ?? [])
  68. .filter((p) => !!p?.id)
  69. .filter((p) => !!p.worktree && !p.worktree.includes("opencode-test"))
  70. .slice()
  71. .sort((a, b) => cmp(a.id, b.id))
  72. input.setGlobalStore("project", projects)
  73. }),
  74. ),
  75. retry(() =>
  76. input.globalSDK.provider.list().then((x) => {
  77. input.setGlobalStore("provider", normalizeProviderList(x.data!))
  78. }),
  79. ),
  80. retry(() =>
  81. input.globalSDK.provider.auth().then((x) => {
  82. input.setGlobalStore("provider_auth", x.data ?? {})
  83. }),
  84. ),
  85. ]
  86. const results = await Promise.allSettled(tasks)
  87. const errors = results.filter((r): r is PromiseRejectedResult => r.status === "rejected").map((r) => r.reason)
  88. if (errors.length) {
  89. const message = formatServerError(errors[0], input.translate)
  90. const more = errors.length > 1 ? input.formatMoreCount(errors.length - 1) : ""
  91. showToast({
  92. variant: "error",
  93. title: input.requestFailedTitle,
  94. description: message + more,
  95. })
  96. }
  97. input.setGlobalStore("ready", true)
  98. }
  99. function groupBySession<T extends { id: string; sessionID: string }>(input: T[]) {
  100. return input.reduce<Record<string, T[]>>((acc, item) => {
  101. if (!item?.id || !item.sessionID) return acc
  102. const list = acc[item.sessionID]
  103. if (list) list.push(item)
  104. if (!list) acc[item.sessionID] = [item]
  105. return acc
  106. }, {})
  107. }
  108. export async function bootstrapDirectory(input: {
  109. directory: string
  110. sdk: OpencodeClient
  111. store: Store<State>
  112. setStore: SetStoreFunction<State>
  113. vcsCache: VcsCache
  114. loadSessions: (directory: string) => Promise<void> | void
  115. translate: (key: string, vars?: Record<string, string | number>) => string
  116. }) {
  117. if (input.store.status !== "complete") input.setStore("status", "loading")
  118. const blockingRequests = {
  119. project: () => input.sdk.project.current().then((x) => input.setStore("project", x.data!.id)),
  120. provider: () =>
  121. input.sdk.provider.list().then((x) => {
  122. input.setStore("provider", normalizeProviderList(x.data!))
  123. }),
  124. agent: () => input.sdk.app.agents().then((x) => input.setStore("agent", x.data ?? [])),
  125. config: () => input.sdk.config.get().then((x) => input.setStore("config", x.data!)),
  126. }
  127. try {
  128. await Promise.all(Object.values(blockingRequests).map((p) => retry(p)))
  129. } catch (err) {
  130. console.error("Failed to bootstrap instance", err)
  131. const project = getFilename(input.directory)
  132. showToast({
  133. variant: "error",
  134. title: input.translate("toast.project.reloadFailed.title", { project }),
  135. description: formatServerError(err, input.translate),
  136. })
  137. input.setStore("status", "partial")
  138. return
  139. }
  140. if (input.store.status !== "complete") input.setStore("status", "partial")
  141. Promise.all([
  142. input.sdk.path.get().then((x) => input.setStore("path", x.data!)),
  143. input.sdk.command.list().then((x) => input.setStore("command", x.data ?? [])),
  144. input.sdk.session.status().then((x) => input.setStore("session_status", x.data!)),
  145. input.loadSessions(input.directory),
  146. input.sdk.mcp.status().then((x) => input.setStore("mcp", x.data!)),
  147. input.sdk.lsp.status().then((x) => input.setStore("lsp", x.data!)),
  148. input.sdk.vcs.get().then((x) => {
  149. const next = x.data ?? input.store.vcs
  150. input.setStore("vcs", next)
  151. if (next?.branch) input.vcsCache.setStore("value", next)
  152. }),
  153. input.sdk.permission.list().then((x) => {
  154. const grouped = groupBySession(
  155. (x.data ?? []).filter((perm): perm is PermissionRequest => !!perm?.id && !!perm.sessionID),
  156. )
  157. batch(() => {
  158. for (const sessionID of Object.keys(input.store.permission)) {
  159. if (grouped[sessionID]) continue
  160. input.setStore("permission", sessionID, [])
  161. }
  162. for (const [sessionID, permissions] of Object.entries(grouped)) {
  163. input.setStore(
  164. "permission",
  165. sessionID,
  166. reconcile(
  167. permissions.filter((p) => !!p?.id).sort((a, b) => cmp(a.id, b.id)),
  168. { key: "id" },
  169. ),
  170. )
  171. }
  172. })
  173. }),
  174. input.sdk.question.list().then((x) => {
  175. const grouped = groupBySession((x.data ?? []).filter((q): q is QuestionRequest => !!q?.id && !!q.sessionID))
  176. batch(() => {
  177. for (const sessionID of Object.keys(input.store.question)) {
  178. if (grouped[sessionID]) continue
  179. input.setStore("question", sessionID, [])
  180. }
  181. for (const [sessionID, questions] of Object.entries(grouped)) {
  182. input.setStore(
  183. "question",
  184. sessionID,
  185. reconcile(
  186. questions.filter((q) => !!q?.id).sort((a, b) => cmp(a.id, b.id)),
  187. { key: "id" },
  188. ),
  189. )
  190. }
  191. })
  192. }),
  193. ]).then(() => {
  194. input.setStore("status", "complete")
  195. })
  196. }