sync.tsx 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226
  1. import type {
  2. Message,
  3. Agent,
  4. Provider,
  5. Session,
  6. Part,
  7. Config,
  8. Path,
  9. File,
  10. FileNode,
  11. Project,
  12. FileDiff,
  13. Todo,
  14. } from "@opencode-ai/sdk"
  15. import { createStore, produce, reconcile } from "solid-js/store"
  16. import { createMemo } from "solid-js"
  17. import { Binary } from "@/utils/binary"
  18. import { createSimpleContext } from "./helper"
  19. import { useSDK } from "./sdk"
  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. project: Project
  28. config: Config
  29. path: Path
  30. session: Session[]
  31. session_diff: {
  32. [sessionID: string]: FileDiff[]
  33. }
  34. todo: {
  35. [sessionID: string]: Todo[]
  36. }
  37. limit: number
  38. message: {
  39. [sessionID: string]: Message[]
  40. }
  41. part: {
  42. [messageID: string]: Part[]
  43. }
  44. node: FileNode[]
  45. changes: File[]
  46. }>({
  47. project: { id: "", worktree: "", time: { created: 0, initialized: 0 } },
  48. config: {},
  49. path: { state: "", config: "", worktree: "", directory: "" },
  50. ready: false,
  51. agent: [],
  52. provider: [],
  53. session: [],
  54. session_diff: {},
  55. todo: {},
  56. limit: 10,
  57. message: {},
  58. part: {},
  59. node: [],
  60. changes: [],
  61. })
  62. const sdk = useSDK()
  63. sdk.event.listen((e) => {
  64. const event = e.details
  65. switch (event.type) {
  66. case "session.updated": {
  67. const result = Binary.search(store.session, event.properties.info.id, (s) => s.id)
  68. if (result.found) {
  69. setStore("session", result.index, reconcile(event.properties.info))
  70. break
  71. }
  72. setStore(
  73. "session",
  74. produce((draft) => {
  75. draft.splice(result.index, 0, event.properties.info)
  76. }),
  77. )
  78. break
  79. }
  80. case "session.diff":
  81. setStore("session_diff", event.properties.sessionID, event.properties.diff)
  82. break
  83. case "todo.updated":
  84. setStore("todo", event.properties.sessionID, event.properties.todos)
  85. break
  86. case "message.updated": {
  87. const messages = store.message[event.properties.info.sessionID]
  88. if (!messages) {
  89. setStore("message", event.properties.info.sessionID, [event.properties.info])
  90. break
  91. }
  92. const result = Binary.search(messages, event.properties.info.id, (m) => m.id)
  93. if (result.found) {
  94. setStore("message", event.properties.info.sessionID, result.index, reconcile(event.properties.info))
  95. break
  96. }
  97. setStore(
  98. "message",
  99. event.properties.info.sessionID,
  100. produce((draft) => {
  101. draft.splice(result.index, 0, event.properties.info)
  102. }),
  103. )
  104. break
  105. }
  106. case "message.part.updated": {
  107. const part = sanitizePart(event.properties.part)
  108. const parts = store.part[part.messageID]
  109. if (!parts) {
  110. setStore("part", part.messageID, [part])
  111. break
  112. }
  113. const result = Binary.search(parts, part.id, (p) => p.id)
  114. if (result.found) {
  115. setStore("part", part.messageID, result.index, reconcile(part))
  116. break
  117. }
  118. setStore(
  119. "part",
  120. part.messageID,
  121. produce((draft) => {
  122. draft.splice(result.index, 0, part)
  123. }),
  124. )
  125. break
  126. }
  127. }
  128. })
  129. const load = {
  130. project: () => sdk.client.project.current().then((x) => setStore("project", x.data!)),
  131. provider: () => sdk.client.config.providers().then((x) => setStore("provider", x.data!.providers)),
  132. path: () => sdk.client.path.get().then((x) => setStore("path", x.data!)),
  133. agent: () => sdk.client.app.agents().then((x) => setStore("agent", x.data ?? [])),
  134. session: () =>
  135. sdk.client.session.list().then((x) => {
  136. const sessions = (x.data ?? [])
  137. .slice()
  138. .sort((a, b) => a.id.localeCompare(b.id))
  139. .slice(0, store.limit)
  140. setStore("session", sessions)
  141. }),
  142. config: () => sdk.client.config.get().then((x) => setStore("config", x.data!)),
  143. changes: () => sdk.client.file.status().then((x) => setStore("changes", x.data!)),
  144. node: () => sdk.client.file.list({ query: { path: "/" } }).then((x) => setStore("node", x.data!)),
  145. }
  146. Promise.all(Object.values(load).map((p) => p())).then(() => setStore("ready", true))
  147. const sanitizer = createMemo(() => new RegExp(`${store.path.directory}/`, "g"))
  148. const sanitize = (text: string) => text.replace(sanitizer(), "")
  149. const absolute = (path: string) => (store.path.directory + "/" + path).replace("//", "/")
  150. const sanitizePart = (part: Part) => {
  151. if (part.type === "tool") {
  152. if (part.state.status === "completed" || part.state.status === "error") {
  153. for (const key in part.state.metadata) {
  154. if (typeof part.state.metadata[key] === "string") {
  155. part.state.metadata[key] = sanitize(part.state.metadata[key] as string)
  156. }
  157. }
  158. for (const key in part.state.input) {
  159. if (typeof part.state.input[key] === "string") {
  160. part.state.input[key] = sanitize(part.state.input[key] as string)
  161. }
  162. }
  163. if ("error" in part.state) {
  164. part.state.error = sanitize(part.state.error as string)
  165. }
  166. }
  167. }
  168. return part
  169. }
  170. return {
  171. data: store,
  172. set: setStore,
  173. get ready() {
  174. return store.ready
  175. },
  176. session: {
  177. get(sessionID: string) {
  178. const match = Binary.search(store.session, sessionID, (s) => s.id)
  179. if (match.found) return store.session[match.index]
  180. return undefined
  181. },
  182. async sync(sessionID: string, _isRetry = false) {
  183. const [session, messages, todo, diff] = await Promise.all([
  184. sdk.client.session.get({ path: { id: sessionID }, throwOnError: true }),
  185. sdk.client.session.messages({ path: { id: sessionID }, query: { limit: 100 } }),
  186. sdk.client.session.todo({ path: { id: sessionID } }),
  187. sdk.client.session.diff({ path: { id: sessionID } }),
  188. ])
  189. setStore(
  190. produce((draft) => {
  191. const match = Binary.search(draft.session, sessionID, (s) => s.id)
  192. if (match.found) draft.session[match.index] = session.data!
  193. if (!match.found) draft.session.splice(match.index, 0, session.data!)
  194. draft.todo[sessionID] = todo.data ?? []
  195. draft.message[sessionID] = messages
  196. .data!.map((x) => x.info)
  197. .slice()
  198. .sort((a, b) => a.id.localeCompare(b.id))
  199. for (const message of messages.data!) {
  200. draft.part[message.info.id] = message.parts
  201. .slice()
  202. .map(sanitizePart)
  203. .sort((a, b) => a.id.localeCompare(b.id))
  204. }
  205. draft.session_diff[sessionID] = diff.data ?? []
  206. }),
  207. )
  208. },
  209. fetch: async (count = 10) => {
  210. setStore("limit", (x) => x + count)
  211. await load.session()
  212. },
  213. more: createMemo(() => store.session.length >= store.limit),
  214. },
  215. load,
  216. absolute,
  217. sanitize,
  218. }
  219. },
  220. })