diff --git a/apps/desktop/src/main/index.ts b/apps/desktop/src/main/index.ts index c7ed0eeb74b..fc1a81d4438 100644 --- a/apps/desktop/src/main/index.ts +++ b/apps/desktop/src/main/index.ts @@ -163,6 +163,7 @@ app.on("open-url", async (event, url) => { let isQuitting = false; let skipQuitConfirmation = false; +let forceFullCleanup = false; export function setSkipQuitConfirmation(): void { skipQuitConfirmation = true; @@ -173,6 +174,13 @@ export function quitApp(): void { app.quit(); } +/** Nuclear quit: also kills host-service(s) and pty-daemon/terminal-host. */ +export function quitAppCompletely(): void { + forceFullCleanup = true; + setSkipQuitConfirmation(); + app.quit(); +} + /** Bypasses before-quit — services are left running for re-adoption on next launch. */ export function exitImmediately(): void { app.exit(0); @@ -214,7 +222,7 @@ app.on("before-quit", async (event) => { isQuitting = true; try { - if (isDev) { + if (isDev || forceFullCleanup) { await runDevQuitCleanup(); } else { // Prod: leave services running so the next launch re-adopts via manifest. @@ -229,8 +237,9 @@ app.on("before-quit", async (event) => { }); /** - * Dev only — kill host-service + terminal-host children. They're spawned - * attached + ref'd in dev, so they'd reparent to init without an explicit stop. + * Full cleanup — kill host-service + terminal-host children. Used in dev (where + * they'd reparent to init without an explicit stop) and on the tray's + * "Quit Superset Completely" path in prod. */ async function runDevQuitCleanup(): Promise { getHostServiceCoordinator().stopAll(); diff --git a/apps/desktop/src/main/lib/tray/index.ts b/apps/desktop/src/main/lib/tray/index.ts index 278ed131fbb..81948961fb1 100644 --- a/apps/desktop/src/main/lib/tray/index.ts +++ b/apps/desktop/src/main/lib/tray/index.ts @@ -2,6 +2,7 @@ import { existsSync } from "node:fs"; import { join } from "node:path"; import { app, + dialog, Menu, type MenuItemConstructorOptions, nativeImage, @@ -9,7 +10,7 @@ import { } from "electron"; import { loadToken } from "lib/trpc/routers/auth/utils/auth-functions"; import { env } from "main/env.main"; -import { focusMainWindow, quitApp } from "main/index"; +import { focusMainWindow, quitApp, quitAppCompletely } from "main/index"; import { getHostServiceCoordinator, type HostServiceStatusEvent, @@ -83,6 +84,26 @@ function openSettings(): void { menuEmitter.emit("open-settings"); } +async function confirmAndQuitCompletely(): Promise { + try { + const { response } = await dialog.showMessageBox({ + type: "warning", + buttons: ["Quit Completely", "Cancel"], + defaultId: 1, + cancelId: 1, + title: "Quit Superset Completely", + message: "Quit Superset and stop all background services?", + detail: + "All open terminal sessions will be killed and any running host-services will be stopped. Use “Close Superset” instead if you want services to keep running for the next launch.", + }); + if (response === 0) { + quitAppCompletely(); + } + } catch (error) { + console.error("[Tray] Quit-completely confirmation failed:", error); + } +} + interface HostInfo { organizationName: string; version: string; @@ -229,9 +250,16 @@ async function updateTrayMenu(): Promise { }, { type: "separator" }, { - label: "Quit Superset", + label: "Close Superset", click: () => quitApp(), }, + { type: "separator" }, + { + label: "Quit Superset Completely", + click: () => { + void confirmAndQuitCompletely(); + }, + }, ]); tray.setContextMenu(menu);