Ctrl K
ComunidadGuías…

electron/main/index.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========

import axios from 'axios';
import {
  app,
  BrowserWindow,
  dialog,
  ipcMain,
  Menu,
  nativeTheme,
  protocol,
  screen,
  session,
  shell,
} from 'electron';
import log from 'electron-log';
import FormData from 'form-data';
import fsp from 'fs/promises';
import mime from 'mime';
import { ChildProcessWithoutNullStreams, spawn } from 'node:child_process';
import crypto from 'node:crypto';
import fs, { existsSync } from 'node:fs';
import http from 'node:http';
import os, { homedir } from 'node:os';
import path from 'node:path';
import { Readable } from 'node:stream';
import { fileURLToPath } from 'node:url';
import kill from 'tree-kill';
import {
  APP_COMMAND_CHANNEL,
  APP_COMMAND_HANDLED_CHANNEL,
  APP_SHELL_NOT_READY_CHANNEL,
  APP_SHELL_READY_CHANNEL,
  APP_SHELL_READY_PROBE_CHANNEL,
  isAppCommandHandled,
  isAppShellLifecycleMessage,
  type AppCommandId,
} from '../../src/shared/appCommands';
import { FILE_PREVIEW_LIMITS } from '../../src/shared/filePreviewContract';
import {
  NATIVE_MENU_LOCALE_CHANNEL,
  type NativeMenuLocale,
} from '../../src/shared/nativeMenu';
import {
  SUBFRAME_LOAD_FAILED_CHANNEL,
  subframeLoadFailureFromEvent,
} from '../../src/shared/subframeLoadFailure';
import {
  isWindowCloseResponse,
  WINDOW_CLOSE_RESPONSE_CHANNEL,
} from '../../src/shared/windowClose';
import { AppShellReadinessGate } from './appShellReadinessGate';
import {
  isBrainStreamRelayDisabled,
  registerBrainStreamRelay,
  type BrainStreamRelay,
} from './brainStreamRelay';
import { CloseCoordinator } from './closeCoordinator';
import { installApplicationMenu } from './commands/applicationMenu';
import {
  installContextMenu,
  type ContextMenuSurfaceKind,
} from './commands/contextMenu';
import {
  applyNativeMenuLocaleChange,
  getNativeMenuMessages,
  getNativeQuitMessages,
  resolveNativeMenuLocale,
} from './commands/nativeMenuMessages';
import { copyBrowserData } from './copy';
import { getOrCreateDesktopInstanceId } from './desktopIdentity';
import { inlineContentDisposition, resolveFileByteRange } from './fileRange';
import { FileReader } from './fileReader';
import {
  checkToolInstalled,
  findAvailablePort,
  killProcessOnPort,
  startBackend,
} from './init';
import {
  checkAndInstallDepsOnUpdate,
  getInstallationStatus,
  PromiseReturnType,
} from './install-deps';
import {
  authorizeLocalFilePath,
  authorizeLocalPreviewPath,
  isMainRendererSender,
} from './localFileSecurity';
import { filePathFromLocalFileUrl } from './localFileUrl';
import {
  authorizeWorkspaceLocalNode,
  openWorkspaceLocalFile,
  rememberBoundedPathGrant,
  revealAuthorizedLocalNode,
  revealUserVisibleLocalNode,
  revealWorkspaceLocalNode,
} from './localPathActions';
import { setRoundedCorners } from './native/macos-window';
import { confirmNativeQuit } from './nativeQuitGuard';
import { RendererAppCommandCoordinator } from './rendererAppCommandCoordinator';
import { registerReviewChangesIpcHandlers } from './reviewChanges';
import {
  isSessionWindowTarget,
  SessionWindowManager,
  sessionWindowRoute,
  type SessionWindowTarget,
} from './sessionWindows';
import {
  completeCodexOAuthCallback,
  getCodexResolverEnv,
  registerCodexSubscriptionAuthIpcHandlers,
} from './subscriptionAuth';
import { disposeAllTerminals, registerTerminalIpcHandlers } from './terminal';
import { registerUpdateIpcHandlers, update } from './update';
import {
  getEmailFolderPath,
  getEnvPath,
  maskProxyUrl,
  readGlobalEnvKey,
  removeEnvKey,
  updateEnvBlock,
  writeEnvFile,
} from './utils/envUtil';
import { createDiagnosticsZip, zipDirectories, zipFolder } from './utils/log';
import { addMcp, readMcpConfig, removeMcp, updateMcp } from './utils/mcpConfig';
import {
  checkVenvExistsForPreCheck,
  getBackendPath,
  isBinaryExists,
} from './utils/process';
import { WebViewManager } from './webview';
import {
  loadWindowStartupState,
  persistWindowState,
  resolveWindowStartupState,
  type PersistedWindowStateV2,
} from './windowState';
import {
  closeWorkspaceSecretBroker,
  ensureWorkspaceSecretBroker,
  getDefaultWorkspaceSecretVault,
  registerWorkspaceSecretIpcHandlers,
} from './workspaceSecrets';

const userData = app.getPath('userData');

// ==================== constants ====================
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const MAIN_DIST = path.join(__dirname, '../..');
const RENDERER_DIST = path.join(MAIN_DIST, 'dist');
const VITE_DEV_SERVER_URL = process.env.VITE_DEV_SERVER_URL;
const VITE_PUBLIC = VITE_DEV_SERVER_URL
  ? path.join(MAIN_DIST, 'public')
  : RENDERER_DIST;

// ==================== global variables ====================
let win: BrowserWindow | null = null;
let createWindowPromise: Promise<void> | null = null;
let webViewManager: WebViewManager | null = null;
let sessionWindows: SessionWindowManager<BrowserWindow> | null = null;
const SESSION_WINDOW_DEFAULT_SIZE = { width: 960, height: 760 };
const SESSION_WINDOW_MINIMUM_SIZE = { width: 480, height: 480 };
let fileReader: FileReader | null = null;
let python_process: ChildProcessWithoutNullStreams | null = null;
let backendPort: number = 5001;
let backendStartPromise: Promise<BackendStartResult> | null = null;
let brainStreamRelay: BrainStreamRelay | null = null;
const localControlCapability = crypto.randomBytes(32).toString('base64url');
let desktopInstanceId: string | null = null;
let browser_port = 9222;
let use_external_cdp = false;
let proxyUrl: string | null = null;
const LEGACY_DESKTOP_INSTANCE_STORAGE_KEY = 'eigent_desktop_instance_id';
// Space roots each app window registered for local previews, by webContents id.
const localFileRootsByRenderer = new Map<number, Set<string>>();
function activeLocalFileRoots(): string[] {
  const roots = new Set<string>();
  for (const windowRoots of localFileRootsByRenderer.values())
    windowRoots.forEach((root) => roots.add(root));
  return [...roots];
}
let protocolUrlQueue: string[] = [];
let isWindowReady = false;
let nativeMenuLocale: NativeMenuLocale | null = null;
const appShellReadinessGate = new AppShellReadinessGate({
  announceReadyProbe: () => {
    const target = win;
    if (!target || target.isDestroyed() || target.webContents.isDestroyed()) {
      return;
    }
    target.webContents.send(APP_SHELL_READY_PROBE_CHANNEL);
  },
});
const rendererAppCommands = new RendererAppCommandCoordinator({
  send: (request) => {
    const target = win;
    if (!target || target.isDestroyed() || target.webContents.isDestroyed()) {
      throw new Error('main renderer is unavailable');
    }
    target.webContents.send(APP_COMMAND_CHANNEL, request);
  },
  diagnostic: (message) => log.warn(message),
});
const closeCoordinator = new CloseCoordinator({
  // Eigent owns a single application window. Closing it exits the app on
  // every platform so shutdown cleanup also stops the local backend.
  defaultIntent: 'quit-app',
  quit: () => app.quit(),
  shouldGuard: () => rendererAppCommands.isReady(),
  // Main spawns the only Brain; without it no task or stream can be running.
  mayHaveRunningWork: () => isPythonProcessRunning(),
  confirmQuitWithoutRenderer: () =>
    confirmNativeQuit({
      lookup: async () => {
        if (!isPythonProcessRunning()) throw new Error('Brain unavailable');
        const response = await axios.get(
          `http://127.0.0.1:${backendPort}/runs/active`,
          {
            timeout: 2500,
            headers: { 'X-Eigent-Local-Capability': localControlCapability },
          }
        );
        return response.data;
      },
      confirm: async (unknown) => {
        const text = getNativeQuitMessages(
          nativeMenuLocale ?? app.getLocale(),
          unknown
        );
        const result = await dialog.showMessageBox({
          type: 'question',
          title: text.title,
          message: text.title,
          detail: text.message,
          buttons: [text.cancel, text.title],
          defaultId: 0,
          cancelId: 0,
          noLink: true,
        });
        return result.response === 1;
      },
    }),
  diagnostic: (message) => log.info(message),
});
let isQuitCleanupInProgress = false;
let windowStateSaveTimer: NodeJS.Timeout | null = null;
const allowDeveloperTools =
  !app.isPackaged || app.commandLine.hasSwitch('enable-devtools');

function getCurrentNativeMenuMessages() {
  nativeMenuLocale ??= resolveNativeMenuLocale(app.getLocale());
  return getNativeMenuMessages(nativeMenuLocale);
}

function installSurfaceContextMenu(
  contents: Electron.WebContents,
  ownerWindow: BrowserWindow,
  surfaceKind: ContextMenuSurfaceKind
): () => void {
  return installContextMenu({
    contents,
    getMessages: getCurrentNativeMenuMessages,
    isDevelopment: allowDeveloperTools,
    menuApi: Menu,
    ownerWindow,
    surfaceKind,
  });
}

async function dispatchRendererAppCommand(
  commandId: AppCommandId
): Promise<void> {
  try {
    if (!win || win.isDestroyed()) {
      await createWindow();
    } else if (createWindowPromise) {
      await createWindowPromise;
    }

    const target = win;
    if (!target || target.isDestroyed()) return;
    if (!target.isVisible()) target.show();
    target.focus();
    rendererAppCommands.dispatch(commandId);
  } catch (error) {
    log.error(`[APP COMMAND] Failed to dispatch ${commandId}:`, error);
  }
}

function installNativeApplicationMenu(): void {
  const platform =
    process.platform === 'darwin' || process.platform === 'win32'
      ? process.platform
      : 'linux';

  installApplicationMenu(Menu, {
    appName: app.getName(),
    dispatchRendererCommand: (commandId) => {
      void dispatchRendererAppCommand(commandId);
    },
    isDevelopment: allowDeveloperTools,
    messages: getCurrentNativeMenuMessages(),
    onOpenExternalError: (error) => {
      log.error('[APPLICATION MENU] Failed to open external URL:', error);
    },
    openExternal: (url) => shell.openExternal(url),
    platform,
    requestQuit: () => closeCoordinator.request('quit-app'),
    closeSecondaryWindow: () => {
      const focused = BrowserWindow.getFocusedWindow();
      if (!focused || !sessionWindows?.ownsContents(focused.webContents.id))
        return false;
      focused.close();
      return true;
    },
  });
}

function persistBrowserWindowState(target: BrowserWindow | null = win): void {
  if (windowStateSaveTimer) {
    clearTimeout(windowStateSaveTimer);
    windowStateSaveTimer = null;
  }
  if (!target || target.isDestroyed()) return;

  try {
    persistWindowState(userData, target);
  } catch (error) {
    log.warn('[WINDOW STATE] Failed to persist window state:', error);
  }
}

function scheduleWindowStateSave(target: BrowserWindow): void {
  if (windowStateSaveTimer) clearTimeout(windowStateSaveTimer);
  windowStateSaveTimer = setTimeout(() => {
    windowStateSaveTimer = null;
    persistBrowserWindowState(target);
  }, 250);
}

/**
 * Real paths the user picked in a native file dialog this session.
 *
 * Chat attachments are chosen from anywhere on disk, so they are legitimately
 * outside every Space root. The dialog itself is the user's authorization, and
 * recording the exact picked path lets those attachments stay revealable
 * without granting the renderer free rein over the file system.
 */
const userSelectedLocalPaths = new Set<string>();
const USER_SELECTED_PATH_LIMIT = 512;

function rememberUserSelectedLocalPath(realPath: string): void {
  rememberBoundedPathGrant(
    userSelectedLocalPaths,
    realPath,
    USER_SELECTED_PATH_LIMIT
  );
}

function resolveDesktopInstanceId(legacyRendererId?: string | null): string {
  if (!desktopInstanceId) {
    desktopInstanceId = getOrCreateDesktopInstanceId(
      userData,
      legacyRendererId
    );
  }
  return desktopInstanceId;
}

async function primeDesktopInstanceIdFromRenderer(): Promise<void> {
  if (desktopInstanceId || !win || win.isDestroyed()) return;
  let legacyRendererId: string | null = null;
  try {
    legacyRendererId = await win.webContents.executeJavaScript(
      `window.localStorage.getItem(${JSON.stringify(
        LEGACY_DESKTOP_INSTANCE_STORAGE_KEY
      )})`,
      true
    );
  } catch (error) {
    log.warn('Unable to read legacy renderer device identity', error);
  }
  resolveDesktopInstanceId(legacyRendererId);
}

const PREVIEW_WEBVIEW_PARTITION = 'persist:session-preview';

const isHttpOrHttpsUrl = (url: unknown): url is string => {
  if (typeof url !== 'string') return false;
  try {
    const parsed = new URL(url);
    return parsed.protocol === 'http:' || parsed.protocol === 'https:';
  } catch {
    return false;
  }
};

function isMainRendererFrame(
  event: Pick<Electron.IpcMainInvokeEvent, 'sender' | 'senderFrame'>
): boolean {
  return Boolean(
    win &&
    !win.isDestroyed() &&
    isMainRendererSender(event.sender.id, win.webContents.id) &&
    event.senderFrame === event.sender.mainFrame
  );
}

/** The main window or a Session window: both run the app renderer. */
function isAppRendererFrame(
  event: Pick<Electron.IpcMainInvokeEvent, 'sender' | 'senderFrame'>
): boolean {
  if (isMainRendererFrame(event)) return true;
  return Boolean(
    sessionWindows?.ownsContents(event.sender.id) &&
    event.senderFrame === event.sender.mainFrame
  );
}

// Settings-only surfaces (workspace secrets) stay with the main window.
function assertMainRendererSender(event: Electron.IpcMainInvokeEvent): void {
  if (!isMainRendererFrame(event)) {
    throw new Error('This operation is restricted to the main renderer');
  }
}

function assertAppRendererSender(event: Electron.IpcMainInvokeEvent): void {
  if (!isAppRendererFrame(event)) {
    throw new Error('This operation is restricted to the app renderer');
  }
}

function localFileAllowedRoots(): string[] {
  // Only active Space roots plus the renderer's static application assets are
  // readable through localfile://. In particular, HOME, userData and the OS
  // temp directory are not trust boundaries for agent-authored HTML.
  return [...activeLocalFileRoots(), RENDERER_DIST, VITE_PUBLIC];
}

async function requireAuthorizedPreviewFile(
  event: Electron.IpcMainInvokeEvent,
  filePath: string
): Promise<string> {
  assertAppRendererSender(event);
  const authorization = await authorizeLocalPreviewPath(
    filePath,
    activeLocalFileRoots(),
    [RENDERER_DIST, VITE_PUBLIC]
  );
  if (!authorization.allowed) {
    throw new Error('Preview file is outside the active workspace');
  }
  return authorization.filePath;
}

// CDP Browser Pool
interface CdpBrowser {
  id: string;
  port: number;
  isExternal: boolean;
  name?: string;
  addedAt: number;
}
let cdp_browser_pool: CdpBrowser[] = [];
let cdpLastAssignedPort = 9223; // tracks the highest port ever assigned, never decreases
let cdpHealthCheckTimer: ReturnType<typeof setInterval> | null = null;

const CDP_POOL_FILE = path.join(os.homedir(), '.eigent', 'cdp-browsers.json');

type BackendStartOptions = {
  forceRestart?: boolean;
};

type BackendStartResult =
  { success: true; port: number } | { success: false; error: string };

function formatErrorMessage(error: unknown): string {
  return error instanceof Error ? error.message : String(error);
}

function isBrokenConsolePipeError(error: unknown): boolean {
  return (
    typeof error === 'object' &&
    error !== null &&
    'code' in error &&
    ((error as NodeJS.ErrnoException).code === 'EPIPE' ||
      (error as NodeJS.ErrnoException).code === 'ERR_STREAM_DESTROYED')
  );
}

function disableConsoleLogTransport(): void {
  if (log.transports.console.level !== false) {
    log.transports.console.level = false;
  }
}

function handleProcessPipeError(error: Error): void {
  if (isBrokenConsolePipeError(error)) {
    disableConsoleLogTransport();
    return;
  }

  setImmediate(() => {
    throw error;
  });
}

process.stdout.on('error', handleProcessPipeError);
process.stderr.on('error', handleProcessPipeError);

function isPythonProcessRunning(): boolean {
  return Boolean(
    python_process &&
    !python_process.killed &&
    python_process.exitCode === null &&
    python_process.signalCode === null
  );
}

function notifyBackendReady(result: BackendStartResult): void {
  if (!win || win.isDestroyed()) {
    return;
  }

  const payload = result.success
    ? {
        success: true,
        port: result.port,
      }
    : {
        success: false,
        error: result.error,
      };
  win.webContents.send('backend-ready', payload);
  for (const sessionWindow of sessionWindows?.allWindows() ?? [])
    sessionWindow.webContents.send('backend-ready', payload);
  // Session windows open after the Brain answers, as they did before quit.
  if (result.success) getSessionWindows().restore();
}

// ==================== Session windows ====================
/** The app window (main or Session) that owns `sender`, if any. */
function appWindowForSender(
  sender: Electron.WebContents
): BrowserWindow | null {
  if (win && !win.isDestroyed() && sender.id === win.webContents.id) return win;
  if (!sessionWindows?.ownsContents(sender.id)) return null;
  const owner = BrowserWindow.fromWebContents(sender);
  return owner && !owner.isDestroyed() ? owner : null;
}

function getSessionWindows(): SessionWindowManager<BrowserWindow> {
  if (!sessionWindows) {
    let saveTimer: NodeJS.Timeout | null = null;
    sessionWindows = new SessionWindowManager<BrowserWindow>({
      userDataPath: userData,
      createWindow: createSessionAppWindow,
      onClosed: (target) =>
        relaySessionWindowMessage(null, { type: 'closed', ...target }),
      scheduleSave: (save) => {
        if (saveTimer) clearTimeout(saveTimer);
        saveTimer = setTimeout(() => {
          saveTimer = null;
          save();
        }, 250);
      },
    });
  }
  return sessionWindows;
}

/** Forwards a renderer message to every other app window. */
function relaySessionWindowMessage(
  senderId: number | null,
  message: unknown
): void {
  if (!message || typeof message !== 'object') return;
  const type = (message as { type?: unknown }).type;
  if (typeof type !== 'string' || type.length > 64) return;
  const targets = [
    ...(win && !win.isDestroyed() ? [win] : []),
    ...(sessionWindows?.allWindows() ?? []),
  ];
  for (const target of targets) {
    if (target.webContents.id === senderId) continue;
    target.webContents.send('session-window:message', message);
  }
  // Requests the main window answers with a dialog need it in front.
  if (type === 'session-control' && win && !win.isDestroyed()) {
    if (win.isMinimized()) win.restore();
    win.focus();
  }
}

/**
 * A window that shows one Session: same frame, preload and partition as the
 * main window, so the Session behaves exactly as it does there.
 */
function createSessionAppWindow(
  target: SessionWindowTarget,
  state: PersistedWindowStateV2 | null
): BrowserWindow {
  const placement = resolveWindowStartupState({
    displays: screen.getAllDisplays(),
    primaryDisplay: screen.getPrimaryDisplay(),
    persistedState: state,
    minimumSize: SESSION_WINDOW_MINIMUM_SIZE,
  });
  const bounds =
    placement.source === 'persisted'
      ? placement.bounds
      : {
          width: Math.min(
            SESSION_WINDOW_DEFAULT_SIZE.width,
            placement.bounds.width
          ),
          height: Math.min(
            SESSION_WINDOW_DEFAULT_SIZE.height,
            placement.bounds.height
          ),
        };
  const sessionWindow = new BrowserWindow({
    title: 'Eigent',
    ...bounds,
    minWidth: placement.minimumSize.width,
    minHeight: placement.minimumSize.height,
    ...appWindowChromeOptions(),
  });
  const disposeContextMenu = installSurfaceContextMenu(
    sessionWindow.webContents,
    sessionWindow,
    'main-renderer'
  );
  sessionWindow.on('closed', disposeContextMenu);
  installPreviewGuestGuards(sessionWindow);
  setupExternalLinkHandling(sessionWindow);
  sessionWindow.once('ready-to-show', () => {
    if (sessionWindow.isDestroyed()) return;
    if (process.platform === 'darwin') {
      try {
        setRoundedCorners(sessionWindow, 20);
      } catch (error) {
        log.error('[MacOS] Failed to apply rounded corners:', error);
      }
    }
    if (placement.shouldMaximize) sessionWindow.maximize();
    else sessionWindow.show();
  });

  const route = sessionWindowRoute(target);
  const load = () => {
    if (sessionWindow.isDestroyed()) return;
    if (VITE_DEV_SERVER_URL) {
      void sessionWindow.loadURL(`${VITE_DEV_SERVER_URL}#${route}`);
    } else {
      void sessionWindow.loadFile(indexHtml, { hash: route });
    }
  };

  // Same recovery as the main window: reload a crashed renderer, retry a
  // failed document load, and tell the page when a preview subframe failed.
  sessionWindow.webContents.on('render-process-gone', (_event, details) => {
    log.error(
      '[SESSION WINDOW] Renderer gone:',
      details.reason,
      details.exitCode
    );
    setTimeout(load, 1000);
  });
  sessionWindow.webContents.on(
    'did-fail-load',
    (_event, errorCode, errorDescription, validatedURL, isMainFrame) => {
      const subframeFailure = subframeLoadFailureFromEvent(
        errorCode,
        validatedURL,
        isMainFrame
      );
      if (subframeFailure && !sessionWindow.isDestroyed()) {
        sessionWindow.webContents.send(
          SUBFRAME_LOAD_FAILED_CHANNEL,
          subframeFailure
        );
      }
      // -3 is ERR_ABORTED: a replaced navigation, not a broken document.
      if (!isMainFrame || errorCode === -3) return;
      log.error(
        `[SESSION WINDOW] Failed to load: ${errorCode} - ${errorDescription} - ${validatedURL}`
      );
      setTimeout(load, 2000);
    }
  );

  load();
  return sessionWindow;
}

function checkBackendHealth(port: number): Promise<boolean> {
  if (!Number.isInteger(port) || port <= 0 || port > 65535) {
    return Promise.resolve(false);
  }

  return new Promise((resolve) => {
    const req = http.get(
      `http://127.0.0.1:${port}/health`,
      { timeout: 1000 },
      (res) => {
        res.resume();
        resolve(res.statusCode === 200);
      }
    );

    req.on('error', () => resolve(false));
    req.on('timeout', () => {
      req.destroy();
      resolve(false);
    });
  });
}

/** Persist pool to disk. */
function saveCdpPool(): void {
  try {
    fs.writeFileSync(CDP_POOL_FILE, JSON.stringify(cdp_browser_pool, null, 2));
  } catch (e) {
    log.error(`[CDP POOL] Failed to save pool: ${e}`);
  }
}

/** Load pool from disk. Mark all as external (process handles are lost after restart). */
function loadCdpPool(): void {
  try {
    if (fs.existsSync(CDP_POOL_FILE)) {
      const data = JSON.parse(fs.readFileSync(CDP_POOL_FILE, 'utf-8'));
      cdp_browser_pool = (data as CdpBrowser[]).map((b) => ({
        ...b,
        isExternal: true,
      }));
      cdpLastAssignedPort = cdp_browser_pool.reduce(
        (max, b) => Math.max(max, b.port),
        cdpLastAssignedPort
      );
      log.info(
        `[CDP POOL] Loaded ${cdp_browser_pool.length} browser(s) from disk, lastAssignedPort=${cdpLastAssignedPort}`
      );
    }
  } catch (e) {
    log.error(`[CDP POOL] Failed to load pool: ${e}`);
    cdp_browser_pool = [];
  }
}

/** Push current pool to frontend. */
function notifyCdpPoolChanged(): void {
  if (win && !win.isDestroyed()) {
    log.info(
      `[CDP POOL] Pushing pool update to frontend (size=${cdp_browser_pool.length})`
    );
    win.webContents.send('cdp-pool-changed', cdp_browser_pool);
  } else {
    log.warn('[CDP POOL] Cannot notify: win is null or destroyed');
  }
}

/** Probe a CDP port. Returns true if alive. */
async function isCdpPortAlive(port: number): Promise<boolean> {
  try {
    const resp = await axios.get(`http://localhost:${port}/json/version`, {
      timeout: 1500,
    });
    return resp.status === 200;
  } catch {
    return false;
  }
}

/** Run one health-check cycle: remove dead browsers, persist & notify if changed. */
async function runPoolHealthCheck(): Promise<void> {
  if (cdp_browser_pool.length === 0) return;
  // Probe a snapshot so add/remove IPC handlers can run safely in parallel.
  const snapshot = [...cdp_browser_pool];
  const results = await Promise.all(
    snapshot.map((b) => isCdpPortAlive(b.port))
  );
  const deadIds = snapshot
    .filter((_, idx) => !results[idx])
    .map((browser) => browser.id);
  if (deadIds.length === 0) return;

  const deadIdSet = new Set(deadIds);
  const removedBrowsers = cdp_browser_pool.filter((b) => deadIdSet.has(b.id));
  if (removedBrowsers.length === 0) return;

  cdp_browser_pool = cdp_browser_pool.filter((b) => !deadIdSet.has(b.id));
  const deadPorts = removedBrowsers.map((b) => b.port);
  if (deadPorts.length > 0) {
    log.info(
      `[CDP POOL] Health-check removed dead ports: ${deadPorts.join(', ')}. pool_size=${cdp_browser_pool.length}`
    );
    saveCdpPool();
    notifyCdpPoolChanged();
  }
}

/** Start periodic health check (call after window is created). */
function startCdpHealthCheck(): void {
  if (cdpHealthCheckTimer) {
    clearInterval(cdpHealthCheckTimer);
    cdpHealthCheckTimer = null;
  }
  log.info('[CDP POOL] Starting health check (interval=3s)');
  // Run once immediately
  runPoolHealthCheck();
  cdpHealthCheckTimer = setInterval(runPoolHealthCheck, 3000);
}

function stopCdpHealthCheck(): void {
  if (cdpHealthCheckTimer) {
    clearInterval(cdpHealthCheckTimer);
    cdpHealthCheckTimer = null;
  }
}

/** Close a browser via CDP Browser.close() WebSocket command. Best-effort.
 *  Uses raw Node.js http upgrade (no external ws dependency needed).
 *  IMPORTANT: Never close the Electron app's own CDP port. */
async function closeBrowserViaCdp(port: number): Promise<void> {
  // Guard: refuse to close the Electron app's own CDP port
  if (port === browser_port) {
    log.warn(
      `[CDP CLOSE] Refusing to close port ${port} (Electron app's own CDP port)`
    );
    return;
  }

  try {
    const resp = await axios.get(`http://localhost:${port}/json/version`, {
      timeout: 2000,
    });
    const wsUrl: string | undefined = resp.data?.webSocketDebuggerUrl;
    if (!wsUrl) {
      log.warn(`[CDP CLOSE] No webSocketDebuggerUrl for port ${port}`);
      return;
    }

    const url = new URL(wsUrl);
    const key = crypto.randomBytes(16).toString('base64');

    await new Promise<void>((resolve) => {
      let resolved = false;
      const done = () => {
        if (!resolved) {
          resolved = true;
          resolve();
        }
      };

      const req = http.request(
        {
          hostname: url.hostname,
          port: url.port,
          path: url.pathname,
          method: 'GET',
          headers: {
            Connection: 'Upgrade',
            Upgrade: 'websocket',
            'Sec-WebSocket-Version': '13',
            'Sec-WebSocket-Key': key,
          },
        },
        () => done()
      );

      const timer = setTimeout(() => {
        req.destroy();
        done();
      }, 3000);

      req.on('upgrade', (_res, socket) => {
        // Handle socket errors to prevent uncaught exceptions
        socket.on('error', () => {});

        // Build a masked WebSocket text frame with Browser.close
        const payload = Buffer.from(
          JSON.stringify({ id: 1, method: 'Browser.close' })
        );
        const mask = crypto.randomBytes(4);
        const header = Buffer.alloc(6);
        header[0] = 0x81; // FIN + text opcode
        header[1] = 0x80 | payload.length; // MASK bit + length (<126)
        mask.copy(header, 2);

        const masked = Buffer.alloc(payload.length);
        for (let i = 0; i < payload.length; i++) {
          masked[i] = payload[i] ^ mask[i & 3];
        }

        socket.write(Buffer.concat([header, masked]));
        log.info(`[CDP CLOSE] Sent Browser.close to port ${port}`);

        // Give Chrome a moment to process, then clean up
        setTimeout(() => {
          clearTimeout(timer);
          socket.destroy();
          done();
        }, 500);
      });

      req.on('error', (err) => {
        log.warn(`[CDP CLOSE] Request error for port ${port}: ${err.message}`);
        clearTimeout(timer);
        done();
      });

      req.end();
    });
    log.info(`[CDP CLOSE] Successfully closed browser on port ${port}`);
  } catch (err) {
    log.warn(`[CDP CLOSE] Best-effort close failed for port ${port}: ${err}`);
  }
}

// ==================== path config ====================
const preload = path.join(__dirname, '../preload/index.mjs');
const indexHtml = path.join(RENDERER_DIST, 'index.html');
const logPath = log.transports.file.getFile().path;

// Profile initialization promise
let profileInitPromise: Promise<void>;

// Set remote debugging port
// Storage strategy:
// 1. Main window: partition 'persist:main_window' in app userData → Eigent account (persistent)
// 2. WebView: partition 'persist:user_login' in app userData → will import cookies from tool_controller via session API
// 3. tool_controller: ~/.eigent/browser_profiles/profile_user_login → source of truth for login cookies
// 4. CDP browser: uses separate profile (doesn't share with main app)
profileInitPromise = findAvailablePort(browser_port).then(async (port) => {
  browser_port = port;
  app.commandLine.appendSwitch('remote-debugging-port', port + '');

  // Create isolated profile for CDP browser only
  const browserProfilesBase = path.join(
    os.homedir(),
    '.eigent',
    'browser_profiles'
  );
  const cdpProfile = path.join(browserProfilesBase, `cdp_profile_${port}`);

  try {
    await fsp.mkdir(cdpProfile, { recursive: true });
    log.info(`[CDP BROWSER] Created CDP profile directory at ${cdpProfile}`);
  } catch (error) {
    log.error(`[CDP BROWSER] Failed to create directory: ${error}`);
  }

  // Set user-data-dir for Chrome DevTools Protocol only
  app.commandLine.appendSwitch('user-data-dir', cdpProfile);

  log.info(`[CDP BROWSER] Chrome DevTools Protocol enabled on port ${port}`);
  log.info(`[CDP BROWSER] CDP profile directory: ${cdpProfile}`);
  log.info(`[STORAGE] Main app userData: ${app.getPath('userData')}`);
});

// Memory optimization settings
app.commandLine.appendSwitch('js-flags', '--max-old-space-size=4096');
app.commandLine.appendSwitch('force-gpu-mem-available-mb', '512');
app.commandLine.appendSwitch('max_old_space_size', '4096');
app.commandLine.appendSwitch('enable-features', 'MemoryPressureReduction');
app.commandLine.appendSwitch('renderer-process-limit', '8');

// Disable Fontations (Rust-based font engine) to prevent crashes on macOS
app.commandLine.appendSwitch('disable-features', 'Fontations');

// ==================== Proxy configuration ====================
// Read proxy from global .env file on startup
proxyUrl = readGlobalEnvKey('HTTP_PROXY');
if (proxyUrl) {
  log.info(`[PROXY] Applying proxy configuration: ${maskProxyUrl(proxyUrl)}`);
  app.commandLine.appendSwitch('proxy-server', proxyUrl);
} else {
  log.info('[PROXY] No proxy configured');
}

// ==================== Anti-fingerprint settings ====================
// Disable automation controlled indicator to avoid detection
app.commandLine.appendSwitch('disable-blink-features', 'AutomationControlled');

// Override User Agent to remove Electron/eigent identifiers
// Dynamically generate User Agent based on actual platform and Chrome version
const getPlatformUA = () => {
  // Use actual Chrome version from Electron instead of hardcoded value
  const chromeVersion = process.versions.chrome || '131.0.0.0';
  switch (process.platform) {
    case 'darwin':
      return `Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${chromeVersion} Safari/537.36`;
    case 'win32':
      return `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${chromeVersion} Safari/537.36`;
    case 'linux':
      return `Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${chromeVersion} Safari/537.36`;
    default:
      return `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${chromeVersion} Safari/537.36`;
  }
};
const normalUserAgent = getPlatformUA();
app.userAgentFallback = normalUserAgent;

// ==================== protocol privileges ====================
// Register custom protocol privileges before app ready
protocol.registerSchemesAsPrivileged([
  {
    scheme: 'localfile',
    privileges: {
      standard: true,
      secure: true,
      supportFetchAPI: true,
      corsEnabled: false,
      bypassCSP: false,
    },
  },
]);

// ==================== app config ====================
process.env.APP_ROOT = MAIN_DIST;
process.env.VITE_PUBLIC = VITE_PUBLIC;

// Always follow OS appearance so renderer `prefers-color-scheme` stays accurate.
nativeTheme.themeSource = 'system';

// Set log level
log.transports.console.level = 'info';
log.transports.file.level = 'info';
log.transports.console.format = '[{level}]{text}';
log.transports.file.format = '[{level}]{text}';

// Disable GPU Acceleration for Windows 7
if (os.release().startsWith('6.1')) app.disableHardwareAcceleration();

// Set application name for Windows 10+ notifications
if (process.platform === 'win32') app.setAppUserModelId(app.getName());

if (!app.requestSingleInstanceLock()) {
  app.quit();
  process.exit(0);
}

// ==================== protocol config ====================
const setupProtocolHandlers = () => {
  if (process.env.NODE_ENV === 'development') {
    const isDefault = app.isDefaultProtocolClient('eigent', process.execPath, [
      path.resolve(process.argv[1]),
    ]);
    if (!isDefault) {
      app.setAsDefaultProtocolClient('eigent', process.execPath, [
        path.resolve(process.argv[1]),
      ]);
    }
  } else {
    app.setAsDefaultProtocolClient('eigent');
  }
};

// ==================== protocol url handle ====================
function handleProtocolUrl(url: string) {
  log.info('enter handleProtocolUrl');

  // If window is not ready, queue the URL
  if (!isWindowReady || !win || win.isDestroyed()) {
    log.info('Window not ready, queuing protocol URL');
    protocolUrlQueue.push(url);
    return;
  }

  void processProtocolUrl(url);
}

// Process a single protocol URL
async function processProtocolUrl(url: string) {
  const urlObj = new URL(url);
  const code = urlObj.searchParams.get('code');
  const token = urlObj.searchParams.get('token');
  const share_token = urlObj.searchParams.get('share_token');

  log.info('urlObj', {
    protocol: urlObj.protocol,
    host: urlObj.host,
    pathname: urlObj.pathname,
  });
  log.info('code present', Boolean(code));
  log.info('token present', Boolean(token));
  log.info('share_token present', Boolean(share_token));

  if (win && !win.isDestroyed()) {
    log.info('urlObj.pathname', urlObj.pathname);

    if (urlObj.pathname === '/oauth') {
      log.info('oauth');
      const provider = urlObj.searchParams.get('provider');
      const code = urlObj.searchParams.get('code');
      const codexResult = await completeCodexOAuthCallback(urlObj);
      if (codexResult.handled) {
        win.webContents.send(
          'subscription-auth:codex-status-changed',
          codexResult.error_code
            ? { error_code: codexResult.error_code }
            : undefined
        );
        return;
      }
      log.info('protocol oauth', provider, Boolean(code));
      win.webContents.send('oauth-authorized', { provider, code });
      return;
    }

    if (token) {
      log.info('protocol token received');
      win.webContents.send('auth-token-received', token);
      return;
    }

    if (code) {
      log.info('protocol code received');
      win.webContents.send('auth-code-received', code);
    }

    if (share_token) {
      win.webContents.send('auth-share-token-received', share_token);
    }
  } else {
    log.error('window not available');
  }
}

// Process all queued protocol URLs
function processQueuedProtocolUrls() {
  if (protocolUrlQueue.length > 0) {
    log.info('Processing queued protocol URLs:', protocolUrlQueue.length);

    // Verify window is ready before processing
    if (!win || win.isDestroyed() || !isWindowReady) {
      log.warn(
        'Window not ready for processing queued URLs, keeping URLs in queue'
      );
      return;
    }

    const urls = [...protocolUrlQueue];
    protocolUrlQueue = [];

    urls.forEach((url) => {
      void processProtocolUrl(url);
    });
  }
}

// ==================== auth callback server ====================
// Local HTTP server for receiving auth callbacks from external login (eigent.ai)
// Works in both dev and production mode, avoids eigent:// protocol issues in dev
let authCallbackServer: http.Server | null = null;
let authCallbackPort: number | null = null;

async function startAuthCallbackServer() {
  if (authCallbackServer) return authCallbackPort;

  const port = await findAvailablePort(19836, 19900);

  authCallbackServer = http.createServer((req, res) => {
    const url = new URL(req.url || '', `http://localhost:${port}`);

    if (url.pathname === '/auth/callback') {
      const token = url.searchParams.get('token');
      log.info('Auth callback URL:', req.url);
      log.info('Auth callback token present:', !!token);
      log.info('Auth callback win available:', !!win && !win.isDestroyed());

      res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
      res.end(`
        <!DOCTYPE html>
        <html><head><title>Login Successful</title>
        <style>
          body { font-family: -apple-system, system-ui, sans-serif; display: flex; justify-content: center; align-items: center; min-height: 100vh; margin: 0; background: #f4f4f9; color: #333; }
          .container { padding: 40px; background: white; border-radius: 12px; box-shadow: 0 4px 20px rgba(0,0,0,0.1); text-align: center; }
        </style></head>
        <body><div class="container">
          <h1>Login Successful</h1>
          <p>You can close this tab and return to Eigent.</p>
        </div></body></html>
      `);

      if (token && win && !win.isDestroyed()) {
        log.info('Auth callback received token');
        win.webContents.send('auth-token-received', token);
        win.show();
        win.focus();
      }
    } else {
      res.writeHead(404);
      res.end('Not Found');
    }
  });

  authCallbackServer.listen(port);
  authCallbackPort = port;
  log.info(`Auth callback server started on port ${port}`);
  return port;
}

// ==================== single instance lock ====================
const setupSingleInstanceLock = () => {
  // The lock is already acquired at module level (requestSingleInstanceLock
  // above). Calling it again here would release and re-acquire the lock,
  // creating a window where a second instance could start. We only need
  // to register the event handlers.
  app.on('second-instance', (event, argv) => {
    log.info('second-instance', argv);
    const url = argv.find((arg) => arg.startsWith('eigent://'));
    if (url) handleProtocolUrl(url);
    if (win) win.show();
  });

  app.on('open-url', (event, url) => {
    log.info('open-url');
    event.preventDefault();
    handleProtocolUrl(url);
  });
};

// ==================== initialize config ====================
const initializeApp = () => {
  setupProtocolHandlers();
  setupSingleInstanceLock();
};

/**
 * Registers all IPC handlers once when the app starts
 * This prevents "Attempted to register a second handler" errors
 * when windows are reopened
 */
// Get backup log path
const getBackupLogPath = () => {
  const userDataPath = app.getPath('userData');
  return path.join(userDataPath, 'logs', 'main.log');
};
// Constants define
const BROWSER_PATHS = {
  win32: {
    chrome: 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
    edge: 'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe',
    firefox: 'C:\\Program Files\\Mozilla Firefox\\firefox.exe',
    qq: 'C:\\Program Files\\Tencent\\QQBrowser\\QQBrowser.exe',
    '360': path.join(
      homedir(),
      'AppData\\Local\\360Chrome\\Chrome\\Application\\360chrome.exe'
    ),
    arc: path.join(homedir(), 'AppData\\Local\\Arc\\User Data\\Arc.exe'),
    dia: path.join(homedir(), 'AppData\\Local\\Dia\\Application\\dia.exe'),
    fellou: path.join(
      homedir(),
      'AppData\\Local\\Fellou\\Application\\fellou.exe'
    ),
  },
  darwin: {
    chrome: '/Applications/Google Chrome.app',
    edge: '/Applications/Microsoft Edge.app',
    firefox: '/Applications/Firefox.app',
    safari: '/Applications/Safari.app',
    arc: '/Applications/Arc.app',
    dia: '/Applications/Dia.app',
    fellou: '/Applications/Fellou.app',
  },
} as const;

// Tool function
const getSystemLanguage = async () => {
  const locale = app.getLocale();
  return locale === 'zh-CN' ? 'zh-cn' : 'en';
};

const checkManagerInstance = (manager: any, name: string) => {
  if (!manager) {
    throw new Error(`${name} not initialized`);
  }
  return manager;
};

function registerIpcHandlers() {
  registerCodexSubscriptionAuthIpcHandlers(ipcMain);
  registerTerminalIpcHandlers();
  registerReviewChangesIpcHandlers();
  registerWorkspaceSecretIpcHandlers(
    ipcMain,
    getDefaultWorkspaceSecretVault(),
    assertMainRendererSender
  );
  // Event streams to the Brain this process runs bypass the renderer's
  // per-host connection limit. Only app window main frames may use it.
  const streamRelayDisabled = isBrainStreamRelayDisabled();
  if (streamRelayDisabled) {
    log.info('[StreamRelay] Disabled by EIGENT_DISABLE_STREAM_RELAY');
  }
  brainStreamRelay = registerBrainStreamRelay({
    ipcMain,
    enabled: !streamRelayDisabled,
    getManagedBackendPort: () =>
      isPythonProcessRunning() ? backendPort : null,
    isTrustedSender: (event) =>
      isAppRendererFrame(event as Electron.IpcMainInvokeEvent),
    log,
  });

  // ==================== auth callback ====================
  ipcMain.handle('get-auth-callback-url', async () => {
    const port = await startAuthCallbackServer();
    return `http://localhost:${port}/auth/callback`;
  });

  // ==================== basic info handler ====================
  ipcMain.handle('get-browser-port', () => {
    log.info('Getting browser port');
    return browser_port;
  });

  ipcMain.handle('get-embedded-browser-runtime', () => {
    const targets = webViewManager?.listAvailableBrowserToolkitTargets() ?? [];
    const targetAvailable = targets.length > 0;
    log.info(
      `[PROJECT BROWSER] Embedded runtime requested: port=${browser_port}, available_targets=${targets.length}`
    );
    return {
      port: browser_port,
      targetAvailable,
      targets,
    };
  });

  // Set browser port
  ipcMain.handle(
    'set-browser-port',
    (event, port: number, isExternal: boolean = false) => {
      log.info(`Setting browser port to ${port}, external: ${isExternal}`);
      browser_port = port;
      use_external_cdp = isExternal;
      return { success: true, port: browser_port, use_external_cdp };
    }
  );

  // Get external CDP flag
  ipcMain.handle('get-use-external-cdp', () => {
    log.info(`Getting use_external_cdp: ${use_external_cdp}`);
    return use_external_cdp;
  });

  // ==================== CDP Browser Pool Management ====================

  // Get all browsers in the pool
  ipcMain.handle('get-cdp-browsers', () => {
    log.debug(`[CDP POOL] GET pool (size=${cdp_browser_pool.length})`);
    return cdp_browser_pool;
  });

  // Add browser to pool
  ipcMain.handle(
    'add-cdp-browser',
    (event, port: number, isExternal: boolean, name?: string) => {
      const existing = cdp_browser_pool.find((b) => b.port === port);
      if (existing) {
        log.warn(
          `[CDP POOL] ADD rejected: port ${port} already exists (id=${existing.id})`
        );
        return {
          success: false,
          error: 'Browser with this port already exists',
        };
      }

      const newBrowser: CdpBrowser = {
        id: `cdp-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
        port,
        isExternal,
        name,
        addedAt: Date.now(),
      };

      cdp_browser_pool.push(newBrowser);
      saveCdpPool();
      notifyCdpPoolChanged();
      log.info(
        `[CDP POOL] ADD: port=${port}, isExternal=${isExternal}, id=${newBrowser.id}, pool_size=${cdp_browser_pool.length}`
      );

      return { success: true, browser: newBrowser };
    }
  );

  // Remove browser from pool (also closes the browser via CDP)
  ipcMain.handle(
    'remove-cdp-browser',
    async (event, browserId: string, closeBrowser: boolean = true) => {
      const index = cdp_browser_pool.findIndex((b) => b.id === browserId);
      if (index === -1) {
        log.warn(`[CDP POOL] REMOVE: browser not found: ${browserId}`);
        return { success: false, error: 'Browser not found' };
      }

      const removed = cdp_browser_pool.splice(index, 1)[0];

      // Close the browser via CDP (best-effort)
      if (closeBrowser) {
        await closeBrowserViaCdp(removed.port);
      }

      saveCdpPool();
      notifyCdpPoolChanged();
      log.info(
        `[CDP POOL] REMOVE: port=${removed.port}, id=${removed.id}, closed=${closeBrowser}, pool_size=${cdp_browser_pool.length}`
      );
      return { success: true, browser: removed };
    }
  );

  // Launch CDP browser with automatic port assignment
  ipcMain.handle('launch-cdp-browser', async () => {
    try {
      // 1. Always increment port from the last assigned port
      // Port 9223 is reserved for the login browser
      let port: number | null = null;
      for (let p = cdpLastAssignedPort + 1; p < 9300; p++) {
        if (!(await isCdpPortAlive(p))) {
          port = p;
          break;
        }
      }
      // Wrap around if we hit the ceiling
      if (port === null) {
        for (let p = 9224; p <= cdpLastAssignedPort && p < 9300; p++) {
          if (
            !cdp_browser_pool.some((b) => b.port === p) &&
            !(await isCdpPortAlive(p))
          ) {
            port = p;
            break;
          }
        }
      }
      if (port === null) {
        return { success: false, error: 'No available port in 9224-9299' };
      }

      // 2. Find Playwright Chromium executable
      const platform = process.platform;
      let cacheDir: string;
      if (platform === 'darwin')
        cacheDir = path.join(homedir(), 'Library/Caches/ms-playwright');
      else if (platform === 'linux')
        cacheDir = path.join(homedir(), '.cache/ms-playwright');
      else if (platform === 'win32')
        cacheDir = path.join(homedir(), 'AppData/Local/ms-playwright');
      else
        return { success: false, error: `Unsupported platform: ${platform}` };

      if (!existsSync(cacheDir)) {
        return {
          success: false,
          error:
            'Playwright Chromium not found. Please run: npx playwright install chromium',
        };
      }

      const chromiumDirs = fs
        .readdirSync(cacheDir)
        .filter((d) => d.startsWith('chromium-'))
        .sort()
        .reverse();
      if (chromiumDirs.length === 0) {
        return {
          success: false,
          error:
            'No Playwright Chromium found. Run: npx playwright install chromium',
        };
      }

      const platformPaths: Record<string, (base: string) => string[]> = {
        darwin: (base) => [
          path.join(
            base,
            'chrome-mac-arm64/Chromium.app/Contents/MacOS/Chromium'
          ),
          path.join(
            base,
            'chrome-mac-arm64/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing'
          ),
          path.join(base, 'chrome-mac/Chromium.app/Contents/MacOS/Chromium'),
          path.join(
            base,
            'chrome-mac/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing'
          ),
        ],
        linux: (base) => [path.join(base, 'chrome-linux/chrome')],
        win32: (base) => [
          path.join(base, 'chrome-win64/chrome.exe'),
          path.join(base, 'chrome-win/chrome.exe'),
        ],
      };

      let chromeExe: string | null = null;
      for (const dir of chromiumDirs) {
        const base = path.join(cacheDir, dir);
        const candidates = platformPaths[platform](base);
        const found = candidates.find((p) => existsSync(p));
        if (found) {
          chromeExe = found;
          break;
        }
      }
      if (!chromeExe) {
        return { success: false, error: 'Chromium executable not found' };
      }

      // 3. Launch browser
      const userDataDir = path.join(
        app.getPath('userData'),
        `cdp_browser_profile_${port}`
      );
      if (!existsSync(userDataDir)) {
        await fsp.mkdir(userDataDir, { recursive: true });
      }

      const proc = spawn(
        chromeExe,
        [
          `--remote-debugging-port=${port}`,
          `--user-data-dir=${userDataDir}`,
          '--no-first-run',
          '--no-default-browser-check',
          '--disable-blink-features=AutomationControlled',
          'about:blank',
        ],
        { detached: false, stdio: 'ignore' }
      );

      proc.on('error', (err) =>
        log.error(`[CDP LAUNCH] Process error port=${port}: ${err}`)
      );

      // 4. Poll for readiness (max 5s)
      let data: any = null;
      const start = Date.now();
      while (Date.now() - start < 5000) {
        try {
          const resp = await axios.get(
            `http://localhost:${port}/json/version`,
            { timeout: 1000 }
          );
          if (resp.status === 200) {
            data = resp.data;
            break;
          }
        } catch {}
        await new Promise((r) => setTimeout(r, 300));
      }

      if (!data) {
        proc.kill();
        return {
          success: false,
          error: `Browser not responding on port ${port} after 5s`,
        };
      }

      // 5. Add to pool automatically
      const newBrowser: CdpBrowser = {
        id: `cdp-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
        port,
        isExternal: false,
        name: `Launched Browser (${port})`,
        addedAt: Date.now(),
      };
      cdp_browser_pool.push(newBrowser);
      cdpLastAssignedPort = port;
      saveCdpPool();
      notifyCdpPoolChanged();

      log.info(
        `[CDP LAUNCH] Success: port=${port}, id=${newBrowser.id}, pool_size=${cdp_browser_pool.length}`
      );
      return { success: true, port, data };
    } catch (err: any) {
      log.error(`[CDP LAUNCH] Failed: ${err}`);
      return { success: false, error: err.message };
    }
  });

  ipcMain.handle('get-app-version', () => app.getVersion());
  ipcMain.handle('get-backend-port', () => backendPort);
  // Session windows call the same local Brain as the main window, so every
  // app window (and only its main frame) may read these.
  ipcMain.handle('get-local-control-capability', (event) => {
    if (!isAppRendererFrame(event)) {
      throw new Error(
        'Local control capability is restricted to the app renderer'
      );
    }
    return localControlCapability;
  });
  ipcMain.handle('get-desktop-instance-id', (event, legacyRendererId) => {
    if (!isAppRendererFrame(event)) {
      throw new Error('Desktop identity is restricted to the app renderer');
    }
    return resolveDesktopInstanceId(
      typeof legacyRendererId === 'string' ? legacyRendererId : null
    );
  });
  ipcMain.handle(
    'set-local-file-preview-roots',
    async (event, roots: unknown) => {
      assertAppRendererSender(event);
      // TODO(security): replace this renderer-declared, process-global root set
      // with main-authoritative, per-surface capabilities. A compromised main
      // renderer can currently register a broad existing directory (including
      // a filesystem root), and the replace semantics let concurrent preview
      // surfaces clear one another's grants. This check is bug containment,
      // not a renderer-compromise boundary.
      if (!Array.isArray(roots) || roots.length > 4) {
        throw new Error('Invalid local file preview roots');
      }

      const nextRoots = new Set<string>();
      for (const root of roots) {
        if (typeof root !== 'string' || !path.isAbsolute(root)) {
          throw new Error('Local file preview roots must be absolute paths');
        }
        const realRoot = await fsp.realpath(root);
        const stats = await fsp.stat(realRoot);
        if (!stats.isDirectory()) {
          throw new Error('Local file preview roots must be directories');
        }
        nextRoots.add(realRoot);
      }

      // Each window replaces only its own grants, so a Session window cannot
      // clear the main window's Space root or the other way round.
      const sender = event.sender;
      if (!localFileRootsByRenderer.has(sender.id))
        sender.once('destroyed', () =>
          localFileRootsByRenderer.delete(sender.id)
        );
      localFileRootsByRenderer.set(sender.id, nextRoots);
      return { success: true, roots: nextRoots.size };
    }
  );

  // ==================== restart app handler ====================
  ipcMain.handle('restart-app', async () => {
    log.info('[RESTART] Restarting app to apply user profile changes');

    // Clean up Python process first
    await cleanupPythonProcess();

    // Schedule relaunch after a short delay
    setTimeout(() => {
      closeCoordinator.markAppQuitInProgress();
      app.relaunch();
      app.quit();
    }, 100);
  });

  ipcMain.handle('restart-backend', async () => {
    try {
      const result = await restartBackendService();
      if (result.success) {
        log.info('Backend restart completed successfully');
      }
      return result;
    } catch (error) {
      log.error('Failed to restart backend:', error);
      return { success: false, error: formatErrorMessage(error) };
    }
  });
  ipcMain.handle('get-system-language', getSystemLanguage);
  ipcMain.handle(
    'is-fullscreen',
    (event) => appWindowForSender(event.sender)?.isFullScreen() || false
  );
  ipcMain.handle('get-home-dir', () => {
    const platform = process.platform;
    return platform === 'win32' ? process.env.USERPROFILE : process.env.HOME;
  });

  // ==================== command execution handler ====================
  ipcMain.handle('get-email-folder-path', async (event, email: string) => {
    return getEmailFolderPath(email);
  });
  ipcMain.handle(
    'execute-command',
    async (event, command: string, email: string) => {
      log.info('execute-command', command);
      const { MCP_REMOTE_CONFIG_DIR } = getEmailFolderPath(email);

      try {
        const { spawn } = await import('child_process');

        const commandWithHost = command;

        log.info(' start execute command:', commandWithHost);

        // Parse command and arguments
        const [cmd, ...args] = commandWithHost.split(' ');
        log.info('start execute command:', commandWithHost.split(' '));
        console.log(cmd, args);
        return new Promise((resolve) => {
          const child = spawn(cmd, args, {
            cwd: process.cwd(),
            env: { ...process.env, MCP_REMOTE_CONFIG_DIR },
            stdio: ['pipe', 'pipe', 'pipe'],
          });

          let stdout = '';
          let stderr = '';

          // Realtime listen standard output
          child.stdout.on('data', (data) => {
            const output = data.toString();
            stdout += output;
            log.info('Real-time output:', output.trim());
          });

          // Realtime listen error output
          child.stderr.on('data', (data) => {
            const output = data.toString();
            stderr += output;
            if (output.includes('OAuth callback server running at')) {
              const url = output
                .split('OAuth callback server running at')[1]
                .trim();
              log.info('detect OAuth callback URL:', url);

              // Notify frontend to callback URL
              if (win && !win.isDestroyed()) {
                const match = url.match(/^https?:\/\/[^:\n]+:\d+/);
                const cleanedUrl = match ? match[0] : null;
                log.info('cleanedUrl', cleanedUrl);
                win.webContents.send('oauth-callback-url', {
                  url: cleanedUrl,
                  provider: 'notion', // TODO: can be set dynamically according to actual situation
                });
              }
            }
            if (output.includes('Press Ctrl+C to exit')) {
              child.kill();
            }
            log.info(' real-time error output:', output.trim());
          });

          // Listen process exit
          child.on('close', (code) => {
            log.info(` command execute complete, exit code: ${code}`);
            resolve({ success: code === null, stdout, stderr });
          });

          // Listen process error
          child.on('error', (error) => {
            log.error(' command execute error:', error);
            resolve({ success: false, error: error.message });
          });
        });
      } catch (error: any) {
        log.error(' command execute failed:', error);
        return { success: false, error: error.message };
      }
    }
  );

  ipcMain.handle('read-file-dataurl', async (event, filePath) => {
    try {
      const authorizedPath = await requireAuthorizedPreviewFile(
        event,
        filePath
      );
      const stats = await fsp.stat(authorizedPath);
      if (stats.size > FILE_PREVIEW_LIMITS.imageBytes) {
        throw new Error(
          `FILE_PREVIEW_TOO_LARGE:${stats.size}:${FILE_PREVIEW_LIMITS.imageBytes}`
        );
      }
      const file = fs.readFileSync(authorizedPath);
      const mimeType =
        mime.getType(path.extname(authorizedPath)) ||
        'application/octet-stream';
      return `data:${mimeType};base64,${file.toString('base64')}`;
    } catch (error: any) {
      log.error('Failed to read file as data URL:', filePath, error);
      throw new Error(`Failed to read file: ${error.message}`);
    }
  });

  // ==================== log export handler ====================
  ipcMain.handle('export-log', async () => {
    try {
      let targetLogPath = logPath;
      if (!fs.existsSync(targetLogPath)) {
        const backupPath = getBackupLogPath();
        if (fs.existsSync(backupPath)) {
          targetLogPath = backupPath;
        } else {
          return { success: false, error: 'no log file' };
        }
      }

      await fsp.access(targetLogPath, fs.constants.R_OK);
      const stats = await fsp.stat(targetLogPath);
      if (stats.size === 0) {
        return { success: true, data: 'log file is empty' };
      }

      const logContent = await fsp.readFile(targetLogPath, 'utf-8');

      // Get app version and system version
      const appVersion = app.getVersion();
      const platform = process.platform;
      const arch = process.arch;
      const systemVersion = `${platform}-${arch}`;
      const defaultFileName = `eigent-${appVersion}-${systemVersion}-${Date.now()}.log`;

      // Show save dialog
      const { canceled, filePath } = await dialog.showSaveDialog({
        title: 'save log file',
        defaultPath: defaultFileName,
        filters: [{ name: 'log file', extensions: ['log', 'txt'] }],
      });

      if (canceled || !filePath) {
        return { success: false, error: '' };
      }

      await fsp.writeFile(filePath, logContent, 'utf-8');
      return { success: true, savedPath: filePath };
    } catch (error: any) {
      return { success: false, error: error.message };
    }
  });

  // Camel (backend) logs live per task at
  // ~/.eigent/<identity>/[project_<id>/]task_<taskId>/camel_logs.
  // Targets the task the user last ran when provided; otherwise exports all.
  ipcMain.handle(
    'export-camel-log',
    async (
      _event,
      email: string,
      taskId?: string,
      projectId?: string,
      userId?: string | number | null
    ) => {
      try {
        if (typeof email !== 'string' || !email) {
          return { success: false, error: 'Missing email' };
        }

        const manager = checkManagerInstance(fileReader, 'FileReader');
        const camelLogEntries = manager.getCamelLogEntries(
          email,
          taskId,
          projectId,
          userId
        );
        if (camelLogEntries.length === 0) {
          return { success: false, error: 'no log file' };
        }

        const appVersion = app.getVersion();
        const defaultFileName = `eigent-camel-logs-${appVersion}-${Date.now()}.zip`;
        const { canceled, filePath } = await dialog.showSaveDialog({
          title: 'Save Camel logs',
          defaultPath: defaultFileName,
          filters: [{ name: 'ZIP archive', extensions: ['zip'] }],
        });

        if (canceled || !filePath) {
          return { success: false, error: '' };
        }

        await zipDirectories(filePath, camelLogEntries);
        return { success: true, savedPath: filePath };
      } catch (error: any) {
        log.error('export-camel-log failed:', error);
        return { success: false, error: error.message };
      }
    }
  );

  ipcMain.handle('get-diagnostics-info', async () => {
    return {
      version: app.getVersion(),
      platform: process.platform,
      arch: process.arch,
    };
  });

  ipcMain.handle(
    'export-diagnostics-zip',
    async (
      _event,
      payload: { description: string; steps?: string } | undefined
    ) => {
      try {
        const description =
          typeof payload?.description === 'string'
            ? payload.description.trim()
            : '';
        if (!description) {
          return { success: false, error: 'Description is required' };
        }
        const steps =
          typeof payload?.steps === 'string' ? payload.steps.trim() : '';

        const logFiles: { src: string; destName: string }[] = [];
        if (fs.existsSync(logPath)) {
          logFiles.push({ src: logPath, destName: 'electron-main.log' });
        }
        const backupResolved = getBackupLogPath();
        if (
          fs.existsSync(backupResolved) &&
          path.resolve(backupResolved) !== path.resolve(logPath)
        ) {
          logFiles.push({
            src: backupResolved,
            destName: 'electron-userdata-logs.log',
          });
        }
        if (logFiles.length === 0) {
          return { success: false, error: 'no log file' };
        }

        const appVersion = app.getVersion();
        const platform = process.platform;
        const arch = process.arch;
        const bugReportText = [
          'Eigent bug report',
          '=================',
          '',
          `App version: ${appVersion}`,
          `OS: ${platform} (${arch})`,
          '',
          'Description',
          '-----------',
          description,
          '',
          ...(steps
            ? ['Steps to reproduce', '-------------------', steps, '']
            : []),
        ].join('\n');

        const defaultFileName = `eigent-diagnostics-${appVersion}-${Date.now()}.zip`;
        const { canceled, filePath } = await dialog.showSaveDialog({
          title: 'Save diagnostics',
          defaultPath: defaultFileName,
          filters: [{ name: 'ZIP archive', extensions: ['zip'] }],
        });

        if (canceled || !filePath) {
          return { success: false, error: '' };
        }

        await createDiagnosticsZip(filePath, bugReportText, logFiles);
        return { success: true, savedPath: filePath };
      } catch (error: any) {
        log.error('export-diagnostics-zip failed:', error);
        return { success: false, error: error.message };
      }
    }
  );

  ipcMain.handle('open-mailto', async (_event, url: string) => {
    try {
      if (typeof url !== 'string' || !url.startsWith('mailto:')) {
        return { success: false, error: 'Invalid mailto URL' };
      }
      await shell.openExternal(url);
      return { success: true };
    } catch (error: any) {
      return { success: false, error: error.message };
    }
  });

  ipcMain.handle('open-external', async (_event, url: string) => {
    try {
      if (!isHttpOrHttpsUrl(url)) {
        return { success: false, error: 'Invalid external URL' };
      }
      await shell.openExternal(url);
      return { success: true };
    } catch (error: any) {
      return { success: false, error: error.message };
    }
  });

  ipcMain.handle(
    'upload-log',
    async (
      event,
      email: string,
      taskId: string,
      baseUrl: string,
      token: string
    ) => {
      let zipPath: string | null = null;

      try {
        // Validate required parameters
        if (!email || !taskId || !baseUrl || !token) {
          return { success: false, error: 'Missing required parameters' };
        }

        // Sanitize taskId to prevent path traversal attacks
        const sanitizedTaskId = taskId.replace(/[^a-zA-Z0-9_-]/g, '');
        if (!sanitizedTaskId) {
          return { success: false, error: 'Invalid task ID' };
        }

        const { MCP_REMOTE_CONFIG_DIR } = getEmailFolderPath(email);
        const logFolderName = `task_${sanitizedTaskId}`;
        const logFolderPath = path.join(MCP_REMOTE_CONFIG_DIR, logFolderName);

        // Check if log folder exists
        if (!fs.existsSync(logFolderPath)) {
          return { success: false, error: 'Log folder not found' };
        }

        zipPath = path.join(MCP_REMOTE_CONFIG_DIR, `${logFolderName}.zip`);
        await zipFolder(logFolderPath, zipPath);

        // Create form data with file stream
        const formData = new FormData();
        const fileStream = fs.createReadStream(zipPath);
        formData.append('file', fileStream);
        formData.append('task_id', sanitizedTaskId);

        // Upload with timeout
        const response = await axios.post(
          baseUrl + '/api/chat/logs',
          formData,
          {
            headers: {
              'Content-Type': 'multipart/form-data',
              Authorization: `Bearer ${token}`,
            },
            timeout: 60000, // 60 second timeout
            maxContentLength: Infinity,
            maxBodyLength: Infinity,
          }
        );

        fileStream.destroy();

        if (response.status === 200) {
          return { success: true, data: response.data };
        } else {
          return { success: false, error: response.data };
        }
      } catch (error: any) {
        log.error('Failed to upload log:', error);
        return { success: false, error: error.message || 'Upload failed' };
      } finally {
        // Clean up zip file
        if (zipPath && fs.existsSync(zipPath)) {
          try {
            fs.unlinkSync(zipPath);
          } catch (cleanupError) {
            log.error('Failed to clean up zip file:', cleanupError);
          }
        }
      }
    }
  );

  // ==================== MCP manage handler ====================
  ipcMain.handle('mcp-install', async (event, name, mcp) => {
    // Convert args from JSON string to array if needed
    if (mcp.args && typeof mcp.args === 'string') {
      try {
        mcp.args = JSON.parse(mcp.args);
      } catch (_error) {
        // If parsing fails, split by comma as fallback
        mcp.args = mcp.args
          .split(',')
          .map((arg: string) => arg.trim())
          .filter((arg: string) => arg !== '');
      }
    }
    addMcp(name, mcp);
    return { success: true };
  });

  ipcMain.handle('mcp-remove', async (event, name) => {
    removeMcp(name);
    return { success: true };
  });

  ipcMain.handle('mcp-update', async (event, name, mcp) => {
    // Convert args from JSON string to array if needed
    if (mcp.args && typeof mcp.args === 'string') {
      try {
        mcp.args = JSON.parse(mcp.args);
      } catch (_error) {
        // If parsing fails, split by comma as fallback
        mcp.args = mcp.args
          .split(',')
          .map((arg: string) => arg.trim())
          .filter((arg: string) => arg !== '');
      }
    }
    updateMcp(name, mcp);
    return { success: true };
  });

  ipcMain.handle('mcp-list', async () => {
    return readMcpConfig();
  });

  // ==================== browser related handler ====================
  // TODO: next version implement
  ipcMain.handle('check-install-browser', async () => {
    try {
      const platform = process.platform;
      const results: Record<string, boolean> = {};
      const paths = BROWSER_PATHS[platform as keyof typeof BROWSER_PATHS];

      if (!paths) {
        log.warn(`not support current platform: ${platform}`);
        return {};
      }

      for (const [browser, execPath] of Object.entries(paths)) {
        results[browser] = existsSync(execPath);
      }

      return results;
    } catch (error: any) {
      log.error('Failed to check browser installation:', error);
      return {};
    }
  });

  ipcMain.handle('start-browser-import', async (event, args) => {
    const isWin = process.platform === 'win32';
    const localAppData = process.env.LOCALAPPDATA || '';
    const appData = process.env.APPDATA || '';
    const home = os.homedir();

    const candidates: Record<string, string> = {
      chrome: isWin
        ? `${localAppData}\\Google\\Chrome\\User Data\\Default`
        : `${home}/Library/Application Support/Google/Chrome/Default`,
      edge: isWin
        ? `${localAppData}\\Microsoft\\Edge\\User Data\\Default`
        : `${home}/Library/Application Support/Microsoft Edge/Default`,
      firefox: isWin
        ? `${appData}\\Mozilla\\Firefox\\Profiles`
        : `${home}/Library/Application Support/Firefox/Profiles`,
      qq: `${localAppData}\\Tencent\\QQBrowser\\User Data\\Default`,
      '360': `${localAppData}\\360Chrome\\Chrome\\User Data\\Default`,
      arc: isWin
        ? `${localAppData}\\Arc\\User Data\\Default`
        : `${home}/Library/Application Support/Arc/Default`,
      dia: `${localAppData}\\Dia\\User Data\\Default`,
      fellou: `${localAppData}\\Fellou\\User Data\\Default`,
      safari: `${home}/Library/Safari`,
    };

    // Filter unchecked browser
    Object.keys(candidates).forEach((key) => {
      const browser = args.find((item: any) => item.browserId === key);
      if (!browser || !browser.checked) {
        delete candidates[key];
      }
    });

    const result: Record<string, string | null> = {};
    for (const [name, p] of Object.entries(candidates)) {
      result[name] = fs.existsSync(p) ? p : null;
    }

    const electronUserDataPath = app.getPath('userData');

    for (const [browserName, browserPath] of Object.entries(result)) {
      if (!browserPath) continue;
      await copyBrowserData(browserName, browserPath, electronUserDataPath);
    }

    return { success: true };
  });

  // ==================== window control handler ====================
  ipcMain.on('window-close', (event) => {
    if (!isMainRendererSender(event.sender.id, win?.webContents.id)) return;
    return closeCoordinator.request('quit-app');
  });
  ipcMain.on(WINDOW_CLOSE_RESPONSE_CHANNEL, (event, response: unknown) => {
    if (!isMainRendererSender(event.sender.id, win?.webContents.id)) return;
    if (!isWindowCloseResponse(response)) return;
    closeCoordinator.respond(response);
  });
  ipcMain.on(NATIVE_MENU_LOCALE_CHANNEL, (event, locale: unknown) => {
    if (!isMainRendererSender(event.sender.id, win?.webContents.id)) return;
    applyNativeMenuLocaleChange(nativeMenuLocale, locale, (nextLocale) => {
      nativeMenuLocale = nextLocale;
      installNativeApplicationMenu();
    });
  });
  ipcMain.on(APP_SHELL_READY_CHANNEL, (event, message: unknown) => {
    if (!isMainRendererSender(event.sender.id, win?.webContents.id)) return;
    if (
      !appShellReadinessGate.canAcceptReady() ||
      !isAppShellLifecycleMessage(message)
    ) {
      return;
    }
    rendererAppCommands.markReady(message.epoch);
    closeCoordinator.markRendererReady();
    log.info(`[APP SHELL] Renderer ready epoch=${message.epoch}`);
  });
  ipcMain.on(APP_SHELL_NOT_READY_CHANNEL, (event, message: unknown) => {
    if (!isMainRendererSender(event.sender.id, win?.webContents.id)) return;
    if (!isAppShellLifecycleMessage(message)) return;
    if (rendererAppCommands.markNotReady('listener-unmounted', message.epoch)) {
      closeCoordinator.markRendererUnavailable('listener-unmounted');
      log.info(`[APP SHELL] Renderer listeners removed epoch=${message.epoch}`);
    }
  });
  ipcMain.on(APP_COMMAND_HANDLED_CHANNEL, (event, receipt: unknown) => {
    if (!isMainRendererSender(event.sender.id, win?.webContents.id)) return;
    if (!isAppCommandHandled(receipt)) return;
    if (!rendererAppCommands.handleReceipt(receipt)) {
      log.warn(
        `[APP COMMAND] Ignored stale handled receipt request=${receipt.requestId} epoch=${receipt.epoch}`
      );
    }
  });
  ipcMain.on('window-minimize', (event) =>
    appWindowForSender(event.sender)?.minimize()
  );
  ipcMain.on('window-toggle-maximize', (event) => {
    const target = appWindowForSender(event.sender);
    if (target?.isMaximized()) {
      target.unmaximize();
    } else {
      target?.maximize();
    }
  });

  // ==================== file operation handler ====================
  ipcMain.handle('select-file', async (event, options = {}) => {
    assertAppRendererSender(event);
    // Attach the picker to the window that asked, main or Session window.
    const result = await dialog.showOpenDialog(
      appWindowForSender(event.sender) ?? win!,
      {
        properties: ['openFile', 'multiSelections'],
        ...options,
      }
    );

    if (!result.canceled && result.filePaths.length > 0) {
      const files = result.filePaths.map((filePath) => ({
        filePath,
        fileName: filePath.split(/[/\\]/).pop() || '',
      }));

      // Picking a file here is the user's own grant to reveal it later, even
      // though attachments routinely live outside every Space root.
      await Promise.all(
        result.filePaths.map(async (filePath) => {
          const realPath = await fsp.realpath(filePath).catch(() => null);
          if (realPath) rememberUserSelectedLocalPath(realPath);
        })
      );

      return {
        success: true,
        files,
        fileCount: files.length,
      };
    }

    return {
      success: false,
      canceled: result.canceled,
    };
  });

  ipcMain.handle('select-agent-plugin-source', async (event) => {
    // Agent plugin import lives in Settings, which only the main window has.
    assertMainRendererSender(event);

    const sourceChoice = await dialog.showMessageBox(win!, {
      type: 'question',
      title: 'Import Agent Plugin',
      message: 'Choose the Agent Plugin source',
      detail:
        'A plugin directory is the standard package format. Archives are supported only as an Eigent import transport.',
      buttons: ['Plugin directory', 'Archive', 'Cancel'],
      defaultId: 0,
      cancelId: 2,
      noLink: true,
    });
    if (sourceChoice.response === 2) {
      return { canceled: true };
    }

    const sourceKind = sourceChoice.response === 0 ? 'directory' : 'archive';
    const result = await dialog.showOpenDialog(win!, {
      title:
        sourceKind === 'directory'
          ? 'Select Agent Plugin directory'
          : 'Select Agent Plugin archive',
      properties: sourceKind === 'directory' ? ['openDirectory'] : ['openFile'],
      filters:
        sourceKind === 'archive'
          ? [
              {
                name: 'Agent Plugin archives',
                extensions: ['zip'],
              },
            ]
          : undefined,
    });
    if (result.canceled || result.filePaths.length !== 1) {
      return { canceled: true };
    }

    const selectedPath = await fsp.realpath(result.filePaths[0]);
    const selectedStat = await fsp.stat(selectedPath);
    if (
      (sourceKind === 'directory' && !selectedStat.isDirectory()) ||
      (sourceKind === 'archive' &&
        (!selectedStat.isFile() ||
          path.extname(selectedPath).toLowerCase() !== '.zip'))
    ) {
      throw new Error('Selected Agent Plugin source has an invalid type');
    }

    return {
      canceled: false,
      source_path: selectedPath,
      display_name: path.basename(selectedPath),
      source_kind: sourceKind,
    };
  });

  // Handle drag-and-drop files - convert File objects to file paths
  ipcMain.handle(
    'process-dropped-files',
    async (event, fileData: Array<{ name: string; path?: string }>) => {
      assertAppRendererSender(event);
      try {
        // In Electron with contextIsolation, we need to get file paths differently
        // The renderer will send us file metadata, and we'll use webUtils if needed
        const files = fileData
          .filter((f) => f.path) // Only process files with valid paths
          .map((f) => {
            const filePath = fs.realpathSync(f.path!);
            // Drag-and-drop is an explicit user gesture equivalent to picking
            // the exact path in the native file dialog.
            rememberUserSelectedLocalPath(filePath);
            return { filePath, fileName: f.name };
          });

        if (files.length === 0) {
          return {
            success: false,
            error: 'No valid file paths found',
          };
        }

        return {
          success: true,
          files,
        };
      } catch (error: any) {
        log.error('Failed to process dropped files:', error);
        return {
          success: false,
          error: error.message,
        };
      }
    }
  );

  // Persist a pasted file (e.g. a clipboard image) so it can join the
  // path-based attachment flow; pasted File objects carry no filesystem path.
  ipcMain.handle(
    'save-pasted-file',
    async (event, fileName: string, data: ArrayBuffer) => {
      assertAppRendererSender(event);
      try {
        const pastedDir = path.join(app.getPath('temp'), 'eigent-pasted');
        await fsp.mkdir(pastedDir, { recursive: true });
        const stamp = new Date()
          .toISOString()
          .replace(/[-:]/g, '')
          .replace(/\..+/, '')
          .replace('T', '-');
        const safeName = (fileName || 'pasted-file').replace(/[^\w.-]+/g, '_');
        const unique = crypto.randomUUID();
        const filePath = path.join(pastedDir, `${stamp}-${unique}-${safeName}`);
        await fsp.writeFile(filePath, Buffer.from(new Uint8Array(data)));
        const realPath = await fsp.realpath(filePath);
        // The paste gesture created this exact attachment on the user's behalf.
        rememberUserSelectedLocalPath(realPath);
        return { success: true, filePath: realPath, fileName: safeName };
      } catch (error: any) {
        log.error('Failed to save pasted file:', error);
        return { success: false, error: error.message };
      }
    }
  );

  ipcMain.handle('reveal-in-folder', async (event, targetPath: string) => {
    assertAppRendererSender(event);
    try {
      // Same containment rules as 'reveal-local-path'; this channel additionally
      // serves chat attachments, which the user picked outside the workspace.
      return await revealUserVisibleLocalNode(
        targetPath,
        activeLocalFileRoots(),
        userSelectedLocalPaths,
        shell
      );
    } catch (error) {
      log.error('Reveal in folder failed', error);
      return {
        success: false,
        error: error instanceof Error ? error.message : 'Failed to reveal path',
      };
    }
  });

  ipcMain.handle('reveal-local-path', async (event, targetPath: string) => {
    assertAppRendererSender(event);
    return revealWorkspaceLocalNode(targetPath, activeLocalFileRoots(), shell);
  });

  ipcMain.handle('open-local-file', async (event, targetPath: string) => {
    assertAppRendererSender(event);
    return openWorkspaceLocalFile(targetPath, activeLocalFileRoots(), shell);
  });

  // Skills: all operations via Brain REST API (backend). No IPC.

  // ==================== read file handler ====================
  ipcMain.handle('read-file', async (_event, filePath: string) => {
    try {
      log.debug('Reading file:', filePath);

      // Check if file exists
      if (!fs.existsSync(filePath)) {
        log.error('File does not exist:', filePath);
        return { success: false, error: 'File does not exist' };
      }
      const stats = await fsp.stat(filePath);
      if (stats.isDirectory()) {
        log.error('Path is a directory, not a file:', filePath);
        return { success: false, error: 'Path is a directory, not a file' };
      }

      const fileContent = await fsp.readFile(filePath);

      return {
        success: true,
        data: fileContent,
        size: fileContent.length,
      };
    } catch (error: any) {
      log.error('Failed to read file:', filePath, error);
      return {
        success: false,
        error: error.message || 'Failed to read file',
      };
    }
  });

  // ==================== delete folder handler ====================
  ipcMain.handle('delete-folder', async (event, email: string) => {
    const { MCP_REMOTE_CONFIG_DIR } = getEmailFolderPath(email);
    try {
      log.info('Deleting folder:', MCP_REMOTE_CONFIG_DIR);

      // Check if folder exists
      if (!fs.existsSync(MCP_REMOTE_CONFIG_DIR)) {
        log.error('Folder does not exist:', MCP_REMOTE_CONFIG_DIR);
        return { success: false, error: 'Folder does not exist' };
      }

      // Check if it's actually a directory
      const stats = await fsp.stat(MCP_REMOTE_CONFIG_DIR);
      if (!stats.isDirectory()) {
        log.error('Path is not a directory:', MCP_REMOTE_CONFIG_DIR);
        return { success: false, error: 'Path is not a directory' };
      }

      // Delete folder recursively
      await fsp.rm(MCP_REMOTE_CONFIG_DIR, { recursive: true, force: true });
      log.info('Folder deleted successfully:', MCP_REMOTE_CONFIG_DIR);

      return {
        success: true,
        message: 'Folder deleted successfully',
      };
    } catch (error: any) {
      log.error('Failed to delete folder:', MCP_REMOTE_CONFIG_DIR, error);
      return {
        success: false,
        error: error.message || 'Failed to delete folder',
      };
    }
  });

  // ==================== get MCP config path handler ====================
  ipcMain.handle('get-mcp-config-path', async (event, email: string) => {
    try {
      const { MCP_REMOTE_CONFIG_DIR, tempEmail } = getEmailFolderPath(email);
      log.info('Getting MCP config path for email:', email);
      log.info('MCP config path:', MCP_REMOTE_CONFIG_DIR);
      return {
        success: MCP_REMOTE_CONFIG_DIR,
        path: MCP_REMOTE_CONFIG_DIR,
        tempEmail: tempEmail,
      };
    } catch (error: any) {
      log.error('Failed to get MCP config path:', error);
      return {
        success: false,
        error: error.message || 'Failed to get MCP config path',
      };
    }
  });

  // ==================== IDE integration handler ====================
  ipcMain.handle(
    'get-project-folder-path',
    async (
      _event,
      email: string,
      projectId: string,
      userId?: string | number | null
    ) => {
      const manager = checkManagerInstance(fileReader, 'FileReader');
      const result = manager.createProjectStructure(email, projectId, userId);
      return result.path;
    }
  );

  ipcMain.handle(
    'open-in-ide',
    async (event, targetPath: string, ide: string) => {
      assertAppRendererSender(event);
      if (ide !== 'vscode' && ide !== 'cursor' && ide !== 'system') {
        return { success: false, error: 'Unsupported editor' };
      }

      const node = await authorizeWorkspaceLocalNode(targetPath, [
        ...activeLocalFileRoots(),
      ]);
      if (!node.success) return node;
      if (ide === 'system') {
        return revealAuthorizedLocalNode(node, shell);
      }

      const getIDECommand = (): string => {
        const platform = process.platform;
        const homeDir = homedir();

        if (ide === 'vscode') {
          if (platform === 'darwin') {
            // macOS: Check common VS Code CLI paths
            const vscodePaths = [
              '/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code',
              '/usr/local/bin/code',
            ];
            for (const p of vscodePaths) {
              if (existsSync(p)) return p;
            }
            log.warn('[IDE] VS Code not found on macOS');
            return '';
          } else if (platform === 'win32') {
            // Windows: Check common VS Code paths
            const vscodePaths = [
              path.join(
                homeDir,
                'AppData',
                'Local',
                'Programs',
                'Microsoft VS Code',
                'Code.exe'
              ),
              'C:\\Program Files\\Microsoft VS Code\\Code.exe',
            ];
            for (const p of vscodePaths) {
              if (existsSync(p)) return p;
            }
            log.warn('[IDE] VS Code not found on Windows');
            return '';
          }
          return 'code'; // Linux
        } else if (ide === 'cursor') {
          if (platform === 'darwin') {
            // macOS: Check common Cursor CLI paths
            const cursorPaths = [
              '/Applications/Cursor.app/Contents/Resources/app/bin/cursor',
              '/usr/local/bin/cursor',
            ];
            for (const p of cursorPaths) {
              if (existsSync(p)) return p;
            }
            log.warn('[IDE] Cursor not found on macOS');
            return '';
          } else if (platform === 'win32') {
            // Windows: Check common Cursor paths
            const cursorPaths = [
              path.join(
                homeDir,
                'AppData',
                'Local',
                'Programs',
                'Cursor',
                'Cursor.exe'
              ),
              path.join(homeDir, 'AppData', 'Local', 'Cursor', 'Cursor.exe'),
            ];
            for (const p of cursorPaths) {
              if (existsSync(p)) return p;
            }
            log.warn('[IDE] Cursor not found on Windows');
            return '';
          }
          return 'cursor'; // Linux
        }
        return '';
      };

      const cmd = getIDECommand();
      if (!cmd) {
        const editorName = ide === 'vscode' ? 'Visual Studio Code' : 'Cursor';
        return { success: false, error: `${editorName} is not installed` };
      }

      return new Promise<{ success: boolean; error?: string }>((resolve) => {
        const child = spawn(cmd, [node.path], {
          shell: false,
          stdio: 'ignore',
          detached: true,
        });
        child.unref();

        child.once('error', (error) => {
          log.warn(`[IDE] Failed to open ${node.path} with ${cmd}:`, error);
          resolve({ success: false, error: error.message });
        });

        child.once('spawn', () => {
          resolve({ success: true });
        });
      });
    }
  );

  // ==================== env handler ====================

  ipcMain.handle('get-env-path', async (_event, email) => {
    return getEnvPath(email);
  });

  ipcMain.handle('get-env-has-key', async (_event, email, key) => {
    const ENV_PATH = getEnvPath(email);
    let content = '';
    try {
      content = fs.existsSync(ENV_PATH)
        ? fs.readFileSync(ENV_PATH, 'utf-8')
        : '';
    } catch (error) {
      log.error('env-remove error:', error);
    }
    let lines = content.split(/\r?\n/);
    return { success: lines.some((line) => line.startsWith(key + '=')) };
  });

  ipcMain.handle('env-write', async (_event, email, { key, value }) => {
    const ENV_PATH = getEnvPath(email);
    let content = '';
    try {
      content = fs.existsSync(ENV_PATH)
        ? fs.readFileSync(ENV_PATH, 'utf-8')
        : '';
    } catch (error) {
      log.error('env-write error:', error);
    }
    let lines = content.split(/\r?\n/);
    lines = updateEnvBlock(lines, { [key]: value });
    writeEnvFile(ENV_PATH, lines.join('\n'));

    // Also write to global .env file for backend process to read
    const GLOBAL_ENV_PATH = path.join(os.homedir(), '.eigent', '.env');
    let globalContent = '';
    try {
      globalContent = fs.existsSync(GLOBAL_ENV_PATH)
        ? fs.readFileSync(GLOBAL_ENV_PATH, 'utf-8')
        : '';
    } catch (error) {
      log.error('global env-write read error:', error);
    }
    let globalLines = globalContent.split(/\r?\n/);
    globalLines = updateEnvBlock(globalLines, { [key]: value });
    try {
      writeEnvFile(GLOBAL_ENV_PATH, globalLines.join('\n'));
      log.info(`env-write: wrote ${key} to both user and global .env files`);
    } catch (error) {
      log.error('global env-write error:', error);
    }

    return { success: true };
  });

  ipcMain.handle('env-remove', async (_event, email, key) => {
    log.info('env-remove', key);
    const ENV_PATH = getEnvPath(email);
    let content = '';
    try {
      content = fs.existsSync(ENV_PATH)
        ? fs.readFileSync(ENV_PATH, 'utf-8')
        : '';
    } catch (error) {
      log.error('env-remove error:', error);
    }
    let lines = content.split(/\r?\n/);
    lines = removeEnvKey(lines, key);
    writeEnvFile(ENV_PATH, lines.join('\n'));
    log.info('env-remove success', ENV_PATH);

    // Also remove from global .env file
    const GLOBAL_ENV_PATH = path.join(os.homedir(), '.eigent', '.env');
    try {
      let globalContent = fs.existsSync(GLOBAL_ENV_PATH)
        ? fs.readFileSync(GLOBAL_ENV_PATH, 'utf-8')
        : '';
      let globalLines = globalContent.split(/\r?\n/);
      globalLines = removeEnvKey(globalLines, key);
      writeEnvFile(GLOBAL_ENV_PATH, globalLines.join('\n'));
      log.info(
        `env-remove: removed ${key} from both user and global .env files`
      );
    } catch (error) {
      log.error('global env-remove error:', error);
    }

    return { success: true };
  });

  // ==================== read global env handler ====================
  const ALLOWED_GLOBAL_ENV_KEYS = new Set(['HTTP_PROXY', 'HTTPS_PROXY']);
  ipcMain.handle('read-global-env', async (_event, key: string) => {
    if (!ALLOWED_GLOBAL_ENV_KEYS.has(key)) {
      log.warn(`[ENV] Blocked read of disallowed global env key: ${key}`);
      return { value: null };
    }
    return { value: readGlobalEnvKey(key) };
  });

  // ==================== Session window handlers ====================
  // The main window opens a Session in its own window; a second request for
  // the same Session focuses the window that is already open.
  ipcMain.handle('session-window:open', (event, target: unknown) => {
    if (!isMainRendererFrame(event)) {
      throw new Error('Only the main window opens Session windows');
    }
    if (!isSessionWindowTarget(target)) {
      throw new Error('Invalid Session window target');
    }
    getSessionWindows().open(target);
    return { success: true };
  });
  ipcMain.handle('session-window:list', (event) => {
    assertAppRendererSender(event);
    return getSessionWindows().openProjectIds();
  });
  // Messages between app windows: presence, sidebar status, requests for the
  // main window. Main only relays them; renderers validate what they accept.
  ipcMain.on('session-window:post', (event, message: unknown) => {
    if (!isAppRendererFrame(event)) return;
    relaySessionWindowMessage(event.sender.id, message);
  });
  // "Open in main window": hand the Session back and close this window.
  ipcMain.handle('session-window:return-to-main', (event) => {
    const target = sessionWindows?.targetForContents(event.sender.id);
    if (!target || event.senderFrame !== event.sender.mainFrame)
      throw new Error('Not a Session window');
    relaySessionWindowMessage(event.sender.id, {
      type: 'open-in-main',
      ...target,
    });
    if (win && !win.isDestroyed()) {
      if (win.isMinimized()) win.restore();
      win.focus();
    }
    sessionWindows?.close(target.projectId);
    return { success: true };
  });

  // ==================== FileReader handler ====================
  ipcMain.handle(
    'open-file',
    async (
      event,
      type: string,
      filePath: string,
      isShowSourceCode: boolean
    ) => {
      const manager = checkManagerInstance(fileReader, 'FileReader');
      const authorizedPath = await requireAuthorizedPreviewFile(
        event,
        filePath
      );
      return manager.openFile(type, authorizedPath, isShowSourceCode);
    }
  );

  ipcMain.handle(
    'get-file-preview-metadata',
    async (event, filePath: string) => {
      const manager = checkManagerInstance(fileReader, 'FileReader');
      const authorizedPath = await requireAuthorizedPreviewFile(
        event,
        filePath
      );
      return manager.getPreviewMetadata(authorizedPath);
    }
  );

  ipcMain.handle(
    'preview-office-buffer',
    async (event, type: string, bytes: Uint8Array) => {
      assertAppRendererSender(event);
      if (!(bytes instanceof Uint8Array)) {
        throw new Error('Invalid remote Office preview payload');
      }
      const manager = checkManagerInstance(fileReader, 'FileReader');
      return manager.previewOfficeBuffer(type, bytes);
    }
  );

  ipcMain.handle('preview-csv-file', async (event, filePath: string) => {
    const manager = checkManagerInstance(fileReader, 'FileReader');
    const authorizedPath = await requireAuthorizedPreviewFile(event, filePath);
    return manager.previewCsvFile(authorizedPath);
  });

  ipcMain.handle(
    'preview-text-file',
    async (event, filePath: string, limit?: number) => {
      const manager = checkManagerInstance(fileReader, 'FileReader');
      const authorizedPath = await requireAuthorizedPreviewFile(
        event,
        filePath
      );
      return manager.previewTextFile(authorizedPath, limit);
    }
  );

  ipcMain.handle('download-file', async (_, url: string) => {
    try {
      const https = await import('https');
      const http = await import('http');

      // extract file name from URL
      const urlObj = new URL(url);
      const fileName = urlObj.pathname.split('/').pop() || 'download';

      // get download directory
      const downloadPath = path.join(app.getPath('downloads'), fileName);

      // create write stream
      const fileStream = fs.createWriteStream(downloadPath);

      // choose module according to protocol
      const client = url.startsWith('https:') ? https : http;

      return new Promise((resolve, reject) => {
        const request = client.get(url, (response) => {
          if (response.statusCode !== 200) {
            reject(new Error(`HTTP ${response.statusCode}`));
            return;
          }

          response.pipe(fileStream);

          fileStream.on('finish', () => {
            fileStream.close();
            shell.showItemInFolder(downloadPath);
            resolve({ success: true, path: downloadPath });
          });

          fileStream.on('error', (err) => {
            reject(err);
          });
        });

        request.on('error', (err) => {
          reject(err);
        });
      });
    } catch (error: any) {
      log.error('Download file error:', error);
      return { success: false, error: error.message };
    }
  });

  ipcMain.handle(
    'get-file-list',
    async (
      _,
      email: string,
      taskId: string,
      projectId?: string,
      userId?: string | number | null
    ) => {
      const manager = checkManagerInstance(fileReader, 'FileReader');
      return manager.getFileList(email, taskId, projectId, userId);
    }
  );

  ipcMain.handle(
    'get-camel-log-file-list',
    async (
      _,
      email: string,
      taskId: string,
      projectId?: string,
      userId?: string | number | null
    ) => {
      const manager = checkManagerInstance(fileReader, 'FileReader');
      return manager.getCamelLogFileList(email, taskId, projectId, userId);
    }
  );

  ipcMain.handle(
    'delete-task-files',
    async (
      _,
      email: string,
      taskId: string,
      projectId?: string,
      userId?: string | number | null,
      spaceId?: string
    ) => {
      const manager = checkManagerInstance(fileReader, 'FileReader');
      return manager.deleteTaskFiles(email, taskId, projectId, userId, spaceId);
    }
  );

  // New project management handlers
  ipcMain.handle(
    'create-project-structure',
    async (
      _,
      email: string,
      projectId: string,
      userId?: string | number | null
    ) => {
      const manager = checkManagerInstance(fileReader, 'FileReader');
      return manager.createProjectStructure(email, projectId, userId);
    }
  );

  ipcMain.handle('get-project-list', async (_, email: string) => {
    const manager = checkManagerInstance(fileReader, 'FileReader');
    return manager.getProjectList(email);
  });

  ipcMain.handle(
    'get-tasks-in-project',
    async (_, email: string, projectId: string) => {
      const manager = checkManagerInstance(fileReader, 'FileReader');
      return manager.getTasksInProject(email, projectId);
    }
  );

  ipcMain.handle(
    'move-task-to-project',
    async (_, email: string, taskId: string, projectId: string) => {
      const manager = checkManagerInstance(fileReader, 'FileReader');
      return manager.moveTaskToProject(email, taskId, projectId);
    }
  );

  ipcMain.handle(
    'get-workspace-file-list',
    async (event, workspaceRoot: string, requestedRelativePaths: unknown) => {
      if (
        typeof workspaceRoot !== 'string' ||
        !Array.isArray(requestedRelativePaths) ||
        requestedRelativePaths.length >
          FILE_PREVIEW_LIMITS.workspaceResolverPaths ||
        requestedRelativePaths.some((value) => typeof value !== 'string')
      ) {
        throw new Error('Invalid workspace file resolver request');
      }
      const manager = checkManagerInstance(fileReader, 'FileReader');
      const authorizedRoot = await requireAuthorizedPreviewFile(
        event,
        workspaceRoot
      );
      return manager.getWorkspaceFileList(
        authorizedRoot,
        requestedRelativePaths
      );
    }
  );

  ipcMain.handle(
    'get-project-file-list',
    async (
      _,
      email: string,
      projectId: string,
      userId?: string | number | null
    ) => {
      const manager = checkManagerInstance(fileReader, 'FileReader');
      return manager.getProjectFileList(email, projectId, userId);
    }
  );

  ipcMain.handle('get-log-folder', async (_, email: string) => {
    const manager = checkManagerInstance(fileReader, 'FileReader');
    return manager.getLogFolder(email);
  });

  // ==================== WebView handler ====================
  const webviewHandlers = [
    { name: 'capture-webview', method: 'captureWebview' },
    { name: 'create-webview', method: 'createWebview' },
    { name: 'hide-webview', method: 'hideWebview' },
    { name: 'show-webview', method: 'showWebview' },
    { name: 'change-view-size', method: 'changeViewSize' },
    { name: 'hide-all-webview', method: 'hideAllWebview' },
    { name: 'get-active-webview', method: 'getActiveWebview' },
    { name: 'set-size', method: 'setSize' },
    { name: 'get-show-webview', method: 'getShowWebview' },
    { name: 'webview-destroy', method: 'destroyWebview' },
  ];

  // Calls that place a view on screen move the agent browser views into the
  // window that asked, so a Session window shows them in its own content.
  const presentingWebviewMethods = new Set(['showWebview', 'setSize']);
  webviewHandlers.forEach(({ name, method }) => {
    ipcMain.handle(name, async (event, ...args) => {
      const manager = checkManagerInstance(webViewManager, 'WebViewManager');
      if (presentingWebviewMethods.has(method)) {
        const presenter = appWindowForSender(event.sender);
        if (presenter) manager.presentIn(presenter);
      }
      return manager[method as keyof typeof manager](...args);
    });
  });

  // ==================== dependency install handler ====================
  ipcMain.handle('install-dependencies', async () => {
    try {
      if (win === null) throw new Error('Window is null');

      // Prevent concurrent installations
      if (isInstallationInProgress) {
        log.info('[DEPS INSTALL] Installation already in progress, waiting...');
        await installationLock;
        return {
          success: true,
          message: 'Installation completed by another process',
        };
      }

      log.info('[DEPS INSTALL] Manual installation/retry triggered');

      // Set lock
      isInstallationInProgress = true;
      installationLock = checkAndInstallDepsOnUpdate({
        win,
        forceInstall: true,
      }).finally(() => {
        isInstallationInProgress = false;
      });

      const result = await installationLock;

      if (!result.success) {
        log.error('[DEPS INSTALL] Manual installation failed:', result.message);
        // Note: Failure event already sent by installDependencies function
        return { success: false, error: result.message };
      }

      log.info('[DEPS INSTALL] Manual installation succeeded');

      // IMPORTANT: Send install-dependencies-complete success event
      if (!win.isDestroyed()) {
        win.webContents.send('install-dependencies-complete', {
          success: true,
          code: 0,
        });
        log.info(
          '[DEPS INSTALL] Sent install-dependencies-complete event after retry'
        );
      }

      // Start backend after retry with cleanup
      await startBackendAfterInstall();

      return { success: true, isInstalled: result.success };
    } catch (error) {
      log.error('[DEPS INSTALL] Manual installation error:', error);
      return { success: false, error: (error as Error).message };
    }
  });

  ipcMain.handle('check-tool-installed', async () => {
    try {
      const isInstalled = await checkToolInstalled();
      return { success: true, isInstalled: isInstalled.success };
    } catch (error) {
      return { success: false, error: (error as Error).message };
    }
  });

  ipcMain.handle('get-installation-status', async () => {
    try {
      const { isInstalling, hasLockFile } = await getInstallationStatus();
      return {
        success: true,
        isInstalling,
        hasLockFile,
        timestamp: Date.now(),
      };
    } catch (error) {
      return { success: false, error: (error as Error).message };
    }
  });

  // ==================== register update related handler ====================
  registerUpdateIpcHandlers(() => closeCoordinator.markAppQuitInProgress());
}

// ==================== ensure eigent directories ====================
const ensureEigentDirectories = () => {
  const eigentBase = path.join(os.homedir(), '.eigent');
  const requiredDirs = [
    eigentBase,
    path.join(eigentBase, 'bin'),
    path.join(eigentBase, 'cache'),
    path.join(eigentBase, 'venvs'),
    path.join(eigentBase, 'runtime'),
    path.join(eigentBase, 'skills'),
  ];

  for (const dir of requiredDirs) {
    if (!fs.existsSync(dir)) {
      log.info(`Creating directory: ${dir}`);
      fs.mkdirSync(dir, { recursive: true });
    }
  }

  log.info('.eigent directory structure ensured');
};

// ==================== skills (used at startup and by IPC) ====================
const SKILLS_ROOT = path.join(os.homedir(), '.eigent', 'skills');
const SKILL_FILE = 'SKILL.md';
const EXAMPLE_SKILL_MARKER = '.eigent-example-skill';

const getExampleSkillsSourceDir = (): string => {
  if (app.isPackaged) {
    return path.join(process.resourcesPath, 'example-skills');
  }
  const devPath = path.join(MAIN_DIST, 'resources', 'example-skills');
  if (existsSync(devPath)) return devPath;
  return path.join(app.getAppPath(), 'resources', 'example-skills');
};

async function copyDirRecursive(src: string, dst: string): Promise<void> {
  await fsp.mkdir(dst, { recursive: true });
  const entries = await fsp.readdir(src, { withFileTypes: true });
  for (const entry of entries) {
    // Skip symlinks to prevent copying files from outside the source tree
    if (entry.isSymbolicLink()) continue;
    const srcPath = path.join(src, entry.name);
    const dstPath = path.join(dst, entry.name);
    if (entry.isDirectory()) {
      await copyDirRecursive(srcPath, dstPath);
    } else {
      await fsp.copyFile(srcPath, dstPath);
    }
  }
}

function parseSkillName(content: string): string | null {
  const match = content.match(/^\s*name\s*:\s*(.+)$/m);
  return match?.[1]?.trim().replace(/^['"]|['"]$/g, '') || null;
}

async function readSkillName(skillDir: string): Promise<string | null> {
  try {
    const content = await fsp.readFile(
      path.join(skillDir, SKILL_FILE),
      'utf-8'
    );
    return parseSkillName(content);
  } catch {
    return null;
  }
}

async function isManagedExampleSkill(
  dstDir: string,
  srcDir: string
): Promise<boolean> {
  if (existsSync(path.join(dstDir, EXAMPLE_SKILL_MARKER))) return true;
  const [dstName, srcName] = await Promise.all([
    readSkillName(dstDir),
    readSkillName(srcDir),
  ]);
  return !!dstName && dstName === srcName;
}

async function writeExampleSkillMarker(
  dstDir: string,
  sourceDirName: string
): Promise<void> {
  await fsp.writeFile(
    path.join(dstDir, EXAMPLE_SKILL_MARKER),
    `source=${sourceDirName}\n`,
    'utf-8'
  );
}

async function listRegularFiles(
  root: string,
  ignoredNames = new Set<string>()
): Promise<Map<string, string>> {
  const files = new Map<string, string>();
  const walk = async (dir: string) => {
    const entries = await fsp.readdir(dir, { withFileTypes: true });
    for (const entry of entries) {
      if (entry.isSymbolicLink() || ignoredNames.has(entry.name)) continue;
      const fullPath = path.join(dir, entry.name);
      if (entry.isDirectory()) {
        await walk(fullPath);
      } else if (entry.isFile()) {
        files.set(path.relative(root, fullPath), fullPath);
      }
    }
  };
  await walk(root);
  return files;
}

async function dirContentsMatch(src: string, dst: string): Promise<boolean> {
  const [srcFiles, dstFiles] = await Promise.all([
    listRegularFiles(src),
    listRegularFiles(dst, new Set([EXAMPLE_SKILL_MARKER])),
  ]);
  if (srcFiles.size !== dstFiles.size) return false;
  for (const [relativePath, srcPath] of srcFiles) {
    const dstPath = dstFiles.get(relativePath);
    if (!dstPath) return false;
    const [srcContent, dstContent] = await Promise.all([
      fsp.readFile(srcPath),
      fsp.readFile(dstPath),
    ]);
    if (!srcContent.equals(dstContent)) return false;
  }
  return true;
}

async function syncDefaultSkillsFromBundle(): Promise<void> {
  if (!existsSync(SKILLS_ROOT)) {
    await fsp.mkdir(SKILLS_ROOT, { recursive: true });
  }
  const exampleDir = getExampleSkillsSourceDir();
  if (!existsSync(exampleDir)) {
    log.warn('Example skills source dir missing:', exampleDir);
    return;
  }
  const sourceEntries = await fsp.readdir(exampleDir, { withFileTypes: true });
  let copiedCount = 0;
  let updatedCount = 0;
  for (const e of sourceEntries) {
    if (!e.isDirectory() || e.name.startsWith('.')) continue;
    const skillMd = path.join(exampleDir, e.name, SKILL_FILE);
    if (!existsSync(skillMd)) continue;
    const destDir = path.join(SKILLS_ROOT, e.name);
    const srcDir = path.join(exampleDir, e.name);
    if (!existsSync(destDir)) {
      await copyDirRecursive(srcDir, destDir);
      await writeExampleSkillMarker(destDir, e.name);
      copiedCount++;
      continue;
    }

    const destStats = await fsp.stat(destDir).catch(() => null);
    if (!destStats?.isDirectory()) continue;

    if (!(await isManagedExampleSkill(destDir, srcDir))) {
      log.warn('Skipping default skill sync due to local conflict:', destDir);
      continue;
    }

    if (await dirContentsMatch(srcDir, destDir)) {
      await writeExampleSkillMarker(destDir, e.name);
      continue;
    }

    await fsp.rm(destDir, { recursive: true, force: true });
    await copyDirRecursive(srcDir, destDir);
    await writeExampleSkillMarker(destDir, e.name);
    updatedCount++;
  }
  if (copiedCount > 0 || updatedCount > 0) {
    log.info(
      `Synced default skill(s) to ~/.eigent/skills: copied=${copiedCount} updated=${updatedCount} from`,
      exampleDir
    );
  }
}

async function seedDefaultSkillsIfEmpty(): Promise<void> {
  await syncDefaultSkillsFromBundle();
}

// ==================== Shared backend startup logic ====================
// Starts backend after installation completes
// Used by both initial startup and retry flows
const startBackendAfterInstall = async () => {
  log.info('[DEPS INSTALL] Starting backend...');

  // Add a small delay to ensure any previous processes are fully cleaned up
  await new Promise((resolve) => setTimeout(resolve, 500));

  // Existing installations originally stored the device id in renderer
  // localStorage. Migrate it before the Brain environment first resolves the
  // new main-process-owned identity file.
  await primeDesktopInstanceIdFromRenderer();
  await checkAndStartBackend();
};

// ==================== installation lock ====================
let isInstallationInProgress = false;
let installationLock: Promise<PromiseReturnType> = Promise.resolve({
  message: 'No installation needed',
  success: true,
});

// ==================== window create ====================
async function createWindow() {
  const existingWindow =
    win && !win.isDestroyed() ? win : BrowserWindow.getAllWindows()[0];
  if (existingWindow && !existingWindow.isDestroyed()) {
    win = existingWindow;
    win.focus();
    return;
  }

  if (createWindowPromise) {
    await createWindowPromise;
    if (win && !win.isDestroyed()) {
      win.focus();
    }
    return;
  }

  createWindowPromise = createWindowInternal().finally(() => {
    createWindowPromise = null;
  });

  return createWindowPromise;
}

/**
 * Frame, background and renderer settings shared by the main window and Session
 * windows, so both look and behave like the same app.
 */
function appWindowChromeOptions(): Electron.BrowserWindowConstructorOptions {
  const isMac = process.platform === 'darwin';
  const isWindows = process.platform === 'win32';
  return {
    // Use native frame on Windows for better native integration
    frame: isWindows ? true : false,
    show: false, // Don't show until content is ready to avoid white screen
    // Only use transparency on macOS and Linux (not supported well on Windows)
    transparent: !isWindows,
    // Solid on Windows; macOS solid without vibrancy; Linux unchanged semi-transparent tint
    backgroundColor: isWindows
      ? nativeTheme.shouldUseDarkColors
        ? '#1e1e1e'
        : '#ffffff'
      : isMac
        ? nativeTheme.shouldUseDarkColors
          ? '#1e1e1e'
          : '#f5f5f5'
        : '#f5f5f580',
    // macOS-specific title bar styling
    titleBarStyle: isMac ? 'hidden' : undefined,
    trafficLightPosition: isMac ? { x: 10, y: 12 } : undefined,
    icon: path.join(VITE_PUBLIC, 'favicon.ico'),
    // Rounded corners on macOS and Linux (as original)
    roundedCorners: !isWindows,
    // Keep the Windows/Linux menu discoverable through Alt without changing
    // the existing clean shell chrome.
    ...(!isMac && {
      autoHideMenuBar: true,
    }),
    webPreferences: {
      // Use a dedicated partition for main window to isolate from webviews
      // This ensures main window's auth data (localStorage) is stored separately and persists across restarts
      partition: 'persist:main_window',
      webSecurity: true,
      preload,
      nodeIntegration: true,
      contextIsolation: true,
      webviewTag: true,
      spellcheck: false,
    },
  };
}

async function createWindowInternal() {
  // Ensure .eigent directories exist before anything else
  ensureEigentDirectories();
  await seedDefaultSkillsIfEmpty();

  // Load persisted CDP browser pool from disk
  loadCdpPool();

  log.info(
    `[PROJECT BROWSER WINDOW] Creating BrowserWindow which will start Chrome with CDP on port ${browser_port}`
  );
  log.info(
    `[PROJECT BROWSER WINDOW] Current user data path: ${app.getPath(
      'userData'
    )}`
  );
  log.info(
    `[PROJECT BROWSER WINDOW] Command line switch user-data-dir: ${app.commandLine.getSwitchValue(
      'user-data-dir'
    )}`
  );

  const startupWindowState = loadWindowStartupState({
    userDataPath: userData,
    displays: screen.getAllDisplays(),
    primaryDisplay: screen.getPrimaryDisplay(),
  });
  log.info(
    `[WINDOW STATE] Restoring ${startupWindowState.source}: ${JSON.stringify(
      startupWindowState.bounds
    )}`
  );

  // Platform-specific window configuration
  // Windows: native frame and solid background. macOS/Linux: frameless; macOS corner radius via native hook.
  appShellReadinessGate.markDocumentLoading();
  rendererAppCommands.markNotReady('window-created');
  win = new BrowserWindow({
    title: 'Eigent',
    ...startupWindowState.bounds,
    minWidth: startupWindowState.minimumSize.width,
    minHeight: startupWindowState.minimumSize.height,
    ...appWindowChromeOptions(),
  });

  const createdWindow = win;
  const disposeMainContextMenu = installSurfaceContextMenu(
    createdWindow.webContents,
    createdWindow,
    'main-renderer'
  );
  createdWindow.on('move', () => scheduleWindowStateSave(createdWindow));
  createdWindow.on('resize', () => scheduleWindowStateSave(createdWindow));
  createdWindow.on('maximize', () => scheduleWindowStateSave(createdWindow));
  createdWindow.on('unmaximize', () => scheduleWindowStateSave(createdWindow));
  createdWindow.on('close', () => persistBrowserWindowState(createdWindow));
  createdWindow.on('closed', () => {
    disposeMainContextMenu();
    if (windowStateSaveTimer) {
      clearTimeout(windowStateSaveTimer);
      windowStateSaveTimer = null;
    }
  });

  installPreviewGuestGuards(createdWindow);

  if (process.platform === 'darwin') {
    win.once('ready-to-show', () => {
      if (win && !win.isDestroyed()) {
        try {
          setRoundedCorners(win, 20);
        } catch (error) {
          log.error('[MacOS] Failed to apply rounded corners:', error);
        }
      }
    });
  }

  // ==================== Handle renderer crashes and failed loads ====================
  win.webContents.on('did-start-navigation', (event) => {
    const didReplaceMainDocument =
      appShellReadinessGate.markDocumentNavigationStarted(
        event.isMainFrame,
        event.isSameDocument
      );
    if (didReplaceMainDocument) {
      rendererAppCommands.markNotReady('main-frame-navigation');
    }
  });
  win.webContents.on('did-finish-load', () => {
    appShellReadinessGate.markDocumentLoaded();
  });
  win.webContents.on('render-process-gone', (event, details) => {
    log.error('[RENDERER] Process gone:', details.reason, details.exitCode);
    appShellReadinessGate.markDocumentLoading();
    rendererAppCommands.markNotReady('render-process-gone');
    if (win && !win.isDestroyed()) {
      // Reload the window after a brief delay
      setTimeout(() => {
        if (win && !win.isDestroyed()) {
          log.info('[RENDERER] Attempting to reload after crash...');
          if (VITE_DEV_SERVER_URL) {
            win.loadURL(VITE_DEV_SERVER_URL);
          } else {
            win.loadFile(indexHtml);
          }
        }
      }, 1000);
    }
  });

  win.webContents.on(
    'did-fail-load',
    (event, errorCode, errorDescription, validatedURL, isMainFrame) => {
      const shouldRetry = appShellReadinessGate.markDocumentLoadFailed(
        isMainFrame,
        errorCode
      );
      if (shouldRetry) rendererAppCommands.markNotReady('did-fail-load');
      log.error(
        `[RENDERER] Failed to load: ${errorCode} - ${errorDescription} - ${validatedURL}`
      );
      const subframeFailure = subframeLoadFailureFromEvent(
        errorCode,
        validatedURL,
        isMainFrame
      );
      if (subframeFailure && !createdWindow.isDestroyed()) {
        createdWindow.webContents.send(
          SUBFRAME_LOAD_FAILED_CHANNEL,
          subframeFailure
        );
      }
      // Retry loading after a delay
      if (shouldRetry) {
        setTimeout(() => {
          if (win && !win.isDestroyed()) {
            log.info('[RENDERER] Retrying load after failure...');
            if (VITE_DEV_SERVER_URL) {
              win.loadURL(VITE_DEV_SERVER_URL);
            } else {
              win.loadFile(indexHtml);
            }
          }
        }, 2000);
      }
    }
  );

  // Main window now uses default userData directly with partition 'persist:main_window'
  // No migration needed - data is already persistent

  // ==================== Import cookies from tool_controller to WebView BEFORE creating WebViews ====================
  // Copy partition data files before any session accesses them
  try {
    const browserProfilesBase = path.join(
      os.homedir(),
      '.eigent',
      'browser_profiles'
    );
    const toolControllerProfile = path.join(
      browserProfilesBase,
      'profile_user_login'
    );
    const toolControllerPartitionPath = path.join(
      toolControllerProfile,
      'Partitions',
      'user_login'
    );

    if (fs.existsSync(toolControllerPartitionPath)) {
      log.info(
        '[COOKIE SYNC] Found tool_controller partition, copying to WebView partition...'
      );

      const targetPartitionPath = path.join(
        app.getPath('userData'),
        'Partitions',
        'user_login'
      );
      log.info('[COOKIE SYNC] From:', toolControllerPartitionPath);
      log.info('[COOKIE SYNC] To:', targetPartitionPath);

      // Ensure target directory exists
      if (!fs.existsSync(path.dirname(targetPartitionPath))) {
        fs.mkdirSync(path.dirname(targetPartitionPath), { recursive: true });
      }

      // Copy the entire partition directory
      fs.cpSync(toolControllerPartitionPath, targetPartitionPath, {
        recursive: true,
        force: true,
      });
      log.info('[COOKIE SYNC] Successfully copied partition data to WebView');

      // Verify cookies were copied
      const targetCookies = path.join(targetPartitionPath, 'Cookies');
      if (fs.existsSync(targetCookies)) {
        const stats = fs.statSync(targetCookies);
        log.info(`[COOKIE SYNC] Cookies file size: ${stats.size} bytes`);
      }
    } else {
      log.info(
        '[COOKIE SYNC] No tool_controller partition found, WebView will start fresh'
      );
    }
  } catch (error) {
    log.error('[COOKIE SYNC] Failed to sync partition data:', error);
  }

  // ==================== initialize manager ====================
  fileReader = new FileReader(win);
  webViewManager = new WebViewManager(win, {
    installContextMenu: (contents, ownerWindow) =>
      installSurfaceContextMenu(contents, ownerWindow, 'automation-view'),
  });

  // create multiple webviews
  log.info(
    `[PROJECT BROWSER] Creating WebViews with partition: persist:user_login`
  );
  for (let i = 1; i <= 8; i++) {
    webViewManager.createWebview(i === 1 ? undefined : i.toString());
  }
  log.info('[PROJECT BROWSER] WebViewManager initialized with webviews');

  // ==================== set event listeners ====================
  setupExternalLinkHandling();
  handleBeforeClose();

  // Start CDP health-check polling (probes every 3s, removes dead browsers)
  startCdpHealthCheck();

  // ==================== auto update ====================
  update(win);

  // ==================== CHECK IF INSTALLATION IS NEEDED BEFORE LOADING CONTENT ====================
  log.info('Pre-checking if dependencies need to be installed...');

  // Check if prebuilt dependencies are available (for packaged app)
  let hasPrebuiltDeps = false;
  if (app.isPackaged) {
    const prebuiltBinDir = path.join(process.resourcesPath, 'prebuilt', 'bin');
    const prebuiltDir = path.join(process.resourcesPath, 'prebuilt');
    const prebuiltVenvDir = path.join(prebuiltDir, 'venv');
    const uvPath = path.join(
      prebuiltBinDir,
      process.platform === 'win32' ? 'uv.exe' : 'uv'
    );
    const bunPath = path.join(
      prebuiltBinDir,
      process.platform === 'win32' ? 'bun.exe' : 'bun'
    );
    const pyvenvCfg = path.join(prebuiltVenvDir, 'pyvenv.cfg');

    const hasVenv = fs.existsSync(pyvenvCfg);
    hasPrebuiltDeps =
      fs.existsSync(uvPath) && fs.existsSync(bunPath) && hasVenv;
    if (hasPrebuiltDeps) {
      log.info(
        '[PRE-CHECK] Prebuilt dependencies found, skipping installation check'
      );
    }
  }

  // Check version and tools status synchronously
  const currentVersion = app.getVersion();
  const versionFile = path.join(app.getPath('userData'), 'version.txt');
  const versionExists = fs.existsSync(versionFile);
  let savedVersion = '';
  if (versionExists) {
    savedVersion = fs.readFileSync(versionFile, 'utf-8').trim();
  }

  const uvExists = await isBinaryExists('uv');
  const bunExists = await isBinaryExists('bun');

  // Check if installation was previously completed
  const backendPath = getBackendPath();
  const installedLockPath = path.join(backendPath, 'uv_installed.lock');
  const installationCompleted = fs.existsSync(installedLockPath);

  // Check venv existence WITHOUT triggering extraction (defers to startBackend when window is visible)
  const { exists: venvExists, path: venvPath } =
    checkVenvExistsForPreCheck(currentVersion);

  // If prebuilt deps are available, skip installation
  const needsInstallation = hasPrebuiltDeps
    ? false
    : !versionExists ||
      savedVersion !== currentVersion ||
      !uvExists ||
      !bunExists ||
      !installationCompleted ||
      !venvExists;

  log.info('Installation check result:', {
    needsInstallation,
    versionExists,
    versionMatch: savedVersion === currentVersion,
    uvExists,
    bunExists,
    installationCompleted,
    venvExists,
    venvPath,
  });

  // Handle localStorage based on installation state
  if (needsInstallation) {
    log.info(
      'Installation needed - resetting initState to carousel while preserving auth data'
    );

    // Instead of deleting the entire localStorage, we'll update only the initState
    // This preserves login information while resetting the initialization flow
    // Set up the injection for when page loads
    win.webContents.once('dom-ready', () => {
      if (!win || win.isDestroyed()) {
        log.warn(
          'Window destroyed before DOM ready - skipping localStorage injection'
        );
        return;
      }
      log.info(
        'DOM ready - updating initState to carousel while preserving auth data'
      );
      win.webContents
        .executeJavaScript(
          `
        (function() {
          try {
            const authStorage = localStorage.getItem('auth-storage');
            if (authStorage) {
              // Preserve existing auth data, only update initState
              const parsed = JSON.parse(authStorage);
              const updatedStorage = {
                ...parsed,
                state: {
                  ...parsed.state,
                  initState: 'carousel'
                }
              };
              localStorage.setItem('auth-storage', JSON.stringify(updatedStorage));
              console.log('[ELECTRON PRE-INJECT] Updated initState to carousel, preserved auth data');
            } else {
              // No existing storage, create new one with carousel state
              const newAuthStorage = {
                state: {
                  token: null,
                  username: null,
                  email: null,
                  user_id: null,
                  appearance: 'light',
                  language: 'system',
                  isFirstLaunch: true,
                  modelType: 'cloud',
                  cloud_model_type: 'gpt-5.4',
                  initState: 'carousel',
                  share_token: null,
                  workerListData: {}
                },
                version: 0
              };
              localStorage.setItem('auth-storage', JSON.stringify(newAuthStorage));
              console.log('[ELECTRON PRE-INJECT] Created fresh auth-storage with carousel state');
            }
          } catch (e) {
            console.error('[ELECTRON PRE-INJECT] Failed to update storage:', e);
          }
        })();
      `
        )
        .catch((err) => {
          log.error('Failed to inject script:', err);
        });
    });
  } else {
    // The proper flow is now handled by useInstallationSetup.ts with dual-check mechanism:
    // 1. Installation complete event → installationCompleted.current = true
    // 2. Backend ready event → backendReady.current = true
    // 3. Only when BOTH are true → setInitState('done')
    //
    // This ensures frontend never shows before backend is ready.
    log.info(
      'Installation already complete - letting useInstallationSetup handle state transitions'
    );
  }

  // Load content
  if (VITE_DEV_SERVER_URL) {
    win.loadURL(VITE_DEV_SERVER_URL);
    win.webContents.openDevTools();
  } else {
    win.loadFile(indexHtml);
  }

  // Wait for window to be ready with timeout
  await new Promise<void>((resolve) => {
    const loadTimeout = setTimeout(() => {
      log.warn('Window content load timeout (10s), showing window anyway...');
      resolve();
    }, 10000);

    win!.webContents.once('did-finish-load', () => {
      clearTimeout(loadTimeout);
      log.info(
        'Window content loaded, starting dependency check immediately...'
      );
      resolve();
    });
  });

  // Show window now that content is loaded (or timeout reached)
  if (win && !win.isDestroyed()) {
    if (startupWindowState.shouldMaximize) win.maximize();
    else win.show();
    log.info('Window shown after content loaded');
  }

  // Mark window as ready and process any queued protocol URLs
  isWindowReady = true;
  log.info('Window is ready, processing queued protocol URLs...');
  processQueuedProtocolUrls();

  // Now check and install dependencies
  let res: PromiseReturnType = await checkAndInstallDepsOnUpdate({ win });
  if (!res.success) {
    log.info('[DEPS INSTALL] Dependency Error: ', res.message);
    // Note: install-dependencies-complete failure event is already sent by installDependencies function
    // in install-deps.ts, so we don't send it again here to avoid duplicate events
    return;
  }
  log.info('[DEPS INSTALL] Dependency Success: ', res.message);

  // IMPORTANT: Wait a bit to ensure React components have mounted and registered event listeners
  // This prevents race condition where events are sent before listeners are ready
  await new Promise((resolve) => setTimeout(resolve, 500));

  // IMPORTANT: Always send install-dependencies-complete event when installation check succeeds
  // This includes both cases: actual installation completed AND installation was skipped (already installed)
  // The frontend needs this event to properly transition from installation screen to main app
  if (!win.isDestroyed()) {
    win.webContents.send('install-dependencies-complete', {
      success: true,
      code: 0,
    });
    log.info(
      '[DEPS INSTALL] Sent install-dependencies-complete event to frontend'
    );
  }

  // Start backend after dependencies are ready
  await startBackendAfterInstall();
}

// ==================== external link handle ====================
const setupExternalLinkHandling = (appWindow: BrowserWindow | null = win) => {
  if (!appWindow) return;

  // Helper function to check if URL is external
  const isExternalUrl = (url: string): boolean => {
    try {
      const urlObj = new URL(url);
      // Allow localhost and internal URLs
      if (urlObj.hostname === 'localhost' || urlObj.hostname === '127.0.0.1') {
        return false;
      }
      // Allow hash navigation
      if (url.startsWith('#') || url.startsWith('/#')) {
        return false;
      }
      // External URLs start with http/https and are not localhost
      return urlObj.protocol === 'http:' || urlObj.protocol === 'https:';
    } catch {
      return false;
    }
  };

  // handle new window open
  appWindow.webContents.setWindowOpenHandler(({ url }) => {
    if (isExternalUrl(url)) {
      shell.openExternal(url);
      return { action: 'deny' };
    }
    return { action: 'deny' };
  });

  // handle navigation
  appWindow.webContents.on('will-navigate', (event, url) => {
    // Only prevent navigation and open external URLs
    // Allow internal navigation like hash changes
    if (isExternalUrl(url)) {
      event.preventDefault();
      shell.openExternal(url);
    }
    // For internal URLs (localhost, hash navigation), allow navigation to proceed
  });

  // srcDoc report previews are sandboxed child frames. Never turn a scripted
  // child-frame navigation into an outbound request: it could encode local
  // workspace contents in the URL even when connect-src is disabled.
  appWindow.webContents.on('will-frame-navigate', (details) => {
    if (!details.isMainFrame && isExternalUrl(details.url)) {
      details.preventDefault();
      log.warn('[HTML PREVIEW] Blocked external frame navigation');
    }
  });
};

/** Guards for preview `<webview>` guests; applied to every app window. */
function installPreviewGuestGuards(appWindow: BrowserWindow): void {
  // Renderer <webview> guests (session preview browser) host arbitrary web
  // content, and the host window itself runs with elevated webPreferences.
  // Enforce safe guest settings at attach time so no tag attribute (even one
  // forged by a compromised renderer) can grant a guest host privileges.
  appWindow.webContents.on(
    'will-attach-webview',
    (event, webPreferences, params) => {
      delete webPreferences.preload;
      webPreferences.nodeIntegration = false;
      webPreferences.contextIsolation = true;
      webPreferences.webSecurity = true;
      webPreferences.partition = PREVIEW_WEBVIEW_PARTITION;

      if (
        params.partition !== PREVIEW_WEBVIEW_PARTITION ||
        !isHttpOrHttpsUrl(params.src)
      ) {
        event.preventDefault();
      }
    }
  );

  // Route window.open / target=_blank into the same guest instead of spawning
  // popup windows, and only allow web URLs. Together with the attach guard
  // above, this is the only main-process involvement the guests need.
  appWindow.webContents.on('did-attach-webview', (_event, contents) => {
    const disposeGuestContextMenu = installSurfaceContextMenu(
      contents,
      appWindow,
      'preview-guest'
    );
    contents.once('destroyed', disposeGuestContextMenu);

    const preventUnsafeNavigation = (
      event: Electron.Event,
      navigationUrl: string
    ) => {
      if (!isHttpOrHttpsUrl(navigationUrl)) {
        event.preventDefault();
      }
    };
    const guestNavigationEvents = contents as unknown as {
      on: (
        eventName: string,
        listener: (event: Electron.Event, navigationUrl: string) => void
      ) => void;
    };

    guestNavigationEvents.on('will-navigate', preventUnsafeNavigation);
    guestNavigationEvents.on('will-frame-navigate', preventUnsafeNavigation);
    guestNavigationEvents.on('will-redirect', preventUnsafeNavigation);
    contents.setWindowOpenHandler(({ url }) => {
      if (isHttpOrHttpsUrl(url)) {
        void contents.loadURL(url);
      }
      return { action: 'deny' };
    });
  });
}

// ==================== check and start backend ====================
async function restartBackendService(): Promise<BackendStartResult> {
  if (backendStartPromise) {
    log.info(
      'Backend startup already in progress, waiting before forced restart...'
    );
    await backendStartPromise;
  }

  log.info('Restarting backend service...');
  return checkAndStartBackend({ forceRestart: true });
}

const checkAndStartBackend = async (
  options: BackendStartOptions = {}
): Promise<BackendStartResult> => {
  if (backendStartPromise) {
    log.info('Backend startup already in progress, waiting...');
    return backendStartPromise;
  }

  backendStartPromise = (async () => {
    log.info('Checking and starting backend service...');
    try {
      if (isPythonProcessRunning()) {
        if (!options.forceRestart) {
          const isHealthy = await checkBackendHealth(backendPort);
          if (isHealthy) {
            log.info('Backend service is already running', {
              port: backendPort,
            });
            const result: BackendStartResult = {
              success: true,
              port: backendPort,
            };
            notifyBackendReady(result);
            return result;
          }

          log.warn(
            'Backend process is running but health check failed; restarting...'
          );
        } else {
          log.info('Cleaning up existing backend process before restart...');
        }

        await cleanupPythonProcess();
      } else if (python_process) {
        python_process = null;
      }

      const isToolInstalled = await checkToolInstalled();
      if (isToolInstalled.success) {
        log.info('Tool installed, starting backend service...');
        const codexResolverEnv = await getCodexResolverEnv();
        const workspaceSecretBroker = await ensureWorkspaceSecretBroker();
        const exampleSkillsDir = getExampleSkillsSourceDir();

        // Start backend and wait for health check to pass
        python_process = await startBackend(
          (port) => {
            backendPort = port;
            log.info('Backend service started successfully', { port });
          },
          {
            ...codexResolverEnv,
            EIGENT_EXAMPLE_SKILLS_DIR: exampleSkillsDir,
            EIGENT_LOCAL_CONTROL_CAPABILITY: localControlCapability,
            EIGENT_DESKTOP_INSTANCE_ID: resolveDesktopInstanceId(),
            EIGENT_ELECTRON_CDP_PORT: String(browser_port),
            EIGENT_WORKSPACE_SECRET_BROKER_ENDPOINT:
              workspaceSecretBroker.endpoint,
            EIGENT_WORKSPACE_SECRET_BROKER_CAPABILITY:
              workspaceSecretBroker.capability,
          }
        );

        // Notify frontend that backend is ready
        log.info('Backend is ready, notifying frontend...');
        const result: BackendStartResult = {
          success: true,
          port: backendPort,
        };
        notifyBackendReady(result);

        python_process?.on('exit', (code, signal) => {
          log.info('Python process exited', { code, signal });
        });

        return result;
      } else {
        log.warn('Tool not installed, cannot start backend service');
        // Notify frontend that backend cannot start
        const result: BackendStartResult = {
          success: false,
          error: 'Tools not installed',
        };
        notifyBackendReady(result);
        return result;
      }
    } catch (error) {
      log.error('Failed to start backend:', error);
      // Notify frontend of backend startup failure
      const result: BackendStartResult = {
        success: false,
        error: formatErrorMessage(error),
      };
      notifyBackendReady(result);
      return result;
    }
  })().finally(() => {
    backendStartPromise = null;
  });

  return backendStartPromise;
};

// ==================== process cleanup ====================
const cleanupPythonProcess = async () => {
  try {
    // First attempt: Try to kill using PID and all children
    if (python_process?.pid) {
      const pid = python_process.pid;
      log.info('Cleaning up Python process and all children', { pid });

      // Remove all listeners to prevent memory leaks
      python_process.removeAllListeners();

      await new Promise<void>((resolve) => {
        // Kill the entire process tree (parent + all children)
        kill(pid, 'SIGTERM', (err) => {
          if (err) {
            log.error('Failed to clean up process tree with SIGTERM:', err);
            // Try SIGKILL as fallback for entire tree
            kill(pid, 'SIGKILL', (killErr) => {
              if (killErr) {
                log.error('Failed to force kill process tree:', killErr);
              }
              resolve();
            });
          } else {
            log.info('Successfully sent SIGTERM to process tree');
            // Give processes 1 second to clean up, then SIGKILL
            setTimeout(() => {
              kill(pid, 'SIGKILL', () => {
                log.info('Sent SIGKILL to ensure cleanup');
                resolve();
              });
            }, 1000);
          }
        });
      });
    }

    // Second attempt: Use port-based cleanup as fallback
    const portFile = path.join(userData, 'port.txt');
    if (fs.existsSync(portFile)) {
      try {
        const port = parseInt(fs.readFileSync(portFile, 'utf-8').trim(), 10);
        if (!isNaN(port) && port > 0 && port < 65536) {
          log.info(`Attempting to kill process on port: ${port}`);
          await killProcessOnPort(port);
        }
        fs.unlinkSync(portFile);
      } catch (error) {
        log.error('Error handling port file:', error);
      }
    }

    // Clean up any temporary files in userData
    try {
      const tempFiles = ['backend.lock', 'uv_installing.lock'];
      for (const file of tempFiles) {
        const filePath = path.join(userData, file);
        if (fs.existsSync(filePath)) {
          fs.unlinkSync(filePath);
        }
      }
    } catch (error) {
      log.error('Error cleaning up temp files:', error);
    }

    python_process = null;
  } catch (error) {
    log.error('Error occurred while cleaning up process:', error);
  }
};

// before close
const handleBeforeClose = () => {
  if (win) closeCoordinator.bindWindow(win);
};

// ==================== app event handle ====================
app.whenReady().then(async () => {
  // Wait for profile initialization to complete
  log.info('[MAIN] Waiting for profile initialization...');
  try {
    await profileInitPromise;
    log.info('[MAIN] Profile initialization completed');
  } catch (error) {
    log.error('[MAIN] Profile initialization failed:', error);
  }

  // ==================== install React DevTools ====================
  // Only install in development mode
  if (VITE_DEV_SERVER_URL) {
    try {
      log.info('[DEVTOOLS] Installing React DevTools extension...');
      // Dynamic import to avoid bundling in production
      const { default: installExtension, REACT_DEVELOPER_TOOLS } =
        await import('electron-devtools-installer');
      const name = await installExtension(REACT_DEVELOPER_TOOLS, {
        loadExtensionOptions: { allowFileAccess: true },
      });
      log.info(`[DEVTOOLS] Successfully installed extension: ${name}`);
    } catch (err) {
      log.error('[DEVTOOLS] Failed to install React DevTools:', err);
      // Don't throw - allow app to continue even if extension installation fails
    }
  }

  // ==================== Anti-fingerprint: Set User Agent for all sessions ====================
  // Use the same dynamic User Agent as app.userAgentFallback
  session.defaultSession.setUserAgent(normalUserAgent);
  // Also set for the user_login partition used by webviews
  session.fromPartition('persist:user_login').setUserAgent(normalUserAgent);
  // And for main_window partition
  session.fromPartition('persist:main_window').setUserAgent(normalUserAgent);
  log.info('[ANTI-FINGERPRINT] User Agent set for all sessions');

  // ==================== Apply proxy to Electron sessions ====================
  if (proxyUrl) {
    const proxyConfig = { proxyRules: proxyUrl };
    await session.defaultSession.setProxy(proxyConfig);
    await session.fromPartition('persist:user_login').setProxy(proxyConfig);
    await session.fromPartition('persist:main_window').setProxy(proxyConfig);
    log.info(
      `[PROXY] Applied proxy to all sessions: ${maskProxyUrl(proxyUrl)}`
    );
  }

  // ==================== download handle ====================
  session.defaultSession.on('will-download', (event, item, _webContents) => {
    item.once('done', (_event, _state) => {
      const itemUrl = item.getURL();
      if (itemUrl.startsWith('localfile:')) {
        shell.showItemInFolder(filePathFromLocalFileUrl(itemUrl));
      }
    });
  });

  // ==================== protocol handle ====================
  // Register protocol handler for both default session and main window session
  const protocolHandler = async (request: Request) => {
    const url = filePathFromLocalFileUrl(request.url);
    const normalizedUrl = url.replace(/^\/([A-Za-z]:[\\/])/, '$1');
    const filePath = path.resolve(path.normalize(normalizedUrl));

    log.info(`[PROTOCOL] Handling localfile request: ${request.url}`);
    log.info(`[PROTOCOL] Resolved path: ${filePath}`);

    const authorization = await authorizeLocalFilePath(
      filePath,
      localFileAllowedRoots()
    );
    if (!authorization.allowed) {
      log.error(
        `[PROTOCOL] Security: Blocked local file (${authorization.reason}): ${filePath}`
      );
      return new Response(
        authorization.reason === 'missing' ? 'File Not Found' : 'Forbidden',
        { status: authorization.reason === 'missing' ? 404 : 403 }
      );
    }
    const authorizedFilePath = authorization.filePath;

    try {
      // Check if file exists
      const stats = await fsp.stat(authorizedFilePath);
      if (!stats.isFile()) return new Response('Not Found', { status: 404 });
      const contentType =
        mime.getType(authorizedFilePath) || 'application/octet-stream';
      const resolvedRange = resolveFileByteRange(
        request.headers.get('range'),
        stats.size
      );
      if (resolvedRange.status === 416) {
        return new Response('Range Not Satisfiable', {
          status: 416,
          headers: { 'Content-Range': `bytes */${stats.size}` },
        });
      }
      const { start, end, status } = resolvedRange;

      const contentLength = stats.size === 0 ? 0 : end - start + 1;
      const headers: Record<string, string> = {
        'Accept-Ranges': 'bytes',
        'Content-Type': contentType,
        'Content-Length': contentLength.toString(),
        'Content-Disposition': inlineContentDisposition(
          path.basename(authorizedFilePath)
        ),
      };
      if (status === 206) {
        headers['Content-Range'] = `bytes ${start}-${end}/${stats.size}`;
      }
      if (request.method === 'HEAD' || stats.size === 0) {
        return new Response(null, { status, headers });
      }

      const stream = fs.createReadStream(authorizedFilePath, { start, end });
      return new Response(Readable.toWeb(stream) as BodyInit, {
        status,
        headers,
      });
    } catch (err) {
      log.error(`[PROTOCOL] Error reading file: ${err}`);
      return new Response('Internal Server Error', { status: 500 });
    }
  };

  // Register on default session
  protocol.handle('localfile', protocolHandler);

  // Also register on main window session
  const mainSession = session.fromPartition('persist:main_window');
  mainSession.protocol.handle('localfile', protocolHandler);

  log.info(
    '[PROTOCOL] Registered localfile protocol on both default and main_window sessions'
  );

  // ==================== initialize app ====================
  initializeApp();
  registerIpcHandlers();
  installNativeApplicationMenu();
  createWindow();
});

// ==================== window close event ====================
app.on('window-all-closed', () => {
  log.info('window-all-closed');

  // Stop polling when no window is open (important on macOS reopen flow).
  stopCdpHealthCheck();

  // Clean up WebView manager
  if (webViewManager) {
    webViewManager.destroy();
    webViewManager = null;
  }

  // Reset window state
  closeCoordinator.unbindWindow();
  rendererAppCommands.markNotReady('window-all-closed');
  win = null;
  isWindowReady = false;
  appShellReadinessGate.markDocumentLoading();
  protocolUrlQueue = [];

  app.quit();
});

// ==================== app activate event ====================
app.on('activate', async () => {
  const allWindows = BrowserWindow.getAllWindows();
  log.info('activate', allWindows.length);

  if (allWindows.length) {
    allWindows[0].focus();
  } else {
    const backendStart = checkAndStartBackend();
    await createWindow();
    const result = await backendStart;
    if (!result.success) {
      log.warn('Backend start during app activation failed:', result.error);
    } else {
      notifyBackendReady(result);
    }
  }
});

// ==================== app exit event ====================
app.on('before-quit', async (event) => {
  log.info('before-quit');
  log.info('quit python_process.pid: ' + python_process?.pid);

  if (!closeCoordinator.isAppQuitInProgress()) {
    event.preventDefault();
    closeCoordinator.request('quit-app');
    return;
  }

  closeCoordinator.markAppQuitInProgress();
  // Keep the Session windows' saved list as it is at quit, then close them.
  sessionWindows?.markQuitting();
  for (const sessionWindow of sessionWindows?.allWindows() ?? [])
    sessionWindow.destroy();

  // Stop CDP health-check polling
  stopCdpHealthCheck();

  // Prevent default quit to ensure cleanup completes
  event.preventDefault();

  if (isQuitCleanupInProgress) return;
  isQuitCleanupInProgress = true;

  try {
    // NOTE: Profile sync removed - we now use app userData directly for all partitions
    // No need to sync between different profile directories

    // Clean up resources
    await disposeAllTerminals();
    brainStreamRelay?.closeAll();

    if (webViewManager) {
      webViewManager.destroy();
      webViewManager = null;
    }

    if (win && !win.isDestroyed()) {
      persistBrowserWindowState(win);
      win.destroy();
      win = null;
    }

    // Wait for Python process cleanup
    await cleanupPythonProcess();
    await closeWorkspaceSecretBroker();

    // Clean up file reader if exists
    if (fileReader) {
      fileReader = null;
    }

    // Clear any remaining timeouts/intervals
    if (global.gc) {
      global.gc();
    }

    // Reset protocol handling state
    isWindowReady = false;
    appShellReadinessGate.markDocumentLoading();
    rendererAppCommands.markNotReady('app-exit');
    protocolUrlQueue = [];

    log.info('All cleanup completed, exiting...');
  } catch (error) {
    log.error('Error during cleanup:', error);
  } finally {
    // Force quit after cleanup
    app.exit(0);
  }
});