1170 lines · 45.5 KB · TypeScript
RawDownload
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 } from "./crash-screen";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"; // 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();}; // 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;}; /** * 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) {    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,        );      },    });  }   // 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 but not currently serving → kick it and wait.  if (status.registered) {    // 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 kickstart supervised service", error);    }    return waitForSupervisedAttach(15_000);  }   // 3. 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;  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.error("Failed to install supervised service; using managed sidecar", error);    return null;  }  return waitForSupervisedAttach(15_000);}; // 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;          const html = sidecarCrashHtml({ reported: errorReportingEnabled });          void window.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(html)}`);        }        return;      }      supervisedMonitorMisses = 0;      if (supervisedDaemonDown) {        supervisedDaemonDown = false;        connection = live;        installBearerAuthHeader(live.baseUrl, live.authToken);        if (window) void window.loadURL(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 createWindow = async (conn: SidecarConnection) => {  const windowState = windowStateKeeper({    defaultWidth: 1280,    defaultHeight: 800,  });   installBearerAuthHeader(conn.baseUrl, conn.authToken);   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,    minHeight: 480,    show: false,    backgroundColor: "#0a0a0a",    autoHideMenuBar: true,    ...(process.platform === "darwin"      ? { titleBarStyle: "hidden" as const, trafficLightPosition: { x: 16, y: 17 } }      : {}),    ...(linuxIcon ? { icon: linuxIcon } : {}),    webPreferences: {      preload: PRELOAD_PATH,      contextIsolation: true,      nodeIntegration: false,      sandbox: true,    },  });  mainWindow = window;   windowState.manage(window);   window.once("closed", () => {    if (mainWindow === window) mainWindow = null;  });   window.once("ready-to-show", () => {    if (!window.isDestroyed()) window.show();  });   window.webContents.setWindowOpenHandler(({ url, disposition }) => {    // JS-initiated `window.open(url, name, "popup=1,...")` calls (OAuth    // sign-in flow in packages/react/src/api/oauth-popup.ts:73) come in    // with disposition "new-window" — allow them as Electron child    // windows so the renderer's popup tracking (closed polling +    // BroadcastChannel handoff) works. Plain `<a target="_blank">`    // link clicks come in as "foreground-tab" / "background-tab" /    // "other" and route to the user's default browser.    if (disposition === "new-window") {      return {        action: "allow",        overrideBrowserWindowOptions: {          autoHideMenuBar: true,          webPreferences: {            // No preload, no nodeIntegration — popup loads third-party            // OAuth provider pages, then a final navigation back to            // 127.0.0.1:<port>/oauth/callback which the session-level            // bearer header injection (installBearerAuthHeader)            // catches automatically. The popup never needs the            // executor IPC bridge.            contextIsolation: true,            nodeIntegration: false,            sandbox: true,          },        },      };    }    void shell.openExternal(url);    return { action: "deny" };  });   // A supervised daemon can pass the health probe and still disappear before  // navigation begins. Treat that as a failed connection instead of leaving the  // user with a visible BrowserWindow that only shows the black background.  // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: Electron navigation rejects when the sidecar vanishes during startup  try {    await window.loadURL(webUrlForConnection(conn));  } catch (error) {    log.error("Failed to load Executor web UI", error);    destroyWindow(window);    // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: caller decides whether to fall back or surface startup failure

Lines 1–500 of 1170.