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
@@ -124,7 +124,7 @@ export async function injectDesktopBridge(page: Page, config: DesktopBridgeConfi
|
||||
return {
|
||||
serverId: cfg.serverId,
|
||||
status: daemonRunning ? "running" : "stopped",
|
||||
listen: null,
|
||||
listen: "127.0.0.1:6767",
|
||||
hostname: null,
|
||||
pid: currentPid,
|
||||
home: "",
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 { DesktopDaemonStatus } from "@/desktop/daemon/desktop-daemon";
|
||||
|
||||
@@ -223,3 +223,50 @@ describe("DaemonStartService", () => {
|
||||
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([]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,13 +4,41 @@ import type { HostRuntimeStore } from "@/runtime/host-runtime";
|
||||
|
||||
export type DaemonStartResult = { ok: true } | { ok: false; error: string };
|
||||
|
||||
type DaemonConnectionStore = Pick<HostRuntimeStore, "upsertConnectionFromListen">;
|
||||
|
||||
export interface DaemonStartServiceDeps {
|
||||
store: Pick<HostRuntimeStore, "upsertConnectionFromListen">;
|
||||
store: DaemonConnectionStore;
|
||||
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 {
|
||||
private readonly store: Pick<HostRuntimeStore, "upsertConnectionFromListen">;
|
||||
private readonly store: DaemonConnectionStore;
|
||||
private readonly invokeStartDesktopDaemon: () => Promise<DesktopDaemonStatus>;
|
||||
private readonly listeners = new Set<() => void>();
|
||||
private lastError: string | null = null;
|
||||
@@ -25,23 +53,8 @@ export class DaemonStartService {
|
||||
this.beginRequest();
|
||||
try {
|
||||
const daemon = await this.invokeStartDesktopDaemon();
|
||||
const listenAddress = daemon.listen?.trim() ?? "";
|
||||
const serverId = daemon.serverId.trim();
|
||||
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 };
|
||||
const result = await upsertDesktopDaemonConnection(this.store, daemon);
|
||||
return result.ok ? result : this.fail(result.error);
|
||||
} catch (error) {
|
||||
return this.fail(error instanceof Error ? error.message : String(error));
|
||||
} finally {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -383,6 +383,7 @@ export function buildHostNewWorkspaceRoute(
|
||||
|
||||
export const SETTINGS_SECTION_SLUGS = [
|
||||
"general",
|
||||
"daemon",
|
||||
"appearance",
|
||||
"shortcuts",
|
||||
"integrations",
|
||||
|
||||
Reference in New Issue
Block a user