diff --git a/.zed/settings.json b/.zed/settings.json
new file mode 100644
index 000000000..a3a5e1e2b
--- /dev/null
+++ b/.zed/settings.json
@@ -0,0 +1,9 @@
+{
+ "format_on_save": "on",
+ "formatter": {
+ "external": {
+ "command": "bunx",
+ "arguments": ["prettier", "--stdin-filepath", "{buffer_path}"]
+ }
+ }
+}
diff --git a/packages/app/src/app.tsx b/packages/app/src/app.tsx
index 1121c2e95..1be9f38d7 100644
--- a/packages/app/src/app.tsx
+++ b/packages/app/src/app.tsx
@@ -1,35 +1,36 @@
import "@/index.css"
-import { ErrorBoundary, Show, Suspense, lazy, type JSX, type ParentProps } from "solid-js"
-import { Router, Route, Navigate } from "@solidjs/router"
-import { MetaProvider } from "@solidjs/meta"
-import { Font } from "@opencode-ai/ui/font"
-import { MarkedProvider } from "@opencode-ai/ui/context/marked"
-import { DiffComponentProvider } from "@opencode-ai/ui/context/diff"
-import { CodeComponentProvider } from "@opencode-ai/ui/context/code"
-import { I18nProvider } from "@opencode-ai/ui/context"
-import { Diff } from "@opencode-ai/ui/diff"
import { Code } from "@opencode-ai/ui/code"
+import { I18nProvider } from "@opencode-ai/ui/context"
+import { CodeComponentProvider } from "@opencode-ai/ui/context/code"
+import { DialogProvider } from "@opencode-ai/ui/context/dialog"
+import { DiffComponentProvider } from "@opencode-ai/ui/context/diff"
+import { MarkedProvider } from "@opencode-ai/ui/context/marked"
+import { Diff } from "@opencode-ai/ui/diff"
+import { Font } from "@opencode-ai/ui/font"
import { ThemeProvider } from "@opencode-ai/ui/theme"
-import { GlobalSyncProvider } from "@/context/global-sync"
-import { PermissionProvider } from "@/context/permission"
-import { LayoutProvider } from "@/context/layout"
+import { MetaProvider } from "@solidjs/meta"
+import { Navigate, Route, Router } from "@solidjs/router"
+import { ErrorBoundary, type JSX, lazy, type ParentProps, Show, Suspense } from "solid-js"
+import { CommandProvider } from "@/context/command"
+import { CommentsProvider } from "@/context/comments"
+import { FileProvider } from "@/context/file"
import { GlobalSDKProvider } from "@/context/global-sdk"
-import { normalizeServerUrl, ServerProvider, useServer } from "@/context/server"
+import { GlobalSyncProvider } from "@/context/global-sync"
+import { HighlightsProvider } from "@/context/highlights"
+import { LanguageProvider, useLanguage } from "@/context/language"
+import { LayoutProvider } from "@/context/layout"
+import { ModelsProvider } from "@/context/models"
+import { NotificationProvider } from "@/context/notification"
+import { PermissionProvider } from "@/context/permission"
+import { usePlatform } from "@/context/platform"
+import { PromptProvider } from "@/context/prompt"
+import { type ServerConnection, ServerProvider, useServer } from "@/context/server"
import { SettingsProvider } from "@/context/settings"
import { TerminalProvider } from "@/context/terminal"
-import { PromptProvider } from "@/context/prompt"
-import { FileProvider } from "@/context/file"
-import { CommentsProvider } from "@/context/comments"
-import { NotificationProvider } from "@/context/notification"
-import { ModelsProvider } from "@/context/models"
-import { DialogProvider } from "@opencode-ai/ui/context/dialog"
-import { CommandProvider } from "@/context/command"
-import { LanguageProvider, useLanguage } from "@/context/language"
-import { usePlatform } from "@/context/platform"
-import { HighlightsProvider } from "@/context/highlights"
-import Layout from "@/pages/layout"
import DirectoryLayout from "@/pages/directory-layout"
+import Layout from "@/pages/layout"
import { ErrorPage } from "./pages/error"
+
const Home = lazy(() => import("@/pages/home"))
const Session = lazy(() => import("@/pages/session"))
const Loading = () =>
@@ -57,7 +58,11 @@ function UiI18nBridge(props: ParentProps) {
declare global {
interface Window {
- __OPENCODE__?: { updaterEnabled?: boolean; serverPassword?: string; deepLinks?: string[]; wsl?: boolean }
+ __OPENCODE__?: {
+ updaterEnabled?: boolean
+ deepLinks?: string[]
+ wsl?: boolean
+ }
}
}
@@ -107,30 +112,6 @@ function RouterRoot(props: ParentProps<{ appChildren?: JSX.Element }>) {
)
}
-const getStoredDefaultServerUrl = (platform: ReturnType) => {
- if (platform.platform !== "web") return
- const result = platform.getDefaultServerUrl?.()
- if (result instanceof Promise) return
- if (!result) return
- return normalizeServerUrl(result)
-}
-
-const resolveDefaultServerUrl = (props: {
- defaultUrl?: string
- storedDefaultServerUrl?: string
- hostname: string
- origin: string
- isDev: boolean
- devHost?: string
- devPort?: string
-}) => {
- if (props.defaultUrl) return props.defaultUrl
- if (props.storedDefaultServerUrl) return props.storedDefaultServerUrl
- if (props.hostname.includes("opencode.ai")) return "http://localhost:4096"
- if (props.isDev) return `http://${props.devHost ?? "localhost"}:${props.devPort ?? "4096"}`
- return props.origin
-}
-
export function AppBaseProviders(props: ParentProps) {
return (
@@ -157,27 +138,19 @@ export function AppBaseProviders(props: ParentProps) {
function ServerKey(props: ParentProps) {
const server = useServer()
return (
-
+
{props.children}
)
}
-export function AppInterface(props: { defaultUrl?: string; children?: JSX.Element; isSidecar?: boolean }) {
- const platform = usePlatform()
- const storedDefaultServerUrl = getStoredDefaultServerUrl(platform)
- const defaultServerUrl = resolveDefaultServerUrl({
- defaultUrl: props.defaultUrl,
- storedDefaultServerUrl,
- hostname: location.hostname,
- origin: window.location.origin,
- isDev: import.meta.env.DEV,
- devHost: import.meta.env.VITE_OPENCODE_SERVER_HOST,
- devPort: import.meta.env.VITE_OPENCODE_SERVER_PORT,
- })
-
+export function AppInterface(props: {
+ children?: JSX.Element
+ defaultServer: ServerConnection.Key
+ servers?: Array
+}) {
return (
-
+
diff --git a/packages/app/src/components/dialog-select-server.tsx b/packages/app/src/components/dialog-select-server.tsx
index 4c3780636..fa5d2d36c 100644
--- a/packages/app/src/components/dialog-select-server.tsx
+++ b/packages/app/src/components/dialog-select-server.tsx
@@ -1,19 +1,18 @@
-import { createResource, createEffect, createMemo, onCleanup, Show } from "solid-js"
-import { createStore, reconcile } from "solid-js/store"
+import { Button } from "@opencode-ai/ui/button"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { Dialog } from "@opencode-ai/ui/dialog"
-import { List } from "@opencode-ai/ui/list"
-import { Button } from "@opencode-ai/ui/button"
-import { IconButton } from "@opencode-ai/ui/icon-button"
-import { TextField } from "@opencode-ai/ui/text-field"
-import { normalizeServerUrl, useServer } from "@/context/server"
-import { usePlatform } from "@/context/platform"
-import { useNavigate } from "@solidjs/router"
-import { useLanguage } from "@/context/language"
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
-import { useGlobalSDK } from "@/context/global-sdk"
+import { IconButton } from "@opencode-ai/ui/icon-button"
+import { List } from "@opencode-ai/ui/list"
+import { TextField } from "@opencode-ai/ui/text-field"
import { showToast } from "@opencode-ai/ui/toast"
+import { useNavigate } from "@solidjs/router"
+import { createEffect, createMemo, createResource, onCleanup, Show } from "solid-js"
+import { createStore, reconcile } from "solid-js/store"
import { ServerRow } from "@/components/server/server-row"
+import { useLanguage } from "@/context/language"
+import { usePlatform } from "@/context/platform"
+import { normalizeServerUrl, ServerConnection, useServer } from "@/context/server"
import { checkServerHealth, type ServerHealth } from "@/utils/server-health"
interface AddRowProps {
@@ -89,7 +88,7 @@ function useServerPreview(fetcher: typeof fetch) {
if (!looksComplete(value)) return
const normalized = normalizeServerUrl(value)
if (!normalized) return
- const result = await checkServerHealth(normalized, fetcher)
+ const result = await checkServerHealth({ url: normalized }, fetcher)
setStatus(result.healthy)
}
@@ -171,14 +170,13 @@ export function DialogSelectServer() {
const dialog = useDialog()
const server = useServer()
const platform = usePlatform()
- const globalSDK = useGlobalSDK()
const language = useLanguage()
const fetcher = platform.fetch ?? globalThis.fetch
const { defaultUrl, canDefault, setDefault } = useDefaultServer(platform, language)
const { previewStatus } = useServerPreview(fetcher)
let listRoot: HTMLDivElement | undefined
const [store, setStore] = createStore({
- status: {} as Record,
+ status: {} as Record,
addServer: {
url: "",
adding: false,
@@ -214,24 +212,25 @@ export function DialogSelectServer() {
})
}
- const replaceServer = (original: string, next: string) => {
- const active = server.url
- const nextActive = active === original ? next : active
+ const replaceServer = (original: ServerConnection.Http, next: string) => {
+ const active = server.key
+ const newConn = server.add(next)
+ if (!newConn) return
- server.add(next)
+ const nextActive = active === ServerConnection.key(original) ? ServerConnection.key(newConn) : active
if (nextActive) server.setActive(nextActive)
- server.remove(original)
+ server.remove(ServerConnection.key(original))
}
const items = createMemo(() => {
- const current = server.url
+ const current = server.current
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])
+ const current = createMemo(() => items().find((x) => ServerConnection.key(x) === server.key) ?? items()[0])
const sortedItems = createMemo(() => {
const list = items()
@@ -246,17 +245,17 @@ export function DialogSelectServer() {
return list.slice().sort((a, b) => {
if (a === active) return -1
if (b === active) return 1
- const diff = rank(store.status[a]) - rank(store.status[b])
+ const diff = rank(store.status[ServerConnection.key(a)]) - rank(store.status[ServerConnection.key(b)])
if (diff !== 0) return diff
return (order.get(a) ?? 0) - (order.get(b) ?? 0)
})
})
async function refreshHealth() {
- const results: Record = {}
+ const results: Record = {}
await Promise.all(
- items().map(async (url) => {
- results[url] = await checkServerHealth(url, fetcher)
+ items().map(async (conn) => {
+ results[ServerConnection.key(conn)] = await checkServerHealth(conn.http, fetcher)
}),
)
setStore("status", reconcile(results))
@@ -269,15 +268,15 @@ export function DialogSelectServer() {
onCleanup(() => clearInterval(interval))
})
- async function select(value: string, persist?: boolean) {
- if (!persist && store.status[value]?.healthy === false) return
+ async function select(conn: ServerConnection.Any, persist?: boolean) {
+ if (!persist && store.status[ServerConnection.key(conn)]?.healthy === false) return
dialog.close()
if (persist) {
- server.add(value)
+ server.add(conn.http.url)
navigate("/")
return
}
- server.setActive(value)
+ server.setActive(ServerConnection.key(conn))
navigate("/")
}
@@ -311,7 +310,7 @@ export function DialogSelectServer() {
setStore("addServer", { adding: true, error: "" })
- const result = await checkServerHealth(normalized, fetcher)
+ const result = await checkServerHealth({ url: normalized }, fetcher)
setStore("addServer", { adding: false })
if (!result.healthy) {
@@ -320,25 +319,25 @@ export function DialogSelectServer() {
}
resetAdd()
- await select(normalized, true)
+ await select({ type: "http", http: { url: normalized } }, true)
}
- async function handleEdit(original: string, value: string) {
- if (store.editServer.busy) return
+ async function handleEdit(original: ServerConnection.Any, value: string) {
+ if (store.editServer.busy || original.type !== "http") return
const normalized = normalizeServerUrl(value)
if (!normalized) {
resetEdit()
return
}
- if (normalized === original) {
+ if (normalized === original.http.url) {
resetEdit()
return
}
setStore("editServer", { busy: true, error: "" })
- const result = await checkServerHealth(normalized, fetcher)
+ const result = await checkServerHealth({ url: normalized }, fetcher)
setStore("editServer", { busy: false })
if (!result.healthy) {
@@ -366,7 +365,7 @@ export function DialogSelectServer() {
handleAdd(store.addServer.url)
}
- const handleEditKey = (event: KeyboardEvent, original: string) => {
+ const handleEditKey = (event: KeyboardEvent, original: ServerConnection.Any) => {
event.stopPropagation()
if (event.key === "Escape") {
event.preventDefault()
@@ -378,7 +377,7 @@ export function DialogSelectServer() {
handleEdit(original, store.editServer.value)
}
- async function handleRemove(url: string) {
+ async function handleRemove(url: ServerConnection.Key) {
server.remove(url)
if ((await platform.getDefaultServerUrl?.()) === url) {
platform.setDefaultServerUrl?.(null)
@@ -390,11 +389,14 @@ export function DialogSelectServer() {
(listRoot = el)}>
x}
+ key={(x) => x.http.url}
onSelect={(x) => {
if (x) select(x)
}}
@@ -428,7 +430,7 @@ export function DialogSelectServer() {
return (
+
{language.t("dialog.server.status.default")}
@@ -456,59 +458,63 @@ export function DialogSelectServer() {
}
/>
-
+
{language.t("dialog.server.current")}
-
- e.stopPropagation()}
- onPointerDown={(e: PointerEvent) => e.stopPropagation()}
- />
-
-
- {
- setStore("editServer", {
- id: i,
- value: i,
- error: "",
- status: store.status[i]?.healthy,
- })
- }}
- >
- {language.t("dialog.server.menu.edit")}
-
-
- setDefault(i)}>
+
+
+ e.stopPropagation()}
+ onPointerDown={(e: PointerEvent) => e.stopPropagation()}
+ />
+
+
+ {
+ setStore("editServer", {
+ id: i.http.url,
+ value: i.http.url,
+ error: "",
+ status: store.status[ServerConnection.key(i)]?.healthy,
+ })
+ }}
+ >
+ {language.t("dialog.server.menu.edit")}
+
+
+ setDefault(i.http.url)}>
+
+ {language.t("dialog.server.menu.default")}
+
+
+
+
+ setDefault(null)}>
+
+ {language.t("dialog.server.menu.defaultRemove")}
+
+
+
+
+ handleRemove(ServerConnection.key(i))}
+ class="text-text-on-critical-base hover:bg-surface-critical-weak"
+ >
- {language.t("dialog.server.menu.default")}
+ {language.t("dialog.server.menu.delete")}
-
-
- setDefault(null)}>
-
- {language.t("dialog.server.menu.defaultRemove")}
-
-
-
-
- handleRemove(i)}
- class="text-text-on-critical-base hover:bg-surface-critical-weak"
- >
- {language.t("dialog.server.menu.delete")}
-
-
-
-
+
+
+
+
diff --git a/packages/app/src/components/prompt-input/submit.test.ts b/packages/app/src/components/prompt-input/submit.test.ts
index 475a0e20f..c3d6a9281 100644
--- a/packages/app/src/components/prompt-input/submit.test.ts
+++ b/packages/app/src/components/prompt-input/submit.test.ts
@@ -12,24 +12,27 @@ let selected = "/repo/worktree-a"
const promptValue: Prompt = [{ type: "text", content: "ls", start: 0, end: 2 }]
-const clientFor = (directory: string) => ({
- session: {
- create: async () => {
- createdSessions.push(directory)
- return { data: { id: `session-${createdSessions.length}` } }
+const clientFor = (directory: string) => {
+ createdClients.push(directory)
+ return {
+ session: {
+ create: async () => {
+ createdSessions.push(directory)
+ return { data: { id: `session-${createdSessions.length}` } }
+ },
+ shell: async () => {
+ sentShell.push(directory)
+ return { data: undefined }
+ },
+ prompt: async () => ({ data: undefined }),
+ command: async () => ({ data: undefined }),
+ abort: async () => ({ data: undefined }),
},
- shell: async () => {
- sentShell.push(directory)
- return { data: undefined }
+ worktree: {
+ create: async () => ({ data: { directory: `${directory}/new` } }),
},
- prompt: async () => ({ data: undefined }),
- command: async () => ({ data: undefined }),
- abort: async () => ({ data: undefined }),
- },
- worktree: {
- create: async () => ({ data: { directory: `${directory}/new` } }),
- },
-})
+ }
+}
beforeAll(async () => {
const rootClient = clientFor("/repo/main")
@@ -88,11 +91,17 @@ beforeAll(async () => {
}))
mock.module("@/context/sdk", () => ({
- useSDK: () => ({
- directory: "/repo/main",
- client: rootClient,
- url: "http://localhost:4096",
- }),
+ useSDK: () => {
+ const sdk = {
+ directory: "/repo/main",
+ client: rootClient,
+ url: "http://localhost:4096",
+ createClient(opts: any) {
+ return clientFor(opts.directory)
+ },
+ }
+ return sdk
+ },
}))
mock.module("@/context/sync", () => ({
diff --git a/packages/app/src/components/prompt-input/submit.ts b/packages/app/src/components/prompt-input/submit.ts
index 6b6f4a4e0..8a3dfc40d 100644
--- a/packages/app/src/components/prompt-input/submit.ts
+++ b/packages/app/src/components/prompt-input/submit.ts
@@ -1,21 +1,20 @@
-import { Accessor } from "solid-js"
-import { useNavigate, useParams } from "@solidjs/router"
-import { createOpencodeClient, type Message } from "@opencode-ai/sdk/v2/client"
+import type { Message } from "@opencode-ai/sdk/v2/client"
import { showToast } from "@opencode-ai/ui/toast"
import { base64Encode } from "@opencode-ai/util/encode"
-import { useLocal } from "@/context/local"
-import { usePrompt, type ImageAttachmentPart, type Prompt } from "@/context/prompt"
+import { useNavigate, useParams } from "@solidjs/router"
+import type { Accessor } from "solid-js"
+import type { FileSelection } from "@/context/file"
+import { useGlobalSync } from "@/context/global-sync"
+import { useLanguage } from "@/context/language"
import { useLayout } from "@/context/layout"
+import { useLocal } from "@/context/local"
+import { type ImageAttachmentPart, type Prompt, usePrompt } from "@/context/prompt"
import { useSDK } from "@/context/sdk"
import { useSync } from "@/context/sync"
-import { useGlobalSync } from "@/context/global-sync"
-import { usePlatform } from "@/context/platform"
-import { useLanguage } from "@/context/language"
import { Identifier } from "@/utils/id"
import { Worktree as WorktreeState } from "@/utils/worktree"
-import type { FileSelection } from "@/context/file"
-import { setCursorPosition } from "./editor-dom"
import { buildRequestParts } from "./build-request-parts"
+import { setCursorPosition } from "./editor-dom"
type PendingPrompt = {
abort: AbortController
@@ -56,7 +55,6 @@ export function createPromptSubmit(input: PromptSubmitInput) {
const sdk = useSDK()
const sync = useSync()
const globalSync = useGlobalSync()
- const platform = usePlatform()
const local = useLocal()
const prompt = usePrompt()
const layout = useLayout()
@@ -175,9 +173,7 @@ export function createPromptSubmit(input: PromptSubmitInput) {
}
if (sessionDirectory !== projectDirectory) {
- client = createOpencodeClient({
- baseUrl: sdk.url,
- fetch: platform.fetch,
+ client = sdk.createClient({
directory: sessionDirectory,
throwOnError: true,
})
@@ -372,7 +368,10 @@ export function createPromptSubmit(input: PromptSubmitInput) {
const timer = { id: undefined as number | undefined }
const timeout = new Promise>>((resolve) => {
timer.id = window.setTimeout(() => {
- resolve({ status: "failed", message: language.t("workspace.error.stillPreparing") })
+ resolve({
+ status: "failed",
+ message: language.t("workspace.error.stillPreparing"),
+ })
}, timeoutMs)
})
diff --git a/packages/app/src/components/server/server-row.tsx b/packages/app/src/components/server/server-row.tsx
index f93bdb33b..12dcebfa9 100644
--- a/packages/app/src/components/server/server-row.tsx
+++ b/packages/app/src/components/server/server-row.tsx
@@ -1,10 +1,19 @@
import { Tooltip } from "@opencode-ai/ui/tooltip"
-import { JSXElement, ParentProps, Show, createEffect, createMemo, createSignal, onCleanup, onMount } from "solid-js"
-import { serverDisplayName } from "@/context/server"
+import {
+ createEffect,
+ createMemo,
+ createSignal,
+ type JSXElement,
+ onCleanup,
+ onMount,
+ type ParentProps,
+ Show,
+} from "solid-js"
+import { type ServerConnection, serverDisplayName } from "@/context/server"
import type { ServerHealth } from "@/utils/server-health"
interface ServerRowProps extends ParentProps {
- url: string
+ conn: ServerConnection.Any
status?: ServerHealth
class?: string
nameClass?: string
@@ -17,7 +26,7 @@ export function ServerRow(props: ServerRowProps) {
const [truncated, setTruncated] = createSignal(false)
let nameRef: HTMLSpanElement | undefined
let versionRef: HTMLSpanElement | undefined
- const name = createMemo(() => serverDisplayName(props.url))
+ const name = createMemo(() => serverDisplayName(props.conn))
const check = () => {
const nameTruncated = nameRef ? nameRef.scrollWidth > nameRef.clientWidth : false
@@ -27,7 +36,7 @@ export function ServerRow(props: ServerRowProps) {
createEffect(() => {
name()
- props.url
+ props.conn.http.url
props.status?.version
queueMicrotask(check)
})
diff --git a/packages/app/src/components/status-popover.tsx b/packages/app/src/components/status-popover.tsx
index ccaa0ff77..006b15780 100644
--- a/packages/app/src/components/status-popover.tsx
+++ b/packages/app/src/components/status-popover.tsx
@@ -1,21 +1,21 @@
-import { createEffect, createMemo, createSignal, For, onCleanup, Show, type Accessor, type JSXElement } from "solid-js"
-import { createStore, reconcile } from "solid-js/store"
-import { useNavigate } from "@solidjs/router"
-import { useDialog } from "@opencode-ai/ui/context/dialog"
-import { Popover } from "@opencode-ai/ui/popover"
-import { Tabs } from "@opencode-ai/ui/tabs"
import { Button } from "@opencode-ai/ui/button"
-import { Switch } from "@opencode-ai/ui/switch"
+import { useDialog } from "@opencode-ai/ui/context/dialog"
import { Icon } from "@opencode-ai/ui/icon"
+import { Popover } from "@opencode-ai/ui/popover"
+import { Switch } from "@opencode-ai/ui/switch"
+import { Tabs } from "@opencode-ai/ui/tabs"
import { showToast } from "@opencode-ai/ui/toast"
-import { useSync } from "@/context/sync"
-import { useSDK } from "@/context/sdk"
-import { normalizeServerUrl, useServer } from "@/context/server"
-import { usePlatform } from "@/context/platform"
-import { useLanguage } from "@/context/language"
-import { DialogSelectServer } from "./dialog-select-server"
+import { useNavigate } from "@solidjs/router"
+import { type Accessor, createEffect, createMemo, createSignal, For, type JSXElement, onCleanup, Show } from "solid-js"
+import { createStore, reconcile } from "solid-js/store"
import { ServerRow } from "@/components/server/server-row"
+import { useLanguage } from "@/context/language"
+import { usePlatform } from "@/context/platform"
+import { useSDK } from "@/context/sdk"
+import { normalizeServerUrl, ServerConnection, useServer } from "@/context/server"
+import { useSync } from "@/context/sync"
import { checkServerHealth, type ServerHealth } from "@/utils/server-health"
+import { DialogSelectServer } from "./dialog-select-server"
const pollMs = 10_000
@@ -32,9 +32,9 @@ const pluginEmptyMessage = (value: string, file: string): JSXElement => {
}
const listServersByHealth = (
- list: string[],
- active: string | undefined,
- status: Record,
+ list: ServerConnection.Any[],
+ active: ServerConnection.Key | undefined,
+ status: Record,
) => {
if (!list.length) return list
const order = new Map(list.map((url, index) => [url, index] as const))
@@ -45,16 +45,16 @@ const listServersByHealth = (
}
return list.slice().sort((a, b) => {
- if (a === active) return -1
- if (b === active) return 1
- const diff = rank(status[a]) - rank(status[b])
+ if (ServerConnection.key(a) === active) return -1
+ if (ServerConnection.key(b) === active) return 1
+ const diff = rank(status[ServerConnection.key(a)]) - rank(status[ServerConnection.key(b)])
if (diff !== 0) return diff
return (order.get(a) ?? 0) - (order.get(b) ?? 0)
})
}
-const useServerHealth = (servers: Accessor, fetcher: typeof fetch) => {
- const [status, setStatus] = createStore({} as Record)
+const useServerHealth = (servers: Accessor, fetcher: typeof fetch) => {
+ const [status, setStatus] = createStore({} as Record)
createEffect(() => {
const list = servers()
@@ -63,8 +63,8 @@ const useServerHealth = (servers: Accessor, fetcher: typeof fetch) =>
const refresh = async () => {
const results: Record = {}
await Promise.all(
- list.map(async (url) => {
- results[url] = await checkServerHealth(url, fetcher)
+ list.map(async (conn) => {
+ results[ServerConnection.key(conn)] = await checkServerHealth(conn.http, fetcher)
}),
)
if (dead) return
@@ -82,7 +82,7 @@ const useServerHealth = (servers: Accessor, fetcher: typeof fetch) =>
return status
}
-const useDefaultServerUrl = (
+const useDefaultServerKey = (
get: (() => string | Promise | null | undefined) | undefined,
) => {
const [url, setUrl] = createSignal()
@@ -117,7 +117,14 @@ const useDefaultServerUrl = (
})
})
- return { url, refresh: () => setTick((value) => value + 1) }
+ return {
+ key: () => {
+ const u = url()
+ if (!u) return
+ return ServerConnection.key({ type: "http", http: { url: u } })
+ },
+ refresh: () => setTick((value) => value + 1),
+ }
}
const useMcpToggle = (input: {
@@ -163,16 +170,16 @@ export function StatusPopover() {
const fetcher = platform.fetch ?? globalThis.fetch
const servers = createMemo(() => {
- const current = server.url
+ const current = server.current
const list = server.list
if (!current) return list
- if (!list.includes(current)) return [current, ...list]
- return [current, ...list.filter((item) => item !== current)]
+ if (list.every((item) => ServerConnection.key(item) !== ServerConnection.key(current))) return [current, ...list]
+ return [current, ...list.filter((item) => ServerConnection.key(item) !== ServerConnection.key(current))]
})
const health = useServerHealth(servers, fetcher)
- const sortedServers = createMemo(() => listServersByHealth(servers(), server.url, health))
+ const sortedServers = createMemo(() => listServersByHealth(servers(), server.key, health))
const mcp = useMcpToggle({ sync, sdk, language })
- const defaultServer = useDefaultServerUrl(platform.getDefaultServerUrl)
+ const defaultServer = useDefaultServerKey(platform.getDefaultServerUrl)
const mcpNames = createMemo(() => Object.keys(sync.data.mcp ?? {}).sort((a, b) => a.localeCompare(b)))
const mcpStatus = (name: string) => sync.data.mcp?.[name]?.status
const mcpConnected = createMemo(() => mcpNames().filter((name) => mcpStatus(name) === "connected").length)
@@ -251,8 +258,9 @@ export function StatusPopover() {
- {(url) => {
- const isBlocked = () => health[url]?.healthy === false
+ {(s) => {
+ const key = ServerConnection.key(s)
+ const isBlocked = () => health[key]?.healthy === false
return (