summaryrefslogtreecommitdiffhomepage
path: root/packages/app/src/context
diff options
context:
space:
mode:
authorAdam <[email protected]>2025-12-30 07:24:35 -0600
committerAdam <[email protected]>2025-12-30 07:24:40 -0600
commite0e07c5d48ef0671f63ca8bd9119169ced493fac (patch)
tree9377eab8b3cc5d3c2976ccae2d32e22db46ce816 /packages/app/src/context
parent281f9e623673e6bbfd9a5f9a8f9aae496abc99f2 (diff)
downloadopencode-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.tsx23
-rw-r--r--packages/app/src/context/layout.tsx30
-rw-r--r--packages/app/src/context/server.tsx186
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)
+ },
+ },
+ }
+ },
+})