import { existsSync } from "node:fs";import { homedir } from "node:os";import { join } from "node:path";import { fileURLToPath } from "node:url";import { app, BrowserWindow, dialog, ipcMain, Menu, type MenuItemConstructorOptions, nativeImage, session, shell,} from "electron";import windowStateKeeper from "electron-window-state";import log from "electron-log/main.js";import updater from "electron-updater";const { autoUpdater } = updater;type UpdateInfo = { readonly version: string };import { attachToSupervisedDaemon, startSidecar, stopSidecar, onUnexpectedSidecarExit, SidecarPortInUseError, type SidecarConnection,} from "./sidecar";import { errorReportingEnabled, exportDiagnostics, exportDiagnosticsInteractive, getCrashReportingConfig, initErrorReporting, reportAProblem,} from "./diagnostics";import { sidecarCrashHtml, startupWindowHtml } from "./crash-screen";import { isExpectedNavigationAbort } from "./navigation-errors";import { isAppQuitting } from "./app-quit";import { replaceSupervisedDaemonForDesktop } from "./supervised-connection";import { bundledExecutorPath, installSupervisedService, restartSupervisedService, supervisedServiceStatus, uninstallSupervisedService,} from "./service";import { announceBackup, confirmResetState, resetExecutorState } from "./reset-state";import { getServerProfiles, getServerSettings, rotateServerToken, setServerProfiles, updateServerSettings,} from "./settings";import { type DesktopServerConnection, type DesktopServerSettings,} from "../shared/server-settings";import { type DesktopUpdateStatus, UPDATE_INSTALL_CHANNEL, UPDATE_STATUS_CHANNEL, UPDATE_STATUS_GET_CHANNEL,} from "../shared/update";import { planCompletedUpdateCheck, planDownloadedUpdate, planFatalAutoInstallOnQuit, planUpdateCheck, statusAfterUpdateError, type UpdateCheckTrigger,} from "./updater-state"; // Pin userData to a friendly app-name-scoped dir BEFORE app.ready so every// Electron-side consumer (electron-store, electron-log, window-state) lands// at a predictable spot. User-mutable executor state (data.db and the optional// executor.jsonc plugin manifest) is pinned separately to ~/.executor in// main/sidecar.ts — that path matches the CLI's default.app.setName("Executor");app.setPath("userData", join(app.getPath("appData"), "Executor")); log.initialize({ preload: true });log.transports.file.level = "info"; // Crash reporting must attach before app.whenReady() so Crashpad covers// every child process (renderer, GPU). Sentry-backed only when a DSN was// baked in at build time; otherwise dumps stay local for the diagnostics// export.initErrorReporting(); let mainWindow: BrowserWindow | null = null;let connection: SidecarConnection | null = null;let authHeaderUnsubscribe: (() => void) | null = null; const PRELOAD_PATH = fileURLToPath(new URL("../preload/index.js", import.meta.url)); const liveMainWindow = (): BrowserWindow | null => { const window = mainWindow; if (!window) return null; if (window.isDestroyed()) { mainWindow = null; return null; } return window;}; const destroyWindow = (window: BrowserWindow) => { if (mainWindow === window) mainWindow = null; if (!window.isDestroyed()) window.destroy();}; const focusMainWindow = () => { const window = liveMainWindow(); if (!window) { if (connection) void createWindow(connection); return; } if (window.isMinimized()) window.restore(); if (!window.isVisible()) window.show(); window.focus();}; const ensureSingleInstance = () => { if (!app.requestSingleInstanceLock()) { app.quit(); return false; } app.on("second-instance", focusMainWindow); return true;}; /** * Stop the local server only when WE own it. A supervised daemon (launchd/etc.) * outlives this app by design — quitting, restarting the window, or resetting * state must never kill it. Spawned sidecars (`child` set) are stopped as before. */const stopConnection = async (conn: SidecarConnection): Promise<void> => { if (conn.supervisedDaemon || !conn.child) return; await stopSidecar(conn.child);}; const webUrlForConnection = (conn: SidecarConnection): string => { const url = new URL(conn.baseUrl); if (conn.authToken) url.searchParams.set("_token", conn.authToken); url.searchParams.set("_executor_desktop_launch", String(process.pid)); return url.toString();}; const htmlDataUrl = (html: string): string => `data:text/html;charset=utf-8,${encodeURIComponent(html)}`; /** * True when a `loadURL` rejection is just Chromium cancelling a navigation * whose window went away — the window closing mid-load is a race we cause, not * a failure to reach the server. */const isTeardownNavigationError = (error: unknown, window: BrowserWindow): boolean => isExpectedNavigationAbort({ // oxlint-disable-next-line executor/no-instanceof-error, executor/no-unknown-error-message -- boundary: Electron rejects loadURL with a plain Node Error carrying the net error name message: error instanceof Error ? error.message : String(error), windowDestroyed: window.isDestroyed(), appQuitting: isAppQuitting(), }); /** * Fire-and-forget navigation. The caller has no way to react to a failure, so * an uncaught rejection here would only reach the main-process unhandled * rejection handler; log it instead, quietly when it is teardown noise. */const loadWindowUrl = async (window: BrowserWindow, url: string): Promise<void> => { // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: Electron navigation rejects; there is no caller left to surface it to try { await window.loadURL(url); } catch (error) { if (isTeardownNavigationError(error, window)) { log.info("Navigation aborted while the window was closing"); return; } log.error("Failed to load window URL", error); }}; // The supervised daemon (and the desktop sidecar) own this data dir — the same// path the CLI's `executor web`/daemon uses, so desktop and CLI share state.const DESKTOP_DATA_DIR = join(homedir(), ".executor"); const delay = (ms: number): Promise<void> => new Promise((r) => setTimeout(r, ms)); const parseVersionParts = (version: string): readonly number[] | null => { const core = version.trim().split(/[+-]/, 1)[0]; if (!core) return null; const parts = core.split(".").map((part) => Number.parseInt(part, 10)); return parts.every((part) => Number.isInteger(part) && part >= 0) ? parts : null;}; const compareVersions = (left: string, right: string): number | null => { const leftParts = parseVersionParts(left); const rightParts = parseVersionParts(right); if (!leftParts || !rightParts) return null; const length = Math.max(leftParts.length, rightParts.length); for (let index = 0; index < length; index += 1) { const l = leftParts[index] ?? 0; const r = rightParts[index] ?? 0; if (l !== r) return l > r ? 1 : -1; } return 0;}; const shouldUpgradeDaemonForDesktop = (daemonVersion: string | null): boolean => { if (!daemonVersion) return false; const comparison = compareVersions(app.getVersion(), daemonVersion); return comparison !== null && comparison > 0;}; const normalizedPath = (path: string): string => path.replaceAll("\\", "/"); const shouldReplaceDaemonForDesktop = (conn: SidecarConnection): boolean => { if (shouldUpgradeDaemonForDesktop(conn.ownerVersion)) return true; if (!app.isPackaged) return false; const ownerPath = conn.ownerExecutablePath; if (!ownerPath) return false; if (!existsSync(ownerPath)) return true; if (conn.ownerClient !== "desktop") return false; return normalizedPath(ownerPath) !== normalizedPath(bundledExecutorPath());}; /** Poll for a reachable supervised daemon until the deadline. */const waitForSupervisedAttach = async ( timeoutMs: number, options: { readonly port?: number } = {},): Promise<SidecarConnection | null> => { const deadline = Date.now() + timeoutMs; for (;;) { const attached = await attachToSupervisedDaemon(); if (attached && (options.port === undefined || attached.port === options.port)) return attached; if (Date.now() >= deadline) return null; await delay(300); }}; const confirmEnableBackgroundService = async (): Promise<boolean> => { // EXECUTOR_TEST_AUTO_CONFIRM_BACKGROUND_SERVICE skips the modal, the same seam // as confirmResetState. Native dialogs are unreachable from CDP/Playwright, so // a first-run boot under automation otherwise blocks here forever with no way // to answer. "1" keeps the background service; any other value declines and // falls back to the managed sidecar. const autoConfirm = process.env.EXECUTOR_TEST_AUTO_CONFIRM_BACKGROUND_SERVICE; if (autoConfirm !== undefined) return autoConfirm === "1"; const { response } = await dialog.showMessageBox({ type: "question", title: "Keep Executor running in the background?", message: "Keep your connections available after you quit Executor?", detail: "Executor can run as a lightweight background service so your MCP tools keep working after you close this window or restart your computer. You can turn this off anytime in Settings.", buttons: ["Keep running in the background", "Not now"], defaultId: 0, cancelId: 1, }); return response === 0;}; const acceptSupervisedConnection = async ( attached: SidecarConnection,): Promise<SidecarConnection | null> => { if (!shouldReplaceDaemonForDesktop(attached)) return attached; const settings = getServerSettings(); return replaceSupervisedDaemonForDesktop(attached, { install: () => installSupervisedService({ port: settings.port, dataDir: DESKTOP_DATA_DIR, }), waitForAttach: () => waitForSupervisedAttach(30_000, { port: settings.port }), attach: attachToSupervisedDaemon, onInstallFailure: (error) => { log.warn("Failed to replace older supervised daemon; re-checking before falling back", error); }, });}; const installSupervisedAndWait = async (reason: string): Promise<SidecarConnection | null> => { const settings = getServerSettings(); // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: install failure falls back to managed-spawn so the app still launches try { await installSupervisedService({ port: settings.port, dataDir: DESKTOP_DATA_DIR, }); } catch (error) { log.warn(`Failed to install supervised service after ${reason}; using managed sidecar`, error); return null; } return waitForSupervisedAttach(15_000, { port: settings.port });}; /** * Resolve a connection to the OS-supervised daemon, installing it on first run * (with consent). Returns null when supervision is unavailable or the user * declined — the caller then falls back to managed-spawn. */const ensureSupervisedConnection = async (): Promise<SidecarConnection | null> => { // 1. Already running → attach. const attached = await attachToSupervisedDaemon(); if (attached) { return acceptSupervisedConnection(attached); } // Headless/e2e: the first-run background-service prompt and the systemd/ // launchd install both need a user (and a real session bus) present. With // this set, fall straight through to managed-spawn after the attach attempt // so the packaged app can be driven without a window server. Mirrors // EXECUTOR_TEST_AUTO_CONFIRM_RESET in reset-state.ts. if (process.env.EXECUTOR_TEST_SKIP_BACKGROUND_SERVICE === "1") return null; const status = await supervisedServiceStatus(); if (!status.supported) return null; // 2. Registered and running, but the first attach missed it. Kick once and // wait only when the kick command succeeds. if (status.registered) { if (status.running) { const attachedAfterStatus = await attachToSupervisedDaemon(); if (attachedAfterStatus) return acceptSupervisedConnection(attachedAfterStatus); // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: a restart failure just falls through to managed-spawn try { await restartSupervisedService(); } catch (error) { log.warn( "Failed to restart running supervised service after attach failed; using managed sidecar", error, ); return null; } return waitForSupervisedAttach(15_000); } // 3. Registered but not running usually means a stale plist/unit/task. Try // restart first, then reinstall the service if the OS manager cannot start it. // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: restart failure triggers supervised service self-heal try { await restartSupervisedService(); } catch (error) { log.warn("Failed to restart registered supervised service; reinstalling", error); return installSupervisedAndWait("registered service restart failure"); } return waitForSupervisedAttach(15_000); } // 4. First run → ask, then install + start. The unit carries no secret; the // supervised daemon mints/loads its bearer from auth.json under DESKTOP_DATA_DIR. if (!(await confirmEnableBackgroundService())) return null; return installSupervisedAndWait("first-run prompt");}; // Crash monitor for the supervised daemon: the OS service manager restarts it// on crash, but during that window the window's requests fail. Poll, show a// reconnecting overlay while it's down, and reload once it's back.let supervisedMonitorTimer: ReturnType<typeof setInterval> | null = null;let supervisedDaemonDown = false;let supervisedMonitorMisses = 0;const SUPERVISED_MONITOR_MISSES_BEFORE_DOWN = 3; const stopSupervisedMonitor = () => { if (supervisedMonitorTimer) clearInterval(supervisedMonitorTimer); supervisedMonitorTimer = null; supervisedDaemonDown = false; supervisedMonitorMisses = 0;}; const armSupervisedMonitor = () => { stopSupervisedMonitor(); supervisedMonitorTimer = setInterval(() => { void (async () => { const live = await attachToSupervisedDaemon(); const window = liveMainWindow(); if (!live) { supervisedMonitorMisses += 1; if (supervisedMonitorMisses < SUPERVISED_MONITOR_MISSES_BEFORE_DOWN) return; if (!supervisedDaemonDown && window) { supervisedDaemonDown = true; showCrashScreen(window); } return; } supervisedMonitorMisses = 0; if (supervisedDaemonDown) { supervisedDaemonDown = false; connection = live; installBearerAuthHeader(live.baseUrl, live.authToken); if (window) void loadWindowUrl(window, webUrlForConnection(live)); } })(); }, 10_000);}; const installBearerAuthHeader = (origin: string, token: string | null) => { authHeaderUnsubscribe?.(); authHeaderUnsubscribe = null; if (!token) return; const headerValue = `Bearer ${token}`; session.defaultSession.webRequest.onBeforeSendHeaders( { urls: [`${origin}/*`] }, (details, callback) => { // Scope the bearer to the app's OWN renderer. OAuth popups run in this same // session but load third-party provider pages; auto-attaching the bearer to // any request they make to the sidecar would make it an ambient credential // (a CSRF vector) for untrusted content — the very thing the bearer model // exists to avoid. The popup only ever needs the bearer-exempt // /oauth/callback and hands its result back via same-origin browser // channels (localStorage/postMessage), so withholding the bearer from any // non-app webContents is safe. Requests with no webContentsId (main // process / network service) still get it. const fromOtherWebContents = details.webContentsId !== undefined && (mainWindow === null || details.webContentsId !== mainWindow.webContents.id); if (fromOtherWebContents) { callback({ requestHeaders: details.requestHeaders }); return; } callback({ requestHeaders: { ...details.requestHeaders, Authorization: headerValue, }, }); }, ); authHeaderUnsubscribe = () => { session.defaultSession.webRequest.onBeforeSendHeaders({ urls: [`${origin}/*`] }, null); };}; /** * Resolve the on-disk path to the Executor app icon. Packaged builds get * the .icns/.ico baked in by electron-builder, but in dev mode Electron * launches via the bare runtime binary with no bundled icon — so we set * it programmatically (dock on mac, BrowserWindow on linux/win). * * In dev `app.getAppPath()` returns the directory of the app's * `package.json` (i.e. `apps/desktop`), which is more robust than * relative path math from the compiled main bundle. */const resolveSourceIconPath = (): string => app.isPackaged ? join(process.resourcesPath, "icon.png") : join(app.getAppPath(), "build", "icon.png"); const resolveLinuxIcon = (): string | undefined => { if (process.platform !== "linux") return undefined; return resolveSourceIconPath();}; /** * Set the macOS dock icon at runtime. Packaged builds already use the * bundle's .icns; this matters only in dev where the bare electron binary * defaults to the Electron diamond. * * `setIcon` accepts a file path but silently no-ops if the underlying * image fails to decode — we route through `nativeImage.createFromPath` * + an `isEmpty()` check so we can surface the bad-path case in the log. */const installDockIcon = () => { if (process.platform !== "darwin") return; if (app.isPackaged) return; if (!app.dock) return; const iconPath = resolveSourceIconPath(); if (!existsSync(iconPath)) { log.warn(`[dock-icon] file missing at ${iconPath}; skipping`); return; } const image = nativeImage.createFromPath(iconPath); if (image.isEmpty()) { log.warn(`[dock-icon] failed to decode ${iconPath}; skipping`); return; } app.dock.setIcon(image); log.info(`[dock-icon] set to ${iconPath} (${image.getSize().width}×${image.getSize().height})`);}; const createMainBrowserWindow = (options: { readonly show: boolean }): BrowserWindow => { const windowState = windowStateKeeper({ defaultWidth: 1280, defaultHeight: 800, }); const linuxIcon = resolveLinuxIcon(); const window = new BrowserWindow({ x: windowState.x, y: windowState.y, width: windowState.width, height: windowState.height, // Keep the window at or above the responsive layout's mobile breakpoint // (Tailwind `md` = 768px). Below it the web shell switches to the mobile // header, whose far-left hamburger would render under the native macOS // traffic lights (issue #1125). Staying >= 768 means the desktop always // renders the sidebar layout, so the lights only ever sit over the sidebar // header, which is offset to clear them (.desktop-macos-titlebar). minWidth: 768,Lines 1–500 of 1329. Load more
import { existsSync } from "node:fs"; import { homedir } from "node:os"; import { join } from "node:path"; import { fileURLToPath } from "node:url"; import { app, BrowserWindow, dialog, ipcMain, Menu, type MenuItemConstructorOptions, nativeImage, session, shell, } from "electron"; import windowStateKeeper from "electron-window-state"; import log from "electron-log/main.js"; import updater from "electron-updater"; const { autoUpdater } = updater; type UpdateInfo = { readonly version: string }; import { attachToSupervisedDaemon, startSidecar, stopSidecar, onUnexpectedSidecarExit, SidecarPortInUseError, type SidecarConnection, } from "./sidecar"; import { errorReportingEnabled, exportDiagnostics, exportDiagnosticsInteractive, getCrashReportingConfig, initErrorReporting, reportAProblem, } from "./diagnostics"; import { sidecarCrashHtml, startupWindowHtml } from "./crash-screen"; import { isExpectedNavigationAbort } from "./navigation-errors"; import { isAppQuitting } from "./app-quit"; import { replaceSupervisedDaemonForDesktop } from "./supervised-connection"; import { bundledExecutorPath, installSupervisedService, restartSupervisedService, supervisedServiceStatus, uninstallSupervisedService, } from "./service"; import { announceBackup, confirmResetState, resetExecutorState } from "./reset-state"; import { getServerProfiles, getServerSettings, rotateServerToken, setServerProfiles, updateServerSettings, } from "./settings"; import { type DesktopServerConnection, type DesktopServerSettings, } from "../shared/server-settings"; import { type DesktopUpdateStatus, UPDATE_INSTALL_CHANNEL, UPDATE_STATUS_CHANNEL, UPDATE_STATUS_GET_CHANNEL, } from "../shared/update"; import { planCompletedUpdateCheck, planDownloadedUpdate, planFatalAutoInstallOnQuit, planUpdateCheck, statusAfterUpdateError, type UpdateCheckTrigger, } from "./updater-state"; // Pin userData to a friendly app-name-scoped dir BEFORE app.ready so every // Electron-side consumer (electron-store, electron-log, window-state) lands // at a predictable spot. User-mutable executor state (data.db and the optional // executor.jsonc plugin manifest) is pinned separately to ~/.executor in // main/sidecar.ts — that path matches the CLI's default. app.setName("Executor"); app.setPath("userData", join(app.getPath("appData"), "Executor")); log.initialize({ preload: true }); log.transports.file.level = "info"; // Crash reporting must attach before app.whenReady() so Crashpad covers // every child process (renderer, GPU). Sentry-backed only when a DSN was // baked in at build time; otherwise dumps stay local for the diagnostics // export. initErrorReporting(); let mainWindow: BrowserWindow | null = null; let connection: SidecarConnection | null = null; let authHeaderUnsubscribe: (() => void) | null = null; const PRELOAD_PATH = fileURLToPath(new URL("../preload/index.js", import.meta.url)); const liveMainWindow = (): BrowserWindow | null => { const window = mainWindow; if (!window) return null; if (window.isDestroyed()) { mainWindow = null; return null; } return window; }; const destroyWindow = (window: BrowserWindow) => { if (mainWindow === window) mainWindow = null; if (!window.isDestroyed()) window.destroy(); }; const focusMainWindow = () => { const window = liveMainWindow(); if (!window) { if (connection) void createWindow(connection); return; } if (window.isMinimized()) window.restore(); if (!window.isVisible()) window.show(); window.focus(); }; const ensureSingleInstance = () => { if (!app.requestSingleInstanceLock()) { app.quit(); return false; } app.on("second-instance", focusMainWindow); return true; }; /** * Stop the local server only when WE own it. A supervised daemon (launchd/etc.) * outlives this app by design — quitting, restarting the window, or resetting * state must never kill it. Spawned sidecars (`child` set) are stopped as before. */ const stopConnection = async (conn: SidecarConnection): Promise<void> => { if (conn.supervisedDaemon || !conn.child) return; await stopSidecar(conn.child); }; const webUrlForConnection = (conn: SidecarConnection): string => { const url = new URL(conn.baseUrl); if (conn.authToken) url.searchParams.set("_token", conn.authToken); url.searchParams.set("_executor_desktop_launch", String(process.pid)); return url.toString(); }; const htmlDataUrl = (html: string): string => `data:text/html;charset=utf-8,${encodeURIComponent(html)}`; /** * True when a `loadURL` rejection is just Chromium cancelling a navigation * whose window went away — the window closing mid-load is a race we cause, not * a failure to reach the server. */ const isTeardownNavigationError = (error: unknown, window: BrowserWindow): boolean => isExpectedNavigationAbort({ // oxlint-disable-next-line executor/no-instanceof-error, executor/no-unknown-error-message -- boundary: Electron rejects loadURL with a plain Node Error carrying the net error name message: error instanceof Error ? error.message : String(error), windowDestroyed: window.isDestroyed(), appQuitting: isAppQuitting(), }); /** * Fire-and-forget navigation. The caller has no way to react to a failure, so * an uncaught rejection here would only reach the main-process unhandled * rejection handler; log it instead, quietly when it is teardown noise. */ const loadWindowUrl = async (window: BrowserWindow, url: string): Promise<void> => { // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: Electron navigation rejects; there is no caller left to surface it to try { await window.loadURL(url); } catch (error) { if (isTeardownNavigationError(error, window)) { log.info("Navigation aborted while the window was closing"); return; } log.error("Failed to load window URL", error); } }; // The supervised daemon (and the desktop sidecar) own this data dir — the same // path the CLI's `executor web`/daemon uses, so desktop and CLI share state. const DESKTOP_DATA_DIR = join(homedir(), ".executor"); const delay = (ms: number): Promise<void> => new Promise((r) => setTimeout(r, ms)); const parseVersionParts = (version: string): readonly number[] | null => { const core = version.trim().split(/[+-]/, 1)[0]; if (!core) return null; const parts = core.split(".").map((part) => Number.parseInt(part, 10)); return parts.every((part) => Number.isInteger(part) && part >= 0) ? parts : null; }; const compareVersions = (left: string, right: string): number | null => { const leftParts = parseVersionParts(left); const rightParts = parseVersionParts(right); if (!leftParts || !rightParts) return null; const length = Math.max(leftParts.length, rightParts.length); for (let index = 0; index < length; index += 1) { const l = leftParts[index] ?? 0; const r = rightParts[index] ?? 0; if (l !== r) return l > r ? 1 : -1; } return 0; }; const shouldUpgradeDaemonForDesktop = (daemonVersion: string | null): boolean => { if (!daemonVersion) return false; const comparison = compareVersions(app.getVersion(), daemonVersion); return comparison !== null && comparison > 0; }; const normalizedPath = (path: string): string => path.replaceAll("\\", "/"); const shouldReplaceDaemonForDesktop = (conn: SidecarConnection): boolean => { if (shouldUpgradeDaemonForDesktop(conn.ownerVersion)) return true; if (!app.isPackaged) return false; const ownerPath = conn.ownerExecutablePath; if (!ownerPath) return false; if (!existsSync(ownerPath)) return true; if (conn.ownerClient !== "desktop") return false; return normalizedPath(ownerPath) !== normalizedPath(bundledExecutorPath()); }; /** Poll for a reachable supervised daemon until the deadline. */ const waitForSupervisedAttach = async ( timeoutMs: number, options: { readonly port?: number } = {}, ): Promise<SidecarConnection | null> => { const deadline = Date.now() + timeoutMs; for (;;) { const attached = await attachToSupervisedDaemon(); if (attached && (options.port === undefined || attached.port === options.port)) return attached; if (Date.now() >= deadline) return null; await delay(300); } }; const confirmEnableBackgroundService = async (): Promise<boolean> => { // EXECUTOR_TEST_AUTO_CONFIRM_BACKGROUND_SERVICE skips the modal, the same seam // as confirmResetState. Native dialogs are unreachable from CDP/Playwright, so // a first-run boot under automation otherwise blocks here forever with no way // to answer. "1" keeps the background service; any other value declines and // falls back to the managed sidecar. const autoConfirm = process.env.EXECUTOR_TEST_AUTO_CONFIRM_BACKGROUND_SERVICE; if (autoConfirm !== undefined) return autoConfirm === "1"; const { response } = await dialog.showMessageBox({ type: "question", title: "Keep Executor running in the background?", message: "Keep your connections available after you quit Executor?", detail: "Executor can run as a lightweight background service so your MCP tools keep working after you close this window or restart your computer. You can turn this off anytime in Settings.", buttons: ["Keep running in the background", "Not now"], defaultId: 0, cancelId: 1, }); return response === 0; }; const acceptSupervisedConnection = async ( attached: SidecarConnection, ): Promise<SidecarConnection | null> => { if (!shouldReplaceDaemonForDesktop(attached)) return attached; const settings = getServerSettings(); return replaceSupervisedDaemonForDesktop(attached, { install: () => installSupervisedService({ port: settings.port, dataDir: DESKTOP_DATA_DIR, }), waitForAttach: () => waitForSupervisedAttach(30_000, { port: settings.port }), attach: attachToSupervisedDaemon, onInstallFailure: (error) => { log.warn("Failed to replace older supervised daemon; re-checking before falling back", error); }, }); }; const installSupervisedAndWait = async (reason: string): Promise<SidecarConnection | null> => { const settings = getServerSettings(); // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: install failure falls back to managed-spawn so the app still launches try { await installSupervisedService({ port: settings.port, dataDir: DESKTOP_DATA_DIR, }); } catch (error) { log.warn(`Failed to install supervised service after ${reason}; using managed sidecar`, error); return null; } return waitForSupervisedAttach(15_000, { port: settings.port }); }; /** * Resolve a connection to the OS-supervised daemon, installing it on first run * (with consent). Returns null when supervision is unavailable or the user * declined — the caller then falls back to managed-spawn. */ const ensureSupervisedConnection = async (): Promise<SidecarConnection | null> => { // 1. Already running → attach. const attached = await attachToSupervisedDaemon(); if (attached) { return acceptSupervisedConnection(attached); } // Headless/e2e: the first-run background-service prompt and the systemd/ // launchd install both need a user (and a real session bus) present. With // this set, fall straight through to managed-spawn after the attach attempt // so the packaged app can be driven without a window server. Mirrors // EXECUTOR_TEST_AUTO_CONFIRM_RESET in reset-state.ts. if (process.env.EXECUTOR_TEST_SKIP_BACKGROUND_SERVICE === "1") return null; const status = await supervisedServiceStatus(); if (!status.supported) return null; // 2. Registered and running, but the first attach missed it. Kick once and // wait only when the kick command succeeds. if (status.registered) { if (status.running) { const attachedAfterStatus = await attachToSupervisedDaemon(); if (attachedAfterStatus) return acceptSupervisedConnection(attachedAfterStatus); // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: a restart failure just falls through to managed-spawn try { await restartSupervisedService(); } catch (error) { log.warn( "Failed to restart running supervised service after attach failed; using managed sidecar", error, ); return null; } return waitForSupervisedAttach(15_000); } // 3. Registered but not running usually means a stale plist/unit/task. Try // restart first, then reinstall the service if the OS manager cannot start it. // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: restart failure triggers supervised service self-heal try { await restartSupervisedService(); } catch (error) { log.warn("Failed to restart registered supervised service; reinstalling", error); return installSupervisedAndWait("registered service restart failure"); } return waitForSupervisedAttach(15_000); } // 4. First run → ask, then install + start. The unit carries no secret; the // supervised daemon mints/loads its bearer from auth.json under DESKTOP_DATA_DIR. if (!(await confirmEnableBackgroundService())) return null; return installSupervisedAndWait("first-run prompt"); }; // Crash monitor for the supervised daemon: the OS service manager restarts it // on crash, but during that window the window's requests fail. Poll, show a // reconnecting overlay while it's down, and reload once it's back. let supervisedMonitorTimer: ReturnType<typeof setInterval> | null = null; let supervisedDaemonDown = false; let supervisedMonitorMisses = 0; const SUPERVISED_MONITOR_MISSES_BEFORE_DOWN = 3; const stopSupervisedMonitor = () => { if (supervisedMonitorTimer) clearInterval(supervisedMonitorTimer); supervisedMonitorTimer = null; supervisedDaemonDown = false; supervisedMonitorMisses = 0; }; const armSupervisedMonitor = () => { stopSupervisedMonitor(); supervisedMonitorTimer = setInterval(() => { void (async () => { const live = await attachToSupervisedDaemon(); const window = liveMainWindow(); if (!live) { supervisedMonitorMisses += 1; if (supervisedMonitorMisses < SUPERVISED_MONITOR_MISSES_BEFORE_DOWN) return; if (!supervisedDaemonDown && window) { supervisedDaemonDown = true; showCrashScreen(window); } return; } supervisedMonitorMisses = 0; if (supervisedDaemonDown) { supervisedDaemonDown = false; connection = live; installBearerAuthHeader(live.baseUrl, live.authToken); if (window) void loadWindowUrl(window, webUrlForConnection(live)); } })(); }, 10_000); }; const installBearerAuthHeader = (origin: string, token: string | null) => { authHeaderUnsubscribe?.(); authHeaderUnsubscribe = null; if (!token) return; const headerValue = `Bearer ${token}`; session.defaultSession.webRequest.onBeforeSendHeaders( { urls: [`${origin}/*`] }, (details, callback) => { // Scope the bearer to the app's OWN renderer. OAuth popups run in this same // session but load third-party provider pages; auto-attaching the bearer to // any request they make to the sidecar would make it an ambient credential // (a CSRF vector) for untrusted content — the very thing the bearer model // exists to avoid. The popup only ever needs the bearer-exempt // /oauth/callback and hands its result back via same-origin browser // channels (localStorage/postMessage), so withholding the bearer from any // non-app webContents is safe. Requests with no webContentsId (main // process / network service) still get it. const fromOtherWebContents = details.webContentsId !== undefined && (mainWindow === null || details.webContentsId !== mainWindow.webContents.id); if (fromOtherWebContents) { callback({ requestHeaders: details.requestHeaders }); return; } callback({ requestHeaders: { ...details.requestHeaders, Authorization: headerValue, }, }); }, ); authHeaderUnsubscribe = () => { session.defaultSession.webRequest.onBeforeSendHeaders({ urls: [`${origin}/*`] }, null); }; }; /** * Resolve the on-disk path to the Executor app icon. Packaged builds get * the .icns/.ico baked in by electron-builder, but in dev mode Electron * launches via the bare runtime binary with no bundled icon — so we set * it programmatically (dock on mac, BrowserWindow on linux/win). * * In dev `app.getAppPath()` returns the directory of the app's * `package.json` (i.e. `apps/desktop`), which is more robust than * relative path math from the compiled main bundle. */ const resolveSourceIconPath = (): string => app.isPackaged ? join(process.resourcesPath, "icon.png") : join(app.getAppPath(), "build", "icon.png"); const resolveLinuxIcon = (): string | undefined => { if (process.platform !== "linux") return undefined; return resolveSourceIconPath(); }; /** * Set the macOS dock icon at runtime. Packaged builds already use the * bundle's .icns; this matters only in dev where the bare electron binary * defaults to the Electron diamond. * * `setIcon` accepts a file path but silently no-ops if the underlying * image fails to decode — we route through `nativeImage.createFromPath` * + an `isEmpty()` check so we can surface the bad-path case in the log. */ const installDockIcon = () => { if (process.platform !== "darwin") return; if (app.isPackaged) return; if (!app.dock) return; const iconPath = resolveSourceIconPath(); if (!existsSync(iconPath)) { log.warn(`[dock-icon] file missing at ${iconPath}; skipping`); return; } const image = nativeImage.createFromPath(iconPath); if (image.isEmpty()) { log.warn(`[dock-icon] failed to decode ${iconPath}; skipping`); return; } app.dock.setIcon(image); log.info(`[dock-icon] set to ${iconPath} (${image.getSize().width}×${image.getSize().height})`); }; const createMainBrowserWindow = (options: { readonly show: boolean }): BrowserWindow => { const windowState = windowStateKeeper({ defaultWidth: 1280, defaultHeight: 800, }); const linuxIcon = resolveLinuxIcon(); const window = new BrowserWindow({ x: windowState.x, y: windowState.y, width: windowState.width, height: windowState.height, // Keep the window at or above the responsive layout's mobile breakpoint // (Tailwind `md` = 768px). Below it the web shell switches to the mobile // header, whose far-left hamburger would render under the native macOS // traffic lights (issue #1125). Staying >= 768 means the desktop always // renders the sidebar layout, so the lights only ever sit over the sidebar // header, which is offset to clear them (.desktop-macos-titlebar). minWidth: 768,