global-sync.tsx 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257
  1. import {
  2. type Message,
  3. type Agent,
  4. type Session,
  5. type Part,
  6. type Config,
  7. type Path,
  8. type File,
  9. type FileNode,
  10. type Project,
  11. type FileDiff,
  12. type Todo,
  13. type SessionStatus,
  14. type ProviderListResponse,
  15. type ProviderAuthResponse,
  16. createOpencodeClient,
  17. } from "@opencode-ai/sdk/v2/client"
  18. import { createStore, produce, reconcile } from "solid-js/store"
  19. import { Binary } from "@opencode-ai/util/binary"
  20. import { createSimpleContext } from "@opencode-ai/ui/context"
  21. import { useGlobalSDK } from "./global-sdk"
  22. import { onMount } from "solid-js"
  23. type State = {
  24. ready: boolean
  25. agent: Agent[]
  26. project: string
  27. provider: ProviderListResponse
  28. config: Config
  29. path: Path
  30. session: Session[]
  31. session_status: {
  32. [sessionID: string]: SessionStatus
  33. }
  34. session_diff: {
  35. [sessionID: string]: FileDiff[]
  36. }
  37. todo: {
  38. [sessionID: string]: Todo[]
  39. }
  40. limit: number
  41. message: {
  42. [sessionID: string]: Message[]
  43. }
  44. part: {
  45. [messageID: string]: Part[]
  46. }
  47. node: FileNode[]
  48. changes: File[]
  49. }
  50. export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimpleContext({
  51. name: "GlobalSync",
  52. init: () => {
  53. const globalSDK = useGlobalSDK()
  54. const [globalStore, setGlobalStore] = createStore<{
  55. ready: boolean
  56. project: Project[]
  57. provider: ProviderListResponse
  58. provider_auth: ProviderAuthResponse
  59. children: Record<string, State>
  60. }>({
  61. ready: false,
  62. project: [],
  63. provider: { all: [], connected: [], default: {} },
  64. provider_auth: {},
  65. children: {},
  66. })
  67. async function bootstrapInstance(directory: string) {
  68. const [store, setStore] = child(directory)
  69. const sdk = createOpencodeClient({
  70. baseUrl: globalSDK.url,
  71. directory,
  72. })
  73. const load = {
  74. project: () => sdk.project.current().then((x) => setStore("project", x.data!.id)),
  75. provider: () => sdk.provider.list().then((x) => setStore("provider", x.data!)),
  76. path: () => sdk.path.get().then((x) => setStore("path", x.data!)),
  77. agent: () => sdk.app.agents().then((x) => setStore("agent", x.data ?? [])),
  78. session: () =>
  79. sdk.session.list().then((x) => {
  80. const sessions = (x.data ?? [])
  81. .slice()
  82. .sort((a, b) => a.id.localeCompare(b.id))
  83. .slice(0, store.limit)
  84. setStore("session", sessions)
  85. }),
  86. status: () => sdk.session.status().then((x) => setStore("session_status", x.data!)),
  87. config: () => sdk.config.get().then((x) => setStore("config", x.data!)),
  88. changes: () => sdk.file.status().then((x) => setStore("changes", x.data!)),
  89. node: () => sdk.file.list({ path: "/" }).then((x) => setStore("node", x.data!)),
  90. }
  91. await Promise.all(Object.values(load).map((p) => p())).then(() => setStore("ready", true))
  92. }
  93. const children: Record<string, ReturnType<typeof createStore<State>>> = {}
  94. function child(directory: string) {
  95. if (!children[directory]) {
  96. setGlobalStore("children", directory, {
  97. project: "",
  98. provider: { all: [], connected: [], default: {} },
  99. config: {},
  100. path: { state: "", config: "", worktree: "", directory: "", home: "" },
  101. ready: false,
  102. agent: [],
  103. session: [],
  104. session_status: {},
  105. session_diff: {},
  106. todo: {},
  107. limit: 5,
  108. message: {},
  109. part: {},
  110. node: [],
  111. changes: [],
  112. })
  113. children[directory] = createStore(globalStore.children[directory])
  114. bootstrapInstance(directory)
  115. }
  116. return children[directory]
  117. }
  118. globalSDK.event.listen((e) => {
  119. const directory = e.name
  120. const event = e.details
  121. if (directory === "global") {
  122. switch (event.type) {
  123. case "global.disposed": {
  124. bootstrap()
  125. break
  126. }
  127. case "project.updated": {
  128. const result = Binary.search(globalStore.project, event.properties.id, (s) => s.id)
  129. if (result.found) {
  130. setGlobalStore("project", result.index, reconcile(event.properties))
  131. return
  132. }
  133. setGlobalStore(
  134. "project",
  135. produce((draft) => {
  136. draft.splice(result.index, 0, event.properties)
  137. }),
  138. )
  139. break
  140. }
  141. }
  142. return
  143. }
  144. const [store, setStore] = child(directory)
  145. switch (event.type) {
  146. case "server.instance.disposed": {
  147. bootstrapInstance(directory)
  148. break
  149. }
  150. case "session.updated": {
  151. const result = Binary.search(store.session, event.properties.info.id, (s) => s.id)
  152. if (result.found) {
  153. setStore("session", result.index, reconcile(event.properties.info))
  154. break
  155. }
  156. setStore(
  157. "session",
  158. produce((draft) => {
  159. draft.splice(result.index, 0, event.properties.info)
  160. }),
  161. )
  162. break
  163. }
  164. case "session.diff":
  165. setStore("session_diff", event.properties.sessionID, event.properties.diff)
  166. break
  167. case "todo.updated":
  168. setStore("todo", event.properties.sessionID, event.properties.todos)
  169. break
  170. case "session.status": {
  171. setStore("session_status", event.properties.sessionID, event.properties.status)
  172. break
  173. }
  174. case "message.updated": {
  175. const messages = store.message[event.properties.info.sessionID]
  176. if (!messages) {
  177. setStore("message", event.properties.info.sessionID, [event.properties.info])
  178. break
  179. }
  180. const result = Binary.search(messages, event.properties.info.id, (m) => m.id)
  181. if (result.found) {
  182. setStore("message", event.properties.info.sessionID, result.index, reconcile(event.properties.info))
  183. break
  184. }
  185. setStore(
  186. "message",
  187. event.properties.info.sessionID,
  188. produce((draft) => {
  189. draft.splice(result.index, 0, event.properties.info)
  190. }),
  191. )
  192. break
  193. }
  194. case "message.part.updated": {
  195. const part = event.properties.part
  196. const parts = store.part[part.messageID]
  197. if (!parts) {
  198. setStore("part", part.messageID, [part])
  199. break
  200. }
  201. const result = Binary.search(parts, part.id, (p) => p.id)
  202. if (result.found) {
  203. setStore("part", part.messageID, result.index, reconcile(part))
  204. break
  205. }
  206. setStore(
  207. "part",
  208. part.messageID,
  209. produce((draft) => {
  210. draft.splice(result.index, 0, part)
  211. }),
  212. )
  213. break
  214. }
  215. }
  216. })
  217. async function bootstrap() {
  218. return Promise.all([
  219. globalSDK.client.project.list().then(async (x) => {
  220. setGlobalStore(
  221. "project",
  222. x
  223. .data!.filter((p) => !p.worktree.includes("opencode-test") && p.vcs)
  224. .sort((a, b) => a.id.localeCompare(b.id)),
  225. )
  226. }),
  227. globalSDK.client.provider.list().then((x) => {
  228. setGlobalStore("provider", x.data ?? {})
  229. }),
  230. globalSDK.client.provider.auth().then((x) => {
  231. setGlobalStore("provider_auth", x.data ?? {})
  232. }),
  233. ]).then(() => setGlobalStore("ready", true))
  234. }
  235. onMount(() => {
  236. bootstrap()
  237. })
  238. return {
  239. data: globalStore,
  240. get ready() {
  241. return globalStore.ready
  242. },
  243. child,
  244. bootstrap,
  245. }
  246. },
  247. })