diff options
| author | Adam <[email protected]> | 2025-12-30 07:24:35 -0600 |
|---|---|---|
| committer | Adam <[email protected]> | 2025-12-30 07:24:40 -0600 |
| commit | e0e07c5d48ef0671f63ca8bd9119169ced493fac (patch) | |
| tree | 9377eab8b3cc5d3c2976ccae2d32e22db46ce816 /packages/app/src/context | |
| parent | 281f9e623673e6bbfd9a5f9a8f9aae496abc99f2 (diff) | |
| download | opencode-e0e07c5d48ef0671f63ca8bd9119169ced493fac.tar.gz opencode-e0e07c5d48ef0671f63ca8bd9119169ced493fac.zip | |
feat(app): change server
Diffstat (limited to 'packages/app/src/context')
| -rw-r--r-- | packages/app/src/context/global-sdk.tsx | 23 | ||||
| -rw-r--r-- | packages/app/src/context/layout.tsx | 30 | ||||
| -rw-r--r-- | packages/app/src/context/server.tsx | 186 |
3 files changed, 212 insertions, 27 deletions
diff --git a/packages/app/src/context/global-sdk.tsx b/packages/app/src/context/global-sdk.tsx index 3732ca085..515be1d7a 100644 --- a/packages/app/src/context/global-sdk.tsx +++ b/packages/app/src/context/global-sdk.tsx @@ -1,34 +1,41 @@ import { createOpencodeClient, type Event } from "@opencode-ai/sdk/v2/client" import { createSimpleContext } from "@opencode-ai/ui/context" import { createGlobalEmitter } from "@solid-primitives/event-bus" +import { onCleanup } from "solid-js" import { usePlatform } from "./platform" +import { useServer } from "./server" export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleContext({ name: "GlobalSDK", - init: (props: { url: string }) => { + init: () => { + const server = useServer() + const abort = new AbortController() + const eventSdk = createOpencodeClient({ - baseUrl: props.url, - // signal: AbortSignal.timeout(1000 * 60 * 10), + baseUrl: server.url, + signal: abort.signal, }) const emitter = createGlobalEmitter<{ [key: string]: Event }>() - eventSdk.global.event().then(async (events) => { + void (async () => { + const events = await eventSdk.global.event() for await (const event of events.stream) { - // console.log("event", event) emitter.emit(event.directory ?? "global", event.payload) } - }) + })().catch(() => undefined) + + onCleanup(() => abort.abort()) const platform = usePlatform() const sdk = createOpencodeClient({ - baseUrl: props.url, + baseUrl: server.url, signal: AbortSignal.timeout(1000 * 60 * 10), fetch: platform.fetch, throwOnError: true, }) - return { url: props.url, client: sdk, event: emitter } + return { url: server.url, client: sdk, event: emitter } }, }) diff --git a/packages/app/src/context/layout.tsx b/packages/app/src/context/layout.tsx index 4ccab98e3..e57f69f8f 100644 --- a/packages/app/src/context/layout.tsx +++ b/packages/app/src/context/layout.tsx @@ -3,6 +3,7 @@ import { batch, createMemo, onMount } from "solid-js" import { createSimpleContext } from "@opencode-ai/ui/context" import { useGlobalSync } from "./global-sync" import { useGlobalSDK } from "./global-sdk" +import { useServer } from "./server" import { Project } from "@opencode-ai/sdk/v2" import { persisted } from "@/utils/persist" @@ -34,10 +35,10 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( init: () => { const globalSdk = useGlobalSDK() const globalSync = useGlobalSync() + const server = useServer() const [store, setStore, _, ready] = persisted( - "layout.v3", + "layout.v4", createStore({ - projects: [] as { worktree: string; expanded: boolean }[], sidebar: { opened: false, width: 280, @@ -86,12 +87,12 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( return project } - const enriched = createMemo(() => store.projects.flatMap(enrich)) + const enriched = createMemo(() => server.projects.list().flatMap(enrich)) const list = createMemo(() => enriched().flatMap(colorize)) onMount(() => { Promise.all( - store.projects.map((project) => { + server.projects.list().map((project) => { return globalSync.project.loadSessions(project.worktree) }), ) @@ -102,32 +103,23 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( projects: { list, open(directory: string) { - if (store.projects.find((x) => x.worktree === directory)) { + if (server.projects.list().find((x) => x.worktree === directory)) { return } globalSync.project.loadSessions(directory) - setStore("projects", (x) => [{ worktree: directory, expanded: true }, ...x]) + server.projects.open(directory) }, close(directory: string) { - setStore("projects", (x) => x.filter((x) => x.worktree !== directory)) + server.projects.close(directory) }, expand(directory: string) { - const index = store.projects.findIndex((x) => x.worktree === directory) - if (index !== -1) setStore("projects", index, "expanded", true) + server.projects.expand(directory) }, collapse(directory: string) { - const index = store.projects.findIndex((x) => x.worktree === directory) - if (index !== -1) setStore("projects", index, "expanded", false) + server.projects.collapse(directory) }, move(directory: string, toIndex: number) { - setStore("projects", (projects) => { - const fromIndex = projects.findIndex((x) => x.worktree === directory) - if (fromIndex === -1 || fromIndex === toIndex) return projects - const result = [...projects] - const [item] = result.splice(fromIndex, 1) - result.splice(toIndex, 0, item) - return result - }) + server.projects.move(directory, toIndex) }, }, sidebar: { diff --git a/packages/app/src/context/server.tsx b/packages/app/src/context/server.tsx new file mode 100644 index 000000000..73b83f461 --- /dev/null +++ b/packages/app/src/context/server.tsx @@ -0,0 +1,186 @@ +import { createOpencodeClient } from "@opencode-ai/sdk/v2/client" +import { createSimpleContext } from "@opencode-ai/ui/context" +import { batch, createEffect, createMemo, createResource, 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 } + +function normalize(input: string) { + const trimmed = input.trim() + if (!trimmed) return + const withProtocol = /^https?:\/\//.test(trimmed) ? trimmed : `http://${trimmed}` + const cleaned = withProtocol.replace(/\/+$/, "") + return cleaned.replace(/^(https?:\/\/[^/]+).*/, "$1") +} + +function displayName(url: string) { + return url + .replace(/^https?:\/\//, "") + .replace(/\/+$/, "") + .split("/")[0] +} + +export const { use: useServer, provider: ServerProvider } = createSimpleContext({ + name: "Server", + init: (props: { defaultUrl: string; forceUrl?: boolean }) => { + const platform = usePlatform() + const fallback = () => normalize(props.defaultUrl) + const [forced, setForced] = createSignal(props.forceUrl ?? false) + + const [store, setStore, _, ready] = persisted( + "server.v2", + createStore({ + list: [] as string[], + active: "", + projects: {} as Record<string, StoredProject[]>, + }), + ) + + function setActive(input: string) { + const url = normalize(input) + if (!url) return + batch(() => { + if (!store.list.includes(url)) { + setStore("list", (list) => [url, ...list]) + } + setStore("active", url) + }) + } + + function remove(input: string) { + const url = normalize(input) + if (!url) return + + const list = store.list.filter((x) => x !== url) + const next = store.active === url ? (list[0] ?? fallback() ?? "") : store.active + + batch(() => { + setStore("list", list) + setStore("active", next) + }) + } + + createEffect(() => { + if (!ready()) return + + const url = fallback() + if (!url) return + + if (forced()) { + batch(() => { + if (!store.list.includes(url)) { + setStore("list", (list) => [url, ...list]) + } + if (store.active !== url) { + setStore("active", url) + } + }) + setForced(false) + return + } + + if (store.list.length === 0) { + batch(() => { + setStore("list", [url]) + setStore("active", url) + }) + return + } + + if (store.active && store.list.includes(store.active)) return + setStore("active", store.list[0]) + }) + + const isReady = createMemo(() => ready() && !!store.active) + + const [healthy, { refetch }] = createResource( + () => store.active, + async (url) => { + if (!url) return true + + const sdk = createOpencodeClient({ + baseUrl: url, + fetch: platform.fetch, + signal: AbortSignal.timeout(2000), + }) + return sdk.global + .health() + .then((x) => x.data?.healthy === true) + .catch(() => false) + }, + { initialValue: true }, + ) + + createEffect(() => { + if (!store.active) return + const interval = setInterval(() => refetch(), 10_000) + onCleanup(() => clearInterval(interval)) + }) + + const projectsList = createMemo(() => store.projects[store.active] ?? []) + + return { + ready: isReady, + healthy, + get url() { + return store.active + }, + get name() { + return displayName(store.active) + }, + get list() { + return store.list + }, + setActive, + add: setActive, + remove, + projects: { + list: projectsList, + open(directory: string) { + const url = store.active + if (!url) return + const current = store.projects[url] ?? [] + if (current.find((x) => x.worktree === directory)) return + setStore("projects", url, [{ worktree: directory, expanded: true }, ...current]) + }, + close(directory: string) { + const url = store.active + if (!url) return + const current = store.projects[url] ?? [] + setStore( + "projects", + url, + current.filter((x) => x.worktree !== directory), + ) + }, + expand(directory: string) { + const url = store.active + if (!url) return + const current = store.projects[url] ?? [] + const index = current.findIndex((x) => x.worktree === directory) + if (index !== -1) setStore("projects", url, index, "expanded", true) + }, + collapse(directory: string) { + const url = store.active + if (!url) return + const current = store.projects[url] ?? [] + const index = current.findIndex((x) => x.worktree === directory) + if (index !== -1) setStore("projects", url, index, "expanded", false) + }, + move(directory: string, toIndex: number) { + const url = store.active + if (!url) return + const current = store.projects[url] ?? [] + 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", url, result) + }, + }, + } + }, +}) |
