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 | |
| parent | 281f9e623673e6bbfd9a5f9a8f9aae496abc99f2 (diff) | |
| download | opencode-e0e07c5d48ef0671f63ca8bd9119169ced493fac.tar.gz opencode-e0e07c5d48ef0671f63ca8bd9119169ced493fac.zip | |
feat(app): change server
Diffstat (limited to 'packages/app')
| -rw-r--r-- | packages/app/src/app.tsx | 93 | ||||
| -rw-r--r-- | packages/app/src/components/dialog-select-server.tsx | 170 | ||||
| -rw-r--r-- | packages/app/src/components/session-lsp-indicator.tsx | 10 | ||||
| -rw-r--r-- | packages/app/src/components/session-mcp-indicator.tsx | 10 | ||||
| -rw-r--r-- | packages/app/src/components/status-bar.tsx | 29 | ||||
| -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 | ||||
| -rw-r--r-- | packages/app/src/pages/home.tsx | 18 | ||||
| -rw-r--r-- | packages/app/src/pages/layout.tsx | 11 |
10 files changed, 498 insertions, 82 deletions
diff --git a/packages/app/src/app.tsx b/packages/app/src/app.tsx index 9e38d5e98..dd2491961 100644 --- a/packages/app/src/app.tsx +++ b/packages/app/src/app.tsx @@ -1,5 +1,5 @@ import "@/index.css" -import { ErrorBoundary, Show } from "solid-js" +import { ErrorBoundary, Show, type ParentProps } from "solid-js" import { Router, Route, Navigate } from "@solidjs/router" import { MetaProvider } from "@solidjs/meta" import { Font } from "@opencode-ai/ui/font" @@ -12,6 +12,7 @@ import { ThemeProvider } from "@opencode-ai/ui/theme" import { GlobalSyncProvider } from "@/context/global-sync" import { LayoutProvider } from "@/context/layout" import { GlobalSDKProvider } from "@/context/global-sdk" +import { ServerProvider, useServer } from "@/context/server" import { TerminalProvider } from "@/context/terminal" import { PromptProvider } from "@/context/prompt" import { NotificationProvider } from "@/context/notification" @@ -30,18 +31,30 @@ declare global { } } -const url = iife(() => { +const serverDefaults = iife(() => { const param = new URLSearchParams(document.location.search).get("url") - if (param) return param + if (param) return { url: param, forced: true } - if (location.hostname.includes("opencode.ai")) return "http://localhost:4096" - if (window.__OPENCODE__) return `http://127.0.0.1:${window.__OPENCODE__.port}` + if (location.hostname.includes("opencode.ai")) return { url: "http://localhost:4096", forced: false } + if (window.__OPENCODE__) return { url: `http://127.0.0.1:${window.__OPENCODE__.port}`, forced: false } if (import.meta.env.DEV) - return `http://${import.meta.env.VITE_OPENCODE_SERVER_HOST ?? "localhost"}:${import.meta.env.VITE_OPENCODE_SERVER_PORT ?? "4096"}` + return { + url: `http://${import.meta.env.VITE_OPENCODE_SERVER_HOST ?? "localhost"}:${import.meta.env.VITE_OPENCODE_SERVER_PORT ?? "4096"}`, + forced: false, + } - return window.location.origin + return { url: window.location.origin, forced: false } }) +function ServerKey(props: ParentProps) { + const server = useServer() + return ( + <Show when={server.url} keyed> + {props.children} + </Show> + ) +} + export function App() { return ( <MetaProvider> @@ -52,38 +65,42 @@ export function App() { <MarkedProvider> <DiffComponentProvider component={Diff}> <CodeComponentProvider component={Code}> - <GlobalSDKProvider url={url}> - <GlobalSyncProvider> - <LayoutProvider> - <NotificationProvider> - <Router - root={(props) => ( - <CommandProvider> - <Layout>{props.children}</Layout> - </CommandProvider> - )} - > - <Route path="/" component={Home} /> - <Route path="/:dir" component={DirectoryLayout}> - <Route path="/" component={() => <Navigate href="session" />} /> - <Route - path="/session/:id?" - component={(p) => ( - <Show when={p.params.id ?? "new"} keyed> - <TerminalProvider> - <PromptProvider> - <Session /> - </PromptProvider> - </TerminalProvider> - </Show> + <ServerProvider defaultUrl={serverDefaults.url} forceUrl={serverDefaults.forced}> + <ServerKey> + <GlobalSDKProvider> + <GlobalSyncProvider> + <LayoutProvider> + <NotificationProvider> + <Router + root={(props) => ( + <CommandProvider> + <Layout>{props.children}</Layout> + </CommandProvider> )} - /> - </Route> - </Router> - </NotificationProvider> - </LayoutProvider> - </GlobalSyncProvider> - </GlobalSDKProvider> + > + <Route path="/" component={Home} /> + <Route path="/:dir" component={DirectoryLayout}> + <Route path="/" component={() => <Navigate href="session" />} /> + <Route + path="/session/:id?" + component={(p) => ( + <Show when={p.params.id ?? "new"} keyed> + <TerminalProvider> + <PromptProvider> + <Session /> + </PromptProvider> + </TerminalProvider> + </Show> + )} + /> + </Route> + </Router> + </NotificationProvider> + </LayoutProvider> + </GlobalSyncProvider> + </GlobalSDKProvider> + </ServerKey> + </ServerProvider> </CodeComponentProvider> </DiffComponentProvider> </MarkedProvider> 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<ServerStatus> { + 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<string, ServerStatus | undefined>, + }) + + 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<string, ServerStatus> = {} + 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 ( + <Dialog title="Servers" description="Switch which OpenCode server this app connects to."> + <div class="flex flex-col gap-4 pb-4"> + <List + search={{ placeholder: "Search servers", autofocus: true }} + emptyMessage="No servers yet" + items={items} + key={(x) => x} + current={current()} + onSelect={(x) => { + if (x) select(x) + }} + > + {(i) => ( + <div + class="flex items-center gap-2 min-w-0 flex-1" + classList={{ "opacity-50": store.status[i]?.healthy === false }} + > + <div + classList={{ + "size-1.5 rounded-full shrink-0": true, + "bg-icon-success-base": store.status[i]?.healthy === true, + "bg-icon-critical-base": store.status[i]?.healthy === false, + "bg-border-weak-base": store.status[i] === undefined, + }} + /> + <span class="truncate">{displayName(i)}</span> + <span class="text-text-weak">{store.status[i]?.version}</span> + </div> + )} + </List> + + <div class="mt-6 px-3 flex flex-col gap-1.5"> + <div class="px-3"> + <h3 class="text-14-regular text-text-weak">Add a server</h3> + </div> + <form onSubmit={handleSubmit}> + <div class="flex items-start gap-2"> + <div class="flex-1 min-w-0 h-auto"> + <TextField + type="text" + label="Server URL" + hideLabel + placeholder="http://localhost:4096" + value={store.url} + onChange={(v) => { + setStore("url", v) + setStore("error", "") + }} + validationState={store.error ? "invalid" : "valid"} + error={store.error} + /> + </div> + <Button type="submit" variant="secondary" icon="plus-small" size="large" disabled={store.adding}> + {store.adding ? "Checking..." : "Add"} + </Button> + </div> + </form> + </div> + </div> + </Dialog> + ) +} 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() { <Show when={lspStats().total > 0}> <Tooltip placement="top" value={tooltipContent()}> <div class="flex items-center gap-1 px-2 cursor-default select-none"> - <Icon - name="code" - size="small" + <div classList={{ - "text-icon-critical-base": lspStats().hasError, - "text-icon-success-base": !lspStats().hasError && lspStats().connected > 0, + "size-1.5 rounded-full": true, + "bg-icon-critical-base": lspStats().hasError, + "bg-icon-success-base": !lspStats().hasError && lspStats().connected > 0, }} /> <span class="text-12-regular text-text-weak">{lspStats().connected} LSP</span> 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 ( <Show when={mcpStats().total > 0}> <Button variant="ghost" onClick={() => dialog.show(() => <DialogSelectMcp />)}> - <Icon - name="mcp" - size="small" + <div classList={{ - "text-icon-critical-base": mcpStats().failed, - "text-icon-success-base": !mcpStats().failed && mcpStats().enabled > 0, + "size-1.5 rounded-full": true, + "bg-icon-critical-base": mcpStats().failed, + "bg-icon-success-base": !mcpStats().failed && mcpStats().enabled > 0, }} /> <span class="text-12-regular text-text-weak">{mcpStats().enabled} MCP</span> diff --git a/packages/app/src/components/status-bar.tsx b/packages/app/src/components/status-bar.tsx index d8a88503f..bfc428fca 100644 --- a/packages/app/src/components/status-bar.tsx +++ b/packages/app/src/components/status-bar.tsx @@ -1,10 +1,14 @@ import { createMemo, Show, type ParentProps } from "solid-js" -import { usePlatform } from "@/context/platform" import { useSync } from "@/context/sync" import { useGlobalSync } from "@/context/global-sync" +import { useServer } from "@/context/server" +import { useDialog } from "@opencode-ai/ui/context/dialog" +import { Button } from "@opencode-ai/ui/button" +import { DialogSelectServer } from "@/components/dialog-select-server" export function StatusBar(props: ParentProps) { - const platform = usePlatform() + const dialog = useDialog() + const server = useServer() const sync = useSync() const globalSync = useGlobalSync() @@ -19,9 +23,24 @@ export function StatusBar(props: ParentProps) { return ( <div class="h-8 w-full shrink-0 flex items-center justify-between px-2 border-t border-border-weak-base bg-background-base"> <div class="flex items-center gap-3"> - <Show when={platform.version}> - <span class="text-12-regular text-text-weak">v{platform.version}</span> - </Show> + <div class="flex items-center gap-1"> + <Button + size="small" + variant="ghost" + onClick={() => { + dialog.show(() => <DialogSelectServer />) + }} + > + <div + classList={{ + "size-1.5 rounded-full": true, + "bg-icon-success-base": server.healthy(), + "bg-icon-critical-base": !server.healthy(), + }} + /> + <span class="text-12-regular text-text-weak">{server.name}</span> + </Button> + </div> <Show when={directoryDisplay()}> <span class="text-12-regular text-text-weak">{directoryDisplay()}</span> </Show> 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) + }, + }, + } + }, +}) diff --git a/packages/app/src/pages/home.tsx b/packages/app/src/pages/home.tsx index 129a50320..94332fcc6 100644 --- a/packages/app/src/pages/home.tsx +++ b/packages/app/src/pages/home.tsx @@ -10,6 +10,8 @@ import { usePlatform } from "@/context/platform" import { DateTime } from "luxon" import { useDialog } from "@opencode-ai/ui/context/dialog" import { DialogSelectDirectory } from "@/components/dialog-select-directory" +import { DialogSelectServer } from "@/components/dialog-select-server" +import { useServer } from "@/context/server" export default function Home() { const sync = useGlobalSync() @@ -17,6 +19,7 @@ export default function Home() { const platform = usePlatform() const dialog = useDialog() const navigate = useNavigate() + const server = useServer() const homedir = createMemo(() => sync.data.path.home) function openProject(directory: string) { @@ -52,6 +55,21 @@ export default function Home() { return ( <div class="mx-auto mt-55"> <Logo class="w-xl opacity-12" /> + <Button + size="large" + variant="ghost" + class="mt-4 mx-auto text-14-regular text-text-weak" + onClick={() => dialog.show(() => <DialogSelectServer />)} + > + <div + classList={{ + "size-2 rounded-full": true, + "bg-icon-success-base": server.healthy(), + "bg-icon-critical-base": !server.healthy(), + }} + /> + {server.name} + </Button> <Switch> <Match when={sync.data.project.length > 0}> <div class="mt-20 w-full flex flex-col gap-4"> diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index 46ea3bd4d..aea7be3aa 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -50,6 +50,7 @@ import { useDialog } from "@opencode-ai/ui/context/dialog" import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme" import { DialogSelectProvider } from "@/components/dialog-select-provider" import { DialogEditProject } from "@/components/dialog-edit-project" +import { DialogSelectServer } from "@/components/dialog-select-server" import { useCommand, type CommandOption } from "@/context/command" import { ConstrainDragXAxis } from "@/utils/solid-dnd" import { DialogSelectDirectory } from "@/components/dialog-select-directory" @@ -353,6 +354,12 @@ export default function Layout(props: ParentProps) { onSelect: () => connectProvider(), }, { + id: "server.switch", + title: "Switch server", + category: "Server", + onSelect: () => openServer(), + }, + { id: "session.previous", title: "Previous session", category: "Session", @@ -427,6 +434,10 @@ export default function Layout(props: ParentProps) { dialog.show(() => <DialogSelectProvider />) } + function openServer() { + dialog.show(() => <DialogSelectServer />) + } + function navigateToProject(directory: string | undefined) { if (!directory) return const lastSession = store.lastSession[directory] |
