Stop local daemon when removing localhost (#1297)

* fix(app): stop local daemon when removing localhost

* fix(app): handle daemon removal rollback

* fix(app): return listen address in desktop e2e mock

* fix(app): refresh daemon status after registration failure

* fix(app): unslop daemon status error coverage

* fix(app): preserve daemon toggle error context

---------

Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
This commit is contained in:
Matt Cowger
2026-06-04 22:30:40 +08:00
committed by GitHub
co-authored by Mohamed Boudra
parent 9e2c75a8a3
commit b5192e577a
9 changed files with 341 additions and 61 deletions
@@ -0,0 +1,51 @@
import { describe, expect, it } from "vitest";
import {
DaemonConnectionRegistrationError,
DaemonManagementOperationError,
getDaemonManagementErrorPresentation,
} from "./daemon-management-error";
describe("getDaemonManagementErrorPresentation", () => {
it("refreshes status when the daemon started but localhost registration failed", () => {
const presentation = getDaemonManagementErrorPresentation(
new DaemonConnectionRegistrationError("Desktop daemon did not return a listen address."),
false,
);
expect(presentation).toEqual({
message:
"Built-in daemon started, but Paseo could not save the localhost connection. Toggle daemon management off and on again, or add localhost manually.",
refreshStatus: true,
});
});
it("does not refresh status for daemon stop failures", () => {
const presentation = getDaemonManagementErrorPresentation(new Error("stop failed"), true);
expect(presentation).toEqual({
message: "Built-in daemon management was paused, but Paseo could not stop the daemon.",
refreshStatus: false,
});
});
it("uses the pre-mutation daemon management state for operation failures", () => {
const presentation = getDaemonManagementErrorPresentation(
new DaemonManagementOperationError(new Error("stop failed"), true),
false,
);
expect(presentation).toEqual({
message: "Built-in daemon management was paused, but Paseo could not stop the daemon.",
refreshStatus: false,
});
});
it("does not refresh status for generic update failures", () => {
const presentation = getDaemonManagementErrorPresentation(new Error("settings failed"), false);
expect(presentation).toEqual({
message: "Unable to update built-in daemon management.",
refreshStatus: false,
});
});
});
@@ -0,0 +1,52 @@
export class DaemonConnectionRegistrationError extends Error {
constructor(message: string) {
super(message);
this.name = "DaemonConnectionRegistrationError";
}
}
export class DaemonManagementOperationError extends Error {
readonly originalError: Error;
readonly wasManagingDaemon: boolean;
constructor(error: Error, wasManagingDaemon: boolean) {
super(error.message);
this.name = error.name;
this.cause = error;
this.originalError = error;
this.wasManagingDaemon = wasManagingDaemon;
}
}
export interface DaemonManagementErrorPresentation {
message: string;
refreshStatus: boolean;
}
export function getDaemonManagementErrorPresentation(
error: Error,
isManagingDaemon: boolean,
): DaemonManagementErrorPresentation {
const presentationError =
error instanceof DaemonManagementOperationError ? error.originalError : error;
const wasManagingDaemon =
error instanceof DaemonManagementOperationError ? error.wasManagingDaemon : isManagingDaemon;
if (presentationError instanceof DaemonConnectionRegistrationError) {
return {
message:
"Built-in daemon started, but Paseo could not save the localhost connection. Toggle daemon management off and on again, or add localhost manually.",
refreshStatus: true,
};
}
if (wasManagingDaemon) {
return {
message: "Built-in daemon management was paused, but Paseo could not stop the daemon.",
refreshStatus: false,
};
}
return {
message: "Unable to update built-in daemon management.",
refreshStatus: false,
};
}
@@ -9,8 +9,15 @@ import {
executeDaemonManagementToggle,
type DaemonManagementToggleResult,
} from "@/desktop/daemon/daemon-management-toggle";
import {
DaemonConnectionRegistrationError,
DaemonManagementOperationError,
getDaemonManagementErrorPresentation,
} from "@/desktop/daemon/daemon-management-error";
import { useDesktopIpcErrorReporter } from "@/desktop/hooks/desktop-ipc-error";
import type { DesktopSettings } from "@/desktop/settings/desktop-settings";
import { getHostRuntimeStore } from "@/runtime/host-runtime";
import { upsertDesktopDaemonConnection } from "@/runtime/daemon-start-service";
import { confirmDialog } from "@/utils/confirm-dialog";
type DesktopDaemonSettings = DesktopSettings["daemon"];
@@ -37,27 +44,51 @@ export function useBuiltInDaemonManagement(
DaemonManagementToggleResult,
Error
>({
mutationFn: () =>
executeDaemonManagementToggle(settings.manageBuiltInDaemon, daemonStatus, {
confirm: () =>
confirmDialog({
title: "Pause built-in daemon",
message:
"This will stop the built-in daemon immediately. Running agents and terminals connected to the built-in daemon will be stopped.",
confirmLabel: "Pause and stop",
cancelLabel: "Cancel",
destructive: true,
}),
persistSettings: (next) => updateSettings(next) as Promise<void>,
startDaemon: startDesktopDaemon,
stopDaemon: stopDesktopDaemon,
}),
mutationFn: async () => {
const wasManagingDaemon = settings.manageBuiltInDaemon;
try {
const result = await executeDaemonManagementToggle(wasManagingDaemon, daemonStatus, {
confirm: () =>
confirmDialog({
title: "Pause built-in daemon",
message:
"This will stop the built-in daemon immediately. Running agents and terminals connected to the built-in daemon will be stopped.",
confirmLabel: "Pause and stop",
cancelLabel: "Cancel",
destructive: true,
}),
persistSettings: (next) => updateSettings(next) as Promise<void>,
startDaemon: startDesktopDaemon,
stopDaemon: stopDesktopDaemon,
});
if (result.kind === "enabled") {
const upsertResult = await upsertDesktopDaemonConnection(
getHostRuntimeStore(),
result.newStatus,
);
if (!upsertResult.ok) {
throw new DaemonConnectionRegistrationError(upsertResult.error);
}
}
return result;
} catch (error) {
throw new DaemonManagementOperationError(
error instanceof Error ? error : new Error(String(error)),
wasManagingDaemon,
);
}
},
onError: (error) => {
const presentation = getDaemonManagementErrorPresentation(
error,
settings.manageBuiltInDaemon,
);
if (presentation.refreshStatus) {
refreshStatus();
}
reportError({
error,
message: settings.manageBuiltInDaemon
? "Built-in daemon management was paused, but Paseo could not stop the daemon."
: "Unable to update built-in daemon management.",
message: presentation.message,
logLabel: "[Settings] Failed to update built-in daemon management",
});
},