diff --git a/packages/app/src/components/dialog-select-server.tsx b/packages/app/src/components/dialog-select-server.tsx index 6d224c6c3f3..787f06f91a6 100644 --- a/packages/app/src/components/dialog-select-server.tsx +++ b/packages/app/src/components/dialog-select-server.tsx @@ -24,9 +24,12 @@ async function checkHealth(url: string, fetch?: typeof globalThis.fetch): Promis .catch(() => ({ healthy: false })) } -export function DialogSelectServer() { - const navigate = useNavigate() - const dialog = useDialog() +interface ServerSelectorProps { + onSelect?: (url: string) => void + showDialog?: boolean +} + +export function ServerSelector(props: ServerSelectorProps) { const server = useServer() const platform = usePlatform() const [store, setStore] = createStore({ @@ -84,14 +87,9 @@ export function DialogSelectServer() { function select(value: string, persist?: boolean) { if (!persist && store.status[value]?.healthy === false) return - dialog.close() - if (persist) { - server.add(value) - navigate("/") - return - } - server.setActive(value) - navigate("/") + if (persist) server.add(value) + if (!persist) server.setActive(value) + props.onSelect?.(value) } async function handleSubmit(e: SubmitEvent) { @@ -114,66 +112,113 @@ export function DialogSelectServer() { select(value, true) } - return ( - -
- x} - current={current()} - onSelect={(x) => { - if (x) select(x) - }} - > - {(i) => ( + const content = ( +
+ x} + current={current()} + onSelect={(x) => { + if (x) select(x) + }} + > + {(i) => ( +
-
+ {serverDisplayName(i)} + {store.status[i]?.version} +
+ )} + + +
+
+

Add a server

+
+
+
+
+ { + setStore("url", v) + setStore("error", "") }} + validationState={store.error ? "invalid" : "valid"} + error={store.error} /> - {serverDisplayName(i)} - {store.status[i]?.version}
- )} - - -
-
-

Add a server

+
- -
-
- { - setStore("url", v) - setStore("error", "") - }} - validationState={store.error ? "invalid" : "valid"} - error={store.error} - /> -
- -
- -
+
+
+ ) + + if (props.showDialog === false) { + return content + } + + return ( + + {content} + + ) +} + +export function DialogSelectServer() { + const navigate = useNavigate() + const dialog = useDialog() + + return ( + { + dialog.close() + navigate("/") + }} + /> + ) +} + +interface DialogSelectServerConnectionProps { + url: string + onRetry: () => void + onClose: () => void +} + +export function DialogSelectServerConnection(props: DialogSelectServerConnectionProps) { + return ( + } + > + { + props.onClose() + props.onRetry() + }} + /> ) } diff --git a/packages/app/src/context/global-sync.tsx b/packages/app/src/context/global-sync.tsx index a0656c5fc60..8cea4d8a859 100644 --- a/packages/app/src/context/global-sync.tsx +++ b/packages/app/src/context/global-sync.tsx @@ -23,9 +23,22 @@ import { Binary } from "@opencode-ai/util/binary" import { retry } from "@opencode-ai/util/retry" import { useGlobalSDK } from "./global-sdk" import { ErrorPage, type InitError } from "../pages/error" -import { batch, createContext, useContext, onCleanup, onMount, type ParentProps, Switch, Match } from "solid-js" +import { + batch, + createContext, + createEffect, + useContext, + onCleanup, + onMount, + type ParentProps, + Switch, + Match, +} from "solid-js" import { showToast } from "@opencode-ai/ui/toast" import { getFilename } from "@opencode-ai/util/path" +import { useDialog } from "@opencode-ai/ui/context/dialog" +import { DialogSelectServerConnection } from "@/components/dialog-select-server" +import { useServer } from "@/context/server" type State = { status: "loading" | "partial" | "complete" @@ -67,12 +80,14 @@ function createGlobalSync() { const [globalStore, setGlobalStore] = createStore<{ ready: boolean error?: InitError + connectionError: boolean path: Path project: Project[] provider: ProviderListResponse provider_auth: ProviderAuthResponse }>({ ready: false, + connectionError: false, path: { state: "", config: "", worktree: "", directory: "", home: "" }, project: [], provider: { all: [], connected: [], default: {} }, @@ -407,15 +422,13 @@ function createGlobalSync() { onCleanup(unsub) async function bootstrap() { + setGlobalStore("connectionError", false) const health = await globalSDK.client.global .health() .then((x) => x.data) .catch(() => undefined) if (!health?.healthy) { - setGlobalStore( - "error", - new Error(`Could not connect to server. Is there a server running at \`${globalSDK.url}\`?`), - ) + setGlobalStore("connectionError", true) return } @@ -471,6 +484,9 @@ function createGlobalSync() { get error() { return globalStore.error }, + get connectionError() { + return globalStore.connectionError + }, child, bootstrap, project: { @@ -481,15 +497,64 @@ function createGlobalSync() { const GlobalSyncContext = createContext>() +function ConnectionErrorOverlay(props: { url: string; onRetry: () => void }) { + const dialog = useDialog() + + onMount(() => { + dialog.show( + () => dialog.close()} />, + { preventClose: true }, + ) + }) + + return
+} + +function ConnectionWatcher(props: { onRetry: () => void }) { + const server = useServer() + const globalSDK = useGlobalSDK() + const dialog = useDialog() + + createEffect((prev: boolean | undefined) => { + const current = server.healthy() + if (prev === true && current === false) { + dialog.show( + () => ( + { + dialog.close() + props.onRetry() + }} + onClose={() => dialog.close()} + /> + ), + { preventClose: true }, + ) + } + return current + }) + + return null +} + export function GlobalSyncProvider(props: ParentProps) { const value = createGlobalSync() + const globalSDK = useGlobalSDK() + return ( + + + - {props.children} + + + {props.children} + ) diff --git a/packages/app/src/pages/home.tsx b/packages/app/src/pages/home.tsx index 275113566ad..b1d081d94bd 100644 --- a/packages/app/src/pages/home.tsx +++ b/packages/app/src/pages/home.tsx @@ -45,10 +45,7 @@ export default function Home() { }) resolve(result) } else { - dialog.show( - () => , - () => resolve(null), - ) + dialog.show(() => , { onClose: () => resolve(null) }) } } diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index cffefd5634d..c40cd8ba65a 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -691,10 +691,7 @@ export default function Layout(props: ParentProps) { }) resolve(result) } else { - dialog.show( - () => , - () => resolve(null), - ) + dialog.show(() => , { onClose: () => resolve(null) }) } } diff --git a/packages/ui/src/context/dialog.tsx b/packages/ui/src/context/dialog.tsx index 8e770750aff..bac9aca7e95 100644 --- a/packages/ui/src/context/dialog.tsx +++ b/packages/ui/src/context/dialog.tsx @@ -13,12 +13,18 @@ import { Dialog as Kobalte } from "@kobalte/core/dialog" type DialogElement = () => JSX.Element +type DialogOptions = { + onClose?: () => void + preventClose?: boolean +} + type Active = { id: string node: JSX.Element dispose: () => void owner: Owner onClose?: () => void + preventClose?: boolean } const Context = createContext>() @@ -34,7 +40,7 @@ function init() { setActive(undefined) } - const show = (element: DialogElement, owner: Owner, onClose?: () => void) => { + const show = (element: DialogElement, owner: Owner, options?: DialogOptions) => { close() const id = Math.random().toString(36).slice(2) @@ -49,6 +55,7 @@ function init() { open={true} onOpenChange={(open) => { if (open) return + if (options?.preventClose) return close() }} > @@ -63,7 +70,7 @@ function init() { if (!dispose) return - setActive({ id, node, dispose, owner, onClose }) + setActive({ id, node, dispose, owner, onClose: options?.onClose, preventClose: options?.preventClose }) } return { @@ -100,9 +107,9 @@ export function useDialog() { get active() { return ctx.active }, - show(element: DialogElement, onClose?: () => void) { + show(element: DialogElement, options?: DialogOptions) { const base = ctx.active?.owner ?? owner - ctx.show(element, base, onClose) + ctx.show(element, base, options) }, close() { ctx.close()