From 2efc57d9ee89a1f6013750a9406d14627ae24e20 Mon Sep 17 00:00:00 2001 From: imbytecat Date: Mon, 9 Feb 2026 03:35:24 +0800 Subject: [PATCH] feat(desktop): show native error dialogs on startup failures Replace silent console.error + app.quit() with dialog.showErrorBox() so users actually see why the app failed to start instead of it just disappearing. Covers server spawn errors, timeout, port allocation failure, mid-session server crashes, and window creation failures. --- apps/desktop/src/main/index.ts | 58 +++++++++++++++++++++++++++++----- 1 file changed, 50 insertions(+), 8 deletions(-) diff --git a/apps/desktop/src/main/index.ts b/apps/desktop/src/main/index.ts index 767af21..3e410e5 100644 --- a/apps/desktop/src/main/index.ts +++ b/apps/desktop/src/main/index.ts @@ -1,7 +1,7 @@ import { spawn } from 'node:child_process' import { createServer } from 'node:net' import { join } from 'node:path' -import { app, BrowserWindow, shell } from 'electron' +import { app, BrowserWindow, dialog, shell } from 'electron' import killProcessTree from 'tree-kill' const DEV_SERVER_URL = 'http://localhost:3000' @@ -9,10 +9,17 @@ const DEV_SERVER_URL = 'http://localhost:3000' let mainWindow: BrowserWindow | null = null let serverProcess: ReturnType | null = null let isQuitting = false +let serverReady = false const shouldAbortWindowLoad = (): boolean => isQuitting || !mainWindow || mainWindow.isDestroyed() +const showErrorAndQuit = (title: string, detail: string) => { + if (isQuitting) return + dialog.showErrorBox(title, detail) + app.quit() +} + const getAvailablePort = (): Promise => new Promise((resolve, reject) => { const server = createServer() @@ -84,10 +91,22 @@ const spawnServer = (port: number): string => { serverProcess.on('error', (err) => { console.error('Failed to start server:', err) + showErrorAndQuit( + 'Startup Failed', + 'A required component failed to start. Please reinstall the app.', + ) }) - serverProcess.on('exit', () => { + serverProcess.on('exit', (code) => { serverProcess = null + if (!isQuitting && serverReady) { + showErrorAndQuit( + 'Service Stopped', + app.isPackaged + ? 'The background service stopped unexpectedly. Please restart the app.' + : `Server process exited unexpectedly (code ${code}). Check the server logs for details.`, + ) + } }) return `http://127.0.0.1:${port}` @@ -98,7 +117,16 @@ const resolveServerUrl = async (): Promise => { return DEV_SERVER_URL } - const port = await getAvailablePort() + let port: number + try { + port = await getAvailablePort() + } catch { + showErrorAndQuit( + 'Startup Failed', + "The app couldn't allocate a local port. Please close other apps and try again.", + ) + return null + } if (isQuitting) { return null @@ -148,15 +176,16 @@ const createWindow = async () => { } if (!ready) { - console.error( + showErrorAndQuit( + 'Startup Failed', app.isPackaged - ? 'Server binary did not start in time.' + ? 'The app is taking too long to start. Please try again.' : 'Dev server not responding. Run `bun dev` in apps/server first.', ) - app.quit() return } + serverReady = true mainWindow.loadURL(serverUrl) } @@ -170,7 +199,12 @@ app .then(createWindow) .catch((e) => { console.error('Failed to create window:', e) - app.quit() + showErrorAndQuit( + "App Couldn't Start", + app.isPackaged + ? "We couldn't open the application window. Please restart your computer and try again." + : `Failed to create window: ${e instanceof Error ? e.message : String(e)}`, + ) }) app.on('window-all-closed', () => { @@ -182,7 +216,15 @@ app.on('window-all-closed', () => { app.on('activate', () => { if (!isQuitting && BrowserWindow.getAllWindows().length === 0) { - createWindow() + createWindow().catch((e) => { + console.error('Failed to re-create window:', e) + showErrorAndQuit( + "App Couldn't Start", + app.isPackaged + ? "We couldn't open the application window. Please restart the app." + : `Failed to re-create window: ${e instanceof Error ? e.message : String(e)}`, + ) + }) } })