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()}
-
-
+ ) : (
+
+ )}
)