directory-sync.ts 5.9 KB

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