Skip to content

Commit 10a43e0

Browse files
committed
fix(app): tighter startup sequence
1 parent 988c989 commit 10a43e0

8 files changed

Lines changed: 82 additions & 41 deletions

File tree

packages/app/src/app.tsx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { QueryClient, QueryClientProvider } from "@tanstack/solid-query"
1313
import { type Duration, Effect } from "effect"
1414
import {
1515
type Component,
16+
createEffect,
1617
createMemo,
1718
createResource,
1819
createSignal,
@@ -161,7 +162,7 @@ const effectMinDuration =
161162
<A, E, R>(e: Effect.Effect<A, E, R>) =>
162163
Effect.all([e, Effect.sleep(duration)], { concurrency: "unbounded" }).pipe(Effect.map((v) => v[0]))
163164

164-
function ConnectionGate(props: ParentProps<{ disableHealthCheck?: boolean }>) {
165+
function ConnectionGate(props: ParentProps<{ disableHealthCheck?: boolean; onReady?: () => void }>) {
165166
const server = useServer()
166167
const checkServerHealth = useCheckServerHealth()
167168

@@ -189,6 +190,16 @@ function ConnectionGate(props: ParentProps<{ disableHealthCheck?: boolean }>) {
189190
),
190191
)
191192

193+
let sent = false
194+
195+
createEffect(() => {
196+
if (sent) return
197+
const ready = checkMode() === "blocking" ? !startupHealthCheck.loading : startupHealthCheck.state !== "pending"
198+
if (!ready) return
199+
sent = true
200+
props.onReady?.()
201+
})
202+
192203
return (
193204
<Show
194205
when={checkMode() === "blocking" ? !startupHealthCheck.loading : startupHealthCheck.state !== "pending"}
@@ -281,14 +292,15 @@ export function AppInterface(props: {
281292
servers?: Array<ServerConnection.Any>
282293
router?: Component<BaseRouterProps>
283294
disableHealthCheck?: boolean
295+
onReady?: () => void
284296
}) {
285297
return (
286298
<ServerProvider
287299
defaultServer={props.defaultServer}
288300
disableHealthCheck={props.disableHealthCheck}
289301
servers={props.servers}
290302
>
291-
<ConnectionGate disableHealthCheck={props.disableHealthCheck}>
303+
<ConnectionGate disableHealthCheck={props.disableHealthCheck} onReady={props.onReady}>
292304
<ServerKey>
293305
<GlobalSDKProvider>
294306
<GlobalSyncProvider>

packages/desktop-electron/src/main/index.ts

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,11 @@ const initEmitter = new EventEmitter()
4141
let initStep: InitStep = { phase: "server_waiting" }
4242

4343
let mainWindow: BrowserWindow | null = null
44+
let splash: BrowserWindow | null = null
45+
let ready = false
4446
let sidecar: CommandChild | null = null
4547
const loadingComplete = defer<void>()
48+
const mainReady = defer<void>()
4649

4750
const pendingDeepLinks: string[] = []
4851

@@ -112,6 +115,11 @@ function emitDeepLinks(urls: string[]) {
112115
}
113116

114117
function focusMainWindow() {
118+
if (!ready) {
119+
splash?.show()
120+
splash?.focus()
121+
return
122+
}
115123
if (!mainWindow) return
116124
mainWindow.show()
117125
mainWindow.focus()
@@ -121,12 +129,14 @@ function setInitStep(step: InitStep) {
121129
initStep = step
122130
logger.log("init step", { step })
123131
initEmitter.emit("step", step)
132+
BrowserWindow.getAllWindows().forEach((win) => {
133+
win.webContents.send("init-step", step)
134+
})
124135
}
125136

126137
async function initialize() {
127138
const needsMigration = !sqliteFileExists()
128139
const sqliteDone = needsMigration ? defer<void>() : undefined
129-
let overlay: BrowserWindow | null = null
130140

131141
const port = await getSidecarPort()
132142
const hostname = "127.0.0.1"
@@ -147,7 +157,7 @@ async function initialize() {
147157

148158
events.on("sqlite", (progress: SqliteMigrationProgress) => {
149159
setInitStep({ phase: "sqlite_waiting" })
150-
if (overlay) sendSqliteMigrationProgress(overlay, progress)
160+
if (splash) sendSqliteMigrationProgress(splash, progress)
151161
if (mainWindow) sendSqliteMigrationProgress(mainWindow, progress)
152162
if (progress.type === "Done") sqliteDone?.resolve()
153163
})
@@ -173,25 +183,30 @@ async function initialize() {
173183
deepLinks: pendingDeepLinks,
174184
}
175185

176-
if (needsMigration) {
177-
const show = await Promise.race([loadingTask.then(() => false), delay(1_000).then(() => true)])
178-
if (show) {
179-
overlay = createLoadingWindow(globals)
180-
await delay(1_000)
181-
}
182-
}
186+
const startup = (async () => {
187+
await loadingTask
188+
setInitStep({ phase: "app_waiting" })
189+
mainWindow = createMainWindow(globals, { show: false })
183190

184-
await loadingTask
191+
const ok = await Promise.race([mainReady.promise.then(() => true), delay(15_000).then(() => false)])
192+
if (!ok) logger.warn("main window ready timed out")
193+
})()
194+
195+
splash = createLoadingWindow(globals)
196+
197+
await startup
185198
setInitStep({ phase: "done" })
186199

187-
if (overlay) {
200+
if (splash) {
188201
await loadingComplete.promise
202+
splash.close()
203+
splash = null
189204
}
190205

191-
mainWindow = createMainWindow(globals)
192206
wireMenu()
193-
194-
overlay?.close()
207+
ready = true
208+
mainWindow?.show()
209+
mainWindow?.focus()
195210
}
196211

197212
function wireMenu() {
@@ -240,6 +255,7 @@ registerIpcHandlers({
240255
wslPath: async (path, mode) => wslPath(path, mode),
241256
resolveAppPath: async (appName) => resolveAppPath(appName),
242257
loadingWindowComplete: () => loadingComplete.resolve(),
258+
mainWindowReady: () => mainReady.resolve(),
243259
runUpdater: async (alertOnFail) => checkForUpdates(alertOnFail),
244260
checkUpdate: async () => checkUpdate(),
245261
installUpdate: async () => installUpdate(),

packages/desktop-electron/src/main/ipc.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ type Deps = {
2626
wslPath: (path: string, mode: "windows" | "linux" | null) => Promise<string>
2727
resolveAppPath: (appName: string) => Promise<string | null>
2828
loadingWindowComplete: () => void
29+
mainWindowReady: () => void
2930
runUpdater: (alertOnFail: boolean) => Promise<void> | void
3031
checkUpdate: () => Promise<{ updateAvailable: boolean; version?: string }>
3132
installUpdate: () => Promise<void> | void
@@ -56,6 +57,7 @@ export function registerIpcHandlers(deps: Deps) {
5657
)
5758
ipcMain.handle("resolve-app-path", (_event: IpcMainInvokeEvent, appName: string) => deps.resolveAppPath(appName))
5859
ipcMain.on("loading-window-complete", () => deps.loadingWindowComplete())
60+
ipcMain.on("main-window-ready", () => deps.mainWindowReady())
5961
ipcMain.handle("run-updater", (_event: IpcMainInvokeEvent, alertOnFail: boolean) => deps.runUpdater(alertOnFail))
6062
ipcMain.handle("check-update", () => deps.checkUpdate())
6163
ipcMain.handle("install-update", () => deps.installUpdate())

packages/desktop-electron/src/main/windows.ts

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ export function setDockIcon() {
5454
if (!icon.isEmpty()) app.dock?.setIcon(icon)
5555
}
5656

57-
export function createMainWindow(globals: Globals) {
57+
export function createMainWindow(globals: Globals, opts: { show?: boolean } = {}) {
5858
const state = windowState({
5959
defaultWidth: 1280,
6060
defaultHeight: 800,
@@ -66,7 +66,7 @@ export function createMainWindow(globals: Globals) {
6666
y: state.y,
6767
width: state.width,
6868
height: state.height,
69-
show: true,
69+
show: opts.show ?? true,
7070
title: "OpenCode",
7171
icon: iconPath(),
7272
backgroundColor,
@@ -98,23 +98,15 @@ export function createMainWindow(globals: Globals) {
9898
}
9999

100100
export function createLoadingWindow(globals: Globals) {
101-
const mode = tone()
102101
const win = new BrowserWindow({
103102
width: 640,
104103
height: 480,
105104
resizable: false,
106105
center: true,
107106
show: true,
107+
frame: false,
108108
icon: iconPath(),
109109
backgroundColor,
110-
...(process.platform === "darwin" ? { titleBarStyle: "hidden" as const } : {}),
111-
...(process.platform === "win32"
112-
? {
113-
frame: false,
114-
titleBarStyle: "hidden" as const,
115-
titleBarOverlay: overlay({ mode }),
116-
}
117-
: {}),
118110
webPreferences: {
119111
preload: join(root, "../preload/index.mjs"),
120112
sandbox: false,

packages/desktop-electron/src/preload/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ const api: ElectronAPI = {
1111
ipcRenderer.removeListener("init-step", handler)
1212
})
1313
},
14+
onInitStep: (cb) => {
15+
const handler = (_: unknown, step: InitStep) => cb(step)
16+
ipcRenderer.on("init-step", handler)
17+
return () => ipcRenderer.removeListener("init-step", handler)
18+
},
1419
getDefaultServerUrl: () => ipcRenderer.invoke("get-default-server-url"),
1520
setDefaultServerUrl: (url) => ipcRenderer.invoke("set-default-server-url", url),
1621
getWslConfig: () => ipcRenderer.invoke("get-wsl-config"),
@@ -60,6 +65,7 @@ const api: ElectronAPI = {
6065
setZoomFactor: (factor) => ipcRenderer.invoke("set-zoom-factor", factor),
6166
setTitlebar: (theme) => ipcRenderer.invoke("set-titlebar", theme),
6267
loadingWindowComplete: () => ipcRenderer.send("loading-window-complete"),
68+
mainWindowReady: () => ipcRenderer.send("main-window-ready"),
6369
runUpdater: (alertOnFail) => ipcRenderer.invoke("run-updater", alertOnFail),
6470
checkUpdate: () => ipcRenderer.invoke("check-update"),
6571
installUpdate: () => ipcRenderer.invoke("install-update"),

packages/desktop-electron/src/preload/types.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
export type InitStep = { phase: "server_waiting" } | { phase: "sqlite_waiting" } | { phase: "done" }
1+
export type InitStep =
2+
| { phase: "server_waiting" }
3+
| { phase: "sqlite_waiting" }
4+
| { phase: "app_waiting" }
5+
| { phase: "done" }
26

37
export type ServerReadyData = {
48
url: string
@@ -19,6 +23,7 @@ export type ElectronAPI = {
1923
killSidecar: () => Promise<void>
2024
installCli: () => Promise<string>
2125
awaitInitialization: (onStep: (step: InitStep) => void) => Promise<ServerReadyData>
26+
onInitStep: (cb: (step: InitStep) => void) => () => void
2227
getDefaultServerUrl: () => Promise<string | null>
2328
setDefaultServerUrl: (url: string | null) => Promise<void>
2429
getWslConfig: () => Promise<WslConfig>
@@ -66,6 +71,7 @@ export type ElectronAPI = {
6671
setZoomFactor: (factor: number) => Promise<void>
6772
setTitlebar: (theme: TitlebarTheme) => Promise<void>
6873
loadingWindowComplete: () => void
74+
mainWindowReady: () => void
6975
runUpdater: (alertOnFail: boolean) => Promise<void>
7076
checkUpdate: () => Promise<{ updateAvailable: boolean; version?: string }>
7177
installUpdate: () => Promise<void>

packages/desktop-electron/src/renderer/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,7 @@ render(() => {
332332
defaultServer={defaultServer.latest ?? ServerConnection.Key.make("sidecar")}
333333
servers={servers()}
334334
router={MemoryRouter}
335+
onReady={() => window.api.mainWindowReady()}
335336
>
336337
<Inner />
337338
</AppInterface>

packages/desktop-electron/src/renderer/loading.tsx

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ render(() => {
2525
})
2626

2727
window.api.awaitInitialization((next) => setStep(next as InitStep)).catch(() => undefined)
28+
const off = window.api.onInitStep((next) => setStep(next))
2829

2930
onMount(() => {
3031
setLine(0)
@@ -41,6 +42,7 @@ render(() => {
4142
})
4243

4344
onCleanup(() => {
45+
off()
4446
listener()
4547
timers.forEach(clearTimeout)
4648
})
@@ -63,20 +65,24 @@ render(() => {
6365
<MetaProvider>
6466
<div class="w-screen h-screen bg-background-base flex items-center justify-center">
6567
<Font />
66-
<div class="flex flex-col items-center gap-11">
67-
<Splash class="w-20 h-25 opacity-15" />
68-
<div class="w-60 flex flex-col items-center gap-4" aria-live="polite">
69-
<span class="w-full overflow-hidden text-center text-ellipsis whitespace-nowrap text-text-strong text-14-normal">
70-
{status()}
71-
</span>
72-
<Progress
73-
value={value()}
74-
class="w-20 [&_[data-slot='progress-track']]:h-1 [&_[data-slot='progress-track']]:border-0 [&_[data-slot='progress-track']]:rounded-none [&_[data-slot='progress-track']]:bg-surface-weak [&_[data-slot='progress-fill']]:rounded-none [&_[data-slot='progress-fill']]:bg-icon-warning-base"
75-
aria-label="Database migration progress"
76-
getValueLabel={({ value }) => `${Math.round(value)}%`}
77-
/>
68+
{phase() === "sqlite_waiting" ? (
69+
<div class="flex flex-col items-center gap-11">
70+
<Splash class="w-20 h-25 opacity-15" />
71+
<div class="w-60 flex flex-col items-center gap-4" aria-live="polite">
72+
<span class="w-full overflow-hidden text-center text-ellipsis whitespace-nowrap text-text-strong text-14-normal">
73+
{status()}
74+
</span>
75+
<Progress
76+
value={value()}
77+
class="w-20 [&_[data-slot='progress-track']]:h-1 [&_[data-slot='progress-track']]:border-0 [&_[data-slot='progress-track']]:rounded-none [&_[data-slot='progress-track']]:bg-surface-weak [&_[data-slot='progress-fill']]:rounded-none [&_[data-slot='progress-fill']]:bg-icon-warning-base"
78+
aria-label="Database migration progress"
79+
getValueLabel={({ value }) => `${Math.round(value)}%`}
80+
/>
81+
</div>
7882
</div>
79-
</div>
83+
) : (
84+
<Splash class="w-16 h-20 opacity-50 animate-pulse" />
85+
)}
8086
</div>
8187
</MetaProvider>
8288
)

0 commit comments

Comments
 (0)