sync.tsx 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307
  1. import type {
  2. Message,
  3. Agent,
  4. Provider,
  5. Session,
  6. Part,
  7. Config,
  8. Todo,
  9. Command,
  10. Permission,
  11. LspStatus,
  12. McpStatus,
  13. FormatterStatus,
  14. } from "@opencode-ai/sdk"
  15. import { createStore, produce, reconcile } from "solid-js/store"
  16. import { useSDK } from "@tui/context/sdk"
  17. import { Binary } from "@/util/binary"
  18. import { createSimpleContext } from "./helper"
  19. import type { Snapshot } from "@/snapshot"
  20. import { useExit } from "./exit"
  21. import { onMount } from "solid-js"
  22. export const { use: useSync, provider: SyncProvider } = createSimpleContext({
  23. name: "Sync",
  24. init: () => {
  25. const [store, setStore] = createStore<{
  26. status: "loading" | "partial" | "complete"
  27. provider: Provider[]
  28. agent: Agent[]
  29. command: Command[]
  30. permission: {
  31. [sessionID: string]: Permission[]
  32. }
  33. config: Config
  34. session: Session[]
  35. session_diff: {
  36. [sessionID: string]: Snapshot.FileDiff[]
  37. }
  38. todo: {
  39. [sessionID: string]: Todo[]
  40. }
  41. message: {
  42. [sessionID: string]: Message[]
  43. }
  44. part: {
  45. [messageID: string]: Part[]
  46. }
  47. lsp: LspStatus[]
  48. mcp: {
  49. [key: string]: McpStatus
  50. }
  51. formatter: FormatterStatus[]
  52. }>({
  53. config: {},
  54. status: "loading",
  55. agent: [],
  56. permission: {},
  57. command: [],
  58. provider: [],
  59. session: [],
  60. session_diff: {},
  61. todo: {},
  62. message: {},
  63. part: {},
  64. lsp: [],
  65. mcp: {},
  66. formatter: [],
  67. })
  68. const sdk = useSDK()
  69. sdk.event.listen((e) => {
  70. const event = e.details
  71. switch (event.type) {
  72. case "permission.updated": {
  73. const permissions = store.permission[event.properties.sessionID]
  74. if (!permissions) {
  75. setStore("permission", event.properties.sessionID, [event.properties])
  76. break
  77. }
  78. const match = Binary.search(permissions, event.properties.id, (p) => p.id)
  79. setStore(
  80. "permission",
  81. event.properties.sessionID,
  82. produce((draft) => {
  83. if (match.found) {
  84. draft[match.index] = event.properties
  85. return
  86. }
  87. draft.push(event.properties)
  88. }),
  89. )
  90. break
  91. }
  92. case "permission.replied": {
  93. const permissions = store.permission[event.properties.sessionID]
  94. const match = Binary.search(permissions, event.properties.permissionID, (p) => p.id)
  95. if (!match.found) break
  96. setStore(
  97. "permission",
  98. event.properties.sessionID,
  99. produce((draft) => {
  100. draft.splice(match.index, 1)
  101. }),
  102. )
  103. break
  104. }
  105. case "todo.updated":
  106. setStore("todo", event.properties.sessionID, event.properties.todos)
  107. break
  108. case "session.diff":
  109. setStore("session_diff", event.properties.sessionID, event.properties.diff)
  110. break
  111. case "session.deleted": {
  112. const result = Binary.search(store.session, event.properties.info.id, (s) => s.id)
  113. if (result.found) {
  114. setStore(
  115. "session",
  116. produce((draft) => {
  117. draft.splice(result.index, 1)
  118. }),
  119. )
  120. }
  121. break
  122. }
  123. case "session.updated":
  124. const result = Binary.search(store.session, event.properties.info.id, (s) => s.id)
  125. if (result.found) {
  126. setStore("session", result.index, reconcile(event.properties.info))
  127. break
  128. }
  129. setStore(
  130. "session",
  131. produce((draft) => {
  132. draft.splice(result.index, 0, event.properties.info)
  133. }),
  134. )
  135. break
  136. case "message.updated": {
  137. const messages = store.message[event.properties.info.sessionID]
  138. if (!messages) {
  139. setStore("message", event.properties.info.sessionID, [event.properties.info])
  140. break
  141. }
  142. const result = Binary.search(messages, event.properties.info.id, (m) => m.id)
  143. if (result.found) {
  144. setStore("message", event.properties.info.sessionID, result.index, reconcile(event.properties.info))
  145. break
  146. }
  147. setStore(
  148. "message",
  149. event.properties.info.sessionID,
  150. produce((draft) => {
  151. draft.splice(result.index, 0, event.properties.info)
  152. if (draft.length > 100) draft.shift()
  153. }),
  154. )
  155. break
  156. }
  157. case "message.removed": {
  158. const messages = store.message[event.properties.sessionID]
  159. const result = Binary.search(messages, event.properties.messageID, (m) => m.id)
  160. if (result.found) {
  161. setStore(
  162. "message",
  163. event.properties.sessionID,
  164. produce((draft) => {
  165. draft.splice(result.index, 1)
  166. }),
  167. )
  168. }
  169. break
  170. }
  171. case "message.part.updated": {
  172. const parts = store.part[event.properties.part.messageID]
  173. if (!parts) {
  174. setStore("part", event.properties.part.messageID, [event.properties.part])
  175. break
  176. }
  177. const result = Binary.search(parts, event.properties.part.id, (p) => p.id)
  178. if (result.found) {
  179. setStore("part", event.properties.part.messageID, result.index, reconcile(event.properties.part))
  180. break
  181. }
  182. setStore(
  183. "part",
  184. event.properties.part.messageID,
  185. produce((draft) => {
  186. draft.splice(result.index, 0, event.properties.part)
  187. }),
  188. )
  189. break
  190. }
  191. case "message.part.removed": {
  192. const parts = store.part[event.properties.messageID]
  193. const result = Binary.search(parts, event.properties.partID, (p) => p.id)
  194. if (result.found)
  195. setStore(
  196. "part",
  197. event.properties.messageID,
  198. produce((draft) => {
  199. draft.splice(result.index, 1)
  200. }),
  201. )
  202. break
  203. }
  204. case "lsp.updated": {
  205. sdk.client.lsp.status().then((x) => setStore("lsp", x.data!))
  206. break
  207. }
  208. }
  209. })
  210. const exit = useExit()
  211. onMount(() => {
  212. // blocking
  213. Promise.all([
  214. sdk.client.config.providers({ throwOnError: true }).then((x) => setStore("provider", x.data!.providers)),
  215. sdk.client.app.agents({ throwOnError: true }).then((x) => setStore("agent", x.data ?? [])),
  216. sdk.client.config.get({ throwOnError: true }).then((x) => setStore("config", x.data!)),
  217. ])
  218. .then(() => {
  219. setStore("status", "partial")
  220. // non-blocking
  221. Promise.all([
  222. sdk.client.session.list().then((x) =>
  223. setStore(
  224. "session",
  225. (x.data ?? []).toSorted((a, b) => a.id.localeCompare(b.id)),
  226. ),
  227. ),
  228. sdk.client.command.list().then((x) => setStore("command", x.data ?? [])),
  229. sdk.client.lsp.status().then((x) => setStore("lsp", x.data!)),
  230. sdk.client.mcp.status().then((x) => setStore("mcp", x.data!)),
  231. sdk.client.formatter.status().then((x) => setStore("formatter", x.data!)),
  232. ]).then(() => {
  233. setStore("status", "complete")
  234. })
  235. })
  236. .catch(async (e) => {
  237. await exit(e)
  238. })
  239. })
  240. const result = {
  241. data: store,
  242. set: setStore,
  243. get status() {
  244. return store.status
  245. },
  246. get ready() {
  247. return store.status !== "loading"
  248. },
  249. session: {
  250. get(sessionID: string) {
  251. const match = Binary.search(store.session, sessionID, (s) => s.id)
  252. if (match.found) return store.session[match.index]
  253. return undefined
  254. },
  255. status(sessionID: string) {
  256. const session = result.session.get(sessionID)
  257. if (!session) return "idle"
  258. if (session.time.compacting) return "compacting"
  259. const messages = store.message[sessionID] ?? []
  260. const last = messages.at(-1)
  261. if (!last) return "idle"
  262. if (last.role === "user") return "working"
  263. return last.time.completed ? "idle" : "working"
  264. },
  265. async sync(sessionID: string) {
  266. if (store.message[sessionID]) return
  267. const now = Date.now()
  268. console.log("syncing", sessionID)
  269. const [session, messages, todo, diff] = await Promise.all([
  270. sdk.client.session.get({ path: { id: sessionID }, throwOnError: true }),
  271. sdk.client.session.messages({ path: { id: sessionID }, query: { limit: 100 } }),
  272. sdk.client.session.todo({ path: { id: sessionID } }),
  273. sdk.client.session.diff({ path: { id: sessionID } }),
  274. ])
  275. console.log("fetched in " + (Date.now() - now), sessionID)
  276. setStore(
  277. produce((draft) => {
  278. const match = Binary.search(draft.session, sessionID, (s) => s.id)
  279. if (match.found) draft.session[match.index] = session.data!
  280. if (!match.found) draft.session.splice(match.index, 0, session.data!)
  281. draft.todo[sessionID] = todo.data ?? []
  282. draft.message[sessionID] = messages.data!.map((x) => x.info)
  283. for (const message of messages.data!) {
  284. draft.part[message.info.id] = message.parts
  285. }
  286. draft.session_diff[sessionID] = diff.data ?? []
  287. }),
  288. )
  289. console.log("synced in " + (Date.now() - now), sessionID)
  290. },
  291. },
  292. }
  293. return result
  294. },
  295. })