// 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 ` 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( "

OpenWhispr sign-in complete.

You can close this tab.

" ); }); 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(); }