From 10a43e0f6a9409a87dc6c3857271ce41bca73e81 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Wed, 8 Apr 2026 13:29:01 -0500 Subject: [PATCH] fix(app): tighter startup sequence --- packages/app/src/app.tsx | 16 ++++++- packages/desktop-electron/src/main/index.ts | 44 +++++++++++++------ packages/desktop-electron/src/main/ipc.ts | 2 + packages/desktop-electron/src/main/windows.ts | 14 ++---- .../desktop-electron/src/preload/index.ts | 6 +++ .../desktop-electron/src/preload/types.ts | 8 +++- .../desktop-electron/src/renderer/index.tsx | 1 + .../desktop-electron/src/renderer/loading.tsx | 32 ++++++++------ 8 files changed, 82 insertions(+), 41 deletions(-) diff --git a/packages/app/src/app.tsx b/packages/app/src/app.tsx index c0715cc940..052bede3f1 100644 --- a/packages/app/src/app.tsx +++ b/packages/app/src/app.tsx @@ -13,6 +13,7 @@ import { QueryClient, QueryClientProvider } from "@tanstack/solid-query" import { type Duration, Effect } from "effect" import { type Component, + createEffect, createMemo, createResource, createSignal, @@ -161,7 +162,7 @@ const effectMinDuration = (e: Effect.Effect) => Effect.all([e, Effect.sleep(duration)], { concurrency: "unbounded" }).pipe(Effect.map((v) => v[0])) -function ConnectionGate(props: ParentProps<{ disableHealthCheck?: boolean }>) { +function ConnectionGate(props: ParentProps<{ disableHealthCheck?: boolean; onReady?: () => void }>) { const server = useServer() const checkServerHealth = useCheckServerHealth() @@ -189,6 +190,16 @@ function ConnectionGate(props: ParentProps<{ disableHealthCheck?: boolean }>) { ), ) + let sent = false + + createEffect(() => { + if (sent) return + const ready = checkMode() === "blocking" ? !startupHealthCheck.loading : startupHealthCheck.state !== "pending" + if (!ready) return + sent = true + props.onReady?.() + }) + return ( router?: Component disableHealthCheck?: boolean + onReady?: () => void }) { return ( - + diff --git a/packages/desktop-electron/src/main/index.ts b/packages/desktop-electron/src/main/index.ts index b635caa4e4..69be02428b 100644 --- a/packages/desktop-electron/src/main/index.ts +++ b/packages/desktop-electron/src/main/index.ts @@ -41,8 +41,11 @@ const initEmitter = new EventEmitter() let initStep: InitStep = { phase: "server_waiting" } let mainWindow: BrowserWindow | null = null +let splash: BrowserWindow | null = null +let ready = false let sidecar: CommandChild | null = null const loadingComplete = defer() +const mainReady = defer() const pendingDeepLinks: string[] = [] @@ -112,6 +115,11 @@ function emitDeepLinks(urls: string[]) { } function focusMainWindow() { + if (!ready) { + splash?.show() + splash?.focus() + return + } if (!mainWindow) return mainWindow.show() mainWindow.focus() @@ -121,12 +129,14 @@ function setInitStep(step: InitStep) { initStep = step logger.log("init step", { step }) initEmitter.emit("step", step) + BrowserWindow.getAllWindows().forEach((win) => { + win.webContents.send("init-step", step) + }) } async function initialize() { const needsMigration = !sqliteFileExists() const sqliteDone = needsMigration ? defer() : undefined - let overlay: BrowserWindow | null = null const port = await getSidecarPort() const hostname = "127.0.0.1" @@ -147,7 +157,7 @@ async function initialize() { events.on("sqlite", (progress: SqliteMigrationProgress) => { setInitStep({ phase: "sqlite_waiting" }) - if (overlay) sendSqliteMigrationProgress(overlay, progress) + if (splash) sendSqliteMigrationProgress(splash, progress) if (mainWindow) sendSqliteMigrationProgress(mainWindow, progress) if (progress.type === "Done") sqliteDone?.resolve() }) @@ -173,25 +183,30 @@ async function initialize() { deepLinks: pendingDeepLinks, } - if (needsMigration) { - const show = await Promise.race([loadingTask.then(() => false), delay(1_000).then(() => true)]) - if (show) { - overlay = createLoadingWindow(globals) - await delay(1_000) - } - } + const startup = (async () => { + await loadingTask + setInitStep({ phase: "app_waiting" }) + mainWindow = createMainWindow(globals, { show: false }) - await loadingTask + const ok = await Promise.race([mainReady.promise.then(() => true), delay(15_000).then(() => false)]) + if (!ok) logger.warn("main window ready timed out") + })() + + splash = createLoadingWindow(globals) + + await startup setInitStep({ phase: "done" }) - if (overlay) { + if (splash) { await loadingComplete.promise + splash.close() + splash = null } - mainWindow = createMainWindow(globals) wireMenu() - - overlay?.close() + ready = true + mainWindow?.show() + mainWindow?.focus() } function wireMenu() { @@ -240,6 +255,7 @@ registerIpcHandlers({ wslPath: async (path, mode) => wslPath(path, mode), resolveAppPath: async (appName) => resolveAppPath(appName), loadingWindowComplete: () => loadingComplete.resolve(), + mainWindowReady: () => mainReady.resolve(), runUpdater: async (alertOnFail) => checkForUpdates(alertOnFail), checkUpdate: async () => checkUpdate(), installUpdate: async () => installUpdate(), diff --git a/packages/desktop-electron/src/main/ipc.ts b/packages/desktop-electron/src/main/ipc.ts index d2cfc25241..ff4a84c328 100644 --- a/packages/desktop-electron/src/main/ipc.ts +++ b/packages/desktop-electron/src/main/ipc.ts @@ -26,6 +26,7 @@ type Deps = { wslPath: (path: string, mode: "windows" | "linux" | null) => Promise resolveAppPath: (appName: string) => Promise loadingWindowComplete: () => void + mainWindowReady: () => void runUpdater: (alertOnFail: boolean) => Promise | void checkUpdate: () => Promise<{ updateAvailable: boolean; version?: string }> installUpdate: () => Promise | void @@ -56,6 +57,7 @@ export function registerIpcHandlers(deps: Deps) { ) ipcMain.handle("resolve-app-path", (_event: IpcMainInvokeEvent, appName: string) => deps.resolveAppPath(appName)) ipcMain.on("loading-window-complete", () => deps.loadingWindowComplete()) + ipcMain.on("main-window-ready", () => deps.mainWindowReady()) ipcMain.handle("run-updater", (_event: IpcMainInvokeEvent, alertOnFail: boolean) => deps.runUpdater(alertOnFail)) ipcMain.handle("check-update", () => deps.checkUpdate()) ipcMain.handle("install-update", () => deps.installUpdate()) diff --git a/packages/desktop-electron/src/main/windows.ts b/packages/desktop-electron/src/main/windows.ts index 170cd877cd..04ada79de3 100644 --- a/packages/desktop-electron/src/main/windows.ts +++ b/packages/desktop-electron/src/main/windows.ts @@ -54,7 +54,7 @@ export function setDockIcon() { if (!icon.isEmpty()) app.dock?.setIcon(icon) } -export function createMainWindow(globals: Globals) { +export function createMainWindow(globals: Globals, opts: { show?: boolean } = {}) { const state = windowState({ defaultWidth: 1280, defaultHeight: 800, @@ -66,7 +66,7 @@ export function createMainWindow(globals: Globals) { y: state.y, width: state.width, height: state.height, - show: true, + show: opts.show ?? true, title: "OpenCode", icon: iconPath(), backgroundColor, @@ -98,23 +98,15 @@ export function createMainWindow(globals: Globals) { } export function createLoadingWindow(globals: Globals) { - const mode = tone() const win = new BrowserWindow({ width: 640, height: 480, resizable: false, center: true, show: true, + frame: false, icon: iconPath(), backgroundColor, - ...(process.platform === "darwin" ? { titleBarStyle: "hidden" as const } : {}), - ...(process.platform === "win32" - ? { - frame: false, - titleBarStyle: "hidden" as const, - titleBarOverlay: overlay({ mode }), - } - : {}), webPreferences: { preload: join(root, "../preload/index.mjs"), sandbox: false, diff --git a/packages/desktop-electron/src/preload/index.ts b/packages/desktop-electron/src/preload/index.ts index 296fcb2f1c..ff6e077388 100644 --- a/packages/desktop-electron/src/preload/index.ts +++ b/packages/desktop-electron/src/preload/index.ts @@ -11,6 +11,11 @@ const api: ElectronAPI = { ipcRenderer.removeListener("init-step", handler) }) }, + onInitStep: (cb) => { + const handler = (_: unknown, step: InitStep) => cb(step) + ipcRenderer.on("init-step", handler) + return () => ipcRenderer.removeListener("init-step", handler) + }, getDefaultServerUrl: () => ipcRenderer.invoke("get-default-server-url"), setDefaultServerUrl: (url) => ipcRenderer.invoke("set-default-server-url", url), getWslConfig: () => ipcRenderer.invoke("get-wsl-config"), @@ -60,6 +65,7 @@ const api: ElectronAPI = { setZoomFactor: (factor) => ipcRenderer.invoke("set-zoom-factor", factor), setTitlebar: (theme) => ipcRenderer.invoke("set-titlebar", theme), loadingWindowComplete: () => ipcRenderer.send("loading-window-complete"), + mainWindowReady: () => ipcRenderer.send("main-window-ready"), runUpdater: (alertOnFail) => ipcRenderer.invoke("run-updater", alertOnFail), checkUpdate: () => ipcRenderer.invoke("check-update"), installUpdate: () => ipcRenderer.invoke("install-update"), diff --git a/packages/desktop-electron/src/preload/types.ts b/packages/desktop-electron/src/preload/types.ts index f8e6d52c7d..95b1bac17c 100644 --- a/packages/desktop-electron/src/preload/types.ts +++ b/packages/desktop-electron/src/preload/types.ts @@ -1,4 +1,8 @@ -export type InitStep = { phase: "server_waiting" } | { phase: "sqlite_waiting" } | { phase: "done" } +export type InitStep = + | { phase: "server_waiting" } + | { phase: "sqlite_waiting" } + | { phase: "app_waiting" } + | { phase: "done" } export type ServerReadyData = { url: string @@ -19,6 +23,7 @@ export type ElectronAPI = { killSidecar: () => Promise installCli: () => Promise awaitInitialization: (onStep: (step: InitStep) => void) => Promise + onInitStep: (cb: (step: InitStep) => void) => () => void getDefaultServerUrl: () => Promise setDefaultServerUrl: (url: string | null) => Promise getWslConfig: () => Promise @@ -66,6 +71,7 @@ export type ElectronAPI = { setZoomFactor: (factor: number) => Promise setTitlebar: (theme: TitlebarTheme) => Promise loadingWindowComplete: () => void + mainWindowReady: () => void runUpdater: (alertOnFail: boolean) => Promise checkUpdate: () => Promise<{ updateAvailable: boolean; version?: string }> installUpdate: () => Promise diff --git a/packages/desktop-electron/src/renderer/index.tsx b/packages/desktop-electron/src/renderer/index.tsx index 44f2e6360c..068956cc5a 100644 --- a/packages/desktop-electron/src/renderer/index.tsx +++ b/packages/desktop-electron/src/renderer/index.tsx @@ -332,6 +332,7 @@ render(() => { defaultServer={defaultServer.latest ?? ServerConnection.Key.make("sidecar")} servers={servers()} router={MemoryRouter} + onReady={() => window.api.mainWindowReady()} > diff --git a/packages/desktop-electron/src/renderer/loading.tsx b/packages/desktop-electron/src/renderer/loading.tsx index 000057e0a8..acf501f378 100644 --- a/packages/desktop-electron/src/renderer/loading.tsx +++ b/packages/desktop-electron/src/renderer/loading.tsx @@ -25,6 +25,7 @@ render(() => { }) window.api.awaitInitialization((next) => setStep(next as InitStep)).catch(() => undefined) + const off = window.api.onInitStep((next) => setStep(next)) onMount(() => { setLine(0) @@ -41,6 +42,7 @@ render(() => { }) onCleanup(() => { + off() listener() timers.forEach(clearTimeout) }) @@ -63,20 +65,24 @@ render(() => {
-
- -
- - {status()} - - `${Math.round(value)}%`} - /> + {phase() === "sqlite_waiting" ? ( +
+ +
+ + {status()} + + `${Math.round(value)}%`} + /> +
-
+ ) : ( + + )}
)