directory-sync.ts 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
  1. import { Binary } from "@kirincode-ai/core/util/binary"
  2. import type { Message, Part, Session } from "@kirincode-ai/sdk/v2/client"
  3. import { createMemo } from "solid-js"
  4. import { produce, reconcile, type SetStoreFunction } from "solid-js/store"
  5. import type { createServerSdkContext } from "./server-sdk"
  6. import type { createServerSyncContextInner } from "./server-sync"
  7. import type { State } from "./global-sync/types"
  8. const cmp = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0)
  9. const sessionFields = new Set([
  10. "session_status",
  11. "session_working",
  12. "session_diff",
  13. "todo",
  14. "permission",
  15. "question",
  16. "message",
  17. "part",
  18. "part_text_accum_delta",
  19. ])
  20. export const createDirSyncContext = (
  21. directory: string,
  22. serverSync: ReturnType<typeof createServerSyncContextInner>,
  23. serverSDK: ReturnType<typeof createServerSdkContext>,
  24. ) => {
  25. const client = serverSDK.createClient({ directory, throwOnError: true })
  26. const current = createMemo(() => serverSync.child(directory, { mcp: true }))
  27. const absolute = (path: string) => (current()[0].path.directory + "/" + path).replace("//", "/")
  28. const data = new Proxy({} as State, {
  29. get(_, property: keyof State) {
  30. if (property === "session_working") return serverSync.session.data.session_working.bind(serverSync.session.data)
  31. if (sessionFields.has(property)) return serverSync.session.data[property as keyof typeof serverSync.session.data]
  32. return current()[0][property]
  33. },
  34. })
  35. const set = ((...input: unknown[]) => {
  36. if (typeof input[0] === "string" && sessionFields.has(input[0])) {
  37. return (serverSync.session.set as (...args: unknown[]) => unknown)(...input)
  38. }
  39. const result = (current()[1] as (...args: unknown[]) => unknown)(...input)
  40. if (input[0] === "session") current()[0].session.forEach(serverSync.session.remember)
  41. return result
  42. }) as SetStoreFunction<State>
  43. const index = (sessionID: string) => {
  44. const session = serverSync.session.get(sessionID)
  45. if (!session || session.directory !== directory) return
  46. const [store, setStore] = current()
  47. const result = Binary.search(store.session, session.id, (item) => item.id)
  48. if (result.found) {
  49. setStore("session", result.index, reconcile(session))
  50. return
  51. }
  52. setStore(
  53. "session",
  54. produce((draft) => void draft.splice(result.index, 0, session)),
  55. )
  56. }
  57. return {
  58. data,
  59. set,
  60. get status() {
  61. return current()[0].status
  62. },
  63. get ready() {
  64. return current()[0].status !== "loading"
  65. },
  66. get project() {
  67. const store = current()[0]
  68. const match = Binary.search(serverSync.data.project, store.project, (project) => project.id)
  69. if (match.found) return serverSync.data.project[match.index]
  70. },
  71. session: {
  72. remember(session: Session) {
  73. serverSync.session.remember(session)
  74. index(session.id)
  75. },
  76. get(sessionID: string) {
  77. const session = serverSync.session.get(sessionID)
  78. if (session?.directory === directory) return session
  79. },
  80. optimistic: {
  81. add(input: { directory?: string; sessionID: string; message: Message; parts: Part[] }) {
  82. serverSync.session.optimistic.add(input)
  83. },
  84. remove(input: { directory?: string; sessionID: string; messageID: string }) {
  85. serverSync.session.optimistic.remove(input)
  86. },
  87. },
  88. addOptimisticMessage(input: {
  89. sessionID: string
  90. messageID: string
  91. parts: Part[]
  92. agent: string
  93. model: { providerID: string; modelID: string }
  94. variant?: string
  95. }) {
  96. serverSync.session.optimistic.add({
  97. sessionID: input.sessionID,
  98. message: {
  99. id: input.messageID,
  100. sessionID: input.sessionID,
  101. role: "user",
  102. time: { created: Date.now() },
  103. agent: input.agent,
  104. model: { ...input.model, variant: input.variant },
  105. },
  106. parts: input.parts,
  107. })
  108. },
  109. async sync(sessionID: string, options?: { force?: boolean }) {
  110. await serverSync.session.sync(sessionID, options)
  111. index(sessionID)
  112. },
  113. diff: serverSync.session.diff,
  114. todo: serverSync.session.todo,
  115. history: serverSync.session.history,
  116. evict(sessionID: string) {
  117. serverSync.session.evict(sessionID)
  118. },
  119. fetch: async (count = 10) => {
  120. const [store, setStore] = current()
  121. setStore("limit", (value) => value + count)
  122. const response = await client.session.list()
  123. const sessions = (response.data ?? [])
  124. .filter((session) => !!session?.id)
  125. .sort((a, b) => cmp(a.id, b.id))
  126. .slice(0, store.limit)
  127. sessions.forEach(serverSync.session.remember)
  128. setStore("session", reconcile(sessions, { key: "id" }))
  129. },
  130. more: createMemo(() => current()[0].session.length >= current()[0].limit),
  131. archive: async (sessionID: string) => {
  132. await serverSDK.client.session.update({ sessionID, time: { archived: Date.now() } })
  133. current()[1](
  134. "session",
  135. produce((draft) => {
  136. const match = Binary.search(draft, sessionID, (session) => session.id)
  137. if (match.found) draft.splice(match.index, 1)
  138. }),
  139. )
  140. },
  141. },
  142. mcp: {
  143. toggle: (name: string) => serverSync.mcp.toggle(directory, name),
  144. },
  145. absolute,
  146. get directory() {
  147. return current()[0].path.directory
  148. },
  149. }
  150. }