diff --git a/apps/x/apps/main/src/main.ts b/apps/x/apps/main/src/main.ts index 1e1d3874..c0f7d39f 100644 --- a/apps/x/apps/main/src/main.ts +++ b/apps/x/apps/main/src/main.ts @@ -1,4 +1,4 @@ -import { app, BrowserWindow, desktopCapturer, protocol, net, shell, session, safeStorage, type Session } from "electron"; +import { app, BrowserWindow, desktopCapturer, dialog, protocol, net, shell, session, safeStorage, type Session } from "electron"; import path from "node:path"; import { setupIpcHandlers, @@ -74,6 +74,24 @@ const APP_LAUNCHED_AT = Date.now(); const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); +// fs.watch failures (EMFILE fd exhaustion, ENOSPC watch limits) surface as +// uncaught exceptions from Node's watcher internals, bypassing chokidar's +// 'error' handlers. Watching is a degradable feature — log and keep running. +// Everything else keeps Electron's default behavior (native error dialog), +// which we replicate since registering any listener suppresses it. +process.on('uncaughtException', (err) => { + const code = (err as NodeJS.ErrnoException | undefined)?.code; + if ((code === 'EMFILE' || code === 'ENOSPC') && (err?.stack ?? '').includes('FSWatcher')) { + console.error('[Main] file watcher error (non-fatal):', err); + return; + } + console.error('[Main] uncaught exception:', err); + dialog.showErrorBox( + 'A JavaScript error occurred in the main process', + err?.stack ?? String(err), + ); +}); + // run this as early in the main process as possible if (started) app.quit(); diff --git a/apps/x/packages/core/src/apps/server.ts b/apps/x/packages/core/src/apps/server.ts index 58565e7b..e73f26b9 100644 --- a/apps/x/packages/core/src/apps/server.ts +++ b/apps/x/packages/core/src/apps/server.ts @@ -719,6 +719,12 @@ async function startWatcher(): Promise { if (watcher) return; const w = chokidar.watch(APPS_DIR, { ignoreInitial: true, + // Installed apps may ship .git/node_modules trees — thousands of files + // no consumer renders, at one watch fd per file (chokidar v4, no fsevents). + ignored: (watchedPath: string) => { + const segments = path.relative(APPS_DIR, watchedPath).split(path.sep); + return segments.includes('.git') || segments.includes('node_modules'); + }, awaitWriteFinish: { stabilityThreshold: 180, pollInterval: 50 }, }); w.on('all', (eventName, absolutePath) => {