Ctrl K
ComunidadGuías…

main.js

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

Archivo original ↗Preguntar a KiBot
// Chromium picks the display backend before JS runs, so appendSwitch is too
// late — the flag has to come from a relaunch.
const { XWAYLAND_FLAG, shouldForceXWayland } = require("./src/helpers/xwayland");
const { createHotkeyRepeatGate } = require("./src/helpers/hotkeyRepeatGate");
const { shouldDisableGpuCompositing } = require("./src/helpers/linuxGpuCompositing");

if (shouldForceXWayland(process.argv)) {
  const { spawn } = require("child_process");
  spawn(process.execPath, [...process.argv.slice(1), XWAYLAND_FLAG], {
    stdio: "inherit",
    detached: true,
  }).unref();
  process.exit(0);
}

const {
  app,
  desktopCapturer,
  globalShortcut,
  BrowserWindow,
  dialog,
  ipcMain,
  net,
  session,
  systemPreferences,
} = require("electron");
const path = require("path");
const http = require("http");
const tls = require("tls");
require("dotenv").config({ path: path.join(__dirname, ".env") });

// Extend Node's TLS trust with the OS store so ws and https.get see corporate
// CAs that Chromium already trusts.
try {
  const currentCAs = tls.getCACertificates();
  const systemCAs = tls.getCACertificates("system");
  if (systemCAs?.length) {
    tls.setDefaultCACertificates([...currentCAs, ...systemCAs]);
  }
} catch (err) {
  require("./src/helpers/debugLogger").warn("System CA merge failed; using existing CA list", {
    error: err?.message,
  });
}

const VALID_CHANNELS = new Set(["development", "staging", "production"]);
const DEFAULT_OAUTH_PROTOCOL_BY_CHANNEL = {
  development: "openwhispr-dev",
  staging: "openwhispr-staging",
  production: "openwhispr",
};
const BASE_WINDOWS_APP_ID = "com.gizmolabs.openwhispr";
const DEFAULT_AUTH_BRIDGE_PORT = 5199;

function isElectronBinaryExec() {
  const execPath = (process.execPath || "").toLowerCase();
  return (
    execPath.includes("/electron.app/contents/macos/electron") ||
    execPath.endsWith("/electron") ||
    execPath.endsWith("\\electron.exe")
  );
}

function inferDefaultChannel() {
  if (process.env.NODE_ENV === "development" || process.defaultApp || isElectronBinaryExec()) {
    return "development";
  }
  return "production";
}

function resolveAppChannel() {
  const rawChannel = (process.env.OPENWHISPR_CHANNEL || process.env.VITE_OPENWHISPR_CHANNEL || "")
    .trim()
    .toLowerCase();

  if (VALID_CHANNELS.has(rawChannel)) {
    return rawChannel;
  }

  return inferDefaultChannel();
}

const APP_CHANNEL = resolveAppChannel();
process.env.OPENWHISPR_CHANNEL = APP_CHANNEL;

function configureChannelUserDataPath() {
  if (APP_CHANNEL === "production") {
    return;
  }

  const isolatedPath = path.join(app.getPath("appData"), `OpenWhispr-${APP_CHANNEL}`);
  app.setPath("userData", isolatedPath);
}

configureChannelUserDataPath();

// Load userData .env (contains DICTATION_KEY, API keys, etc.) early — before
// hotkey registration, which needs DICTATION_KEY before the renderer loads.
require("dotenv").config({
  path: path.join(app.getPath("userData"), ".env"),
  override: false,
});

// Chromium's Windows-only occlusion tracker misclassifies the always-on-top
// transparent pill as occluded, throttling its renderer and jittering animations.
if (process.platform === "win32") {
  app.commandLine.appendSwitch("disable-features", "CalculateNativeWinOcclusion");
}

// Fix transparent window flickering on Linux: --enable-transparent-visuals requires
// the compositor to set up an ARGB visual before any windows are created.
if (process.platform === "linux") {
  app.commandLine.appendSwitch("gtk-version", "3");
  app.commandLine.appendSwitch("enable-transparent-visuals");
}

// Linux composites on the GPU except while an NVIDIA driver is loaded (#203). Anyone else whose
// transparent windows flicker can add --disable-gpu-compositing to the launcher's flags file
// (scripts/lib/linux-launcher.js).
const gpuCompositingDisabledForNvidia = shouldDisableGpuCompositing();
if (gpuCompositingDisabledForNvidia) {
  app.commandLine.appendSwitch("disable-gpu-compositing");
}

// Wayland: packaged builds use the wrapper script (scripts/afterPack.js) to
// force --ozone-platform=x11 before Electron starts. appendSwitch below is a
// best-effort fallback for unpackaged dev mode (may not take effect on E39+).
if (process.platform === "linux" && process.env.XDG_SESSION_TYPE === "wayland") {
  app.commandLine.appendSwitch("enable-features", "WaylandWindowDecorations");
}

// Set desktop filename so Wayland compositors can match windows to the .desktop entry.
// This allows XDG portals (e.g. PipeWire) to persist permissions across sessions.
if (process.platform === "linux") {
  app.setDesktopName("open-whispr.desktop");
}

// Group all windows under single taskbar entry on Windows
if (process.platform === "win32") {
  const windowsAppId =
    APP_CHANNEL === "production" ? BASE_WINDOWS_APP_ID : `${BASE_WINDOWS_APP_ID}.${APP_CHANNEL}`;
  app.setAppUserModelId(windowsAppId);
}

function getOAuthProtocol() {
  const fromEnv = (process.env.VITE_OPENWHISPR_PROTOCOL || process.env.OPENWHISPR_PROTOCOL || "")
    .trim()
    .toLowerCase();

  if (/^[a-z][a-z0-9+.-]*$/.test(fromEnv)) {
    return fromEnv;
  }

  return (
    DEFAULT_OAUTH_PROTOCOL_BY_CHANNEL[APP_CHANNEL] || DEFAULT_OAUTH_PROTOCOL_BY_CHANNEL.production
  );
}

const OAUTH_PROTOCOL = getOAuthProtocol();

const { registerLinuxUrlSchemeHandler } = require("./src/helpers/linuxUrlSchemeHandler");

function shouldRegisterProtocolWithAppArg() {
  return Boolean(process.defaultApp) || isElectronBinaryExec();
}

function getDefaultHtmlHandler() {
  try {
    const { execFileSync } = require("child_process");
    return (
      execFileSync("xdg-mime", ["query", "default", "text/html"], {
        encoding: "utf8",
        timeout: 3000,
      }).trim() || null
    );
  } catch {
    return null;
  }
}

function restoreHtmlHandlerIfChanged(original) {
  try {
    const { execFileSync } = require("child_process");
    const current = execFileSync("xdg-mime", ["query", "default", "text/html"], {
      encoding: "utf8",
      timeout: 3000,
    }).trim();
    if (current && current !== original) {
      execFileSync("xdg-mime", ["default", original, "text/html"], { timeout: 3000 });
    }
  } catch {
    // xdg-mime unavailable or failed
  }
}

// True source of truth for whether openwhispr:// resolves on Linux — the same
// MIME database xdg-open consults. Returns true for deb/rpm/flatpak/AUR installs
// (scheme registered via the packaged .desktop MimeType; registerLinuxUrlSchemeHandler
// first takes it back from an AppImage/tar.gz entry) and false for AppImage/tar.gz
// runs whose own registration failed, so we never enable a dead-end OAuth flow.
// Used to recover from setAsDefaultProtocolClient's KDE false negative.
function isOAuthSchemeRegistered() {
  if (process.platform !== "linux") return false;
  try {
    const { execFileSync } = require("child_process");
    const handler = execFileSync(
      "xdg-mime",
      ["query", "default", `x-scheme-handler/${OAUTH_PROTOCOL}`],
      { encoding: "utf8", timeout: 3000 }
    ).trim();
    return handler.length > 0;
  } catch {
    return false;
  }
}

// In development, always include the app path argument so macOS/Windows/Linux
// can launch the project app instead of opening bare Electron.
function getProtocolAppArgs() {
  if (!shouldRegisterProtocolWithAppArg()) return [];
  return [process.argv[1] ? path.resolve(process.argv[1]) : path.resolve(".")];
}

// Register custom protocol for OAuth callbacks.
function registerOpenWhisprProtocol() {
  const protocol = OAUTH_PROTOCOL;
  if (process.platform === "darwin" && app.isDefaultProtocolClient(protocol)) return true;
  const htmlHandler = process.platform === "linux" ? getDefaultHtmlHandler() : null;
  const appArgs = getProtocolAppArgs();

  let result;
  if (appArgs.length > 0) {
    result = app.setAsDefaultProtocolClient(protocol, process.execPath, appArgs);
  } else {
    result = app.setAsDefaultProtocolClient(protocol);
  }

  if (htmlHandler) {
    restoreHtmlHandlerIfChanged(htmlHandler);
  }

  return result;
}

// On Linux, setAsDefaultProtocolClient can only name open-whispr.desktop, which
// AppImage and tar.gz installs don't have, so those (and development) register
// their own handler entry first and skip it. Otherwise it runs as before, and
// since it returns a false negative on KDE/Wayland, fall back to probing the
// system MIME database for an actual handler. This keeps OAuth enabled where the
// callback can resolve and correctly gated where it can't.
const linuxSchemeHandler =
  process.platform === "linux"
    ? registerLinuxUrlSchemeHandler(OAUTH_PROTOCOL, getProtocolAppArgs())
    : null;
const protocolRegistered =
  linuxSchemeHandler?.registered || registerOpenWhisprProtocol() || isOAuthSchemeRegistered();
if (!protocolRegistered) {
  console.warn(`[Auth] Failed to register ${OAUTH_PROTOCOL}:// protocol handler`);
}

const gotSingleInstanceLock = app.requestSingleInstanceLock();

if (!gotSingleInstanceLock) {
  console.warn(
    "[Startup] Could not acquire the single-instance lock; another OpenWhispr instance may already be running. Exiting."
  );
  app.exit(0);
}

const isLiveWindow = (window) => window && !window.isDestroyed();

// Ensure macOS menus use the proper casing for the app name
if (process.platform === "darwin" && app.getName() !== "OpenWhispr") {
  app.setName("OpenWhispr");
}

// Add global error handling for uncaught exceptions
process.on("uncaughtException", (error) => {
  console.error("Uncaught Exception:", error);
  // Don't exit the process for EPIPE errors as they're harmless
  if (error.code === "EPIPE") {
    return;
  }
  // For other errors, log and continue
  console.error("Error stack:", error.stack);
});

process.on("unhandledRejection", (reason, promise) => {
  console.error("Unhandled Rejection at:", promise, "reason:", reason);
});

// Import helper module classes (but don't instantiate yet - wait for app.whenReady())
const EnvironmentManager = require("./src/helpers/environment");
const WindowManager = require("./src/helpers/windowManager");
const DatabaseManager = require("./src/helpers/database");
const ClipboardManager = require("./src/helpers/clipboard");
const WhisperManager = require("./src/helpers/whisper");
const ParakeetManager = require("./src/helpers/parakeet");
const DiarizationManager = require("./src/helpers/diarization");
const TrayManager = require("./src/helpers/tray");
const dockManager = require("./src/helpers/dockManager");
const autoStart = require("./src/helpers/autoStart");
const IPCHandlers = require("./src/helpers/ipcHandlers");
const CliBridge = require("./src/helpers/cliBridge");
const { createRegistryLoader } = require("./src/helpers/registryLoader");
const { registerRegistryIpc } = require("./src/helpers/registryIpc");
const { replaceRegistryData, bundledRegistryData } = require("./src/models/registryData.js");
const { configureInstalledModels, withInstalledModels } = require("./src/helpers/installedModels");
const { withTrustedDownloads } = require("./src/helpers/registryDownloadTrust");
const registryTrustedKeys = require("./src/models/registryTrustedKeys.json");
let registryLoader = null;
let disposeRegistryIpc = null;
const UpdateManager = require("./src/updater");
const GlobeKeyManager = require("./src/helpers/globeKeyManager");
const DevServerManager = require("./src/helpers/devServerManager");
const WindowsKeyManager = require("./src/helpers/windowsKeyManager");
const LinuxKeyManager = require("./src/helpers/linuxKeyManager");
const TextEditMonitor = require("./src/helpers/textEditMonitor");
const SelectionManager = require("./src/helpers/selectionManager");
const { PermissionGuideManager } = require("./src/helpers/permissionGuideManager");
const { AgentWindowManager } = require("./src/helpers/agentWindowManager");
const WhisperCudaManager = require("./src/helpers/whisperCudaManager");
const WhisperVulkanManager = require("./src/helpers/whisperVulkanManager");
const {
  migrateLegacyBinDir,
  detectOrphanedGpuPacks,
  detectOutdatedGpuPacks,
} = require("./src/helpers/gpuBinaryManager");
const { resetWhisperGpuFailureOnUpgrade } = require("./src/helpers/whisperGpuUpgradeReset");
const GoogleCalendarManager = require("./src/helpers/googleCalendarManager");
const MicrosoftCalendarManager = require("./src/helpers/microsoftCalendarManager");
const AppleCalendarManager = require("./src/helpers/appleCalendarManager");
const CalendarReminderScheduler = require("./src/helpers/calendarReminderScheduler");
const MeetingProcessDetector = require("./src/helpers/meetingProcessDetector");
const AudioActivityDetector = require("./src/helpers/audioActivityDetector");
const {
  collectAudioCaptureHelperPids,
  createExcludedProcessIdProvider,
} = require("./src/helpers/electronProcessIds");
const AudioTapManager = require("./src/helpers/audioTapManager");
const LinuxPortalAudioManager = require("./src/helpers/linuxPortalAudioManager");
const WindowsLoopbackAudioManager = require("./src/helpers/windowsLoopbackAudioManager");
const MeetingAecManager = require("./src/helpers/meetingAecManager");
const MeetingDetectionEngine = require("./src/helpers/meetingDetectionEngine");
const { applyOpenWhisprOriginHeader } = require("./src/helpers/sessionHeaders");
const { i18nMain, changeLanguage } = require("./src/helpers/i18nMain");
const { ensureYdotool } = require("./src/helpers/ensureYdotool");
const sidecarRegistry = require("./src/helpers/sidecarRegistry");
const { reapStaleSidecars } = require("./src/helpers/sidecarReaper");
const { resolvePrewarmModel } = require("./src/helpers/localServerPolicy");

// Manager instances - initialized after app.whenReady()
let debugLogger = null;
let environmentManager = null;
let windowManager = null;
let hotkeyManager = null;
let databaseManager = null;
let clipboardManager = null;
let connectorManager = null;
let whisperManager = null;
let parakeetManager = null;
let diarizationManager = null;
let trayManager = null;
let updateManager = null;
let globeKeyManager = null;
let windowsKeyManager = null;
let linuxKeyManager = null;
let textEditMonitor = null;
let selectionManager = null;
let whisperCudaManager = null;
let whisperVulkanManager = null;
let googleCalendarManager = null;
let microsoftCalendarManager = null;
let appleCalendarManager = null;
let calendarReminderScheduler = null;
let meetingDetectionEngine = null;
let audioTapManager = null;
let linuxPortalAudioManager = null;
let windowsLoopbackAudioManager = null;
let meetingAecManager = null;
let semanticSearch = null;
let ipcHandlers = null;
let cliBridge = null;
let globeKeyAlertShown = false;
let macAccessibilityFeaturesReady = false;
let startMacAccessibilityFeatures = null;
let authBridgeServer = null;
let pendingNoteCloudId = null;
let pendingNoteRetryTimer = null;
let pendingNoteRetryCount = 0;
const WHISPER_WAKE_REWARM_DELAY_MS = 3000;
let wakeRewarmTimer = null;

function parseAuthBridgePort() {
  const raw = (process.env.OPENWHISPR_AUTH_BRIDGE_PORT || "").trim();
  if (!raw) return DEFAULT_AUTH_BRIDGE_PORT;

  const parsed = Number(raw);
  if (!Number.isInteger(parsed) || parsed < 1 || parsed > 65535) {
    return DEFAULT_AUTH_BRIDGE_PORT;
  }

  return parsed;
}

const AUTH_BRIDGE_HOST = "127.0.0.1";
const AUTH_BRIDGE_PORT = parseAuthBridgePort();
const AUTH_BRIDGE_PATH = "/oauth/callback";

// Set up PATH for production builds to find system tools (whisper.cpp, ffmpeg)
function setupProductionPath() {
  if (process.platform === "darwin" && process.env.NODE_ENV !== "development") {
    const commonPaths = [
      "/usr/local/bin",
      "/opt/homebrew/bin",
      "/usr/bin",
      "/bin",
      "/usr/sbin",
      "/sbin",
    ];

    const currentPath = process.env.PATH || "";
    const pathsToAdd = commonPaths.filter((p) => !currentPath.includes(p));

    if (pathsToAdd.length > 0) {
      process.env.PATH = `${currentPath}:${pathsToAdd.join(":")}`;
    }
  }
}

// Phase 1: Initialize managers + IPC handlers before window content loads
// Best-effort cleanup of the orphaned portal restore-token file older builds wrote. See PR #904.
const LINUX_RESTORE_TOKEN_FILENAME = ".linux-system-audio-restore-token.json";

function cleanupOrphanedLinuxRestoreToken() {
  if (process.platform !== "linux") return;
  try {
    const fs = require("fs");
    fs.unlinkSync(path.join(app.getPath("userData"), LINUX_RESTORE_TOKEN_FILENAME));
  } catch {}
}

function syncAutoStartEntry() {
  try {
    if (autoStart.syncAutoStartEntry()) {
      debugLogger.info("Re-pointed the launch-at-login entry at the current executable");
    }
  } catch (error) {
    debugLogger.warn("Failed to sync the launch-at-login entry", { error: error?.message });
  }
}

// Reading the login item touches the OS, and failing to answer "did the session
// start us?" must not stop the app from starting at all. Falling back to false
// just shows the window, which is what every launch did before.
function wasLaunchedAtLoginHidden() {
  try {
    return autoStart.wasLaunchedAtLoginHidden();
  } catch (error) {
    if (debugLogger) debugLogger.warn("Failed to detect a login launch", { error: error?.message });
    return false;
  }
}

function initializeCoreManagers() {
  setupProductionPath();

  debugLogger = require("./src/helpers/debugLogger");
  debugLogger.ensureFileLogging();
  if (process.platform === "linux") {
    // The compositing mode is settled once the GPU process has reported its info; a GPU
    // process crash can still drop it to software later, logged below.
    app
      .getGPUInfo("basic")
      .catch(() => {})
      .then(() => {
        debugLogger.info("Linux GPU compositing", {
          status: app.getGPUFeatureStatus().gpu_compositing,
          disabledForNvidia: gpuCompositingDisabledForNvidia,
        });
      });
  }
  app.on("child-process-gone", (_event, details) => {
    if (details.type !== "GPU") return;
    debugLogger.warn("GPU process gone", { reason: details.reason, exitCode: details.exitCode });
  });
  // Registration runs before app ready, when the logger cannot write its file yet.
  if (linuxSchemeHandler?.reason) {
    debugLogger.warn("Could not register the Linux URL scheme handler entry", {
      protocol: OAUTH_PROTOCOL,
      reason: linuxSchemeHandler.reason,
      protocolRegistered,
    });
  }

  environmentManager = new EnvironmentManager();
  const uiLanguage = environmentManager.getUiLanguage(app.getLocale());
  changeLanguage(uiLanguage);
  debugLogger.refreshLogLevel();

  windowManager = new WindowManager();
  hotkeyManager = windowManager.hotkeyManager;
  databaseManager = new DatabaseManager();
  // Restore the last validated account scope before any window, IPC handler,
  // or meeting flow can read or create notes. Offline launches keep the
  // account's data visible; a stale or rotated credential fails the hash
  // check and restores nothing.
  const accountScopeBinding = require("./src/helpers/accountScopeBinding");
  const bootAccountId = accountScopeBinding.resolveBootAccountScope({
    token: require("./src/helpers/tokenStore").get(),
    binding: accountScopeBinding.read(),
  });
  if (bootAccountId) databaseManager.setActiveAccountId(bootAccountId);
  clipboardManager = new ClipboardManager();
  const { createConnectorManager } = require("./src/helpers/connectors/connectorManager");
  const { createPendingActions } = require("./src/helpers/connectors/pendingActions");
  const { createActionLog } = require("./src/helpers/connectors/actionLog");
  const { createCredentialStore } = require("./src/helpers/connectors/credentialStore");
  const { createConnectorCredentials } = require("./src/helpers/connectors/connectorCredentials");
  const { createConnectors } = require("./src/helpers/connectors/createConnectors");
  const { runOAuthLoopbackFlow, OAuthFlowError } = require("./src/helpers/oauthLoopbackFlow");
  const { broadcastToWindows } = require("./src/helpers/windowBroadcast");
  const { connectorAccountIdFrom } = require("./src/helpers/connectors/connectorIpc");
  // The same account receipts are filed under (ipcHandlers' getAccountScope):
  // the one bound to the credential in use. databaseManager's scope doesn't
  // move when one signed-in token replaces another.
  const getConnectorAccountScope = () =>
    accountScopeBinding.resolveActiveAccountScope({
      ...require("./src/helpers/tokenStore").getState(),
      binding: accountScopeBinding.read(),
    });
  const getConnectorAccountId = connectorAccountIdFrom(getConnectorAccountScope);
  const connectorCredentials = createConnectorCredentials({
    store: createCredentialStore({
      dir: path.join(app.getPath("userData"), "connectors"),
      secretCrypto: require("./src/helpers/secretCrypto"),
      logger: debugLogger,
    }),
    getAccountId: getConnectorAccountId,
  });
  connectorManager = createConnectorManager({
    connectors: createConnectors({
      fetch: (url, init) => net.fetch(url, { ...init, useSessionCookies: false }),
      i18n: i18nMain,
      runOAuthLoopbackFlow,
      OAuthFlowError,
      credentials: connectorCredentials,
      logger: debugLogger,
      env: process.env,
      openExternal: (url) => require("./src/helpers/externalUrlOpener").openExternalUrl(url),
      writeClipboard: (text, webContents) => clipboardManager.writeClipboard(text, webContents),
      // Read only when revoking a Gmail login on the calendar's Google project;
      // the calendar manager is created below.
      getGoogleCalendarAccounts: () => googleCalendarManager?.getAccounts() ?? [],
      broadcast: broadcastToWindows,
      notifyStatusChanged: () => void connectorManager?.notifyStatusChanged(),
    }),
    pendingActions: createPendingActions(),
    actionLog: createActionLog(databaseManager),
    logger: debugLogger,
    getAccountId: getConnectorAccountId,
    credentials: connectorCredentials,
    onStatusChanged: (statuses) => broadcastToWindows("connector-status-changed", statuses),
  });
  // Pending cards expire in main even when no renderer ever answers them.
  setInterval(() => connectorManager.sweepExpired(), 60 * 1000).unref();
  whisperManager = new WhisperManager();
  if (process.platform !== "darwin") {
    whisperCudaManager = new WhisperCudaManager();
    whisperVulkanManager = new WhisperVulkanManager();
    // Heal installs from before GPU packs got per-pack directories; must run
    // before startup pre-warm resolves any GPU binary path.
    const LlamaVulkanManager = require("./src/helpers/llamaVulkanManager");
    const llamaVulkanManager = new LlamaVulkanManager();
    const clearedPacks = migrateLegacyBinDir([
      whisperCudaManager,
      whisperVulkanManager,
      llamaVulkanManager,
    ]);
    if (clearedPacks.length > 0) {
      // No window exists yet — persist the notice; the window that shows it
      // as a toast takes it (#1606, #2554).
      require("./src/helpers/gpuPackMigrationNotice").record(clearedPacks);
    }
    // The 1.8.3 migration deleted lib-carrying packs without recording that
    // notice, leaving those users on a silent CPU fallback: an enabled flag
    // with no pack on disk only happens via such data loss. recordOnce gates
    // each pack to one notice so a dismissed toast doesn't return every launch.
    const gpuPacks = [
      {
        manager: whisperCudaManager,
        enabledEnvVar: "WHISPER_CUDA_ENABLED",
        group: "whisper",
        backend: "cuda",
      },
      {
        manager: whisperVulkanManager,
        enabledEnvVar: "WHISPER_VULKAN_ENABLED",
        group: "whisper",
        backend: "vulkan",
      },
      { manager: llamaVulkanManager, enabledEnvVar: "LLAMA_VULKAN_ENABLED" },
    ];
    // A pack whose backend failed here and then vanished (a distro launcher
    // deletes it, #2168) was no loss: drop its flag as a delete would, so no
    // notice names it, even after an upgrade clears the remembered failure.
    const failedBackends = WhisperManager.resolveFailedGpuBackends(process.env.WHISPER_GPU_FAILED);
    for (const { manager, enabledEnvVar, backend } of gpuPacks) {
      if (
        failedBackends.includes(backend) &&
        process.env[enabledEnvVar] === "true" &&
        !manager.isDownloaded() &&
        !manager.needsUpdate()
      ) {
        delete process.env[enabledEnvVar];
        environmentManager.removeKeyFromEnvFile(enabledEnvVar).catch((err) => {
          debugLogger.error("Failed to clear a vanished GPU pack's flag from .env", {
            key: enabledEnvVar,
            error: err.message,
          });
        });
      }
    }
    const orphanedPacks = detectOrphanedGpuPacks(gpuPacks);
    if (orphanedPacks.length > 0) {
      require("./src/helpers/gpuPackMigrationNotice").recordOnce(orphanedPacks);
    }
    // A pack an older release installed that this version can't use (#2424)
    // otherwise looks like a pack that was never downloaded: say so once per
    // app version, even to a user who already saw the orphan notice for it.
    // Only whisper packs can be outdated, so only a user on local whisper
    // (the only mode whose Settings shows the pack) is told.
    const outdatedPacks = detectOutdatedGpuPacks(gpuPacks);
    if (outdatedPacks.length > 0) {
      const whisperInUse = !!process.env.LOCAL_WHISPER_MODEL;
      debugLogger.info("GPU packs from an older release need re-downloading", {
        packs: outdatedPacks,
        notified: whisperInUse,
      });
      if (whisperInUse) {
        require("./src/helpers/gpuPackMigrationNotice").recordOnce(
          outdatedPacks,
          `outdated-${app.getVersion()}`
        );
      }
    }
    // Lets every server start resolve its GPU backend from installed packs
    whisperManager.setGpuBinaryManagers({ cuda: whisperCudaManager, vulkan: whisperVulkanManager });
  }
  parakeetManager = new ParakeetManager();
  diarizationManager = new DiarizationManager();
  calendarReminderScheduler = new CalendarReminderScheduler(databaseManager);
  googleCalendarManager = new GoogleCalendarManager(
    databaseManager,
    windowManager,
    calendarReminderScheduler
  );
  microsoftCalendarManager = new MicrosoftCalendarManager(
    databaseManager,
    calendarReminderScheduler
  );
  appleCalendarManager = new AppleCalendarManager(databaseManager, calendarReminderScheduler);
  const meetingProcessDetector = new MeetingProcessDetector();
  meetingDetectionEngine = new MeetingDetectionEngine(
    calendarReminderScheduler,
    meetingProcessDetector,
    new AudioActivityDetector(
      // The capture-helper managers are created a few lines below; the provider
      // is only invoked on mic events, long after initialization completes.
      createExcludedProcessIdProvider(() =>
        collectAudioCaptureHelperPids([
          audioTapManager,
          linuxPortalAudioManager,
          windowsLoopbackAudioManager,
        ])
      ),
      () => meetingProcessDetector.getDetectedProcesses().length > 0
    ),
    windowManager,
    databaseManager
  );
  windowManager.meetingDetectionEngine = meetingDetectionEngine;
  calendarReminderScheduler.meetingDetectionEngine = meetingDetectionEngine;
  updateManager = new UpdateManager();
  updateManager.setWindowManager(windowManager);
  windowsKeyManager = new WindowsKeyManager();
  linuxKeyManager = new LinuxKeyManager();
  textEditMonitor = new TextEditMonitor();
  selectionManager = new SelectionManager({ clipboardManager, textEditMonitor });
  audioTapManager = new AudioTapManager();
  linuxPortalAudioManager = new LinuxPortalAudioManager();
  windowsLoopbackAudioManager = new WindowsLoopbackAudioManager();
  // Warm the capability cache off the hot path so the first meeting start
  // doesn't pay the probe spawn. No-ops on non-Windows.
  windowsLoopbackAudioManager.getCapability().catch(() => {});
  cleanupOrphanedLinuxRestoreToken();
  syncAutoStartEntry();
  meetingAecManager = new MeetingAecManager();
  windowManager.textEditMonitor = textEditMonitor;
  windowManager.selectionManager = selectionManager;
  windowManager.windowsKeyManager = windowsKeyManager;
  windowManager.linuxKeyManager = linuxKeyManager;
  windowManager.permissionGuide = new PermissionGuideManager(windowManager);
  windowManager.agentWindow = new AgentWindowManager(windowManager);
  if (process.platform === "linux") {
    windowManager.hotkeyManager.nativeListenerProbe = () => linuxKeyManager.checkAvailability();
  } else if (process.platform === "win32") {
    windowManager.hotkeyManager.nativeListenerProbe = () => windowsKeyManager.checkAvailability();
  }

  // IPC handlers must be registered before window content loads
  ipcHandlers = new IPCHandlers({
    environmentManager,
    databaseManager,
    clipboardManager,
    whisperManager,
    parakeetManager,
    diarizationManager,
    windowManager,
    updateManager,
    windowsKeyManager,
    linuxKeyManager,
    textEditMonitor,
    selectionManager,
    whisperCudaManager,
    whisperVulkanManager,
    googleCalendarManager,
    microsoftCalendarManager,
    appleCalendarManager,
    connectorManager,
    meetingDetectionEngine,
    audioTapManager,
    linuxPortalAudioManager,
    windowsLoopbackAudioManager,
    meetingAecManager,
    getSemanticSearch: () => semanticSearch,
    getTrayManager: () => trayManager,
    oauthProtocolRegistered: protocolRegistered,
    oauthProtocol: OAUTH_PROTOCOL,
  });
}

function registerSidecars() {
  if (whisperManager) sidecarRegistry.register("whisper", () => whisperManager.stopServer());
  if (parakeetManager) sidecarRegistry.register("parakeet", () => parakeetManager.stopServer());
  if (diarizationManager) {
    sidecarRegistry.register("diarization", () => diarizationManager.shutdown());
  }
  const modelManager = require("./src/helpers/modelManagerBridge").default;
  sidecarRegistry.register("llama", () => modelManager.stopServer());
  const onnxWorkerClient = require("./src/helpers/onnxWorkerClient");
  sidecarRegistry.register("onnx", () => onnxWorkerClient.stop());
}

// Phase 2: Non-critical setup after windows are visible
function initializeDeferredManagers() {
  ensureYdotool().catch((err) => {
    require("./src/helpers/debugLogger").warn(
      "ydotool setup error",
      { error: err?.message },
      "clipboard"
    );
  });
  if (process.platform !== "darwin") {
    clipboardManager.preWarmAccessibility();
  }
  trayManager = new TrayManager();
  globeKeyManager = new GlobeKeyManager({
    // Lets the listener put the user's macOS Globe action back after a crash.
    preferenceStatePath: path.join(app.getPath("userData"), "globe-preference-state.json"),
  });

  if (process.platform === "darwin") {
    globeKeyManager.on("error", (error) => {
      if (globeKeyAlertShown) {
        return;
      }
      globeKeyAlertShown = true;

      const detailLines = [
        error?.message || i18nMain.t("startup.globeHotkey.details.unknown"),
        i18nMain.t("startup.globeHotkey.details.fallback"),
      ];

      if (process.env.NODE_ENV === "development") {
        detailLines.push(i18nMain.t("startup.globeHotkey.details.devHint"));
      } else {
        detailLines.push(i18nMain.t("startup.globeHotkey.details.reinstallHint"));
      }

      dialog.showMessageBox({
        type: "warning",
        title: i18nMain.t("startup.globeHotkey.title"),
        message: i18nMain.t("startup.globeHotkey.message"),
        detail: detailLines.join("\n\n"),
      });
    });
  }

  googleCalendarManager.start();
  microsoftCalendarManager.start();
  appleCalendarManager.start();
  meetingDetectionEngine.start();
}

let pendingEnterpriseAudioTest = null;
// The control panel acts on each link once, so a link it can't use never
// reopens Settings on a later mount; Settings still reads it until consumed.
let enterpriseAudioTestClaimed = false;
ipcMain.handle("get-pending-enterprise-audio-test", (_event, consume = false) => {
  const pending = pendingEnterpriseAudioTest;
  if (consume) pendingEnterpriseAudioTest = null;
  return pending;
});
ipcMain.handle("claim-enterprise-audio-test", () => {
  if (enterpriseAudioTestClaimed) return null;
  enterpriseAudioTestClaimed = true;
  return pendingEnterpriseAudioTest;
});
function handleEnterpriseAudioLink(url) {
  const request = require("./src/helpers/enterpriseAudioDeepLink").parseEnterpriseAudioDeepLink(
    url,
    OAUTH_PROTOCOL
  );
  if (!request) return false;
  pendingEnterpriseAudioTest = request;
  enterpriseAudioTestClaimed = false;
  void windowManager?.showControlPanel();
  windowManager?.controlPanelWindow?.webContents.send("enterprise-audio-test-requested");
  return true;
}

app.on("open-url", (event, url) => {
  event.preventDefault();
  if (!url.startsWith(`${OAUTH_PROTOCOL}://`)) return;

  if (url.includes("upgrade-success")) {
    handleUpgradeDeepLink();
    return;
  }

  if (handleEnterpriseAudioLink(url)) return;

  if (isNoteDeepLink(url)) {
    void handleNoteDeepLink(url);
    return;
  }

  if (isInvitationDeepLink(url)) {
    handleInvitationDeepLink(url);
    return;
  }

  void handleOAuthDeepLink(url);

  windowManager?.showControlPanel();
});

function isInvitationDeepLink(url) {
  return url.slice(`${OAUTH_PROTOCOL}://`.length).startsWith("invitations/");
}

// Deep links can arrive before windowManager exists (cold start) or before the
// renderer has mounted its listener. The token is stashed here and the renderer
// pulls it via `get-pending-invitation-token` on mount; the push below is a
// best-effort fast path for an already-running app.
let pendingInvitationDeepLinkToken = null;

ipcMain.handle("get-pending-invitation-token", () => {
  const token = pendingInvitationDeepLinkToken;
  pendingInvitationDeepLinkToken = null;
  return token;
});

function isNoteDeepLink(url) {
  return url.slice(`${OAUTH_PROTOCOL}://`.length).startsWith("notes/");
}

function parseNoteCloudId(deepLinkUrl) {
  try {
    const match = deepLinkUrl.match(/notes\/([^/?#]+)/);
    const cloudId = match?.[1] ? decodeURIComponent(match[1]) : "";
    return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(cloudId)
      ? cloudId
      : null;
  } catch {
    return null;
  }
}

function clearPendingNoteDeepLink() {
  clearTimeout(pendingNoteRetryTimer);
  pendingNoteRetryTimer = null;
  pendingNoteCloudId = null;
  pendingNoteRetryCount = 0;
}

async function flushPendingNoteDeepLink() {
  if (!pendingNoteCloudId || !windowManager || !databaseManager) return;

  try {
    // Surface the panel on the first attempt only; retries just poll the
    // database so they can't repeatedly steal focus.
    if (pendingNoteRetryCount === 0) {
      await windowManager.createControlPanelWindow();
    }

    const note = databaseManager.getNoteByCloudId(pendingNoteCloudId);
    if (!note) {
      // Cloud sync may still be hydrating during a cold launch. Retry briefly so
      // the handoff can resolve a note pulled after the protocol event arrived.
      pendingNoteRetryCount += 1;
      if (pendingNoteRetryCount <= 10) {
        clearTimeout(pendingNoteRetryTimer);
        pendingNoteRetryTimer = setTimeout(() => {
          void flushPendingNoteDeepLink();
        }, 1000);
      } else {
        console.warn("Note deep link could not resolve a local note", {
          cloudId: pendingNoteCloudId,
        });
        clearPendingNoteDeepLink();
      }
      return;
    }

    const payload = { noteId: note.id, folderId: note.folder_id ?? null };
    clearPendingNoteDeepLink();
    await windowManager.queueNoteNavigation(payload);
  } catch (error) {
    console.error("Note deep link failed:", error);
    clearPendingNoteDeepLink();
  }
}

async function handleNoteDeepLink(deepLinkUrl) {
  const cloudId = parseNoteCloudId(deepLinkUrl);
  if (!cloudId) {
    console.warn("Invalid note deep link");
    return;
  }

  clearPendingNoteDeepLink();
  pendingNoteCloudId = cloudId;
  await flushPendingNoteDeepLink();
}

function handleInvitationDeepLink(deepLinkUrl) {
  try {
    const match = deepLinkUrl.match(/invitations\/([^/?#]+)/);
    const token = match?.[1];
    if (!token) return;
    pendingInvitationDeepLinkToken = token;
    if (!windowManager) return;
    if (isLiveWindow(windowManager.controlPanelWindow)) {
      windowManager.showControlPanel();
      // Best-effort fast path — the get-pending-invitation-token pull is the reliable path.
      windowManager.controlPanelWindow.webContents.send("workspace-invitation-token", token);
    } else {
      windowManager.createControlPanelWindow();
    }
  } catch (error) {
    console.error("Invitation deep link parse failed:", error);
  }
}

function resolveAuthUrl() {
  const fs = require("fs");
  const envPath = path.join(__dirname, "src", "dist", "runtime-env.json");
  let runtimeEnv = {};
  try {
    if (fs.existsSync(envPath)) runtimeEnv = JSON.parse(fs.readFileSync(envPath, "utf8"));
  } catch {}
  return (
    process.env.AUTH_URL ||
    process.env.VITE_AUTH_URL ||
    runtimeEnv.VITE_AUTH_URL ||
    "https://auth.openwhispr.com"
  );
}

function getOauthCookieName() {
  return process.env.NODE_ENV === "production"
    ? "__Secure-openwhispr.session_token"
    : "openwhispr.session_token";
}

// Older website builds send the signed cookie value as `?token=`; trade it
// for the raw session.token the bearer plugin expects.
async function exchangeSignedTokenForRawBearer(signedToken) {
  try {
    const res = await net.fetch(`${resolveAuthUrl()}/api/auth/get-session`, {
      headers: { Cookie: `${getOauthCookieName()}=${signedToken}` },
      signal: AbortSignal.timeout(5000),
      useSessionCookies: false,
    });
    if (!res.ok) return null;
    const data = await res.json();
    return data?.session?.token || null;
  } catch (err) {
    if (debugLogger) {
      debugLogger.warn("Signed-token bearer exchange failed (non-fatal)", {
        error: err?.message,
      });
    }
    return null;
  }
}

// One-time bridge for users upgrading from a build that injected the session
// cookie into Electron's jar: exchange the existing cookie for a raw bearer
// token, store it, and remove the cookie. Non-fatal — failures fall through
// to the normal sign-in flow.
async function migrateCookieToBearerToken() {
  const tokenStore = require("./src/helpers/tokenStore");
  if (tokenStore.get()) return;

  const cookieName = getOauthCookieName();
  const authUrl = resolveAuthUrl();

  try {
    const cookies = await session.defaultSession.cookies.get({ url: authUrl, name: cookieName });
    if (!cookies.length) return;

    const rawToken = await exchangeSignedTokenForRawBearer(cookies[0].value);
    if (!rawToken) return;

    tokenStore.set(rawToken);
    await session.defaultSession.cookies.remove(authUrl, cookieName);
    if (debugLogger) debugLogger.debug("Migrated cookie to bearer token");
  } catch (err) {
    if (debugLogger) {
      debugLogger.warn("Cookie→bearer token migration failed (non-fatal)", {
        error: err?.message,
      });
    }
  }
}

// Persist the bearer token and reload the control panel so the renderer's
// authClient sends `Authorization: Bearer <token>` on its next request.
async function applySessionTokenAndRefresh(token) {
  if (!token) return;
  if (!isLiveWindow(windowManager?.controlPanelWindow)) return;

  const tokenStore = require("./src/helpers/tokenStore");
  tokenStore.set(token);

  const appUrl = DevServerManager.getAppUrl(true);
  if (appUrl) {
    windowManager.controlPanelWindow.loadURL(appUrl);
  } else {
    const fileInfo = DevServerManager.getAppFilePath(true);
    if (fileInfo) {
      windowManager.controlPanelWindow.loadFile(fileInfo.path, { query: fileInfo.query });
    }
  }

  if (debugLogger) {
    debugLogger.debug("Applied bearer token and reloaded control panel", {
      appChannel: APP_CHANNEL,
      oauthProtocol: OAUTH_PROTOCOL,
    });
  }
  windowManager.showControlPanel();
}

async function handleOAuthDeepLink(deepLinkUrl) {
  try {
    const parsed = new URL(deepLinkUrl);
    const bearerToken = parsed.searchParams.get("bearer_token");
    if (bearerToken) {
      void applySessionTokenAndRefresh(bearerToken);
      return;
    }
    const signedToken = parsed.searchParams.get("token");
    if (!signedToken) return;
    const rawToken = await exchangeSignedTokenForRawBearer(signedToken);
    if (rawToken) void applySessionTokenAndRefresh(rawToken);
  } catch (err) {
    if (debugLogger) debugLogger.error("Failed to handle OAuth deep link:", err);
  }
}

function handleUpgradeDeepLink() {
  if (isLiveWindow(windowManager?.controlPanelWindow)) {
    windowManager.controlPanelWindow.webContents.executeJavaScript(
      'window.dispatchEvent(new Event("upgrade-success"))'
    );
    windowManager.showControlPanel();
  }
}

function parseJsonBody(req) {
  return new Promise((resolve, reject) => {
    let raw = "";
    req.on("data", (chunk) => {
      raw += chunk;
      if (raw.length > 32 * 1024) {
        reject(new Error("Request body too large"));
        req.destroy();
      }
    });
    req.on("end", () => {
      if (!raw) return resolve({});
      try {
        resolve(JSON.parse(raw));
      } catch {
        reject(new Error("Invalid JSON payload"));
      }
    });
    req.on("error", reject);
  });
}

function writeCorsHeaders(res) {
  res.setHeader("Access-Control-Allow-Origin", "*");
  res.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS");
  res.setHeader("Access-Control-Allow-Headers", "Content-Type");
}

function startAuthBridgeServer() {
  if (APP_CHANNEL !== "development" || authBridgeServer) {
    return;
  }

  authBridgeServer = http.createServer(async (req, res) => {
    writeCorsHeaders(res);
    if (req.method === "OPTIONS") {
      res.writeHead(204);
      res.end();
      return;
    }

    const requestUrl = new URL(req.url || "/", `http://${AUTH_BRIDGE_HOST}:${AUTH_BRIDGE_PORT}`);
    if (requestUrl.pathname !== AUTH_BRIDGE_PATH) {
      res.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
      res.end("Not found");
      return;
    }

    let token = requestUrl.searchParams.get("bearer_token") || requestUrl.searchParams.get("token");
    if (!token && req.method === "POST") {
      try {
        const body = await parseJsonBody(req);
        token = body?.bearer_token || body?.token || null;
      } catch (error) {
        res.writeHead(400, { "Content-Type": "text/plain; charset=utf-8" });
        res.end(error.message || "Invalid request");
        return;
      }
    }

    if (!token) {
      res.writeHead(400, { "Content-Type": "text/plain; charset=utf-8" });
      res.end("Missing token");
      return;
    }

    void applySessionTokenAndRefresh(token);

    res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
    res.end(
      "<html><body><h3>OpenWhispr sign-in complete.</h3><p>You can close this tab.</p></body></html>"
    );
  });

  authBridgeServer.on("error", (error) => {
    if (debugLogger) {
      debugLogger.error("OAuth auth bridge server failed:", error);
    }
  });

  authBridgeServer.listen(AUTH_BRIDGE_PORT, AUTH_BRIDGE_HOST, () => {
    if (debugLogger) {
      debugLogger.debug("OAuth auth bridge server started", {
        url: `http://${AUTH_BRIDGE_HOST}:${AUTH_BRIDGE_PORT}${AUTH_BRIDGE_PATH}`,
      });
    }
  });
}

// Startup restores the saved activation mode before any hotkey registers. Desktop
// backends (GNOME, KDE, Hyprland) register the saved hotkey a moment later, in
// this mode, so check that hotkey rather than the provisional default it
// replaces: a supported Hold is kept and an unsupported one becomes Tap before
// registration. Elsewhere, check the hotkey that registered. This is a runtime
// fallback, not a change to the user's saved preference: the next launch retries it.
async function dropUnsupportedStartupHold() {
  if (windowManager.getActivationMode() !== "push") return;
  const manager = windowManager.hotkeyManager;
  const hotkey = manager.isUsingNativeShortcut()
    ? await manager.getSavedDictationHotkey()
    : manager.getCurrentHotkey();
  if (!manager.supportsPushToTalk(hotkey)) {
    const changed = await windowManager.setActivationModeCache("tap");
    if (changed) {
      for (const browserWindow of BrowserWindow.getAllWindows()) {
        if (!browserWindow.isDestroyed()) {
          browserWindow.webContents.send("setting-updated", {
            key: "activationMode",
            value: "tap",
          });
        }
      }
    }
  }
}

// A desktop backend that cannot register falls back to globalShortcut after the
// first check, and globalShortcut may still be reading the saved hotkey, so
// check again once that registration settles.
async function checkStartupHold() {
  windowManager.hotkeyManager.once("hotkey-loaded", () => {
    dropUnsupportedStartupHold().catch((err) => {
      debugLogger.warn("[HotkeyManager] Startup activation mode recheck failed", {
        error: err.message,
      });
    });
  });
  await dropUnsupportedStartupHold();
}

// Main application startup
async function startApp() {
  // Await so a stale sidecar is confirmed dead before new ones can spawn and
  // contend for its port or storage lock.
  await reapStaleSidecars();

  // The registry loader reports cache problems before core managers exist, so the
  // logger starts first. Requiring it again in initializeCoreManagers is a no-op.
  debugLogger = require("./src/helpers/debugLogger");
  debugLogger.ensureFileLogging();

  // Only local signature/cache verification is awaited. Remote refresh starts
  // later, so offline startup and settings migrations see a usable catalog.
  const registryMetadata = require("./src/models/registryMetadata.json");
  configureInstalledModels(path.join(app.getPath("userData"), "installed-models.json"));
  registryLoader = createRegistryLoader({
    cachePath: path.join(app.getPath("userData"), "model-registry.json"),
    bundledSnapshot: {
      data: bundledRegistryData,
      manifestVersion: registryMetadata.manifestVersion,
    },
    clientContract: registryMetadata.clientContract,
    trustedKeys: registryTrustedKeys,
    appVersion: app.getVersion(),
    applySnapshot: (snapshot) =>
      replaceRegistryData({
        ...snapshot,
        // Installed records pass the same download checks as the catalog.
        data: withTrustedDownloads(
          withInstalledModels(snapshot.data, app.getVersion()),
          bundledRegistryData,
          (id) => debugLogger.warn("Refused a catalog download from an untrusted source", { id })
        ),
      }),
    fetch: (...args) => net.fetch(...args),
    onError: (error) =>
      debugLogger.warn("Model registry update unavailable", { error: error.message }),
  });
  await registryLoader.start();
  disposeRegistryIpc = registerRegistryIpc(ipcMain, (channel, snapshot) => {
    for (const window of BrowserWindow.getAllWindows()) {
      if (!window.isDestroyed()) window.webContents.send(channel, snapshot);
    }
  });

  // Phase 1: Core managers + IPC handlers before windows
  initializeCoreManagers();
  await environmentManager.init();
  // After any upgrade the GPU gets one fresh attempt: clear the remembered
  // failure before the whisper pre-warm below resolves its GPU backend.
  resetWhisperGpuFailureOnUpgrade(environmentManager);
  registerSidecars();
  startAuthBridgeServer();

  cliBridge = new CliBridge(ipcHandlers);
  cliBridge.start().catch((err) => {
    debugLogger.error("CLI bridge failed to start", { error: err.message });
    cliBridge = null;
  });

  await migrateCookieToBearerToken();
  ipcHandlers.loadLocalModelPolicy();

  applyOpenWhisprOriginHeader(session.defaultSession);

  await windowManager.setActivationModeCache(environmentManager.getActivationMode());
  windowManager.setFloatingIconAutoHide(environmentManager.getFloatingIconAutoHide());
  windowManager.setPanelStartPosition(environmentManager.getPanelStartPosition());

  let activationModeChangeQueue = Promise.resolve();
  ipcMain.on("activation-mode-changed", (_event, mode) => {
    activationModeChangeQueue = activationModeChangeQueue
      .then(async () => {
        const success = await windowManager.setActivationModeCache(mode);
        const effectiveMode = windowManager.getActivationMode();
        if (success) {
          environmentManager.saveActivationMode(effectiveMode);
        } else {
          for (const browserWindow of BrowserWindow.getAllWindows()) {
            if (!browserWindow.isDestroyed()) {
              browserWindow.webContents.send("setting-updated", {
                key: "activationMode",
                value: effectiveMode,
              });
            }
          }
        }
        windowManager.resetWindowsPushState();
        windowManager.reconcileNativeKeyListeners();
      })
      .catch((err) => {
        debugLogger.error("Failed to change activation mode", { error: err.message }, "hotkey");
      });
  });

  ipcMain.on("floating-icon-auto-hide-changed", (_event, enabled) => {
    windowManager.setFloatingIconAutoHide(enabled);
    environmentManager.saveFloatingIconAutoHide(enabled);
    // Relay to the floating icon window so it can react immediately
    if (windowManager.mainWindow && !windowManager.mainWindow.isDestroyed()) {
      windowManager.mainWindow.webContents.send("floating-icon-auto-hide-changed", enabled);
    }
  });

  ipcMain.on("start-minimized-changed", (_event, enabled) => {
    if (debugLogger) debugLogger.info("Start minimized changed", { enabled });
    environmentManager.saveStartMinimized(enabled);
  });

  ipcMain.on("panel-start-position-changed", (_event, position) => {
    windowManager.setPanelStartPosition(position);
    environmentManager.savePanelStartPosition(position);
  });

  dockManager.init();

  // In development, wait for Vite dev server to be ready
  if (process.env.NODE_ENV === "development") {
    await new Promise((resolve) => setTimeout(resolve, 500));
  }

  // Create windows FIRST so the user sees UI as soon as possible.
  // A login launch goes to the tray whatever the preference says: the user asked
  // the OS to start us, not to put a window in front of them at every login.
  const launchedHidden = wasLaunchedAtLoginHidden();
  const startMinimized = environmentManager.getStartMinimized() || launchedHidden;
  if (debugLogger) debugLogger.info("Start minimized", { enabled: startMinimized, launchedHidden });
  windowManager.startedMinimized = startMinimized;
  await windowManager.createMainWindow();
  await checkStartupHold();
  if (!startMinimized) {
    await windowManager.createControlPanelWindow();
  }

  // Windows/Linux cold start delivers protocol URLs via argv (macOS uses
  // open-url); without this scan a deep link that launches the app is lost.
  const initialProtocolUrl = process.argv.find((arg) => arg.startsWith(`${OAUTH_PROTOCOL}://`));
  if (initialProtocolUrl && handleEnterpriseAudioLink(initialProtocolUrl)) {
    // The renderer consumes the saved setup request after settings mount.
  } else if (initialProtocolUrl && isNoteDeepLink(initialProtocolUrl)) {
    await handleNoteDeepLink(initialProtocolUrl);
  } else {
    if (initialProtocolUrl && process.platform !== "darwin") {
      if (initialProtocolUrl.includes("upgrade-success")) {
        handleUpgradeDeepLink();
      } else if (isInvitationDeepLink(initialProtocolUrl)) {
        handleInvitationDeepLink(initialProtocolUrl);
      } else {
        void handleOAuthDeepLink(initialProtocolUrl);
      }
    }
    await flushPendingNoteDeepLink();
  }

  await hotkeyManager.hyprlandRegistrationReady;

  // Set up voice agent hotkey (dictation routed straight to the dictation
  // agent, bypassing cleanup). Tap-only slots gate autorepeat like the
  // dictation toggle does.
  const isVoiceAgentPress = createHotkeyRepeatGate();
  const voiceAgentHotkeyCallback = () => {
    if (!isVoiceAgentPress()) return;
    windowManager.sendToggleVoiceAgent();
  };
  windowManager._voiceAgentHotkeyCallback = voiceAgentHotkeyCallback;

  const savedVoiceAgentKey = environmentManager.getVoiceAgentKey?.() || "";
  if (savedVoiceAgentKey) {
    const result = await hotkeyManager.registerSlot(
      "voiceAgent",
      savedVoiceAgentKey,
      voiceAgentHotkeyCallback
    );
    if (!result.success) {
      debugLogger.warn(
        "Failed to restore voice agent hotkey",
        { hotkey: savedVoiceAgentKey },
        "hotkey"
      );
    }
    hotkeyManager.notifyRestoreFailures(savedVoiceAgentKey, result);
  }

  // Set up translation hotkey (dictation cleaned up and translated into the
  // configured target language before pasting)
  const isTranslationPress = createHotkeyRepeatGate();
  const translationHotkeyCallback = () => {
    if (!isTranslationPress()) return;
    windowManager.sendToggleTranslation();
  };
  windowManager._translationHotkeyCallback = translationHotkeyCallback;

  const savedTranslationKey = environmentManager.getTranslationKey?.() || "";
  if (savedTranslationKey) {
    const result = await hotkeyManager.registerSlot(
      "translation",
      savedTranslationKey,
      translationHotkeyCallback
    );
    if (!result.success) {
      debugLogger.warn(
        "Failed to restore translation hotkey",
        { hotkey: savedTranslationKey },
        "hotkey"
      );
    }
    hotkeyManager.notifyRestoreFailures(savedTranslationKey, result);
  }

  // Set up meeting mode hotkey
  const isMeetingPress = createHotkeyRepeatGate();
  const meetingHotkeyCallback = () => {
    if (!isMeetingPress()) return;
    debugLogger.info("Meeting hotkey triggered", {}, "meeting");
    // Usually pressed mid-call: an open panel updates behind the meeting app.
    windowManager.startManualMeeting({ activate: false });
  };

  const savedMeetingKey = environmentManager.getMeetingKey?.() || "";
  if (savedMeetingKey) {
    const result = await hotkeyManager.registerSlot(
      "meeting",
      savedMeetingKey,
      meetingHotkeyCallback
    );
    debugLogger.info(
      "Meeting hotkey startup registration",
      { savedMeetingKey, ...result },
      "meeting"
    );
    hotkeyManager.notifyRestoreFailures(savedMeetingKey, result);
  }

  ipcMain.handle("register-meeting-hotkey", async (_event, hotkey) => {
    if (hotkey) {
      const result = await hotkeyManager.registerSlot("meeting", hotkey, meetingHotkeyCallback, {
        atomic: true,
      });
      windowManager.reconcileNativeKeyListeners();
      if (result.success) {
        environmentManager.saveMeetingKey(hotkey);
        return { success: true };
      }
      return { success: false, message: result.error };
    } else {
      const removed = await hotkeyManager.unregisterSlot("meeting");
      if (removed === false) return { success: false };
      environmentManager.saveMeetingKey("");
      windowManager.reconcileNativeKeyListeners();
      return { success: true };
    }
  });

  // Phase 2: Initialize remaining managers after windows are visible
  initializeDeferredManagers();
  if (process.platform === "darwin") {
    // Restore a Globe preference marker left by a crash without starting any
    // Accessibility-protected event monitors during onboarding.
    await globeKeyManager.restoreLeftoverSystemPreference();
  }

  app.on("browser-window-focus", () => {
    if (googleCalendarManager) googleCalendarManager.syncOnFocus();
    if (microsoftCalendarManager) microsoftCalendarManager.syncOnFocus();
    if (appleCalendarManager) appleCalendarManager.syncOnFocus();
  });

  const { powerMonitor } = require("electron");
  powerMonitor.on("resume", () => {
    if (calendarReminderScheduler) calendarReminderScheduler.onWakeFromSleep();
    if (googleCalendarManager) {
      googleCalendarManager.onWakeFromSleep();
    }
    if (microsoftCalendarManager) microsoftCalendarManager.onWakeFromSleep();
    if (appleCalendarManager) appleCalendarManager.onWakeFromSleep();
    // Sleep evicts the local GPU model from VRAM; reload it once the driver settles. See #766.
    if (wakeRewarmTimer) clearTimeout(wakeRewarmTimer);
    wakeRewarmTimer = setTimeout(() => {
      wakeRewarmTimer = null;
      whisperManager?.onWakeFromSleep().catch((err) => {
        debugLogger.debug("whisper wake re-warm error (non-fatal)", { error: err.message });
      });
    }, WHISPER_WAKE_REWARM_DELAY_MS);
  });

  // Non-blocking server pre-warming; GPU backend resolved by the manager
  const whisperSettings = {
    localTranscriptionProvider: process.env.LOCAL_TRANSCRIPTION_PROVIDER || "",
    whisperModel: process.env.LOCAL_WHISPER_MODEL,
  };
  whisperManager.initializeAtStartup(whisperSettings).catch((err) => {
    debugLogger.debug("Whisper startup init error (non-fatal)", { error: err.message });
  });

  const parakeetSettings = {
    localTranscriptionProvider: process.env.LOCAL_TRANSCRIPTION_PROVIDER || "",
    parakeetModel: process.env.PARAKEET_MODEL,
    language: process.env.DICTATION_LANGUAGE,
  };
  parakeetManager.initializeAtStartup(parakeetSettings).catch((err) => {
    debugLogger.debug("Parakeet startup init error (non-fatal)", { error: err.message });
  });

  // TODO: drop legacy REASONING_PROVIDER / LOCAL_REASONING_MODEL fallbacks after 2 releases.
  const cleanupProvider = process.env.CLEANUP_PROVIDER || process.env.REASONING_PROVIDER;
  const cleanupLocalModel = process.env.LOCAL_CLEANUP_MODEL || process.env.LOCAL_REASONING_MODEL;
  const prewarmModel = resolvePrewarmModel({
    cleanupProvider,
    cleanupModel: cleanupLocalModel,
    dictationAgentProvider: process.env.DICTATION_AGENT_PROVIDER,
    dictationAgentModel: process.env.LOCAL_DICTATION_AGENT_MODEL,
  });
  if (prewarmModel) {
    const modelManager = require("./src/helpers/modelManagerBridge").default;
    modelManager.prewarmServer(prewarmModel).catch((err) => {
      debugLogger.debug("llama-server pre-warm error (non-fatal)", {
        model: prewarmModel,
        error: err.message,
      });
    });
  }

  // Auto-download diarization models if binary is available
  if (
    diarizationManager.getBinaryPath() &&
    (!diarizationManager.isModelDownloaded() || !diarizationManager.isVadModelDownloaded())
  ) {
    diarizationManager.downloadModels().catch((err) => {
      debugLogger.debug("Diarization model auto-download error (non-fatal)", {
        error: err.message,
      });
    });
  }

  const QdrantManager = require("./src/helpers/qdrantManager");
  const SemanticSearchLifecycle = require("./src/helpers/semanticSearchLifecycle");
  const localEmbeddings = require("./src/helpers/localEmbeddings");
  const qdrantManager = new QdrantManager();
  semanticSearch = new SemanticSearchLifecycle({
    qdrant: qdrantManager,
    vectorIndex: require("./src/helpers/vectorIndex"),
    embeddings: localEmbeddings,
    noteEmbedText: localEmbeddings.LocalEmbeddings.noteEmbedText,
    database: databaseManager,
    logger: debugLogger,
  });
  sidecarRegistry.register("qdrant", () => semanticSearch.stop());

  if (process.platform === "win32") {
    const nircmdStatus = clipboardManager.getNircmdStatus();
    debugLogger.debug("Windows paste tool status", nircmdStatus);
  }

  trayManager.setWindows(windowManager.mainWindow, windowManager.controlPanelWindow);
  trayManager.setWindowManager(windowManager);
  // The tray's listen item is a toggle, so it has to rebuild when dictation
  // starts or stops.
  windowManager.onDictationStateChanged = () => trayManager.updateTrayMenu();
  trayManager.setCreateControlPanelCallback(() => windowManager.createControlPanelWindow());
  await trayManager.createTray();

  updateManager.checkForUpdatesOnStartup();

  if (process.platform === "darwin") {
    const { isGlobeLikeHotkey, isMouseButtonHotkey } = require("./src/helpers/hotkeyManager");
    let globeKeyDownTime = 0;
    let globeKeyIsRecording = false;
    let globeLastStopTime = 0;
    const MIN_HOLD_DURATION_MS = 150;
    const POST_STOP_COOLDOWN_MS = 300;

    globeKeyManager.on("globe-down", async () => {
      const currentHotkey = hotkeyManager.getCurrentHotkey && hotkeyManager.getCurrentHotkey();
      const mainWindowLive = isLiveWindow(windowManager.mainWindow);
      debugLogger?.debug("[Globe] globe-down received", {
        currentHotkey,
        mainWindowLive,
        activationMode: mainWindowLive ? windowManager.getActivationMode() : "n/a",
      });

      // Forward to control panel for hotkey capture
      if (isLiveWindow(windowManager.controlPanelWindow)) {
        windowManager.controlPanelWindow.webContents.send("globe-key-pressed");
      }

      // Handle dictation if Globe/Fn is one of the dictation hotkeys
      const dictationUsesGlobe = hotkeyManager.getSlotHotkeys("dictation").some(isGlobeLikeHotkey);
      if (dictationUsesGlobe) {
        if (mainWindowLive && windowManager.isDictationProcessing()) {
          debugLogger?.debug("[Globe] Ignored — dictation processing");
        } else if (mainWindowLive) {
          // Capture target app PID BEFORE showing the overlay
          if (textEditMonitor) textEditMonitor.captureTargetPid();
          const activationMode = windowManager.getActivationMode();
          if (activationMode === "push") {
            const now = Date.now();
            if (now - globeLastStopTime < POST_STOP_COOLDOWN_MS) {
              debugLogger?.debug("[Globe] Ignored — cooldown active");
              return;
            }
            windowManager.showDictationPanel();
            windowManager.sendPrepareDictation();
            const pressTime = now;
            globeKeyDownTime = pressTime;
            globeKeyIsRecording = false;
            setTimeout(async () => {
              if (globeKeyDownTime === pressTime && !globeKeyIsRecording) {
                globeKeyIsRecording = true;
                debugLogger?.debug("[Globe] Starting dictation (push hold)");
                windowManager.sendStartDictation();
              }
            }, MIN_HOLD_DURATION_MS);
          } else {
            windowManager.sendToggleDictation();
          }
        } else {
          debugLogger?.debug("[Globe] Ignored — mainWindow not live");
        }
      }

      // Check voice agent slot for Globe/Fn key
      const voiceAgentUsesGlobe = hotkeyManager
        .getSlotHotkeys("voiceAgent")
        .some(isGlobeLikeHotkey);
      const translationUsesGlobe = hotkeyManager
        .getSlotHotkeys("translation")
        .some(isGlobeLikeHotkey);
      if (voiceAgentUsesGlobe) {
        windowManager.sendToggleVoiceAgent();
      }
      if (translationUsesGlobe) {
        windowManager.sendToggleTranslation();
      }
      if (!voiceAgentUsesGlobe && !translationUsesGlobe && !dictationUsesGlobe) {
        debugLogger?.debug("[Globe] Ignored — hotkey is not GLOBE", { currentHotkey });
      }
    });

    globeKeyManager.on("globe-up", async () => {
      debugLogger?.debug("[Globe] globe-up received", { wasRecording: globeKeyIsRecording });

      // Forward to control panel for hotkey capture (Fn key released)
      if (isLiveWindow(windowManager.controlPanelWindow)) {
        windowManager.controlPanelWindow.webContents.send("globe-key-released");
      }

      if (hotkeyManager.getSlotHotkeys("dictation").some(isGlobeLikeHotkey)) {
        const activationMode = windowManager.getActivationMode();
        if (activationMode === "push") {
          if (globeKeyDownTime === 0 && !globeKeyIsRecording) {
            // The press was ignored (dictation was processing); releasing it
            // must not cancel preparation or hide the thinking pill.
            debugLogger?.debug("[Globe] Release without a registered press — ignored");
          } else {
            globeKeyDownTime = 0;
            globeLastStopTime = Date.now();
            if (globeKeyIsRecording) {
              globeKeyIsRecording = false;
              debugLogger?.debug("[Globe] Stopping dictation (push release)");
              windowManager.sendStopDictation();
            } else {
              windowManager.sendCancelDictationPreparation();
              windowManager.hideDictationPanel();
            }
          }
        }
      }

      // Fn release also stops compound push-to-talk for Fn+F-key hotkeys
      windowManager.handleMacPushModifierUp("fn");
    });

    // Another key was pressed while Fn was held — user is using Fn as a
    // navigation modifier (Fn+Arrow → Home, Fn+Backspace → Forward Delete, etc.).
    // Cancel any bare-Fn push-to-talk in progress instead of transcribing noise.
    // Only the bare-Fn path uses globeKeyDownTime/globeKeyIsRecording, so compound
    // Fn-hotkey push-to-talk and tap mode are untouched.
    globeKeyManager.on("globe-interrupted", () => {
      if (globeKeyDownTime === 0 && !globeKeyIsRecording) {
        return;
      }
      const wasRecording = globeKeyIsRecording;
      debugLogger?.debug("[Globe] Fn+key interrupted push-to-talk", { wasRecording });
      globeKeyDownTime = 0;
      globeKeyIsRecording = false;
      globeLastStopTime = Date.now();
      if (wasRecording) {
        windowManager.sendCancelDictation();
      } else {
        windowManager.sendCancelDictationPreparation();
        windowManager.hideDictationPanel();
      }
    });

    globeKeyManager.on("modifier-up", (modifier) => {
      if (windowManager?.handleMacPushModifierUp) {
        windowManager.handleMacPushModifierUp(modifier);
      }
    });

    // Right-side single modifier handling (e.g., RightOption as hotkey)
    let rightModDownTime = 0;
    let rightModIsRecording = false;
    let rightModLastStopTime = 0;
    let rightModActiveKey = null;

    globeKeyManager.on("right-modifier-down", async (modifier) => {
      hotkeyManager.logHotkeyEvent("macos-native", "down", modifier);
      // Check voice agent slot for right-modifier
      if (hotkeyManager.slotHasHotkey("voiceAgent", modifier)) {
        windowManager.sendToggleVoiceAgent();
      }
      if (hotkeyManager.slotHasHotkey("translation", modifier)) {
        windowManager.sendToggleTranslation();
      }

      if (!hotkeyManager.slotHasHotkey("dictation", modifier)) return;
      if (!isLiveWindow(windowManager.mainWindow)) return;
      if (windowManager.isDictationProcessing()) return;

      const activationMode = windowManager.getActivationMode();
      if (textEditMonitor) textEditMonitor.captureTargetPid();
      if (activationMode === "push") {
        if (rightModActiveKey && rightModActiveKey !== modifier) return;
        const now = Date.now();
        if (now - rightModLastStopTime < POST_STOP_COOLDOWN_MS) return;
        windowManager.showDictationPanel();
        windowManager.sendPrepareDictation();
        const pressTime = now;
        rightModActiveKey = modifier;
        rightModDownTime = pressTime;
        rightModIsRecording = false;
        setTimeout(() => {
          if (rightModDownTime === pressTime && !rightModIsRecording) {
            rightModIsRecording = true;
            windowManager.sendStartDictation();
          }
        }, MIN_HOLD_DURATION_MS);
      } else {
        windowManager.sendToggleDictation();
      }
    });

    globeKeyManager.on("right-modifier-up", async (modifier) => {
      hotkeyManager.logHotkeyEvent("macos-native", "up", modifier);
      if (hotkeyManager.slotHasHotkey("dictation", modifier)) {
        if (!isLiveWindow(windowManager.mainWindow)) return;

        const activationMode = windowManager.getActivationMode();
        if (activationMode === "push" && (!rightModActiveKey || rightModActiveKey === modifier)) {
          if (rightModDownTime === 0 && !rightModIsRecording) {
            // The press was ignored (dictation was processing); releasing it
            // must not cancel preparation or hide the thinking pill.
            debugLogger?.debug("[RightMod] Release without a registered press — ignored");
          } else {
            rightModActiveKey = null;
            rightModDownTime = 0;
            rightModLastStopTime = Date.now();
            if (rightModIsRecording) {
              rightModIsRecording = false;
              windowManager.sendStopDictation();
            } else {
              windowManager.sendCancelDictationPreparation();
              windowManager.hideDictationPanel();
            }
          }
        }
      }

      const rightModToBase = {
        RightCommand: "command",
        RightOption: "option",
        RightControl: "control",
        RightShift: "shift",
      };
      const baseMod = rightModToBase[modifier];
      if (baseMod && windowManager?.handleMacPushModifierUp) {
        windowManager.handleMacPushModifierUp(baseMod);
      }
    });

    const MAC_NATIVE_HOTKEY_SLOTS = ["dictation", "voiceAgent", "translation"];
    const syncMacNativeHotkeyConfiguration = () => {
      globeKeyManager.setConfiguration(
        hotkeyManager.getMacNativeListenerConfig(MAC_NATIVE_HOTKEY_SLOTS)
      );
    };

    // Mouse Button 4/5 handling (e.g., Logitech MX Master side buttons)
    let mouseButtonDownTime = 0;
    let mouseButtonIsRecording = false;
    let mouseButtonLastStopTime = 0;
    let mouseButtonActiveButton = null;

    globeKeyManager.on("mouse-button-down", async (button) => {
      if (hotkeyManager.isInListeningMode && hotkeyManager.isInListeningMode()) return;
      if (!isMouseButtonHotkey(button)) return;
      hotkeyManager.logHotkeyEvent("macos-native", "down", button);

      if (hotkeyManager.slotHasHotkey("voiceAgent", button)) {
        windowManager.sendToggleVoiceAgent();
      }
      if (hotkeyManager.slotHasHotkey("translation", button)) {
        windowManager.sendToggleTranslation();
      }

      if (!hotkeyManager.slotHasHotkey("dictation", button)) return;
      if (!isLiveWindow(windowManager.mainWindow)) return;
      if (windowManager.isDictationProcessing()) return;

      const activationMode = windowManager.getActivationMode();
      if (textEditMonitor) textEditMonitor.captureTargetPid();

      if (activationMode === "push") {
        if (mouseButtonActiveButton && mouseButtonActiveButton !== button) return;
        const now = Date.now();
        if (now - mouseButtonLastStopTime < POST_STOP_COOLDOWN_MS) return;
        windowManager.showDictationPanel();
        windowManager.sendPrepareDictation();
        const pressTime = now;
        mouseButtonActiveButton = button;
        mouseButtonDownTime = pressTime;
        mouseButtonIsRecording = false;
        setTimeout(() => {
          if (mouseButtonDownTime === pressTime && !mouseButtonIsRecording) {
            mouseButtonIsRecording = true;
            windowManager.sendStartDictation();
          }
        }, MIN_HOLD_DURATION_MS);
      } else {
        windowManager.sendToggleDictation();
      }
    });

    globeKeyManager.on("mouse-button-up", async (button) => {
      if (hotkeyManager.isInListeningMode && hotkeyManager.isInListeningMode()) return;
      if (!isMouseButtonHotkey(button)) return;
      hotkeyManager.logHotkeyEvent("macos-native", "up", button);

      if (!hotkeyManager.slotHasHotkey("dictation", button)) return;
      if (!isLiveWindow(windowManager.mainWindow)) return;

      const activationMode = windowManager.getActivationMode();
      if (
        activationMode === "push" &&
        (!mouseButtonActiveButton || mouseButtonActiveButton === button)
      ) {
        if (mouseButtonDownTime === 0 && !mouseButtonIsRecording) {
          // The press was ignored (dictation was processing); releasing it
          // must not cancel preparation or hide the thinking pill.
          debugLogger?.debug("[MouseButton] Release without a registered press — ignored");
        } else {
          mouseButtonActiveButton = null;
          mouseButtonDownTime = 0;
          mouseButtonLastStopTime = Date.now();
          if (mouseButtonIsRecording) {
            mouseButtonIsRecording = false;
            windowManager.sendStopDictation();
          } else {
            windowManager.sendCancelDictationPreparation();
            windowManager.hideDictationPanel();
          }
        }
      }
    });

    // If accessibility is missing, notify the normal control panel after the
    // protected macOS features have started. During onboarding the permissions
    // screen owns this guidance, so its event has no ControlPanel listener.
    const checkAndNotifyAccessibility = () => {
      if (!systemPreferences.isTrustedAccessibilityClient(false)) {
        debugLogger.info("[Accessibility] macOS accessibility not trusted — notifying renderers");
        if (isLiveWindow(windowManager.controlPanelWindow)) {
          windowManager.controlPanelWindow.webContents.send("accessibility-missing");
        }
      }
    };

    let accessibilityFeaturesStarted = false;
    startMacAccessibilityFeatures = () => {
      if (accessibilityFeaturesStarted) return;
      accessibilityFeaturesStarted = true;
      clipboardManager.preWarmAccessibility();
      syncMacNativeHotkeyConfiguration();
      globeKeyManager.start();
      setTimeout(checkAndNotifyAccessibility, 3000);
    };

    if (macAccessibilityFeaturesReady) {
      startMacAccessibilityFeatures();
    }

    hotkeyManager.on("hotkey-loaded", syncMacNativeHotkeyConfiguration);

    ipcMain.on("hotkey-listening-mode-changed", (_event, enabled) => {
      if (enabled) {
        startMacAccessibilityFeatures();
        // Let mouse buttons through so they can be captured, but keep macOS's
        // Globe action down so choosing Globe cannot flash the emoji viewer.
        globeKeyManager.setConfiguration({ mouseButtons: [], suppressGlobeAction: true });
      } else {
        syncMacNativeHotkeyConfiguration();
      }
    });

    // Allow renderer to request an accessibility check (e.g. on sign-in).
    // Also sends accessibility-missing events if untrusted.
    ipcMain.handle("check-accessibility-trusted", () => {
      const trusted = systemPreferences.isTrustedAccessibilityClient(false);
      if (!trusted) {
        checkAndNotifyAccessibility();
      }
      return trusted;
    });

    // Reset native key state when hotkey changes
    ipcMain.on("hotkey-changed", (_event, _newHotkey) => {
      globeKeyDownTime = 0;
      globeKeyIsRecording = false;
      globeLastStopTime = 0;
      rightModDownTime = 0;
      rightModIsRecording = false;
      rightModLastStopTime = 0;
      mouseButtonDownTime = 0;
      mouseButtonIsRecording = false;
      mouseButtonLastStopTime = 0;
      syncMacNativeHotkeyConfiguration();
    });
  }

  // Windows and Linux share the same native low-level key listener model: one hook
  // process per watched key (Electron globalShortcut can't see modifier-only or
  // right-side-modifier combos), routed to the owning slot here. macOS is handled
  // separately above via globeKeyManager.
  if (process.platform === "win32" || process.platform === "linux") {
    const isWindows = process.platform === "win32";
    const nativeKeyManager = isWindows ? windowsKeyManager : linuxKeyManager;
    debugLogger.debug("[Push-to-Talk] Native key listener setup starting");

    // Dictation supports push-to-talk and needs the overlay window; meeting
    // drives other windows (matching their globalShortcut callbacks and macOS).
    const dispatchNativeKeyDown = (key) => {
      hotkeyManager.logHotkeyEvent("native-listener", "down", key);
      if (hotkeyManager.slotHasHotkey("dictation", key)) {
        if (!isLiveWindow(windowManager.mainWindow)) return;
        if (windowManager.getActivationMode() === "push") {
          windowManager.startWindowsPushToTalk(key);
        } else {
          windowManager.sendToggleDictation();
        }
        return;
      }
      if (hotkeyManager.slotHasHotkey("voiceAgent", key)) {
        windowManager.sendToggleVoiceAgent();
      } else if (hotkeyManager.slotHasHotkey("translation", key)) {
        windowManager.sendToggleTranslation();
      } else if (hotkeyManager.slotHasHotkey("meeting", key)) {
        windowManager.startManualMeeting({ activate: false });
      }
    };

    // Only dictation drives push-to-talk, so only its key-up matters.
    const dispatchNativeKeyUp = (key) => {
      hotkeyManager.logHotkeyEvent("native-listener", "up", key);
      if (!hotkeyManager.slotHasHotkey("dictation", key)) return;
      if (windowManager.winPushState?.active) {
        windowManager.handleWindowsPushKeyUp(key);
      } else if (
        isLiveWindow(windowManager.mainWindow) &&
        windowManager.getActivationMode() === "push"
      ) {
        windowManager.handleWindowsPushKeyUp(key);
      }
    };

    nativeKeyManager.on("key-down", dispatchNativeKeyDown);
    nativeKeyManager.on("key-up", dispatchNativeKeyUp);

    nativeKeyManager.on("error", (error) => {
      debugLogger.warn("[Push-to-Talk] Native key listener error", { error: error.message });
      if (isWindows && isLiveWindow(windowManager.mainWindow)) {
        windowManager.mainWindow.webContents.send("windows-ptt-unavailable", {
          reason: "error",
          message: error.message,
        });
      }
    });

    nativeKeyManager.on("unavailable", () => {
      debugLogger.debug(
        "[Push-to-Talk] Native key listener unavailable - falling back to toggle mode"
      );
      if (isWindows && isLiveWindow(windowManager.mainWindow)) {
        windowManager.mainWindow.webContents.send("windows-ptt-unavailable", {
          reason: "binary_not_found",
          message: i18nMain.t("windows.pttUnavailable"),
        });
      }
    });

    nativeKeyManager.on("ready", () => {
      debugLogger.debug("[Push-to-Talk] Native key listener ready and listening");
    });

    if (!isWindows) {
      nativeKeyManager.on("permission-denied", () => {
        debugLogger.warn(
          "[Push-to-Talk] Linux key listener has no permission to access input devices"
        );
        // GNOME, KDE and Hyprland run this listener only as a spare release
        // source in Hold; their own shortcut still presses and releases.
        if (!hotkeyManager.reliesOnLinuxKeyListener()) return;
        // Settings owns the recovery (toast, Hold disabled, back to Tap) and it
        // renders in the control panel, not the pill this event used to reach.
        for (const browserWindow of BrowserWindow.getAllWindows()) {
          if (!browserWindow.isDestroyed()) {
            browserWindow.webContents.send("linux-ptt-permission-denied");
          }
        }
      });
    }

    windowManager.initializeNativeKeyListeners().catch((err) => {
      debugLogger.warn("[Push-to-Talk] Native key listener startup failed", {
        error: err.message,
      });
    });

    ipcMain.on("hotkey-changed", () => {
      windowManager.resetWindowsPushState();
      windowManager.reconcileNativeKeyListeners();
    });
  }
}

ipcMain.on("mac-accessibility-features-ready", (_event, expectedAccountScope) => {
  if (process.platform !== "darwin") return;
  if (expectedAccountScope) {
    const accountScopeBinding = require("./src/helpers/accountScopeBinding");
    const currentAccountScope = accountScopeBinding.resolveActiveAccountScope({
      ...require("./src/helpers/tokenStore").getState(),
      binding: accountScopeBinding.read(),
    });
    if (!accountScopeBinding.matchesActiveAccountScope(expectedAccountScope, currentAccountScope)) {
      debugLogger.info("[Accessibility] Ignoring stale account-scoped readiness signal");
      return;
    }
  }
  macAccessibilityFeaturesReady = true;
  startMacAccessibilityFeatures?.();
});

// Listen for usage limit reached from dictation overlay, forward to control panel
ipcMain.on("limit-reached", (_event, data) => {
  if (isLiveWindow(windowManager?.controlPanelWindow)) {
    windowManager.controlPanelWindow.webContents.send("limit-reached", data);
  }
});

// App event handlers
if (gotSingleInstanceLock) {
  app.on("second-instance", async (_event, commandLine) => {
    await app.whenReady();
    if (!windowManager) {
      return;
    }

    if (isLiveWindow(windowManager.controlPanelWindow)) {
      windowManager.showControlPanel();
      if (windowManager.controlPanelWindow.webContents.isCrashed()) {
        windowManager.loadControlPanel();
      }
    } else {
      windowManager.createControlPanelWindow();
    }

    if (isLiveWindow(windowManager.mainWindow)) {
      windowManager.enforceMainWindowOnTop();
    } else {
      windowManager.createMainWindow();
    }

    // Check for OAuth protocol URL in command line arguments (Windows/Linux)
    const url = commandLine.find((arg) => arg.startsWith(`${OAUTH_PROTOCOL}://`));
    if (url) {
      if (handleEnterpriseAudioLink(url)) return;
      if (url.includes("upgrade-success")) {
        handleUpgradeDeepLink();
      } else if (isNoteDeepLink(url)) {
        await handleNoteDeepLink(url);
      } else if (isInvitationDeepLink(url)) {
        handleInvitationDeepLink(url);
      } else {
        void handleOAuthDeepLink(url);
      }
    }
  });

  app
    .whenReady()
    .then(() => {
      // On Linux, --enable-transparent-visuals requires a short delay before creating
      // windows to allow the compositor to set up the ARGB visual correctly.
      // Without this delay, transparent windows flicker on both X11 and Wayland.
      const delay = process.platform === "linux" ? 300 : 0;
      return new Promise((resolve) => setTimeout(resolve, delay));
    })
    .then(() => {
      if (process.platform === "win32") {
        session.defaultSession.setDisplayMediaRequestHandler((_request, callback) => {
          // Only the loopback audio track is used; the video source is
          // discarded by the renderer, so skip thumbnail generation.
          desktopCapturer
            .getSources({ types: ["screen"], thumbnailSize: { width: 0, height: 0 } })
            .then((sources) => {
              if (sources.length > 0) {
                callback({ video: sources[0], audio: "loopback" });
              } else {
                callback(null);
              }
            })
            .catch((error) => {
              console.error("Display media request failed:", error);
              callback(null);
            });
        });
      }

      startApp().catch((error) => {
        console.error("Failed to start app:", error);
        dialog.showErrorBox(
          i18nMain.t("startup.error.title"),
          i18nMain.t("startup.error.message", { error: error.message })
        );
        app.exit(1);
      });
    });

  app.on("window-all-closed", () => {
    // Don't quit on macOS when all windows are closed
    // The app should stay in the dock/menu bar
    if (process.platform !== "darwin") {
      app.quit();
    }
    // On macOS, keep the app running even without windows
  });

  app.on("browser-window-focus", (event, window) => {
    // Only apply always-on-top to the dictation window, not the control panel
    if (windowManager && isLiveWindow(windowManager.mainWindow)) {
      // Check if the focused window is the dictation window
      if (window === windowManager.mainWindow) {
        windowManager.enforceMainWindowOnTop();
      }
    }

    // Control panel doesn't need any special handling on focus
    // It should behave like a normal window
  });

  app.on("activate", () => {
    // On macOS, re-create windows when dock icon is clicked
    if (BrowserWindow.getAllWindows().length === 0) {
      if (windowManager) {
        windowManager.createMainWindow();
        windowManager.createControlPanelWindow();
      }
    } else {
      // Show control panel when dock icon is clicked (most common user action)
      if (windowManager && isLiveWindow(windowManager.controlPanelWindow)) {
        windowManager.showControlPanel();
      } else if (windowManager) {
        // If control panel doesn't exist, create it
        windowManager.createControlPanelWindow();
      }

      // Ensure dictation panel maintains its always-on-top status
      if (windowManager && isLiveWindow(windowManager.mainWindow)) {
        windowManager.enforceMainWindowOnTop();
      }
    }
  });

  let isShuttingDown = false;
  app.on("before-quit", (event) => {
    if (isShuttingDown) return;
    isShuttingDown = true;
    if (updateManager && updateManager.isQuittingForUpdate) {
      // Quit must proceed for the installer to run, so no preventDefault;
      // sidecar shutdown is best-effort (the reaper cleans up orphans on relaunch).
      performSyncTeardown();
      sidecarRegistry.shutdownAll().catch(() => {});
      return;
    }
    event.preventDefault();
    performSyncTeardown();
    Promise.allSettled([
      sidecarRegistry.shutdownAll(),
      Promise.race([
        ipcHandlers?.metaActivation?.shutdown(),
        new Promise((resolve) => setTimeout(resolve, 1500)),
      ]),
      // A teamspace delete still in its Undo window, which the toast already reported as done.
      Promise.race([
        isLiveWindow(windowManager?.controlPanelWindow)
          ? windowManager.controlPanelWindow.webContents.executeJavaScript(
              "window.commitHeldSpaceDeletes?.()"
            )
          : undefined,
        new Promise((resolve) => setTimeout(resolve, 1500)),
      ]),
    ]).finally(() => app.exit(0));
  });
}

function performSyncTeardown() {
  registryLoader?.stop();
  disposeRegistryIpc?.();
  disposeRegistryIpc = null;
  if (wakeRewarmTimer) {
    clearTimeout(wakeRewarmTimer);
    wakeRewarmTimer = null;
  }
  clearPendingNoteDeepLink();
  if (authBridgeServer) {
    authBridgeServer.close();
    authBridgeServer = null;
  }
  if (cliBridge) {
    cliBridge.stop().catch(() => {});
    cliBridge = null;
  }
  if (hotkeyManager) {
    hotkeyManager.unregisterAll();
  } else {
    globalShortcut.unregisterAll();
  }
  if (globeKeyManager) globeKeyManager.stop();
  if (windowsKeyManager) windowsKeyManager.stop();
  if (linuxKeyManager) linuxKeyManager.stop();
  if (meetingDetectionEngine) meetingDetectionEngine.stop();
  if (googleCalendarManager) googleCalendarManager.stop();
  if (microsoftCalendarManager) microsoftCalendarManager.stop();
  if (appleCalendarManager) appleCalendarManager.stop();
  if (calendarReminderScheduler) calendarReminderScheduler.stop();
  if (audioTapManager) audioTapManager.stop().catch(() => {});
  if (linuxPortalAudioManager) linuxPortalAudioManager.stop().catch(() => {});
  if (windowsLoopbackAudioManager) windowsLoopbackAudioManager.stop().catch(() => {});
  if (meetingAecManager) meetingAecManager.stop().catch(() => {});
  if (ipcHandlers) ipcHandlers._cleanupTextEditMonitor();
  if (textEditMonitor) textEditMonitor.stopMonitoring();
  if (updateManager) updateManager.cleanup();
}