Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions apps/desktop/src/lib/trpc/routers/settings/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { settings, type TerminalPreset } from "@superset/local-db";
import { localDb } from "main/lib/local-db";
import { DEFAULT_CONFIRM_ON_QUIT } from "shared/constants";
import { DEFAULT_RINGTONE_ID, RINGTONES } from "shared/ringtones";
import { z } from "zod";
import { publicProcedure, router } from "../..";
Expand Down Expand Up @@ -158,5 +159,26 @@ export const createSettingsRouter = () => {

return { success: true };
}),

getConfirmOnQuit: publicProcedure.query(() => {
const row = getSettings();
// Default to true (confirm on quit enabled by default)
return row.confirmOnQuit ?? DEFAULT_CONFIRM_ON_QUIT;
}),

setConfirmOnQuit: publicProcedure
.input(z.object({ enabled: z.boolean() }))
.mutation(({ input }) => {
localDb
.insert(settings)
.values({ id: 1, confirmOnQuit: input.enabled })
.onConflictDoUpdate({
target: settings.id,
set: { confirmOnQuit: input.enabled },
})
.run();

return { success: true };
}),
});
};
108 changes: 97 additions & 11 deletions apps/desktop/src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ import { initSentry } from "./lib/sentry";
initSentry();

import path from "node:path";
import { app, BrowserWindow } from "electron";
import { settings } from "@superset/local-db";
import { app, BrowserWindow, dialog } from "electron";
import { makeAppSetup } from "lib/electron-app/factories/app/setup";
import { PROTOCOL_SCHEME } from "shared/constants";
import { DEFAULT_CONFIRM_ON_QUIT, PROTOCOL_SCHEME } from "shared/constants";
import { setupAgentHooks } from "./lib/agent-setup";
import { posthog } from "./lib/analytics";
import { initAppState } from "./lib/app-state";
Expand Down Expand Up @@ -91,10 +92,100 @@ app.on("open-url", async (event, url) => {
await processDeepLink(url);
});

// Track when app is quitting to suppress expected termination errors
type QuitState =
| "idle"
| "confirming"
| "confirmed"
| "cleaning"
| "ready-to-quit";
let quitState: QuitState = "idle";
let isQuitting = false;
app.on("before-quit", () => {
let skipConfirmation = false;

/**
* Check if the user has enabled the confirm-on-quit setting
*/
function getConfirmOnQuitSetting(): boolean {
try {
const row = localDb.select().from(settings).get();
return row?.confirmOnQuit ?? DEFAULT_CONFIRM_ON_QUIT;
} catch {
return DEFAULT_CONFIRM_ON_QUIT;
}
}

/**
* Skip the confirmation dialog for the next quit (e.g., auto-updater)
*/
export function setSkipQuitConfirmation(): void {
skipConfirmation = true;
}

/**
* Skip the confirmation dialog and quit immediately
*/
export function quitWithoutConfirmation(): void {
skipConfirmation = true;
app.quit();
}

app.on("before-quit", async (event) => {
isQuitting = true;

if (quitState === "ready-to-quit") return;
if (quitState === "cleaning" || quitState === "confirming") {
event.preventDefault();
return;
}

// Check if we need to show confirmation
if (quitState === "idle") {
const shouldConfirm = !skipConfirmation && getConfirmOnQuitSetting();

if (shouldConfirm) {
event.preventDefault();
quitState = "confirming";

try {
const { response } = await dialog.showMessageBox({
type: "question",
buttons: ["Quit", "Cancel"],
defaultId: 0,
cancelId: 1,
title: "Quit Superset",
message: "Are you sure you want to quit?",
});

if (response === 1) {
// User cancelled
quitState = "idle";
isQuitting = false;
return;
}
} catch (error) {
// Dialog failed - proceed with quit to avoid stuck state
console.error("[main] Quit confirmation dialog failed:", error);
}

// User confirmed or dialog failed, proceed with quit
quitState = "confirmed";
app.quit();
return;
}

// No confirmation needed
quitState = "confirmed";
}

event.preventDefault();
quitState = "cleaning";

try {
await Promise.all([terminalManager.cleanup(), posthog?.shutdown()]);
} finally {
quitState = "ready-to-quit";
app.quit();
}
});

process.on("uncaughtException", (error) => {
Expand All @@ -111,8 +202,8 @@ process.on("unhandledRejection", (reason) => {
const gotTheLock = app.requestSingleInstanceLock();

if (!gotTheLock) {
// Another instance is already running, quit this one
app.quit();
// Another instance is already running, exit immediately without triggering before-quit
app.exit(0);
} else {
// Handle deep links when second instance is launched (Windows/Linux)
app.on("second-instance", async (_event, argv) => {
Expand Down Expand Up @@ -144,10 +235,5 @@ if (!gotTheLock) {
if (coldStartUrl) {
await processDeepLink(coldStartUrl);
}

// Clean up all terminals and analytics when app is quitting
app.on("before-quit", async () => {
await Promise.all([terminalManager.cleanup(), posthog?.shutdown()]);
});
})();
}
3 changes: 3 additions & 0 deletions apps/desktop/src/main/lib/auto-updater.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { EventEmitter } from "node:events";
import { app, dialog } from "electron";
import { autoUpdater } from "electron-updater";
import { env } from "main/env.main";
import { setSkipQuitConfirmation } from "main/index";
import { AUTO_UPDATE_STATUS, type AutoUpdateStatus } from "shared/auto-update";
import { PLATFORM } from "shared/constants";

Expand Down Expand Up @@ -49,6 +50,8 @@ export function installUpdate(): void {
emitStatus(AUTO_UPDATE_STATUS.IDLE);
return;
}
// Skip confirmation dialog - quitAndInstall internally calls app.quit()
setSkipQuitConfirmation();
autoUpdater.quitAndInstall(false, true);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { Label } from "@superset/ui/label";
import { Switch } from "@superset/ui/switch";
import { trpc } from "renderer/lib/trpc";

export function BehaviorSettings() {
const utils = trpc.useUtils();
const { data: confirmOnQuit, isLoading } =
trpc.settings.getConfirmOnQuit.useQuery();
const setConfirmOnQuit = trpc.settings.setConfirmOnQuit.useMutation({
onMutate: async ({ enabled }) => {
// Cancel outgoing fetches
await utils.settings.getConfirmOnQuit.cancel();
// Snapshot previous value
const previous = utils.settings.getConfirmOnQuit.getData();
// Optimistically update
utils.settings.getConfirmOnQuit.setData(undefined, enabled);
return { previous };
},
onError: (_err, _vars, context) => {
// Rollback on error
if (context?.previous !== undefined) {
utils.settings.getConfirmOnQuit.setData(undefined, context.previous);
}
},
onSettled: () => {
// Refetch to ensure sync with server
utils.settings.getConfirmOnQuit.invalidate();
},
});

const handleToggle = (enabled: boolean) => {
setConfirmOnQuit.mutate({ enabled });
};

return (
<div className="p-6 max-w-4xl w-full">
<div className="mb-8">
<h2 className="text-xl font-semibold">Behavior</h2>
<p className="text-sm text-muted-foreground mt-1">
Configure app behavior and preferences
</p>
</div>

<div className="space-y-6">
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="confirm-on-quit" className="text-sm font-medium">
Confirm before quitting
</Label>
<p className="text-xs text-muted-foreground">
Show a confirmation dialog when quitting the app
</p>
</div>
<Switch
id="confirm-on-quit"
checked={confirmOnQuit ?? true}
onCheckedChange={handleToggle}
disabled={isLoading || setConfirmOnQuit.isPending}
/>
</div>
</div>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { SettingsSection } from "renderer/stores";
import { AccountSettings } from "./AccountSettings";
import { AppearanceSettings } from "./AppearanceSettings";
import { BehaviorSettings } from "./BehaviorSettings";
import { KeyboardShortcutsSettings } from "./KeyboardShortcutsSettings";
import { PresetsSettings } from "./PresetsSettings";
import { ProjectSettings } from "./ProjectSettings";
Expand All @@ -21,6 +22,7 @@ export function SettingsContent({ activeSection }: SettingsContentProps) {
{activeSection === "ringtones" && <RingtonesSettings />}
{activeSection === "keyboard" && <KeyboardShortcutsSettings />}
{activeSection === "presets" && <PresetsSettings />}
{activeSection === "behavior" && <BehaviorSettings />}
</div>
);
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { cn } from "@superset/ui/utils";
import {
HiOutlineAdjustmentsHorizontal,
HiOutlineBell,
HiOutlineCog6Tooth,
HiOutlineCommandLine,
Expand Down Expand Up @@ -43,6 +44,11 @@ const GENERAL_SECTIONS: {
label: "Presets",
icon: <HiOutlineCog6Tooth className="h-4 w-4" />,
},
{
id: "behavior",
label: "Behavior",
icon: <HiOutlineAdjustmentsHorizontal className="h-4 w-4" />,
},
];

export function GeneralSettings({
Expand Down
3 changes: 2 additions & 1 deletion apps/desktop/src/renderer/stores/app-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ export type SettingsSection =
| "appearance"
| "keyboard"
| "presets"
| "ringtones";
| "ringtones"
| "behavior";

interface AppState {
currentView: AppView;
Expand Down
3 changes: 3 additions & 0 deletions apps/desktop/src/shared/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,6 @@ export const NOTIFICATION_EVENTS = {
AGENT_COMPLETE: "agent-complete",
FOCUS_TAB: "focus-tab",
} as const;

// Default user preference values
export const DEFAULT_CONFIRM_ON_QUIT = true;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE `settings` ADD `confirm_on_quit` integer;
Loading
Loading