Bladeren bron

fix(app): tighter startup sequence

Adam 1 week geleden
bovenliggende
commit
10a43e0f6a

+ 14 - 2
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 =
   <A, E, R>(e: Effect.Effect<A, E, R>) =>
     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 (
     <Show
       when={checkMode() === "blocking" ? !startupHealthCheck.loading : startupHealthCheck.state !== "pending"}
@@ -281,6 +292,7 @@ export function AppInterface(props: {
   servers?: Array<ServerConnection.Any>
   router?: Component<BaseRouterProps>
   disableHealthCheck?: boolean
+  onReady?: () => void
 }) {
   return (
     <ServerProvider
@@ -288,7 +300,7 @@ export function AppInterface(props: {
       disableHealthCheck={props.disableHealthCheck}
       servers={props.servers}
     >
-      <ConnectionGate disableHealthCheck={props.disableHealthCheck}>
+      <ConnectionGate disableHealthCheck={props.disableHealthCheck} onReady={props.onReady}>
         <ServerKey>
           <GlobalSDKProvider>
             <GlobalSyncProvider>

+ 30 - 14
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<void>()
+const mainReady = defer<void>()
 
 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<void>() : 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(),

+ 2 - 0
packages/desktop-electron/src/main/ipc.ts

@@ -26,6 +26,7 @@ type Deps = {
   wslPath: (path: string, mode: "windows" | "linux" | null) => Promise<string>
   resolveAppPath: (appName: string) => Promise<string | null>
   loadingWindowComplete: () => void
+  mainWindowReady: () => void
   runUpdater: (alertOnFail: boolean) => Promise<void> | void
   checkUpdate: () => Promise<{ updateAvailable: boolean; version?: string }>
   installUpdate: () => Promise<void> | 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())

+ 3 - 11
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,

+ 6 - 0
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"),

+ 7 - 1
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<void>
   installCli: () => Promise<string>
   awaitInitialization: (onStep: (step: InitStep) => void) => Promise<ServerReadyData>
+  onInitStep: (cb: (step: InitStep) => void) => () => void
   getDefaultServerUrl: () => Promise<string | null>
   setDefaultServerUrl: (url: string | null) => Promise<void>
   getWslConfig: () => Promise<WslConfig>
@@ -66,6 +71,7 @@ export type ElectronAPI = {
   setZoomFactor: (factor: number) => Promise<void>
   setTitlebar: (theme: TitlebarTheme) => Promise<void>
   loadingWindowComplete: () => void
+  mainWindowReady: () => void
   runUpdater: (alertOnFail: boolean) => Promise<void>
   checkUpdate: () => Promise<{ updateAvailable: boolean; version?: string }>
   installUpdate: () => Promise<void>

+ 1 - 0
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()}
               >
                 <Inner />
               </AppInterface>

+ 19 - 13
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(() => {
     <MetaProvider>
       <div class="w-screen h-screen bg-background-base flex items-center justify-center">
         <Font />
-        <div class="flex flex-col items-center gap-11">
-          <Splash class="w-20 h-25 opacity-15" />
-          <div class="w-60 flex flex-col items-center gap-4" aria-live="polite">
-            <span class="w-full overflow-hidden text-center text-ellipsis whitespace-nowrap text-text-strong text-14-normal">
-              {status()}
-            </span>
-            <Progress
-              value={value()}
-              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"
-              aria-label="Database migration progress"
-              getValueLabel={({ value }) => `${Math.round(value)}%`}
-            />
+        {phase() === "sqlite_waiting" ? (
+          <div class="flex flex-col items-center gap-11">
+            <Splash class="w-20 h-25 opacity-15" />
+            <div class="w-60 flex flex-col items-center gap-4" aria-live="polite">
+              <span class="w-full overflow-hidden text-center text-ellipsis whitespace-nowrap text-text-strong text-14-normal">
+                {status()}
+              </span>
+              <Progress
+                value={value()}
+                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"
+                aria-label="Database migration progress"
+                getValueLabel={({ value }) => `${Math.round(value)}%`}
+              />
+            </div>
           </div>
-        </div>
+        ) : (
+          <Splash class="w-16 h-20 opacity-50 animate-pulse" />
+        )}
       </div>
     </MetaProvider>
   )