server.tsx 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  1. import { createOpencodeClient } from "@opencode-ai/sdk/v2/client"
  2. import { createSimpleContext } from "@opencode-ai/ui/context"
  3. import { batch, createEffect, createMemo, createSignal, onCleanup } from "solid-js"
  4. import { createStore } from "solid-js/store"
  5. import { usePlatform } from "@/context/platform"
  6. import { persisted } from "@/utils/persist"
  7. type StoredProject = { worktree: string; expanded: boolean }
  8. export function normalizeServerUrl(input: string) {
  9. const trimmed = input.trim()
  10. if (!trimmed) return
  11. const withProtocol = /^https?:\/\//.test(trimmed) ? trimmed : `http://${trimmed}`
  12. return withProtocol.replace(/\/+$/, "")
  13. }
  14. export function serverDisplayName(url: string) {
  15. if (!url) return ""
  16. return url
  17. .replace(/^https?:\/\//, "")
  18. .replace(/\/+$/, "")
  19. .split("/")[0]
  20. }
  21. function projectsKey(url: string) {
  22. if (!url) return ""
  23. const host = url.replace(/^https?:\/\//, "").split(":")[0]
  24. if (host === "localhost" || host === "127.0.0.1") return "local"
  25. return url
  26. }
  27. export const { use: useServer, provider: ServerProvider } = createSimpleContext({
  28. name: "Server",
  29. init: (props: { defaultUrl: string }) => {
  30. const platform = usePlatform()
  31. const [store, setStore, _, ready] = persisted(
  32. "server.v3",
  33. createStore({
  34. list: [] as string[],
  35. projects: {} as Record<string, StoredProject[]>,
  36. }),
  37. )
  38. const [active, setActiveRaw] = createSignal("")
  39. function setActive(input: string) {
  40. const url = normalizeServerUrl(input)
  41. if (!url) return
  42. setActiveRaw(url)
  43. }
  44. function add(input: string) {
  45. const url = normalizeServerUrl(input)
  46. if (!url) return
  47. const fallback = normalizeServerUrl(props.defaultUrl)
  48. if (fallback && url === fallback) {
  49. setActiveRaw(url)
  50. return
  51. }
  52. batch(() => {
  53. if (!store.list.includes(url)) {
  54. setStore("list", store.list.length, url)
  55. }
  56. setActiveRaw(url)
  57. })
  58. }
  59. function remove(input: string) {
  60. const url = normalizeServerUrl(input)
  61. if (!url) return
  62. const list = store.list.filter((x) => x !== url)
  63. const next = active() === url ? (list[0] ?? normalizeServerUrl(props.defaultUrl) ?? "") : active()
  64. batch(() => {
  65. setStore("list", list)
  66. setActiveRaw(next)
  67. })
  68. }
  69. createEffect(() => {
  70. if (!ready()) return
  71. if (active()) return
  72. const url = normalizeServerUrl(props.defaultUrl)
  73. if (!url) return
  74. setActiveRaw(url)
  75. })
  76. const isReady = createMemo(() => ready() && !!active())
  77. const [healthy, setHealthy] = createSignal<boolean | undefined>(undefined)
  78. const check = (url: string) => {
  79. const sdk = createOpencodeClient({
  80. baseUrl: url,
  81. fetch: platform.fetch,
  82. signal: AbortSignal.timeout(3000),
  83. })
  84. return sdk.global
  85. .health()
  86. .then((x) => x.data?.healthy === true)
  87. .catch(() => false)
  88. }
  89. createEffect(() => {
  90. const url = active()
  91. if (!url) return
  92. setHealthy(undefined)
  93. let alive = true
  94. let busy = false
  95. const run = () => {
  96. if (busy) return
  97. busy = true
  98. void check(url)
  99. .then((next) => {
  100. if (!alive) return
  101. setHealthy(next)
  102. })
  103. .finally(() => {
  104. busy = false
  105. })
  106. }
  107. run()
  108. const interval = setInterval(run, 10_000)
  109. onCleanup(() => {
  110. alive = false
  111. clearInterval(interval)
  112. })
  113. })
  114. const origin = createMemo(() => projectsKey(active()))
  115. const projectsList = createMemo(() => store.projects[origin()] ?? [])
  116. const isLocal = createMemo(() => origin() === "local")
  117. return {
  118. ready: isReady,
  119. healthy,
  120. isLocal,
  121. get url() {
  122. return active()
  123. },
  124. get name() {
  125. return serverDisplayName(active())
  126. },
  127. get list() {
  128. return store.list
  129. },
  130. setActive,
  131. add,
  132. remove,
  133. projects: {
  134. list: projectsList,
  135. open(directory: string) {
  136. const key = origin()
  137. if (!key) return
  138. const current = store.projects[key] ?? []
  139. if (current.find((x) => x.worktree === directory)) return
  140. setStore("projects", key, [{ worktree: directory, expanded: true }, ...current])
  141. },
  142. close(directory: string) {
  143. const key = origin()
  144. if (!key) return
  145. const current = store.projects[key] ?? []
  146. setStore(
  147. "projects",
  148. key,
  149. current.filter((x) => x.worktree !== directory),
  150. )
  151. },
  152. expand(directory: string) {
  153. const key = origin()
  154. if (!key) return
  155. const current = store.projects[key] ?? []
  156. const index = current.findIndex((x) => x.worktree === directory)
  157. if (index !== -1) setStore("projects", key, index, "expanded", true)
  158. },
  159. collapse(directory: string) {
  160. const key = origin()
  161. if (!key) return
  162. const current = store.projects[key] ?? []
  163. const index = current.findIndex((x) => x.worktree === directory)
  164. if (index !== -1) setStore("projects", key, index, "expanded", false)
  165. },
  166. move(directory: string, toIndex: number) {
  167. const key = origin()
  168. if (!key) return
  169. const current = store.projects[key] ?? []
  170. const fromIndex = current.findIndex((x) => x.worktree === directory)
  171. if (fromIndex === -1 || fromIndex === toIndex) return
  172. const result = [...current]
  173. const [item] = result.splice(fromIndex, 1)
  174. result.splice(toIndex, 0, item)
  175. setStore("projects", key, result)
  176. },
  177. },
  178. }
  179. },
  180. })