| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206 |
- import { createOpencodeClient } from "@opencode-ai/sdk/v2/client"
- import { createSimpleContext } from "@opencode-ai/ui/context"
- import { batch, createEffect, createMemo, createSignal, onCleanup } from "solid-js"
- import { createStore } from "solid-js/store"
- import { usePlatform } from "@/context/platform"
- import { 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(/\/+$/, "")
- .split("/")[0]
- }
- 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(
- "server.v3",
- createStore({
- list: [] as string[],
- projects: {} as Record<string, StoredProject[]>,
- }),
- )
- const [active, setActiveRaw] = createSignal("")
- function setActive(input: string) {
- const url = normalizeServerUrl(input)
- if (!url) return
- setActiveRaw(url)
- }
- function add(input: string) {
- const url = normalizeServerUrl(input)
- if (!url) return
- const fallback = normalizeServerUrl(props.defaultUrl)
- if (fallback && url === fallback) {
- setActiveRaw(url)
- return
- }
- batch(() => {
- if (!store.list.includes(url)) {
- setStore("list", store.list.length, url)
- }
- setActiveRaw(url)
- })
- }
- function remove(input: string) {
- const url = normalizeServerUrl(input)
- if (!url) return
- const list = store.list.filter((x) => x !== url)
- const next = active() === url ? (list[0] ?? normalizeServerUrl(props.defaultUrl) ?? "") : active()
- batch(() => {
- setStore("list", list)
- setActiveRaw(next)
- })
- }
- createEffect(() => {
- if (!ready()) return
- if (active()) return
- const url = normalizeServerUrl(props.defaultUrl)
- if (!url) return
- setActiveRaw(url)
- })
- const isReady = createMemo(() => ready() && !!active())
- const [healthy, setHealthy] = createSignal<boolean | undefined>(undefined)
- 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 = active()
- if (!url) return
- setHealthy(undefined)
- let alive = true
- let busy = false
- const run = () => {
- if (busy) return
- busy = true
- void check(url)
- .then((next) => {
- if (!alive) return
- setHealthy(next)
- })
- .finally(() => {
- busy = false
- })
- }
- run()
- const interval = setInterval(run, 10_000)
- onCleanup(() => {
- alive = false
- clearInterval(interval)
- })
- })
- const origin = createMemo(() => projectsKey(active()))
- const projectsList = createMemo(() => store.projects[origin()] ?? [])
- const isLocal = createMemo(() => origin() === "local")
- return {
- ready: isReady,
- healthy,
- isLocal,
- get url() {
- return active()
- },
- get name() {
- return serverDisplayName(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)
- },
- },
- }
- },
- })
|