feat(desktop): stop the daemon when you quit the app (#2454)

Quitting the desktop app now shuts down the daemon it started, so
"restart the app" is a complete reset users can act on without first
learning what a daemon is. The old default kept it alive; the toggle
under Settings > Host still opts back in.

Existing installs already persisted `keepRunningAfterQuit: true` from
the old default, so a new default alone would only reach fresh installs.
A one-time migration resets it, and an explicit toggle afterwards
persists the migration flag and is never overridden again.

Only a desktop-managed daemon is stopped — one started with
`paseo daemon start` is left alone.
This commit is contained in:
Mohamed Boudra
2026-07-26 21:55:37 +02:00
committed by GitHub
parent fe28850fad
commit 392095c1b2
7 changed files with 67 additions and 9 deletions

View File

@@ -382,5 +382,5 @@ $PASEO_HOME/
## Deployment models
1. **Local daemon** (default): `paseo daemon start` on `127.0.0.1:6767`
2. **Managed desktop**: Electron app spawns daemon as subprocess
2. **Managed desktop**: Electron app spawns daemon as subprocess, and stops it again on quit so that "restart the app" is a complete reset. Settings > Host > "Keep daemon running after quit" opts out. Only a daemon the desktop started is stopped — a daemon you started yourself with `paseo daemon start` is left alone (`paseo.pid` records `desktopManaged`).
3. **Remote + relay**: Daemon behind firewall, relay bridges with E2E encryption

View File

@@ -43,7 +43,8 @@ This architecture means:
- The daemon can run on any machine: laptop, VM, remote server
- Multiple clients can connect simultaneously
- Agents keep running when you close the app
- Agents keep running when a client disconnects — the daemon owns them, not the client
- Quitting the desktop app stops the daemon it started, so "restart the app" is a real fix; a daemon you run yourself is unaffected
## Target user

View File

@@ -28,7 +28,7 @@ export const DEFAULT_DESKTOP_SETTINGS: DesktopSettings = {
releaseChannel: "stable",
daemon: {
manageBuiltInDaemon: true,
keepRunningAfterQuit: true,
keepRunningAfterQuit: false,
},
};

View File

@@ -31,7 +31,7 @@ const DEFAULT_DESKTOP: DesktopSettings = {
releaseChannel: "stable",
daemon: {
manageBuiltInDaemon: true,
keepRunningAfterQuit: true,
keepRunningAfterQuit: false,
},
};

View File

@@ -7,12 +7,12 @@ import {
stopDesktopManagedDaemonOnQuitIfNeeded,
} from "./quit-lifecycle";
const SETTINGS_KEEP_RUNNING = DEFAULT_DESKTOP_SETTINGS;
const SETTINGS_STOP_ON_QUIT = {
const SETTINGS_STOP_ON_QUIT = DEFAULT_DESKTOP_SETTINGS;
const SETTINGS_KEEP_RUNNING = {
...DEFAULT_DESKTOP_SETTINGS,
daemon: {
...DEFAULT_DESKTOP_SETTINGS.daemon,
keepRunningAfterQuit: false,
keepRunningAfterQuit: true,
},
};
@@ -29,7 +29,7 @@ function waitForQuitLifecycle(): Promise<void> {
}
describe("quit-lifecycle", () => {
it("only stops when keepRunningAfterQuit is explicitly disabled", () => {
it("stops by default and only keeps running when keepRunningAfterQuit is enabled", () => {
expect(shouldStopDesktopManagedDaemonOnQuit(SETTINGS_STOP_ON_QUIT)).toBe(true);
expect(shouldStopDesktopManagedDaemonOnQuit(SETTINGS_KEEP_RUNNING)).toBe(false);
});

View File

@@ -162,6 +162,50 @@ describe("desktop-settings", () => {
expect(persisted).toBe(raw);
});
it("resets the pre-existing keep-running default so the daemon stops with the app", async () => {
const userDataPath = await createTempUserDataDir();
directories.add(userDataPath);
await writeFile(
settingsFilePath(userDataPath),
JSON.stringify({
version: 1,
settings: {
releaseChannel: "stable",
daemon: { manageBuiltInDaemon: true, keepRunningAfterQuit: true },
},
migrations: { legacyRendererSettingsImported: true },
}),
);
const store = createDesktopSettingsStore({ userDataPath });
const settings = await store.get();
expect(settings.daemon.keepRunningAfterQuit).toBe(false);
});
it("keeps an explicit keep-running choice across restarts", async () => {
const userDataPath = await createTempUserDataDir();
directories.add(userDataPath);
await writeFile(
settingsFilePath(userDataPath),
JSON.stringify({
version: 1,
settings: {
releaseChannel: "stable",
daemon: { manageBuiltInDaemon: true, keepRunningAfterQuit: true },
},
migrations: { legacyRendererSettingsImported: true },
}),
);
await createDesktopSettingsStore({ userDataPath }).patch({
daemon: { keepRunningAfterQuit: true },
});
const settings = await createDesktopSettingsStore({ userDataPath }).get();
expect(settings.daemon.keepRunningAfterQuit).toBe(true);
});
it("migrates desktop-owned values from legacy renderer settings once", async () => {
const userDataPath = await createTempUserDataDir();
directories.add(userDataPath);

View File

@@ -22,6 +22,11 @@ interface PersistedDesktopSettingsDocument {
settings: DesktopSettings;
migrations: {
legacyRendererSettingsImported: boolean;
// Installs created before the stop-on-quit default persisted the old
// `keepRunningAfterQuit: true` default to disk, so the new default alone
// would only reach fresh installs. Reset it once; a later explicit toggle
// persists this flag and is never overridden again.
daemonStopOnQuitDefaultApplied: boolean;
};
}
@@ -35,7 +40,7 @@ export const DEFAULT_DESKTOP_SETTINGS: DesktopSettings = {
releaseChannel: "stable",
daemon: {
manageBuiltInDaemon: true,
keepRunningAfterQuit: true,
keepRunningAfterQuit: false,
},
};
@@ -72,6 +77,7 @@ function buildDefaultDocument(): PersistedDesktopSettingsDocument {
},
migrations: {
legacyRendererSettingsImported: false,
daemonStopOnQuitDefaultApplied: true,
},
};
}
@@ -180,11 +186,18 @@ function coerceDocument(input: unknown): PersistedDesktopSettingsDocument {
const migrations = isRecord(input.migrations)
? {
legacyRendererSettingsImported: input.migrations.legacyRendererSettingsImported === true,
daemonStopOnQuitDefaultApplied: input.migrations.daemonStopOnQuitDefaultApplied === true,
}
: {
legacyRendererSettingsImported: false,
daemonStopOnQuitDefaultApplied: false,
};
if (!migrations.daemonStopOnQuitDefaultApplied) {
settings.daemon.keepRunningAfterQuit = DEFAULT_DESKTOP_SETTINGS.daemon.keepRunningAfterQuit;
migrations.daemonStopOnQuitDefaultApplied = true;
}
return {
version: 1,
settings,