| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217 |
- import { createOpencodeClient } from "@opencode-ai/sdk/v2/client"
- import { createSimpleContext } from "@opencode-ai/ui/context"
- import { batch, createEffect, createMemo, onCleanup } from "solid-js"
- import { createStore } from "solid-js/store"
- import { usePlatform } from "@/context/platform"
- import { Persist, persisted } from "@/utils/persist"
- type StoredProject = { worktree: string; expanded: boolean }
- export function normalizeServerUrl(input: string) {
- const trimmed = input.trim()
- if (!trimmed) return
- const withProtocol = /^https?:\/\//.test(trimmed) ? trimmed : `http://${trimmed}`
- return withProtocol.replace(/\/+$/, "")
- }
- export function serverDisplayName(url: string) {
- if (!url) return ""
- return url.replace(/^https?:\/\//, "").replace(/\/+$/, "")
- }
- function projectsKey(url: string) {
- if (!url) return ""
- const host = url.replace(/^https?:\/\//, "").split(":")[0]
- if (host === "localhost" || host === "127.0.0.1") return "local"
- return url
- }
- export const { use: useServer, provider: ServerProvider } = createSimpleContext({
- name: "Server",
- init: (props: { defaultUrl: string }) => {
- const platform = usePlatform()
- const [store, setStore, _, ready] = persisted(
- Persist.global("server", ["server.v3"]),
- createStore({
- list: [] as string[],
- projects: {} as Record<string, StoredProject[]>,
- lastProject: {} as Record<string, string>,
- }),
- )
- const [state, setState] = createStore({
- active: "",
- healthy: undefined as boolean | undefined,
- })
- const healthy = () => state.healthy
- function setActive(input: string) {
- const url = normalizeServerUrl(input)
- if (!url) return
- setState("active", url)
- }
- function add(input: string) {
- const url = normalizeServerUrl(input)
- if (!url) return
- const fallback = normalizeServerUrl(props.defaultUrl)
- if (fallback && url === fallback) {
- setState("active", url)
- return
- }
- batch(() => {
- if (!store.list.includes(url)) {
- setStore("list", store.list.length, url)
- }
- setState("active", url)
- })
- }
- function remove(input: string) {
- const url = normalizeServerUrl(input)
- if (!url) return
- const list = store.list.filter((x) => x !== url)
- const next = state.active === url ? (list[0] ?? normalizeServerUrl(props.defaultUrl) ?? "") : state.active
- batch(() => {
- setStore("list", list)
- setState("active", next)
- })
- }
- createEffect(() => {
- if (!ready()) return
- if (state.active) return
- const url = normalizeServerUrl(props.defaultUrl)
- if (!url) return
- setState("active", url)
- })
- const isReady = createMemo(() => ready() && !!state.active)
- const check = (url: string) => {
- const sdk = createOpencodeClient({
- baseUrl: url,
- fetch: platform.fetch,
- signal: AbortSignal.timeout(3000),
- })
- return sdk.global
- .health()
- .then((x) => x.data?.healthy === true)
- .catch(() => false)
- }
- createEffect(() => {
- const url = state.active
- if (!url) return
- setState("healthy", undefined)
- let alive = true
- let busy = false
- const run = () => {
- if (busy) return
- busy = true
- void check(url)
- .then((next) => {
- if (!alive) return
- setState("healthy", next)
- })
- .finally(() => {
- busy = false
- })
- }
- run()
- const interval = setInterval(run, 10_000)
- onCleanup(() => {
- alive = false
- clearInterval(interval)
- })
- })
- const origin = createMemo(() => projectsKey(state.active))
- const projectsList = createMemo(() => store.projects[origin()] ?? [])
- const isLocal = createMemo(() => origin() === "local")
- return {
- ready: isReady,
- healthy,
- isLocal,
- get url() {
- return state.active
- },
- get name() {
- return serverDisplayName(state.active)
- },
- get list() {
- return store.list
- },
- setActive,
- add,
- remove,
- projects: {
- list: projectsList,
- open(directory: string) {
- const key = origin()
- if (!key) return
- const current = store.projects[key] ?? []
- if (current.find((x) => x.worktree === directory)) return
- setStore("projects", key, [{ worktree: directory, expanded: true }, ...current])
- },
- close(directory: string) {
- const key = origin()
- if (!key) return
- const current = store.projects[key] ?? []
- setStore(
- "projects",
- key,
- current.filter((x) => x.worktree !== directory),
- )
- },
- expand(directory: string) {
- const key = origin()
- if (!key) return
- const current = store.projects[key] ?? []
- const index = current.findIndex((x) => x.worktree === directory)
- if (index !== -1) setStore("projects", key, index, "expanded", true)
- },
- collapse(directory: string) {
- const key = origin()
- if (!key) return
- const current = store.projects[key] ?? []
- const index = current.findIndex((x) => x.worktree === directory)
- if (index !== -1) setStore("projects", key, index, "expanded", false)
- },
- move(directory: string, toIndex: number) {
- const key = origin()
- if (!key) return
- const current = store.projects[key] ?? []
- const fromIndex = current.findIndex((x) => x.worktree === directory)
- if (fromIndex === -1 || fromIndex === toIndex) return
- const result = [...current]
- const [item] = result.splice(fromIndex, 1)
- result.splice(toIndex, 0, item)
- setStore("projects", key, result)
- },
- last() {
- const key = origin()
- if (!key) return
- return store.lastProject[key]
- },
- touch(directory: string) {
- const key = origin()
- if (!key) return
- setStore("lastProject", key, directory)
- },
- },
- }
- },
- })
|