mirror of
https://github.com/anomalyco/opencode.git
synced 2026-05-21 19:35:10 +00:00
fix(app): tighter startup sequence
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -332,6 +332,7 @@ render(() => {
|
||||
defaultServer={defaultServer.latest ?? ServerConnection.Key.make("sidecar")}
|
||||
servers={servers()}
|
||||
router={MemoryRouter}
|
||||
onReady={() => window.api.mainWindowReady()}
|
||||
>
|
||||
<Inner />
|
||||
</AppInterface>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user