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:
co-authored by
Mohamed Boudra
parent
9e2c75a8a3
commit
b5192e577a
@@ -84,6 +84,7 @@ import {
|
||||
import { Combobox, ComboboxItem, type ComboboxOption } from "@/components/ui/combobox";
|
||||
import { DesktopPermissionsSection } from "@/desktop/components/desktop-permissions-section";
|
||||
import { IntegrationsSection } from "@/desktop/components/integrations-section";
|
||||
import { LocalDaemonSection } from "@/desktop/components/desktop-updates-section";
|
||||
import { isElectronRuntime } from "@/desktop/host";
|
||||
import { useDesktopAppUpdater } from "@/desktop/updates/use-desktop-app-updater";
|
||||
import { formatVersionWithPrefix } from "@/desktop/updates/desktop-updates";
|
||||
@@ -133,6 +134,7 @@ interface SidebarSectionItem {
|
||||
|
||||
const SIDEBAR_SECTION_ITEMS: SidebarSectionItem[] = [
|
||||
{ id: "general", label: "General", icon: Settings },
|
||||
{ id: "daemon", label: "Daemon", icon: Server, desktopOnly: true },
|
||||
{ id: "appearance", label: "Appearance", icon: Palette },
|
||||
{ id: "shortcuts", label: "Shortcuts", icon: Keyboard, 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 },
|
||||
];
|
||||
|
||||
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
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -1325,18 +1345,7 @@ export default function SettingsScreen({ view }: SettingsScreenProps) {
|
||||
|
||||
const content = (() => {
|
||||
if (view.kind === "host") {
|
||||
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={handleHostRemoved} />;
|
||||
}
|
||||
return renderHostSettingsContent(view, handleHostRemoved);
|
||||
}
|
||||
if (view.kind === "projects") {
|
||||
return <ProjectsScreen view={view} />;
|
||||
@@ -1356,6 +1365,8 @@ export default function SettingsScreen({ view }: SettingsScreenProps) {
|
||||
handleTerminalScrollbackLinesChange={handleTerminalScrollbackLinesChange}
|
||||
/>
|
||||
);
|
||||
case "daemon":
|
||||
return <LocalDaemonSection />;
|
||||
case "appearance":
|
||||
return <AppearanceSection />;
|
||||
case "shortcuts":
|
||||
|
||||
@@ -7,7 +7,10 @@ import { AdaptiveRenameModal } from "@/components/rename-modal";
|
||||
import { SettingsTextAreaCard } from "@/components/settings-textarea";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { startDesktopDaemon, stopDesktopDaemon } from "@/desktop/daemon/desktop-daemon";
|
||||
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 { useDaemonConfig } from "@/hooks/use-daemon-config";
|
||||
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_HOST_HEADER: SheetHeader = { title: "Remove host" };
|
||||
|
||||
function useHostProfile(serverId: string): HostProfile | null {
|
||||
const daemons = useHosts();
|
||||
@@ -276,7 +278,7 @@ export function HostSettingsPage({
|
||||
|
||||
{isLocalDaemon ? <LocalDaemonSection /> : null}
|
||||
|
||||
<RemoveHostSection host={host} onRemoved={onHostRemoved} />
|
||||
<RemoveHostSection host={host} isLocalDaemon={isLocalDaemon} onRemoved={onHostRemoved} />
|
||||
</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 { removeHost } = useHostMutations();
|
||||
const { updateSettings } = useDesktopSettings();
|
||||
const { data: daemonStatusData, setStatus } = useDaemonStatus();
|
||||
const [isConfirming, setIsConfirming] = useState(false);
|
||||
const [isRemoving, setIsRemoving] = useState(false);
|
||||
const daemonStatus = daemonStatusData?.status ?? null;
|
||||
|
||||
const destructiveTextStyle = useMemo(
|
||||
() => ({ color: theme.colors.destructive }),
|
||||
@@ -843,9 +856,45 @@ function RemoveHostSection({ host, onRemoved }: { host: HostProfile; onRemoved?:
|
||||
setIsConfirming(false);
|
||||
}, [isRemoving]);
|
||||
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(() => {
|
||||
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(() => {
|
||||
setIsConfirming(false);
|
||||
onRemoved?.();
|
||||
@@ -853,10 +902,29 @@ function RemoveHostSection({ host, onRemoved }: { host: HostProfile; onRemoved?:
|
||||
})
|
||||
.catch((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));
|
||||
}, [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(
|
||||
() => <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.row}>
|
||||
<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}>
|
||||
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>
|
||||
</View>
|
||||
<Button
|
||||
@@ -890,13 +962,15 @@ function RemoveHostSection({ host, onRemoved }: { host: HostProfile; onRemoved?:
|
||||
|
||||
{isConfirming ? (
|
||||
<AdaptiveModalSheet
|
||||
header={REMOVE_HOST_HEADER}
|
||||
header={confirmationHeader}
|
||||
visible
|
||||
onClose={handleCloseConfirm}
|
||||
testID="remove-host-confirm-modal"
|
||||
>
|
||||
<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>
|
||||
<View style={styles.confirmActions}>
|
||||
<Button
|
||||
|
||||
Reference in New Issue
Block a user