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 07:30:40 -07:00
committed by GitHub
parent 9e2c75a8a3
commit b5192e577a
9 changed files with 341 additions and 61 deletions

View File

@@ -124,7 +124,7 @@ export async function injectDesktopBridge(page: Page, config: DesktopBridgeConfi
return { return {
serverId: cfg.serverId, serverId: cfg.serverId,
status: daemonRunning ? "running" : "stopped", status: daemonRunning ? "running" : "stopped",
listen: null, listen: "127.0.0.1:6767",
hostname: null, hostname: null,
pid: currentPid, pid: currentPid,
home: "", home: "",

View File

@@ -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,
});
});
});

View File

@@ -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,
};
}

View File

@@ -9,8 +9,15 @@ import {
executeDaemonManagementToggle, executeDaemonManagementToggle,
type DaemonManagementToggleResult, type DaemonManagementToggleResult,
} from "@/desktop/daemon/daemon-management-toggle"; } 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 { useDesktopIpcErrorReporter } from "@/desktop/hooks/desktop-ipc-error";
import type { DesktopSettings } from "@/desktop/settings/desktop-settings"; 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"; import { confirmDialog } from "@/utils/confirm-dialog";
type DesktopDaemonSettings = DesktopSettings["daemon"]; type DesktopDaemonSettings = DesktopSettings["daemon"];
@@ -37,27 +44,51 @@ export function useBuiltInDaemonManagement(
DaemonManagementToggleResult, DaemonManagementToggleResult,
Error Error
>({ >({
mutationFn: () => mutationFn: async () => {
executeDaemonManagementToggle(settings.manageBuiltInDaemon, daemonStatus, { const wasManagingDaemon = settings.manageBuiltInDaemon;
confirm: () => try {
confirmDialog({ const result = await executeDaemonManagementToggle(wasManagingDaemon, daemonStatus, {
title: "Pause built-in daemon", confirm: () =>
message: confirmDialog({
"This will stop the built-in daemon immediately. Running agents and terminals connected to the built-in daemon will be stopped.", title: "Pause built-in daemon",
confirmLabel: "Pause and stop", message:
cancelLabel: "Cancel", "This will stop the built-in daemon immediately. Running agents and terminals connected to the built-in daemon will be stopped.",
destructive: true, confirmLabel: "Pause and stop",
}), cancelLabel: "Cancel",
persistSettings: (next) => updateSettings(next) as Promise<void>, destructive: true,
startDaemon: startDesktopDaemon, }),
stopDaemon: stopDesktopDaemon, 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) => { onError: (error) => {
const presentation = getDaemonManagementErrorPresentation(
error,
settings.manageBuiltInDaemon,
);
if (presentation.refreshStatus) {
refreshStatus();
}
reportError({ reportError({
error, error,
message: settings.manageBuiltInDaemon message: presentation.message,
? "Built-in daemon management was paused, but Paseo could not stop the daemon."
: "Unable to update built-in daemon management.",
logLabel: "[Settings] Failed to update built-in daemon management", logLabel: "[Settings] Failed to update built-in daemon management",
}); });
}, },

View File

@@ -1,5 +1,5 @@
import { describe, expect, it, vi } from "vitest"; import { describe, expect, it, vi } from "vitest";
import { DaemonStartService } from "./daemon-start-service"; import { DaemonStartService, upsertDesktopDaemonConnection } from "./daemon-start-service";
import type { HostRuntimeStore } from "./host-runtime"; import type { HostRuntimeStore } from "./host-runtime";
import type { DesktopDaemonStatus } from "@/desktop/daemon/desktop-daemon"; import type { DesktopDaemonStatus } from "@/desktop/daemon/desktop-daemon";
@@ -223,3 +223,50 @@ describe("DaemonStartService", () => {
expect(notifications).toBe(countAfterFirst); expect(notifications).toBe(countAfterFirst);
}); });
}); });
describe("upsertDesktopDaemonConnection", () => {
it("upserts a valid desktop daemon status", async () => {
const fake = createFakeStore();
const result = await upsertDesktopDaemonConnection(fake.store, makeStatus());
expect(result).toEqual({ ok: true });
expect(fake.upserts).toEqual([
{ listenAddress: "127.0.0.1:6767", serverId: "srv_desktop", hostname: "desktop" },
]);
});
it("rejects a missing listen address without upserting", async () => {
const fake = createFakeStore();
const result = await upsertDesktopDaemonConnection(fake.store, makeStatus({ listen: null }));
expect(result).toEqual({
ok: false,
error: "Desktop daemon did not return a listen address.",
});
expect(fake.upserts).toEqual([]);
});
it("rejects a missing server id without upserting", async () => {
const fake = createFakeStore();
const result = await upsertDesktopDaemonConnection(fake.store, makeStatus({ serverId: "" }));
expect(result).toEqual({
ok: false,
error: "Desktop daemon did not return a server id.",
});
expect(fake.upserts).toEqual([]);
});
it("rejects an unsupported listen address without upserting", async () => {
const fake = createFakeStore();
const result = await upsertDesktopDaemonConnection(fake.store, makeStatus({ listen: "???" }));
expect(result.ok).toBe(false);
expect(result.ok ? "" : result.error).toContain("unsupported listen address");
expect(fake.upserts).toEqual([]);
});
});

View File

@@ -4,13 +4,41 @@ import type { HostRuntimeStore } from "@/runtime/host-runtime";
export type DaemonStartResult = { ok: true } | { ok: false; error: string }; export type DaemonStartResult = { ok: true } | { ok: false; error: string };
type DaemonConnectionStore = Pick<HostRuntimeStore, "upsertConnectionFromListen">;
export interface DaemonStartServiceDeps { export interface DaemonStartServiceDeps {
store: Pick<HostRuntimeStore, "upsertConnectionFromListen">; store: DaemonConnectionStore;
startDesktopDaemon?: () => Promise<DesktopDaemonStatus>; startDesktopDaemon?: () => Promise<DesktopDaemonStatus>;
} }
export async function upsertDesktopDaemonConnection(
store: DaemonConnectionStore,
daemon: DesktopDaemonStatus,
): Promise<DaemonStartResult> {
const listenAddress = daemon.listen?.trim() ?? "";
const serverId = daemon.serverId.trim();
if (!listenAddress) {
return { ok: false, error: "Desktop daemon did not return a listen address." };
}
if (!serverId) {
return { ok: false, error: "Desktop daemon did not return a server id." };
}
if (!connectionFromListen(listenAddress)) {
return {
ok: false,
error: `Desktop daemon returned an unsupported listen address: ${listenAddress}`,
};
}
await store.upsertConnectionFromListen({
listenAddress,
serverId,
hostname: daemon.hostname,
});
return { ok: true };
}
export class DaemonStartService { export class DaemonStartService {
private readonly store: Pick<HostRuntimeStore, "upsertConnectionFromListen">; private readonly store: DaemonConnectionStore;
private readonly invokeStartDesktopDaemon: () => Promise<DesktopDaemonStatus>; private readonly invokeStartDesktopDaemon: () => Promise<DesktopDaemonStatus>;
private readonly listeners = new Set<() => void>(); private readonly listeners = new Set<() => void>();
private lastError: string | null = null; private lastError: string | null = null;
@@ -25,23 +53,8 @@ export class DaemonStartService {
this.beginRequest(); this.beginRequest();
try { try {
const daemon = await this.invokeStartDesktopDaemon(); const daemon = await this.invokeStartDesktopDaemon();
const listenAddress = daemon.listen?.trim() ?? ""; const result = await upsertDesktopDaemonConnection(this.store, daemon);
const serverId = daemon.serverId.trim(); return result.ok ? result : this.fail(result.error);
if (!listenAddress) {
return this.fail("Desktop daemon did not return a listen address.");
}
if (!serverId) {
return this.fail("Desktop daemon did not return a server id.");
}
if (!connectionFromListen(listenAddress)) {
return this.fail(`Desktop daemon returned an unsupported listen address: ${listenAddress}`);
}
await this.store.upsertConnectionFromListen({
listenAddress,
serverId,
hostname: daemon.hostname,
});
return { ok: true };
} catch (error) { } catch (error) {
return this.fail(error instanceof Error ? error.message : String(error)); return this.fail(error instanceof Error ? error.message : String(error));
} finally { } finally {

View File

@@ -84,6 +84,7 @@ import {
import { Combobox, ComboboxItem, type ComboboxOption } from "@/components/ui/combobox"; import { Combobox, ComboboxItem, type ComboboxOption } from "@/components/ui/combobox";
import { DesktopPermissionsSection } from "@/desktop/components/desktop-permissions-section"; import { DesktopPermissionsSection } from "@/desktop/components/desktop-permissions-section";
import { IntegrationsSection } from "@/desktop/components/integrations-section"; import { IntegrationsSection } from "@/desktop/components/integrations-section";
import { LocalDaemonSection } from "@/desktop/components/desktop-updates-section";
import { isElectronRuntime } from "@/desktop/host"; import { isElectronRuntime } from "@/desktop/host";
import { useDesktopAppUpdater } from "@/desktop/updates/use-desktop-app-updater"; import { useDesktopAppUpdater } from "@/desktop/updates/use-desktop-app-updater";
import { formatVersionWithPrefix } from "@/desktop/updates/desktop-updates"; import { formatVersionWithPrefix } from "@/desktop/updates/desktop-updates";
@@ -133,6 +134,7 @@ interface SidebarSectionItem {
const SIDEBAR_SECTION_ITEMS: SidebarSectionItem[] = [ const SIDEBAR_SECTION_ITEMS: SidebarSectionItem[] = [
{ id: "general", label: "General", icon: Settings }, { id: "general", label: "General", icon: Settings },
{ id: "daemon", label: "Daemon", icon: Server, desktopOnly: true },
{ id: "appearance", label: "Appearance", icon: Palette }, { id: "appearance", label: "Appearance", icon: Palette },
{ id: "shortcuts", label: "Shortcuts", icon: Keyboard, desktopOnly: true }, { id: "shortcuts", label: "Shortcuts", icon: Keyboard, desktopOnly: true },
{ id: "integrations", label: "Integrations", icon: Puzzle, desktopOnly: true }, { id: "integrations", label: "Integrations", icon: Puzzle, desktopOnly: true },
@@ -155,6 +157,24 @@ const HOST_SECTION_ITEMS: HostSectionItem[] = [
{ id: "host", label: "Host", icon: Server }, { id: "host", label: "Host", icon: Server },
]; ];
function renderHostSettingsContent(
view: Extract<SettingsView, { kind: "host" }>,
onHostRemoved: () => void,
): ReactNode {
switch (view.section) {
case "connections":
return <HostConnectionsPage serverId={view.serverId} />;
case "agents":
return <HostAgentsPage serverId={view.serverId} />;
case "workspaces":
return <HostWorkspacesPage serverId={view.serverId} />;
case "providers":
return <HostProvidersPage serverId={view.serverId} />;
case "host":
return <HostSettingsPage serverId={view.serverId} onHostRemoved={onHostRemoved} />;
}
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Trigger + sidebar style helpers // Trigger + sidebar style helpers
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -1325,18 +1345,7 @@ export default function SettingsScreen({ view }: SettingsScreenProps) {
const content = (() => { const content = (() => {
if (view.kind === "host") { if (view.kind === "host") {
switch (view.section) { return renderHostSettingsContent(view, handleHostRemoved);
case "connections":
return <HostConnectionsPage serverId={view.serverId} />;
case "agents":
return <HostAgentsPage serverId={view.serverId} />;
case "workspaces":
return <HostWorkspacesPage serverId={view.serverId} />;
case "providers":
return <HostProvidersPage serverId={view.serverId} />;
case "host":
return <HostSettingsPage serverId={view.serverId} onHostRemoved={handleHostRemoved} />;
}
} }
if (view.kind === "projects") { if (view.kind === "projects") {
return <ProjectsScreen view={view} />; return <ProjectsScreen view={view} />;
@@ -1356,6 +1365,8 @@ export default function SettingsScreen({ view }: SettingsScreenProps) {
handleTerminalScrollbackLinesChange={handleTerminalScrollbackLinesChange} handleTerminalScrollbackLinesChange={handleTerminalScrollbackLinesChange}
/> />
); );
case "daemon":
return <LocalDaemonSection />;
case "appearance": case "appearance":
return <AppearanceSection />; return <AppearanceSection />;
case "shortcuts": case "shortcuts":

View File

@@ -7,7 +7,10 @@ import { AdaptiveRenameModal } from "@/components/rename-modal";
import { SettingsTextAreaCard } from "@/components/settings-textarea"; import { SettingsTextAreaCard } from "@/components/settings-textarea";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Switch } from "@/components/ui/switch"; import { Switch } from "@/components/ui/switch";
import { startDesktopDaemon, stopDesktopDaemon } from "@/desktop/daemon/desktop-daemon";
import { LocalDaemonSection } from "@/desktop/components/desktop-updates-section"; import { LocalDaemonSection } from "@/desktop/components/desktop-updates-section";
import { useDaemonStatus } from "@/desktop/hooks/use-daemon-status";
import { useDesktopSettings } from "@/desktop/settings/desktop-settings";
import { PairDeviceModal } from "@/desktop/components/pair-device-modal"; import { PairDeviceModal } from "@/desktop/components/pair-device-modal";
import { useDaemonConfig } from "@/hooks/use-daemon-config"; import { useDaemonConfig } from "@/hooks/use-daemon-config";
import { useIsLocalDaemon } from "@/hooks/use-is-local-daemon"; import { useIsLocalDaemon } from "@/hooks/use-is-local-daemon";
@@ -72,7 +75,6 @@ function formatDaemonVersionBadge(version: string | null): string | null {
} }
const REMOVE_CONNECTION_HEADER: SheetHeader = { title: "Remove connection" }; const REMOVE_CONNECTION_HEADER: SheetHeader = { title: "Remove connection" };
const REMOVE_HOST_HEADER: SheetHeader = { title: "Remove host" };
function useHostProfile(serverId: string): HostProfile | null { function useHostProfile(serverId: string): HostProfile | null {
const daemons = useHosts(); const daemons = useHosts();
@@ -276,7 +278,7 @@ export function HostSettingsPage({
{isLocalDaemon ? <LocalDaemonSection /> : null} {isLocalDaemon ? <LocalDaemonSection /> : null}
<RemoveHostSection host={host} onRemoved={onHostRemoved} /> <RemoveHostSection host={host} isLocalDaemon={isLocalDaemon} onRemoved={onHostRemoved} />
</View> </View>
); );
} }
@@ -826,11 +828,22 @@ function PairDeviceRow() {
); );
} }
function RemoveHostSection({ host, onRemoved }: { host: HostProfile; onRemoved?: () => void }) { function RemoveHostSection({
host,
isLocalDaemon,
onRemoved,
}: {
host: HostProfile;
isLocalDaemon: boolean;
onRemoved?: () => void;
}) {
const { theme } = useUnistyles(); const { theme } = useUnistyles();
const { removeHost } = useHostMutations(); const { removeHost } = useHostMutations();
const { updateSettings } = useDesktopSettings();
const { data: daemonStatusData, setStatus } = useDaemonStatus();
const [isConfirming, setIsConfirming] = useState(false); const [isConfirming, setIsConfirming] = useState(false);
const [isRemoving, setIsRemoving] = useState(false); const [isRemoving, setIsRemoving] = useState(false);
const daemonStatus = daemonStatusData?.status ?? null;
const destructiveTextStyle = useMemo( const destructiveTextStyle = useMemo(
() => ({ color: theme.colors.destructive }), () => ({ color: theme.colors.destructive }),
@@ -843,9 +856,45 @@ function RemoveHostSection({ host, onRemoved }: { host: HostProfile; onRemoved?:
setIsConfirming(false); setIsConfirming(false);
}, [isRemoving]); }, [isRemoving]);
const handleCancel = useCallback(() => setIsConfirming(false), []); const handleCancel = useCallback(() => setIsConfirming(false), []);
const rollbackLocalhostRemoval = useCallback(
async (shouldRestartDaemon: boolean) => {
await updateSettings({ daemon: { manageBuiltInDaemon: true } });
if (!shouldRestartDaemon) {
return;
}
setStatus(await startDesktopDaemon());
},
[setStatus, updateSettings],
);
const handleConfirmRemove = useCallback(() => { const handleConfirmRemove = useCallback(() => {
setIsRemoving(true); setIsRemoving(true);
void removeHost(host.serverId) const remove = async () => {
let didDisableDaemonManagement = false;
let didStopDaemon = false;
if (isLocalDaemon) {
try {
await updateSettings({ daemon: { manageBuiltInDaemon: false } });
didDisableDaemonManagement = true;
if (daemonStatus?.status === "running" && daemonStatus.desktopManaged) {
setStatus(await stopDesktopDaemon());
didStopDaemon = true;
}
await removeHost(host.serverId);
} catch (error) {
if (didDisableDaemonManagement) {
try {
await rollbackLocalhostRemoval(didStopDaemon);
} catch (rollbackError) {
console.error("[HostPage] Failed to roll back localhost removal", rollbackError);
}
}
throw error;
}
return;
}
await removeHost(host.serverId);
};
void remove()
.then(() => { .then(() => {
setIsConfirming(false); setIsConfirming(false);
onRemoved?.(); onRemoved?.();
@@ -853,10 +902,29 @@ function RemoveHostSection({ host, onRemoved }: { host: HostProfile; onRemoved?:
}) })
.catch((error) => { .catch((error) => {
console.error("[HostPage] Failed to remove host", error); console.error("[HostPage] Failed to remove host", error);
Alert.alert("Error", "Unable to remove host"); Alert.alert(
"Error",
isLocalDaemon ? "Unable to remove localhost connection" : "Unable to remove host",
);
}) })
.finally(() => setIsRemoving(false)); .finally(() => setIsRemoving(false));
}, [host.serverId, onRemoved, removeHost]); }, [
daemonStatus,
host.serverId,
isLocalDaemon,
onRemoved,
removeHost,
rollbackLocalhostRemoval,
setStatus,
updateSettings,
]);
const confirmationHeader = useMemo<SheetHeader>(
() => ({
title: isLocalDaemon ? "Remove localhost connection and stop daemon?" : "Remove host",
}),
[isLocalDaemon],
);
const removeIcon = useMemo( const removeIcon = useMemo(
() => <Trash2 size={theme.iconSize.sm} color={theme.colors.destructive} />, () => <Trash2 size={theme.iconSize.sm} color={theme.colors.destructive} />,
@@ -870,9 +938,13 @@ function RemoveHostSection({ host, onRemoved }: { host: HostProfile; onRemoved?:
<View style={settingsStyles.card}> <View style={settingsStyles.card}>
<View style={settingsStyles.row}> <View style={settingsStyles.row}>
<View style={settingsStyles.rowContent}> <View style={settingsStyles.rowContent}>
<Text style={settingsStyles.rowTitle}>Remove host</Text> <Text style={settingsStyles.rowTitle}>
{isLocalDaemon ? "Remove localhost connection" : "Remove host"}
</Text>
<Text style={settingsStyles.rowHint}> <Text style={settingsStyles.rowHint}>
Removes this host and its saved connections from this device {isLocalDaemon
? "Removes localhost from this device and stops the built-in daemon"
: "Removes this host and its saved connections from this device"}
</Text> </Text>
</View> </View>
<Button <Button
@@ -890,13 +962,15 @@ function RemoveHostSection({ host, onRemoved }: { host: HostProfile; onRemoved?:
{isConfirming ? ( {isConfirming ? (
<AdaptiveModalSheet <AdaptiveModalSheet
header={REMOVE_HOST_HEADER} header={confirmationHeader}
visible visible
onClose={handleCloseConfirm} onClose={handleCloseConfirm}
testID="remove-host-confirm-modal" testID="remove-host-confirm-modal"
> >
<Text style={styles.confirmText}> <Text style={styles.confirmText}>
Remove {host.label}? This will delete its saved connections. {isLocalDaemon
? "This will remove the localhost connection, turn off built-in daemon management, and stop the managed daemon. Remote hosts remain connected."
: `Remove ${host.label}? This will delete its saved connections.`}
</Text> </Text>
<View style={styles.confirmActions}> <View style={styles.confirmActions}>
<Button <Button

View File

@@ -383,6 +383,7 @@ export function buildHostNewWorkspaceRoute(
export const SETTINGS_SECTION_SLUGS = [ export const SETTINGS_SECTION_SLUGS = [
"general", "general",
"daemon",
"appearance", "appearance",
"shortcuts", "shortcuts",
"integrations", "integrations",