sync.tsx 9.4 KB

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