diff options
| author | Brendan Allan <[email protected]> | 2026-03-12 16:10:52 +0800 |
|---|---|---|
| committer | GitHub <[email protected]> | 2026-03-12 08:10:52 +0000 |
| commit | b76ead3fe80a6159fdbfcc9b82c7c6318be68e7f (patch) | |
| tree | bad16ba8185403eb9dc97b6c996bbfa78ae7918b /packages/desktop-electron/src | |
| parent | 51835ecf90e23b34957f4dde843bbba1134f17fe (diff) | |
| download | opencode-b76ead3fe80a6159fdbfcc9b82c7c6318be68e7f.tar.gz opencode-b76ead3fe80a6159fdbfcc9b82c7c6318be68e7f.zip | |
refactor(desktop): rework default server initialization and connection handling (#16965)
Diffstat (limited to 'packages/desktop-electron/src')
| -rw-r--r-- | packages/desktop-electron/src/main/index.ts | 139 | ||||
| -rw-r--r-- | packages/desktop-electron/src/main/server.ts | 45 | ||||
| -rw-r--r-- | packages/desktop-electron/src/preload/types.ts | 1 | ||||
| -rw-r--r-- | packages/desktop-electron/src/renderer/index.tsx | 94 | ||||
| -rw-r--r-- | packages/desktop-electron/src/renderer/loading.tsx | 7 |
5 files changed, 93 insertions, 193 deletions
diff --git a/packages/desktop-electron/src/main/index.ts b/packages/desktop-electron/src/main/index.ts index 7b6acd147..64c2eb10f 100644 --- a/packages/desktop-electron/src/main/index.ts +++ b/packages/desktop-electron/src/main/index.ts @@ -31,35 +31,13 @@ import { registerIpcHandlers, sendDeepLinks, sendMenuCommand, sendSqliteMigratio import { initLogging } from "./logging" import { parseMarkdown } from "./markdown" import { createMenu } from "./menu" -import { - checkHealth, - checkHealthOrAskRetry, - getDefaultServerUrl, - getSavedServerUrl, - getWslConfig, - setDefaultServerUrl, - setWslConfig, - spawnLocalServer, -} from "./server" +import { getDefaultServerUrl, getWslConfig, setDefaultServerUrl, setWslConfig, spawnLocalServer } from "./server" import { createLoadingWindow, createMainWindow, setDockIcon } from "./windows" -type ServerConnection = - | { variant: "existing"; url: string } - | { - variant: "cli" - url: string - password: null | string - health: { - wait: Promise<void> - } - events: any - } - const initEmitter = new EventEmitter() let initStep: InitStep = { phase: "server_waiting" } let mainWindow: BrowserWindow | null = null -const loadingWindow: BrowserWindow | null = null let sidecar: CommandChild | null = null const loadingComplete = defer<void>() @@ -131,77 +109,48 @@ function setInitStep(step: InitStep) { initEmitter.emit("step", step) } -async function setupServerConnection(): Promise<ServerConnection> { - const customUrl = await getSavedServerUrl() - - if (customUrl && (await checkHealthOrAskRetry(customUrl))) { - serverReady.resolve({ url: customUrl, password: null }) - return { variant: "existing", url: customUrl } - } +async function initialize() { + const needsMigration = !sqliteFileExists() + const sqliteDone = needsMigration ? defer<void>() : undefined + let overlay: BrowserWindow | null = null const port = await getSidecarPort() const hostname = "127.0.0.1" - const localUrl = `http://${hostname}:${port}` - - if (await checkHealth(localUrl)) { - serverReady.resolve({ url: localUrl, password: null }) - return { variant: "existing", url: localUrl } - } - + const url = `http://${hostname}:${port}` const password = randomUUID() + + logger.log("spawning sidecar", { url }) const { child, health, events } = spawnLocalServer(hostname, port, password) sidecar = child - - return { - variant: "cli", - url: localUrl, + serverReady.resolve({ + url, + username: "opencode", password, - health, - events, - } -} - -async function initialize() { - const needsMigration = !sqliteFileExists() - const sqliteDone = needsMigration ? defer<void>() : undefined + }) const loadingTask = (async () => { - logger.log("setting up server connection") - const serverConnection = await setupServerConnection() - logger.log("server connection ready", { - variant: serverConnection.variant, - url: serverConnection.url, - }) - - const cliHealthCheck = (() => { - if (serverConnection.variant == "cli") { - return async () => { - const { events, health } = serverConnection - events.on("sqlite", (progress: SqliteMigrationProgress) => { - setInitStep({ phase: "sqlite_waiting" }) - if (loadingWindow) sendSqliteMigrationProgress(loadingWindow, progress) - if (mainWindow) sendSqliteMigrationProgress(mainWindow, progress) - if (progress.type === "Done") sqliteDone?.resolve() - }) - await health.wait - serverReady.resolve({ - url: serverConnection.url, - password: serverConnection.password, - }) - } - } else { - serverReady.resolve({ url: serverConnection.url, password: null }) - return null - } - })() + logger.log("sidecar connection started", { url }) - logger.log("server connection started") + events.on("sqlite", (progress: SqliteMigrationProgress) => { + setInitStep({ phase: "sqlite_waiting" }) + if (overlay) sendSqliteMigrationProgress(overlay, progress) + if (mainWindow) sendSqliteMigrationProgress(mainWindow, progress) + if (progress.type === "Done") sqliteDone?.resolve() + }) - if (cliHealthCheck) { - if (needsMigration) await sqliteDone?.promise - cliHealthCheck?.() + if (needsMigration) { + await sqliteDone?.promise } + await Promise.race([ + health.wait, + delay(30_000).then(() => { + throw new Error("Sidecar health check timed out") + }), + ]).catch((error) => { + logger.error("sidecar health check failed", error) + }) + logger.log("loading task finished") })() @@ -211,32 +160,26 @@ async function initialize() { deepLinks: pendingDeepLinks, } - const loadingWindow = await (async () => { - if (needsMigration /** TOOD: 1 second timeout */) { - // showLoading = await Promise.race([init.then(() => false).catch(() => false), delay(1000).then(() => true)]) - const loadingWindow = createLoadingWindow(globals) - await delay(1000) - return loadingWindow - } else { - logger.log("showing main window without loading window") - mainWindow = createMainWindow(globals) - wireMenu() + wireMenu() + + if (needsMigration) { + const show = await Promise.race([loadingTask.then(() => false), delay(1_000).then(() => true)]) + if (show) { + overlay = createLoadingWindow(globals) + await delay(1_000) } - })() + } await loadingTask setInitStep({ phase: "done" }) - if (loadingWindow) { + if (overlay) { await loadingComplete.promise } - if (!mainWindow) { - mainWindow = createMainWindow(globals) - wireMenu() - } + mainWindow = createMainWindow(globals) - loadingWindow?.close() + overlay?.close() } function wireMenu() { diff --git a/packages/desktop-electron/src/main/server.ts b/packages/desktop-electron/src/main/server.ts index 92018e72e..2d09d119f 100644 --- a/packages/desktop-electron/src/main/server.ts +++ b/packages/desktop-electron/src/main/server.ts @@ -1,6 +1,4 @@ -import { dialog } from "electron" - -import { getConfig, serve, type CommandChild, type Config } from "./cli" +import { serve, type CommandChild } from "./cli" import { DEFAULT_SERVER_URL_KEY, WSL_ENABLED_KEY } from "./constants" import { store } from "./store" @@ -31,15 +29,6 @@ export function setWslConfig(config: WslConfig) { store.set(WSL_ENABLED_KEY, config.enabled) } -export async function getSavedServerUrl(): Promise<string | null> { - const direct = getDefaultServerUrl() - if (direct) return direct - - const config = await getConfig().catch(() => null) - if (!config) return null - return getServerUrlFromConfig(config) -} - export function spawnLocalServer(hostname: string, port: number, password: string) { const { child, exit, events } = serve(hostname, port, password) @@ -94,36 +83,4 @@ export async function checkHealth(url: string, password?: string | null): Promis } } -export async function checkHealthOrAskRetry(url: string): Promise<boolean> { - while (true) { - if (await checkHealth(url)) return true - - const result = await dialog.showMessageBox({ - type: "warning", - message: `Could not connect to configured server:\n${url}\n\nWould you like to retry or start a local server instead?`, - title: "Connection Failed", - buttons: ["Retry", "Start Local"], - defaultId: 0, - cancelId: 1, - }) - - if (result.response === 0) continue - return false - } -} - -export function normalizeHostnameForUrl(hostname: string) { - if (hostname === "0.0.0.0") return "127.0.0.1" - if (hostname === "::") return "[::1]" - if (hostname.includes(":") && !hostname.startsWith("[")) return `[${hostname}]` - return hostname -} - -export function getServerUrlFromConfig(config: Config) { - const server = config.server - if (!server?.port) return null - const host = server.hostname ? normalizeHostnameForUrl(server.hostname) : "127.0.0.1" - return `http://${host}:${server.port}` -} - export type { CommandChild } diff --git a/packages/desktop-electron/src/preload/types.ts b/packages/desktop-electron/src/preload/types.ts index 43bdf1e6c..ae4ca213d 100644 --- a/packages/desktop-electron/src/preload/types.ts +++ b/packages/desktop-electron/src/preload/types.ts @@ -2,6 +2,7 @@ export type InitStep = { phase: "server_waiting" } | { phase: "sqlite_waiting" } export type ServerReadyData = { url: string + username: string | null password: string | null } diff --git a/packages/desktop-electron/src/renderer/index.tsx b/packages/desktop-electron/src/renderer/index.tsx index b5193d626..e313d5594 100644 --- a/packages/desktop-electron/src/renderer/index.tsx +++ b/packages/desktop-electron/src/renderer/index.tsx @@ -9,9 +9,8 @@ import { ServerConnection, useCommand, } from "@opencode-ai/app" -import { Splash } from "@opencode-ai/ui/logo" import type { AsyncStorage } from "@solid-primitives/storage" -import { type Accessor, createResource, type JSX, onCleanup, onMount, Show } from "solid-js" +import { createResource, onCleanup, onMount, Show } from "solid-js" import { render } from "solid-js/web" import { MemoryRouter } from "@solidjs/router" import pkg from "../../package.json" @@ -19,7 +18,6 @@ import { initI18n, t } from "./i18n" import { UPDATER_ENABLED } from "./updater" import { webviewZoom } from "./webview-zoom" import "./styles.css" -import type { ServerReadyData } from "../preload/types" const root = document.getElementById("root") if (import.meta.env.DEV && !(root instanceof HTMLElement)) { @@ -198,11 +196,13 @@ const createPlatform = (): Platform => { await window.api.setWslConfig({ enabled }) }, - getDefaultServerUrl: async () => { - return window.api.getDefaultServerUrl().catch(() => null) + getDefaultServer: async () => { + const url = await window.api.getDefaultServerUrl().catch(() => null) + if (!url) return null + return ServerConnection.Key.make(url) }, - setDefaultServerUrl: async (url: string | null) => { + setDefaultServer: async (url: string | null) => { await window.api.setDefaultServerUrl(url) }, @@ -240,6 +240,31 @@ listenForDeepLinks() render(() => { const platform = createPlatform() + // Fetch sidecar credentials (available immediately, before health check) + const [sidecar] = createResource(() => window.api.awaitInitialization(() => undefined)) + + const [defaultServer] = createResource(() => + platform.getDefaultServer?.().then((url) => { + if (url) return ServerConnection.key({ type: "http", http: { url } }) + }), + ) + + const servers = () => { + const data = sidecar() + if (!data) return [] + const server: ServerConnection.Sidecar = { + displayName: "Local Server", + type: "sidecar", + variant: "base", + http: { + url: data.url, + username: data.username ?? undefined, + password: data.password ?? undefined, + }, + } + return [server] as ServerConnection.Any[] + } + function handleClick(e: MouseEvent) { const link = (e.target as HTMLElement).closest("a.external-link") as HTMLAnchorElement | null if (link?.href) { @@ -248,6 +273,12 @@ render(() => { } } + function Inner() { + const cmd = useCommand() + menuTrigger = (id) => cmd.trigger(id) + return null + } + onMount(() => { document.addEventListener("click", handleClick) onCleanup(() => { @@ -258,55 +289,20 @@ render(() => { return ( <PlatformProvider value={platform}> <AppBaseProviders> - <ServerGate> - {(data) => { - const server: ServerConnection.Sidecar = { - displayName: "Local Server", - type: "sidecar", - variant: "base", - http: { - url: data().url, - username: "opencode", - password: data().password ?? undefined, - }, - } - - function Inner() { - const cmd = useCommand() - - menuTrigger = (id) => cmd.trigger(id) - - return null - } - + <Show when={!defaultServer.loading && !sidecar.loading}> + {(_) => { return ( - <AppInterface defaultServer={ServerConnection.key(server)} servers={[server]} router={MemoryRouter}> + <AppInterface + defaultServer={defaultServer.latest ?? ServerConnection.Key.make("sidecar")} + servers={servers()} + router={MemoryRouter} + > <Inner /> </AppInterface> ) }} - </ServerGate> + </Show> </AppBaseProviders> </PlatformProvider> ) }, root!) - -// Gate component that waits for the server to be ready -function ServerGate(props: { children: (data: Accessor<ServerReadyData>) => JSX.Element }) { - const [serverData] = createResource(() => window.api.awaitInitialization(() => undefined)) - console.log({ serverData }) - if (serverData.state === "errored") throw serverData.error - - return ( - <Show - when={serverData.state !== "pending" && serverData()} - fallback={ - <div class="h-screen w-screen flex flex-col items-center justify-center bg-background-base"> - <Splash class="w-16 h-20 opacity-50 animate-pulse" /> - </div> - } - > - {(data) => props.children(data)} - </Show> - ) -} diff --git a/packages/desktop-electron/src/renderer/loading.tsx b/packages/desktop-electron/src/renderer/loading.tsx index 165950352..000057e0a 100644 --- a/packages/desktop-electron/src/renderer/loading.tsx +++ b/packages/desktop-electron/src/renderer/loading.tsx @@ -1,5 +1,5 @@ -import { render } from "solid-js/web" import { MetaProvider } from "@solidjs/meta" +import { render } from "solid-js/web" import "@opencode-ai/app/index.css" import { Font } from "@opencode-ai/ui/font" import { Splash } from "@opencode-ai/ui/logo" @@ -34,7 +34,10 @@ render(() => { const listener = window.api.onSqliteMigrationProgress((progress: SqliteMigrationProgress) => { if (progress.type === "InProgress") setPercent(Math.max(0, Math.min(100, progress.value))) - if (progress.type === "Done") setPercent(100) + if (progress.type === "Done") { + setPercent(100) + setStep({ phase: "done" }) + } }) onCleanup(() => { |
