const { app, components, BrowserWindow, ipcMain, shell, session } = require('electron'); let autoUpdater; try { autoUpdater = require('electron-updater').autoUpdater; } catch (_) {} const path = require('path'); const { spawn, execFileSync } = require('child_process'); const os = require('os'); const fs = require('fs'); const getPort = require('get-port'); const http = require('http'); const affiliateTracking = require('./affiliateTracking'); const tray = require('./tray'); const workflowsLifecycle = require('./workflowsLifecycle'); // openswarm:// protocol must register synchronously at top of main.js, before gotLock branching. if (process.defaultApp) { if (process.argv.length >= 2) { app.setAsDefaultProtocolClient('openswarm', process.execPath, [path.resolve(process.argv[1])]); } } else { app.setAsDefaultProtocolClient('openswarm'); } let pendingDeepLink = null; function forwardDeepLinkToRenderer(url) { if (!url) return; // openswarm:// splits by host: "auth" = subscription token, "oauth/{p}/complete" = OAuth claim. let channel = 'openswarm:auth-url'; try { const u = new URL(url); if (u.host === 'oauth' && u.pathname.endsWith('/complete')) { channel = 'openswarm:oauth-claim'; } } catch (_) {} if (mainWindow && mainWindow.webContents && !mainWindow.webContents.isLoading()) { mainWindow.webContents.send(channel, url); } else { pendingDeepLink = { channel, url }; } } function extractOpenswarmUrl(argv) { return argv && argv.find((a) => typeof a === 'string' && a.startsWith('openswarm://')); } const gotLock = app.requestSingleInstanceLock(); if (!gotLock) { app.exit(0); } else { app.on('second-instance', (_event, argv) => { // Windows/Linux: openswarm:// click re-launches the app with the URL as argv. const url = extractOpenswarmUrl(argv); if (url) forwardDeepLinkToRenderer(url); if (mainWindow) { if (mainWindow.isMinimized()) mainWindow.restore(); mainWindow.focus(); } }); } // macOS-only: openswarm:// clicks fire this event instead of relaunching the process. app.on('open-url', (event, url) => { event.preventDefault(); forwardDeepLinkToRenderer(url); if (mainWindow) mainWindow.focus(); }); // Windows AppUserModelID: required so native toast notifications fire // instead of falling back to legacy balloon tips. Must be set BEFORE the // first Notification is created. electron-builder also injects this at // install time but setting it here defends against ad-hoc dev runs. if (process.platform === 'win32') { try { app.setAppUserModelId('com.openswarm.app'); } catch (_) {} } app.commandLine.appendSwitch('disable-features', 'HardwareMediaKeyHandling'); app.commandLine.appendSwitch('ignore-gpu-blocklist'); app.commandLine.appendSwitch('enable-gpu-rasterization'); app.commandLine.appendSwitch('enable-zero-copy'); app.commandLine.appendSwitch('autoplay-policy', 'no-user-gesture-required'); let mainWindow = null; let backendProcess = null; let backendPort = null; let cachedUpdateStatus = { status: 'idle', info: null, error: null }; let splashWindow = null; let mainWindowReady = false; let isQuittingFromSplash = false; const recentBackendStderr = []; let splashDataUrlCache = null; const isPackaged = app.isPackaged; const isDev = process.env.ELECTRON_DEV === '1'; const iconPath = process.platform === 'win32' ? path.join(__dirname, 'build', 'icon.ico') : path.join(__dirname, 'build', 'icon.png'); // build/ is electron-builder input, not in the asar; splash uses splash/icon.png. const iconPngPath = path.join(__dirname, 'splash', 'icon.png'); function loadSplashDataUrl() { if (splashDataUrlCache) return splashDataUrlCache; try { const html = fs.readFileSync(path.join(__dirname, 'splash', 'splash.html'), 'utf8'); const iconBytes = fs.readFileSync(iconPngPath); const iconDataUrl = 'data:image/png;base64,' + iconBytes.toString('base64'); const finalHtml = html.replace('__OPENSWARM_LOGO__', iconDataUrl); splashDataUrlCache = 'data:text/html;charset=utf-8;base64,' + Buffer.from(finalHtml).toString('base64'); return splashDataUrlCache; } catch (err) { console.warn('[splash] failed to load splash payload:', err && err.message); return null; } } function createSplashWindow() { const dataUrl = loadSplashDataUrl(); if (!dataUrl) return null; const w = new BrowserWindow({ width: 460, height: 340, frame: false, resizable: false, movable: true, minimizable: false, maximizable: false, fullscreenable: false, skipTaskbar: true, show: true, center: true, backgroundColor: '#0a0a10', // opaque to dodge Windows DWM transparency quirks title: 'OpenSwarm', icon: iconPath, webPreferences: { // Splash is fully self-contained (data URL), so nodeIntegration is safe. nodeIntegration: true, contextIsolation: false, sandbox: false, backgroundThrottling: false, }, }); w.setMenuBarVisibility(false); w.loadURL(dataUrl); // Splash close before main window means user bailed; quit so backend doesn't leak. w.on('closed', () => { splashWindow = null; if (!mainWindowReady && !isQuittingFromSplash) { isQuittingFromSplash = true; try { if (!isDev) killBackend(); } catch (_) {} app.quit(); } }); return w; } function emitSplashStatus(payload) { if (splashWindow && !splashWindow.isDestroyed() && splashWindow.webContents) { try { splashWindow.webContents.send('splash:status', payload); } catch (_) {} } } function osStillStartingText() { if (process.platform === 'win32') { return 'Still starting, Windows Defender is scanning files (first launch only)…'; } if (process.platform === 'darwin') { return 'Still starting, macOS is verifying the bundle (first launch only)…'; } return 'Still starting (first launch is slower than subsequent launches)…'; } function osTakingTooLongText() { if (process.platform === 'win32') { return 'Backend is taking longer than usual. Defender scans of 14k files can take a few minutes on slow drives.'; } if (process.platform === 'darwin') { return 'Backend is taking longer than usual. macOS first-launch checks can be slow on cold cache.'; } return 'Backend is taking longer than usual. You can wait, view logs, or restart.'; } /** Resolves the user's real PATH; macOS GUI apps inherit only launchd's minimal PATH. */ function getShellPath() { if (process.platform !== 'darwin' || isDev) return process.env.PATH || ''; try { const userShell = process.env.SHELL || '/bin/zsh'; const result = execFileSync(userShell, ['-ilc', 'echo $PATH'], { encoding: 'utf8', timeout: 5000, env: { ...process.env, HOME: os.homedir() }, }); const resolved = result.trim(); if (resolved) return resolved; } catch (_) {} const systemPaths = []; try { const base = fs.readFileSync('/etc/paths', 'utf8'); for (const line of base.split('\n')) { const p = line.trim(); if (p) systemPaths.push(p); } } catch (_) {} try { const pathsD = '/etc/paths.d'; if (fs.existsSync(pathsD)) { for (const file of fs.readdirSync(pathsD).sort()) { const content = fs.readFileSync(path.join(pathsD, file), 'utf8'); for (const line of content.split('\n')) { const p = line.trim(); if (p) systemPaths.push(p); } } } } catch (_) {} const home = os.homedir(); const fallbackDirs = [ path.join(home, '.local/bin'), path.join(home, '.volta/bin'), path.join(home, '.fnm/aliases/default/bin'), path.join(home, '.bun/bin'), path.join(home, '.cargo/bin'), '/opt/homebrew/bin', '/usr/local/bin', ]; const nvmDir = path.join(home, '.nvm/versions/node'); try { if (fs.existsSync(nvmDir)) { const versions = fs.readdirSync(nvmDir).sort().reverse(); if (versions.length) { fallbackDirs.unshift(path.join(nvmDir, versions[0], 'bin')); } } } catch (_) {} const seen = new Set(); const dirs = []; for (const d of [...fallbackDirs, ...systemPaths, ...(process.env.PATH || '').split(':')]) { if (!d || seen.has(d)) continue; seen.add(d); try { if (fs.statSync(d).isDirectory()) dirs.push(d); } catch {} } return dirs.join(':'); } function getResourcePath(...segments) { if (isPackaged) { return path.join(process.resourcesPath, ...segments); } return path.join(__dirname, '..', ...segments); } function getPythonPath() { // macOS uses Python.app/Contents/MacOS/python3 so LSUIElement suppresses the Dock entry. if (isPackaged) { const envPath = path.join(process.resourcesPath, 'python-env'); if (process.platform === 'win32') { return path.join(envPath, 'python.exe'); } if (process.platform === 'darwin') { const wrapped = path.join(envPath, 'Python.app', 'Contents', 'MacOS', 'python3'); if (fs.existsSync(wrapped)) return wrapped; } return path.join(envPath, 'bin', 'python3'); } if (process.platform === 'win32') { return path.join(__dirname, '..', 'backend', '.venv', 'Scripts', 'python.exe'); } return path.join(__dirname, '..', 'backend', '.venv', 'bin', 'python3'); } // Both arches staged to avoid per-arch beforePack hooks. function getBundledNodePath() { if (!isPackaged) return null; const arch = process.arch === 'x64' ? 'x64' : (process.arch === 'arm64' ? 'arm64' : null); if (!arch) return null; const candidate = process.platform === 'win32' ? path.join(process.resourcesPath, 'node', arch, 'node.exe') : path.join(process.resourcesPath, 'node', arch, 'bin', 'node'); return fs.existsSync(candidate) ? candidate : null; } // Never wall-clock times out: cold-Defender Windows can take minutes. function waitForBackend(port, opts = {}) { const proc = opts.process || null; const start = Date.now(); return new Promise((resolve, reject) => { let settled = false; let stillStartingNotified = false; let actionsShown = false; const finish = (fn, val) => { if (settled) return; settled = true; fn(val); }; if (proc) { proc.once('exit', (code) => { // code === null = we killed it ourselves (normal shutdown). if (code !== 0 && code !== null) { finish(reject, new Error(`Backend process exited with code ${code} during startup`)); } }); } function check() { if (settled) return; const elapsed = Date.now() - start; if (elapsed > 60_000 && !stillStartingNotified) { stillStartingNotified = true; emitSplashStatus({ text: osStillStartingText(), level: 'warning' }); } if (elapsed > 180_000 && !actionsShown) { actionsShown = true; emitSplashStatus({ text: osTakingTooLongText(), level: 'warning', showActions: true, logs: recentBackendStderr.slice(-20).join(''), }); } const req = http.get(`http://127.0.0.1:${port}/api/health/check`, (res) => { if (res.statusCode === 200) { finish(resolve); } else { setTimeout(check, 500); } }); req.on('error', () => setTimeout(check, 500)); req.setTimeout(2000, () => { req.destroy(); setTimeout(check, 500); }); } check(); }); } // Windows EDR stalls each bind probe; if we don't get a preferred port in 3s, fall back to OS-assigned. async function pickBackendPort() { const PREFERRED_TIMEOUT_MS = 3000; const preferred = getPort({ port: getPort.makeRange(8324, 8424) }); let timeoutHandle; const timeout = new Promise((resolve) => { timeoutHandle = setTimeout(() => resolve(null), PREFERRED_TIMEOUT_MS); }); const winner = await Promise.race([preferred, timeout]); clearTimeout(timeoutHandle); if (winner !== null) return winner; console.warn(`[boot] getPort.makeRange(8324,8424) stalled past ${PREFERRED_TIMEOUT_MS}ms, falling back to OS-assigned port`); return await getPort({ port: 0 }); } async function startBackend() { backendPort = await pickBackendPort(); const pythonPath = getPythonPath(); const backendDir = getResourcePath('backend'); const projectRoot = isPackaged ? process.resourcesPath : path.join(__dirname, '..'); const shellPath = getShellPath(); let installMethod = process.env.OPENSWARM_INSTALL_METHOD; if (!installMethod) { if (!isPackaged) { installMethod = 'dev'; } else if (process.platform === 'darwin') { installMethod = 'dmg'; } else if (process.platform === 'win32') { installMethod = 'windows-setup'; } else if (process.platform === 'linux') { installMethod = 'appimage'; } else { installMethod = 'unknown'; } } const env = { ...process.env, PATH: shellPath, OPENSWARM_PACKAGED: isPackaged ? '1' : '0', OPENSWARM_PORT: String(backendPort), OPENSWARM_ELECTRON_PATH: process.execPath, OPENSWARM_INSTALL_METHOD: installMethod, // Asar-relative reads fail in packaged builds, inject app version instead. OPENSWARM_APP_VERSION: app.getVersion(), // Python's stdlib locale/tz are unreliable cross-OS, inject canonical BCP 47 + IANA. OPENSWARM_LOCALE: app.getLocale(), OPENSWARM_TIMEZONE: Intl.DateTimeFormat().resolvedOptions().timeZone || '', PYTHONDONTWRITEBYTECODE: '1', // PEP 540: force open() to UTF-8 on Windows (cp1252 otherwise). PYTHONUTF8: '1', }; // Bundled node avoids a second Dock entry on fresh Macs and ~5-15s cold-start tail vs ELECTRON_RUN_AS_NODE. const bundledNode = getBundledNodePath(); if (bundledNode) { env.OPENSWARM_NODE_PATH = bundledNode; } if (isPackaged) { // Windows site-packages lives under Lib/, not lib/python3.13/. const pythonEnvSitePackages = process.platform === 'win32' ? path.join(process.resourcesPath, 'python-env', 'Lib', 'site-packages') : path.join(process.resourcesPath, 'python-env', 'lib', 'python3.13', 'site-packages'); const debuggerDir = getResourcePath('debugger'); env.PYTHONPATH = [projectRoot, debuggerDir, pythonEnvSitePackages].join(path.delimiter); } console.log(`Starting backend: ${pythonPath} on port ${backendPort}`); console.log(`Project root: ${projectRoot}`); backendProcess = spawn( pythonPath, ['-m', 'uvicorn', 'backend.main:app', '--host', '127.0.0.1', '--port', String(backendPort)], { cwd: projectRoot, env, stdio: ['pipe', 'pipe', 'pipe'], } ); backendProcess.stdout.on('data', (data) => { const text = data.toString(); process.stdout.write(`[backend] ${text}`); if (text.indexOf('Application startup complete') !== -1) { emitSplashStatus('Loading components…'); } }); backendProcess.stderr.on('data', (data) => { const text = data.toString(); process.stderr.write(`[backend] ${text}`); recentBackendStderr.push(text); while (recentBackendStderr.length > 60) recentBackendStderr.shift(); }); backendProcess.on('exit', (code) => { console.log(`Backend exited with code ${code}`); if (code !== 0 && code !== null && mainWindow) { mainWindow.webContents.executeJavaScript( `document.title = "OpenSwarm (backend crashed)";` ); } }); emitSplashStatus('Starting backend…'); await waitForBackend(backendPort, { process: backendProcess }); console.log(`Backend ready on port ${backendPort}`); await loadAuthToken(); // Tray must stay resident so schedules survive window close. try { tray.setup({ backendPort, authToken }); workflowsLifecycle.setBackend({ port: backendPort, token: authToken }); workflowsLifecycle.setActiveChangeListener((active) => { const title = active.length ? (active[0].title || 'workflow') : null; tray.setStatus({ activeTitle: title, paused: false }); }); workflowsLifecycle.startPolling(); } catch (e) { console.warn('[tray] setup failed:', e?.message || e); } } // Per-install bearer token; mirrors backend/config/paths.py. let authToken = ''; function getAuthTokenFilePath() { if (isPackaged) { if (process.platform === 'darwin') { return path.join(os.homedir(), 'Library', 'Application Support', 'OpenSwarm', 'data', 'auth.token'); } else if (process.platform === 'win32') { return path.join(process.env.APPDATA || os.homedir(), 'OpenSwarm', 'data', 'auth.token'); } else { const xdg = process.env.XDG_DATA_HOME || path.join(os.homedir(), '.local', 'share'); return path.join(xdg, 'OpenSwarm', 'data', 'auth.token'); } } return path.join(__dirname, '..', 'backend', 'data', 'auth.token'); } async function loadAuthToken() { const tokenPath = getAuthTokenFilePath(); // 20 * 100ms = 2s retry budget; backend writes the token before HTTP bind, so first-try almost always. for (let attempt = 0; attempt < 20; attempt++) { try { const contents = fs.readFileSync(tokenPath, 'utf8').trim(); if (contents) { authToken = contents; console.log(`[auth] loaded token from ${tokenPath}`); return; } } catch (_) {} await new Promise(r => setTimeout(r, 100)); } console.warn(`[auth] FAILED to load auth token from ${tokenPath} after 2s, WS/HTTP will be rejected`); } function createWindow() { mainWindow = new BrowserWindow({ width: 1400, height: 900, minWidth: 800, minHeight: 600, title: 'OpenSwarm', icon: iconPath, titleBarStyle: 'hiddenInset', // Hidden until ready-to-show so the splash-to-main swap doesn't white-flash. show: false, backgroundColor: '#1a1a1f', webPreferences: { preload: path.join(__dirname, 'preload.js'), nodeIntegration: false, contextIsolation: true, webviewTag: true, }, }); if (isDev) { mainWindow.loadURL(`http://localhost:3000`); } else { const frontendPath = getResourcePath('frontend', 'index.html'); mainWindow.loadFile(frontendPath); } mainWindow.webContents.on('will-attach-webview', (_event, webPreferences, params) => { webPreferences.plugins = true; webPreferences.enableBlinkFeatures = 'EncryptedMedia'; // Setting preload from React races contextBridge.expose and ends up empty, so force-attach here. webPreferences.preload = path.join(__dirname, 'webview-preload.js'); try { console.log('[openswarm:attach-webview] forced preload=', webPreferences.preload, 'src=', params.src); } catch (_) {} }); mainWindow.webContents.on('will-navigate', (event, url) => { if (isDev && url.startsWith('http://localhost:3000')) return; if (url.startsWith('file://')) return; event.preventDefault(); mainWindow.webContents.send('webview-new-window', url, mainWindow.webContents.id); }); // Flush any cold-launch deep link captured before the window existed. mainWindow.webContents.once('did-finish-load', () => { if (pendingDeepLink) { if (typeof pendingDeepLink === 'string') { mainWindow.webContents.send('openswarm:auth-url', pendingDeepLink); } else { mainWindow.webContents.send(pendingDeepLink.channel, pendingDeepLink.url); } pendingDeepLink = null; } }); mainWindow.on('closed', () => { mainWindow = null; }); // Throttle to 1 per 2s per direction; window drags otherwise spam blur/focus. let _lastFocusEvent = 0; const FOCUS_THROTTLE_MS = 2000; const sendFocusEvent = (kind) => { const now = Date.now(); if (now - _lastFocusEvent < FOCUS_THROTTLE_MS) return; _lastFocusEvent = now; sendToRenderer('openswarm:window-focus', { kind, ts: now }); }; mainWindow.on('blur', () => sendFocusEvent('blur')); mainWindow.on('focus', () => sendFocusEvent('focus')); } function sendToRenderer(channel, ...args) { if (mainWindow && !mainWindow.isDestroyed()) { mainWindow.webContents.send(channel, ...args); } } function setupAutoUpdater() { if (!autoUpdater) return; // Download on detect, install on quit: OS can't replace a running .app/.exe. autoUpdater.autoDownload = true; autoUpdater.autoInstallOnAppQuit = true; autoUpdater.on('update-available', (info) => { console.log(`Update available: ${info.version}`); cachedUpdateStatus = { status: 'available', info, error: null }; sendToRenderer('update-available', info); }); autoUpdater.on('update-not-available', (info) => { console.log('App is up to date'); cachedUpdateStatus = { status: 'not-available', info, error: null }; sendToRenderer('update-not-available', info); }); autoUpdater.on('download-progress', (progress) => { cachedUpdateStatus = { status: 'downloading', info: progress, error: null }; sendToRenderer('download-progress', progress); }); autoUpdater.on('update-downloaded', (info) => { console.log(`Update downloaded: ${info.version}`); cachedUpdateStatus = { status: 'downloaded', info, error: null }; sendToRenderer('update-downloaded', info); }); autoUpdater.on('error', (err) => { console.error('Auto-update error:', err); cachedUpdateStatus = { status: 'error', info: null, error: err?.message || String(err) }; sendToRenderer('update-error', err?.message || String(err)); }); autoUpdater.checkForUpdates().catch((err) => { console.log('Update check skipped:', err.message); }); // Always-on users (lid never closed) miss the once-at-startup check, so re-check every 4h. setInterval(() => { autoUpdater.checkForUpdates().catch((err) => { console.log('Periodic update check failed:', err.message); }); }, 4 * 60 * 60 * 1000); } function killBackend() { if (backendProcess) { console.log('Killing backend process...'); if (process.platform === 'win32') { // child.kill() leaves grandchildren orphaned; taskkill /T /F walks the tree. try { require('child_process').execFileSync( 'taskkill', ['/PID', String(backendProcess.pid), '/T', '/F'], { stdio: 'ignore' }, ); } catch (_) { try { backendProcess.kill(); } catch (_) {} } } else { backendProcess.kill('SIGTERM'); setTimeout(() => { if (backendProcess && !backendProcess.killed) { backendProcess.kill('SIGKILL'); } }, 3000); } backendProcess = null; } } app.whenReady().then(async () => { // Cold-launch openswarm:// arrives in argv on Windows/Linux (macOS uses open-url instead). const initialDeepLink = extractOpenswarmUrl(process.argv); if (initialDeepLink) forwardDeepLinkToRenderer(initialDeepLink); if (process.platform === 'darwin' && !isPackaged) { try { app.dock.setIcon(iconPath); } catch (_) {} } session.defaultSession.setPermissionRequestHandler((_wc, permission, callback) => { const allowed = [ 'media', 'mediaKeySystem', 'protected-media-identifier', 'geolocation', 'notifications', 'midi', 'midiSysex', 'clipboard-read', 'clipboard-sanitized-write', 'pointerLock', 'fullscreen', 'idle-detection', ]; console.log('Permission request:', permission, '->', allowed.includes(permission) ? 'granted' : 'denied'); callback(allowed.includes(permission)); }); session.defaultSession.setPermissionCheckHandler((_wc, permission) => { const allowed = [ 'media', 'mediaKeySystem', 'protected-media-identifier', 'clipboard-read', 'clipboard-sanitized-write', 'pointerLock', 'fullscreen', 'idle-detection', ]; return allowed.includes(permission); }); // Read-only logging; modifying interceptors break Widevine header set-up. session.defaultSession.webRequest.onSendHeaders( { urls: ['*://*/*widevine*license*'] }, (details) => { console.log(`[drm-req] ${details.method} ${details.url}`); for (const [k, v] of Object.entries(details.requestHeaders || {})) { if (/content-type|origin|referer|auth|accept/i.test(k)) { console.log(`[drm-req] ${k}: ${v}`); } } }, ); session.defaultSession.webRequest.onCompleted( { urls: ['*://*/*widevine*', '*://*/*license*'] }, (details) => { console.log(`[drm-net] ${details.method} ${details.url} → ${details.statusCode}`); }, ); session.defaultSession.webRequest.onErrorOccurred( { urls: ['*://*/*widevine*', '*://*/*license*'] }, (details) => { console.log(`[drm-net] FAILED ${details.method} ${details.url} → ${details.error}`); }, ); splashWindow = createSplashWindow(); emitSplashStatus('Starting OpenSwarm…'); let widevinePromise; if (components && typeof components.whenReady === 'function') { widevinePromise = components.whenReady().then( () => { console.log('Widevine CDM ready'); if (typeof components.status === 'function') { console.log('CDM component status:', JSON.stringify(components.status())); } }, (err) => { console.warn('Widevine CDM not available:', err && err.message); } ); } else { console.log('CastLabs components API not available, using standard Electron (no DRM)'); widevinePromise = Promise.resolve(); } try { if (isDev) { backendPort = parseInt(process.env.OPENSWARM_PORT || '8324', 10); console.log(`Dev mode: using existing backend on port ${backendPort}`); emitSplashStatus('Connecting to dev backend…'); } else { await startBackend(); } emitSplashStatus('Almost ready…'); // Hidden-launch: --hidden arg (set by workflowsLifecycle.setLoginItem // on Windows + Linux; macOS uses openAsHidden) means skip the main // window so tray + scheduler run in background. User enabled // "Always-on" -> app boots invisibly. const launchedHidden = process.argv.includes('--hidden'); if (!launchedHidden) { createWindow(); } else if (splashWindow && !splashWindow.isDestroyed()) { isQuittingFromSplash = false; try { splashWindow.destroy(); } catch (_) {} splashWindow = null; } if (!isDev) { setupAutoUpdater(); if (mainWindow) mainWindow.webContents.on('did-finish-load', () => { if (cachedUpdateStatus.status === 'available') { sendToRenderer('update-available', cachedUpdateStatus.info); } else if (cachedUpdateStatus.status === 'downloaded') { sendToRenderer('update-downloaded', cachedUpdateStatus.info); } }); } // Swap on ready-to-show (post-first-paint); avoids white-flash on mount. if (mainWindow) { const swapToMain = () => { if (mainWindowReady || mainWindow.isDestroyed()) return; mainWindowReady = true; try { mainWindow.show(); mainWindow.focus(); } catch (_) {} // 120ms gap lets the OS raise main before splash hides; avoids a single-frame gap on Windows. setTimeout(() => { if (splashWindow && !splashWindow.isDestroyed()) { splashWindow.destroy(); } splashWindow = null; }, 120); }; mainWindow.once('ready-to-show', swapToMain); // ready-to-show never fires if renderer load fails (dev server down); swap anyway so error is visible. mainWindow.webContents.once('did-fail-load', (_e, errorCode, errorDescription, validatedURL) => { console.warn('[boot] mainWindow load failed:', errorCode, errorDescription, validatedURL); if (isDev) swapToMain(); }); } widevinePromise.catch(() => {}); affiliateTracking.maybeRunFirstLaunchHandshake({ shell, userDataDir: app.getPath('userData'), isDev, isPackaged, }).catch((err) => { console.warn('[affiliate] handshake failed:', err && err.message); }); } catch (err) { console.error('Failed to start:', err); // Do NOT app.quit(); user picks the next step from the splash actions. emitSplashStatus({ text: "OpenSwarm couldn't start: " + (err && err.message ? err.message : String(err)), level: 'error', showActions: true, logs: recentBackendStderr.slice(-30).join(''), }); } }); app.on('web-contents-created', (_event, contents) => { // Google/OpenAI auth pages blacklist Electron UA, so spoof Chrome on popups. if ( contents.getType() === 'window' && mainWindow && contents !== mainWindow.webContents ) { const OAUTH_POPUP_UA = process.platform === 'win32' ? 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ' + '(KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36' : 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 ' + '(KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36'; contents.setUserAgent(OAUTH_POPUP_UA); } contents.setWindowOpenHandler(({ url, disposition }) => { if (disposition === 'foreground-tab' || disposition === 'background-tab') { if (mainWindow && !mainWindow.isDestroyed()) { mainWindow.webContents.send('webview-new-window', url, contents.id); } return { action: 'deny' }; } // Anthropic still works here; Google/OpenAI route via shell.openExternal (see _EXTERNAL_BROWSER_PROVIDERS). return { action: 'allow', overrideBrowserWindowOptions: { parent: mainWindow || undefined, width: 520, height: 680, center: true, fullscreen: false, fullscreenable: false, resizable: true, minimizable: false, maximizable: false, }, }; }); contents.on('did-create-window', (childWindow) => { if (mainWindow && !mainWindow.isDestroyed() && !childWindow.isDestroyed()) { childWindow.setParentWindow(mainWindow); // Electron can spawn child fullscreen if parent was; force out. if (childWindow.isFullScreen()) childWindow.setFullScreen(false); } }); // postMessage relay fails on cross-origin redirects, intercept at the navigation layer. const forwardOauthCallback = (url) => { try { const u = new URL(url); const onRouter = (u.hostname === 'localhost' || u.hostname === '127.0.0.1') && u.port === '20128' && u.pathname === '/callback'; if (!onRouter) return; const code = u.searchParams.get('code'); const state = u.searchParams.get('state'); const error = u.searchParams.get('error'); if (!code && !error) return; if (mainWindow && !mainWindow.isDestroyed()) { mainWindow.webContents.send('openswarm:oauth-callback', { code, state, error }); } } catch {} }; contents.on('did-navigate', (_e, url) => forwardOauthCallback(url)); contents.on('did-redirect-navigation', (_e, url) => forwardOauthCallback(url)); if (contents.getType() === 'webview') { contents.on('console-message', (_e, level, message, line, sourceId) => { if (message.includes('widevine') || message.includes('drm') || message.includes('license') || message.includes('MediaKeySession') || message.includes('EME') || message.includes('[drm-diag]') || message.includes('openswarm') || level >= 2) { const tag = ['LOG', 'INFO', 'WARN', 'ERROR'][level] || 'LOG'; const src = sourceId ? sourceId.split('/').pop() : ''; console.log(`[webview:${tag}] ${message}${src ? ` (${src}:${line})` : ''}`); } }); // Lazy attach avoids races with DevTools. try { contents.debugger.on('detach', (_e, reason) => { console.log(`[cdp] detach on wcId ${contents.id}: ${reason}`); cdpAxIndexCache.delete(contents.id); }); } catch (e) {} contents.on('destroyed', () => { cdpAxIndexCache.delete(contents.id); cdpQueueByWcId.delete(contents.id); }); contents.on('render-process-gone', () => { cdpAxIndexCache.delete(contents.id); cdpQueueByWcId.delete(contents.id); }); // Inject on dom-ready in main world; bypasses Trusted Types CSP that blocks inline