From e0e07c5d48ef0671f63ca8bd9119169ced493fac Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Tue, 30 Dec 2025 07:24:35 -0600 Subject: feat(app): change server --- .../app/src/components/dialog-select-server.tsx | 170 +++++++++++++++++++++ .../app/src/components/session-lsp-indicator.tsx | 10 +- .../app/src/components/session-mcp-indicator.tsx | 10 +- packages/app/src/components/status-bar.tsx | 29 +++- 4 files changed, 202 insertions(+), 17 deletions(-) create mode 100644 packages/app/src/components/dialog-select-server.tsx (limited to 'packages/app/src/components') diff --git a/packages/app/src/components/dialog-select-server.tsx b/packages/app/src/components/dialog-select-server.tsx new file mode 100644 index 000000000..b041bbab6 --- /dev/null +++ b/packages/app/src/components/dialog-select-server.tsx @@ -0,0 +1,170 @@ +import { createEffect, createMemo, onCleanup } from "solid-js" +import { createStore, reconcile } from "solid-js/store" +import { useDialog } from "@opencode-ai/ui/context/dialog" +import { Dialog } from "@opencode-ai/ui/dialog" +import { List } from "@opencode-ai/ui/list" +import { TextField } from "@opencode-ai/ui/text-field" +import { Button } from "@opencode-ai/ui/button" +import { useServer } from "@/context/server" +import { usePlatform } from "@/context/platform" +import { createOpencodeClient } from "@opencode-ai/sdk/v2/client" +import { useNavigate } from "@solidjs/router" + +type ServerStatus = { healthy: boolean; version?: string } + +function displayName(url: string) { + return url + .replace(/^https?:\/\//, "") + .replace(/\/+$/, "") + .split("/")[0] +} + +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") +} + +async function checkHealth(url: string, fetch?: typeof globalThis.fetch): Promise { + const sdk = createOpencodeClient({ + baseUrl: url, + fetch, + signal: AbortSignal.timeout(3000), + }) + return sdk.global + .health() + .then((x) => ({ healthy: x.data?.healthy === true, version: x.data?.version })) + .catch(() => ({ healthy: false })) +} + +export function DialogSelectServer() { + const navigate = useNavigate() + const dialog = useDialog() + const server = useServer() + const platform = usePlatform() + const [store, setStore] = createStore({ + url: "", + adding: false, + error: "", + status: {} as Record, + }) + + const items = createMemo(() => { + const current = server.url + const list = server.list + if (!current) return list + if (!list.includes(current)) return [current, ...list] + return [current, ...list.filter((x) => x !== current)] + }) + + const current = createMemo(() => items().find((x) => x === server.url) ?? items()[0]) + + async function refreshHealth() { + const results: Record = {} + await Promise.all( + items().map(async (url) => { + results[url] = await checkHealth(url, platform.fetch) + }), + ) + setStore("status", reconcile(results)) + } + + createEffect(() => { + items() + refreshHealth() + const interval = setInterval(refreshHealth, 10_000) + onCleanup(() => clearInterval(interval)) + }) + + function select(value: string) { + if (store.status[value]?.healthy === false) return + dialog.close() + server.setActive(value) + navigate("/") + } + + async function handleSubmit(e: SubmitEvent) { + e.preventDefault() + const value = normalize(store.url) + if (!value) return + + setStore("adding", true) + setStore("error", "") + + const result = await checkHealth(value, platform.fetch) + setStore("adding", false) + + if (!result.healthy) { + setStore("error", "Could not connect to server") + return + } + + setStore("url", "") + select(value) + } + + return ( + +
+ x} + current={current()} + onSelect={(x) => { + if (x) select(x) + }} + > + {(i) => ( +
+
+ {displayName(i)} + {store.status[i]?.version} +
+ )} + + +
+
+

Add a server

+
+
+
+
+ { + setStore("url", v) + setStore("error", "") + }} + validationState={store.error ? "invalid" : "valid"} + error={store.error} + /> +
+ +
+
+
+
+
+ ) +} diff --git a/packages/app/src/components/session-lsp-indicator.tsx b/packages/app/src/components/session-lsp-indicator.tsx index 98d6d6dfd..ac3a39997 100644 --- a/packages/app/src/components/session-lsp-indicator.tsx +++ b/packages/app/src/components/session-lsp-indicator.tsx @@ -1,5 +1,4 @@ import { createMemo, Show } from "solid-js" -import { Icon } from "@opencode-ai/ui/icon" import { useSync } from "@/context/sync" import { Tooltip } from "@opencode-ai/ui/tooltip" @@ -24,12 +23,11 @@ export function SessionLspIndicator() { 0}>
- 0, + "size-1.5 rounded-full": true, + "bg-icon-critical-base": lspStats().hasError, + "bg-icon-success-base": !lspStats().hasError && lspStats().connected > 0, }} /> {lspStats().connected} LSP diff --git a/packages/app/src/components/session-mcp-indicator.tsx b/packages/app/src/components/session-mcp-indicator.tsx index 17a6f2e1a..489223b9b 100644 --- a/packages/app/src/components/session-mcp-indicator.tsx +++ b/packages/app/src/components/session-mcp-indicator.tsx @@ -1,6 +1,5 @@ import { createMemo, Show } from "solid-js" import { Button } from "@opencode-ai/ui/button" -import { Icon } from "@opencode-ai/ui/icon" import { useDialog } from "@opencode-ai/ui/context/dialog" import { useSync } from "@/context/sync" import { DialogSelectMcp } from "@/components/dialog-select-mcp" @@ -21,12 +20,11 @@ export function SessionMcpIndicator() { return ( 0}> +
{directoryDisplay()} -- cgit v1.2.3