From f2dfa83e83b25c3199b260d3524cd0bce27228fc Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Tue, 20 Oct 2026 07:30:17 +0200 Subject: [PATCH] refactor(hosts): remove management settings UI from phase one Keep connected-host management in the CLI and shared SDK while preserving app saved connections and plugin access. --- docs/connected-hosts.md | 5 +- .../app/e2e/browser/connected-hosts.spec.ts | 99 ------ .../e2e/support/helpers/connected-hosts.ts | 132 -------- .../app/src/components/settings/index.tsx | 16 +- packages/app/src/connected-hosts/index.tsx | 317 ------------------ packages/app/src/connected-hosts/model.ts | 192 ----------- .../app/src/screens/settings/host-page.tsx | 2 - 7 files changed, 5 insertions(+), 758 deletions(-) delete mode 100644 packages/app/e2e/browser/connected-hosts.spec.ts delete mode 100644 packages/app/e2e/support/helpers/connected-hosts.ts delete mode 100644 packages/app/src/connected-hosts/index.tsx delete mode 100644 packages/app/src/connected-hosts/model.ts diff --git a/docs/connected-hosts.md b/docs/connected-hosts.md index ded6313a2d..22ba2443f9 100644 --- a/docs/connected-hosts.md +++ b/docs/connected-hosts.md @@ -1,8 +1,7 @@ # Connected hosts A saved host belongs to the daemon that stores it. The app's saved connections remain a separate list. -Connect A to B in A's settings under **Hosts connected to this daemon**, through `paseo --host A host`, -or through `paseo.hosts`. Consumers select B by its learned `serverId`; management uses the stable +Connect A to B through `paseo --host A host` or `paseo.hosts`. Consumers select B by its learned `serverId`; management uses the stable `registrationId` created before B is reachable. ## Ownership and authority @@ -27,7 +26,7 @@ current authority. A routed API cannot select a second hop, and saving a host gr A producer that registers a host through a routed API is qualified on B as `route:::`, with each component percent-encoded. This namespace belongs to the authenticated link principal. An ordinary direct B consumer does not become that producer by supplying a source key. -The optional `server_info.sourceId` reports this producer identity for source-owner controls; it does not describe the authenticated admission principal. +The optional `server_info.sourceId` reports this producer identity separately from the authenticated admission principal. Trusted server plugins retain the authority of their runtime; they are not sandboxed by this feature. ## Connections and recovery diff --git a/packages/app/e2e/browser/connected-hosts.spec.ts b/packages/app/e2e/browser/connected-hosts.spec.ts deleted file mode 100644 index bb86b7b949..0000000000 --- a/packages/app/e2e/browser/connected-hosts.spec.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { test } from "../support/fixtures"; -import { - expectConnectedHostControlsFit, - closeConnectedHostDetails, - toggleConnectedHost, - openRestartableConnectedHosts, - beginConnectedHostDraft, - reconnectManagingHost, - expectConnectedHostDraftRetained, - saveConnectedHost, - openConnectedHosts, - openCompactConnectedHosts, - addConnectedHost, - expectConnectionError, - correctConnection, - inspectConnectedHost, - expectSavedHostDetails, - disableConnectedHost, - enableConnectedHost, - editConnectedHost, - removeConnectedHost, -} from "../support/helpers/connected-hosts"; - -test("invalid input stays editable; unreachable hosts remain manageable", async ({ - page, -}, testInfo) => { - await openConnectedHosts(page); - await addConnectedHost(page, "ssh://-invalid", "Build box"); - await expectConnectionError(page); - await correctConnection(page, "tcp://127.0.0.1:1?password=private-secret"); - await inspectConnectedHost(page, "Build box"); - await expectSavedHostDetails(page); - await page.screenshot({ path: testInfo.outputPath("connected-host-details.png") }); - await editConnectedHost(page); - await disableConnectedHost(page); - await enableConnectedHost(page); - await removeConnectedHost(page, "Renamed box"); -}); - -test.describe("compact connected hosts", () => { - test.use({ viewport: { width: 390, height: 844 } }); - test("saved hosts can be inspected and disabled on a phone layout", async ({ - page, - }, testInfo) => { - await openCompactConnectedHosts(page); - await addConnectedHost(page, "tcp://127.0.0.1:1", "Compact host"); - await inspectConnectedHost(page, "Compact host"); - await expectSavedHostDetails(page); - await page.screenshot({ - path: testInfo.outputPath("connected-host-compact.png"), - fullPage: true, - }); - await disableConnectedHost(page); - await removeConnectedHost(page, "Compact host"); - }); -}); - -test("connection loss retains the unsaved name and credential input", async ({ page }) => { - const host = await openRestartableConnectedHosts(page); - try { - await beginConnectedHostDraft(page); - await reconnectManagingHost(host); - await expectConnectedHostDraftRetained(page); - await saveConnectedHost(page); - await inspectConnectedHost(page, "Retained draft"); - } finally { - await host.close(); - } -}); - -import { startIsolatedHostDaemon } from "../support/helpers/isolated-host-daemon"; - -test.describe("compact connected host controls", () => { - test.use({ viewport: { width: 390, height: 844 } }); - test("long host names keep both actions visible with details open and closed", async ({ - page, - }, testInfo) => { - const destination = await startIsolatedHostDaemon("srv_connected_layout"); - const label = "Production build host — development-infrastructure-europe-west"; - try { - await openCompactConnectedHosts(page); - await addConnectedHost(page, `127.0.0.1:${destination.port}`, label); - await expectConnectedHostControlsFit(page, label, true); - await inspectConnectedHost(page, label); - await expectConnectedHostControlsFit(page, label, true); - await closeConnectedHostDetails(page); - await toggleConnectedHost(page, true); - await expectConnectedHostControlsFit(page, label, false); - await inspectConnectedHost(page, label); - await expectConnectedHostControlsFit(page, label, false); - await page.screenshot({ path: testInfo.outputPath("connected-host-controls.png") }); - await closeConnectedHostDetails(page); - await toggleConnectedHost(page, false); - await expectConnectedHostControlsFit(page, label, true); - } finally { - await destination.close(); - } - }); -}); diff --git a/packages/app/e2e/support/helpers/connected-hosts.ts b/packages/app/e2e/support/helpers/connected-hosts.ts deleted file mode 100644 index a608141418..0000000000 --- a/packages/app/e2e/support/helpers/connected-hosts.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { expect, type Page } from "@playwright/test"; -import { gotoAppShell, openSettings } from "./app"; -import { openCompactSettings, openHostSection } from "./settings"; -import { getServerId } from "./server-id"; - -export async function openConnectedHosts(page: Page) { - await gotoAppShell(page); - await openSettings(page); - await openHostSection(page, getServerId(), "host"); - await expect(page.getByText("No hosts connected to this daemon", { exact: true })).toBeVisible(); -} -export async function addConnectedHost(page: Page, connection: string, label: string) { - await page.getByRole("button", { name: "Add host", exact: true }).click(); - await page.getByRole("textbox", { name: "Connection string", exact: true }).fill(connection); - await page.getByRole("textbox", { name: "Host name", exact: true }).fill(label); - await page.getByRole("textbox", { name: "Metadata", exact: true }).fill('{"team":"infra"}'); - await saveConnectedHost(page); -} -export async function saveConnectedHost(page: Page) { - await page.getByRole("button", { name: "Save host", exact: true }).click(); -} -export async function expectConnectionError(page: Page) { - await expect(page.getByText("Invalid host connection.", { exact: false })).toBeVisible(); - await expect(page.getByRole("textbox", { name: "Connection string", exact: true })).toBeVisible(); -} -export async function correctConnection(page: Page, value: string) { - await page.getByRole("textbox", { name: "Connection string", exact: true }).fill(value); - await saveConnectedHost(page); -} -export async function inspectConnectedHost(page: Page, label: string) { - await expect( - page.getByText(/^(connecting|offline|error|online|disabled) · (Enabled|Disabled) · User$/), - ).toBeVisible(); - await page.getByRole("button", { name: `Details for ${label}`, exact: true }).click(); - await expect(page.getByText("Connected host details", { exact: true })).toBeVisible(); -} -export async function expectSavedHostDetails(page: Page) { - await expect(page.getByText('{"team":"infra"}', { exact: true })).toBeVisible(); - await expect(page.getByText("Never", { exact: true })).toBeVisible(); - await expect( - page.getByRole("textbox", { name: "Replacement connection", exact: true }), - ).toHaveValue(""); -} -export async function disableConnectedHost(page: Page) { - await page.getByRole("button", { name: "Cancel", exact: true }).click(); - await page.getByRole("button", { name: "Disable", exact: true }).click(); - await expect(page.getByText(/disabled · Disabled/)).toBeVisible(); -} -export async function enableConnectedHost(page: Page) { - await page.getByRole("button", { name: "Enable", exact: true }).click(); - await expect(page.getByRole("button", { name: "Disable", exact: true })).toBeVisible(); -} -export async function removeConnectedHost(page: Page, label: string) { - await inspectConnectedHost(page, label); - page.once("dialog", (dialog) => dialog.accept()); - await page.getByRole("button", { name: "Remove host", exact: true }).click(); - await expect(page.getByText("No hosts connected to this daemon", { exact: true })).toBeVisible(); -} - -export async function openCompactConnectedHosts(page: Page) { - await gotoAppShell(page); - await openCompactSettings(page, new URL(page.url()).pathname); - await openHostSection(page, getServerId(), "host"); - await expect(page.getByText("No hosts connected to this daemon", { exact: true })).toBeVisible(); -} - -import { startIsolatedHostDaemon, type IsolatedHostDaemon } from "./isolated-host-daemon"; -import { addDirectHostFromSettings, selectSettingsHost } from "./settings"; - -export async function openRestartableConnectedHosts(page: Page): Promise { - const host = await startIsolatedHostDaemon("srv_connected_host_draft"); - await gotoAppShell(page); - await openSettings(page); - await addDirectHostFromSettings(page, { host: "127.0.0.1", port: host.port }); - await selectSettingsHost(page, host.serverId); - await openHostSection(page, host.serverId, "host"); - await expect(page.getByText("No hosts connected to this daemon", { exact: true })).toBeVisible(); - return host; -} -export async function beginConnectedHostDraft(page: Page) { - await page.getByRole("button", { name: "Add host", exact: true }).click(); - await page.getByRole("textbox", { name: "Host name", exact: true }).fill("Retained draft"); - await page - .getByRole("textbox", { name: "Connection string", exact: true }) - .fill("tcp://127.0.0.1:1?password=draft-secret"); -} -export async function reconnectManagingHost(host: IsolatedHostDaemon) { - await host.restart(); -} -export async function expectConnectedHostDraftRetained(page: Page) { - await expect(page.getByRole("textbox", { name: "Host name", exact: true })).toHaveValue( - "Retained draft", - { timeout: 30000 }, - ); - await expect(page.getByRole("textbox", { name: "Connection string", exact: true })).toHaveValue( - "tcp://127.0.0.1:1?password=draft-secret", - ); -} - -export async function editConnectedHost(page: Page) { - await page.getByRole("switch", { name: "Replace destination identity", exact: true }).click(); - await saveConnectedHost(page); - await expect( - page.getByText("Identity replacement requires a replacement connection", { exact: true }), - ).toBeVisible(); - await page - .getByRole("textbox", { name: "Replacement connection", exact: true }) - .fill("tcp://127.0.0.1:1?password=replacement-secret"); - await page.getByRole("textbox", { name: "Host name", exact: true }).fill("Renamed box"); - await page.getByRole("textbox", { name: "Metadata", exact: true }).fill('{"team":"tools"}'); - await saveConnectedHost(page); - await inspectConnectedHost(page, "Renamed box"); - await expect(page.getByText('{"team":"tools"}', { exact: true })).toBeVisible(); - await expect( - page.getByRole("textbox", { name: "Replacement connection", exact: true }), - ).toHaveValue(""); -} - -export async function expectConnectedHostControlsFit(page: Page, label: string, enabled: boolean) { - const details = page.getByRole("button", { name: `Details for ${label}`, exact: true }); - const toggle = page.getByRole("button", { name: enabled ? "Disable" : "Enable", exact: true }); - await details.scrollIntoViewIfNeeded(); - await expect(details).toBeInViewport({ ratio: 1 }); - await expect(toggle).toBeInViewport({ ratio: 1 }); -} -export async function closeConnectedHostDetails(page: Page) { - await page.getByRole("button", { name: "Cancel", exact: true }).click(); -} -export async function toggleConnectedHost(page: Page, enabled: boolean) { - await page.getByRole("button", { name: enabled ? "Disable" : "Enable", exact: true }).click(); - await expect(page.getByText(enabled ? /disabled · Disabled/ : /online · Enabled/)).toBeVisible(); -} diff --git a/packages/app/src/components/settings/index.tsx b/packages/app/src/components/settings/index.tsx index 2bc32f9dcf..f31a0e7b38 100644 --- a/packages/app/src/components/settings/index.tsx +++ b/packages/app/src/components/settings/index.tsx @@ -31,7 +31,6 @@ export { SettingsCollapsibleRow } from "./collapsible-row"; interface AppSettingsRowProps extends Omit { hint?: ReactNode; labelAccessory?: ReactNode; - stacked?: boolean; } export function SettingsCard({ children, testID }: { children: ReactNode; testID?: string }) { @@ -56,16 +55,12 @@ export function SettingsRow({ error, children, testID, - stacked = false, }: AppSettingsRowProps) { const compact = useIsCompactFormFactor(); - const rowStyle = useMemo( - () => [settingsStyles.row, compact && styles.compactRow, stacked && styles.stackedRow], - [compact, stacked], - ); + const rowStyle = useMemo(() => [settingsStyles.row, compact && styles.compactRow], [compact]); return ( - + {labelAccessory ? ( {label} @@ -81,9 +76,7 @@ export function SettingsRow({ ) : null} - {children ? ( - {children} - ) : null} + {children ? {children} : null} ); } @@ -200,9 +193,6 @@ export function SettingsAction({ actionLabel, onPress, disabled, ...row }: Setti } const styles = StyleSheet.create((theme) => ({ - stackedRow: { flexDirection: "column", alignItems: "stretch", gap: theme.spacing[3] }, - stackedLabel: { flexBasis: "auto", marginRight: 0, minWidth: 0 }, - stackedControl: { width: "100%" }, compactRow: { flexWrap: "wrap", gap: theme.spacing[3] }, label: { flexGrow: 1, flexShrink: 1, flexBasis: 160, marginRight: theme.spacing[3] }, labelRow: { diff --git a/packages/app/src/connected-hosts/index.tsx b/packages/app/src/connected-hosts/index.tsx deleted file mode 100644 index 5ba25afdd7..0000000000 --- a/packages/app/src/connected-hosts/index.tsx +++ /dev/null @@ -1,317 +0,0 @@ -import { useCallback, useEffect, useState, useSyncExternalStore } from "react"; -import { Text, View } from "react-native"; -import { StyleSheet } from "react-native-unistyles"; -import { createPaseoApi, type HostSummary, type HostDirectory } from "@getpaseo/client"; -import type { DaemonClient } from "@getpaseo/client/internal/daemon-client"; -import { - SettingsCard, - SettingsRow, - SettingsInput, - SettingsSwitch, - SettingsSection, -} from "@/components/settings"; -import { Button } from "@/components/ui/button"; -import { Field, FormTextInput } from "@/components/ui/form-field"; -import { useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-runtime"; -import { useIsCompactFormFactor } from "@/constants/layout"; -import { settingsStyles } from "@/styles/settings"; -import { confirmDialog } from "@/utils/confirm-dialog"; -import { ConnectedHostsModel, type Editor } from "./model"; - -export function ConnectedHostsSection({ serverId }: { serverId: string }) { - const client = useHostRuntimeClient(serverId); - const connected = useHostRuntimeIsConnected(serverId); - return ( - - {client ? ( - - ) : ( - - - - )} - - ); -} -function useModel(client: DaemonClient, connected: boolean) { - const [model] = useState(() => new ConnectedHostsModel(createPaseoApi(client))); - useEffect(() => { - model.setConnected(connected); - }, [model, connected]); - useEffect(() => model.close, [model]); - return model; -} -function ConnectedHosts({ client, connected }: { client: DaemonClient; connected: boolean }) { - const model = useModel(client, connected); - const state = useSyncExternalStore(model.subscribe, model.getState, model.getState); - const info = client.getLastServerInfoMessage(); - const canManage = info?.permissions?.includes("tunnel.manage") === true; - const directory = state.load.status === "loaded" ? state.load.directory : null; - const editor = state.editor; - return ( - - {state.load.status === "loading" ? ( - - - - ) : null} - {state.load.status === "error" ? ( - - - - - - ) : null} - {directory ? ( - - ) : null} - {directory && canManage && !editor ? ( - - ) : null} - {editor ? ( - - ) : null} - {state.error ? ( - - - - - - ) : null} - {state.pending ? ( - - Saving host change… - - ) : null} - - ); -} -function HostList({ - directory, - model, - canManage, - pending, -}: { - directory: HostDirectory; - model: ConnectedHostsModel; - canManage: boolean; - pending: boolean; -}) { - return ( - - {directory.hosts.length === 0 ? ( - - ) : ( - directory.hosts.map((host) => ( - - )) - )} - - ); -} -function HostRow({ - host, - model, - canManage, - pending, -}: { - host: HostSummary; - model: ConnectedHostsModel; - canManage: boolean; - pending: boolean; -}) { - const compact = useIsCompactFormFactor(); - const edit = useCallback(() => model.edit(host), [host, model]); - const toggle = useCallback(() => void model.setEnabled(host), [host, model]); - return ( - - - - {canManage ? ( - - ) : null} - - - ); -} -function HostDetails({ host }: { host: HostSummary }) { - return ( - - - - - - - - - - - - {host.error ? : null} - - ); -} -function HostEditor({ - editor, - directory, - model, - canManage, - manageSources, - sourceId, - pending, -}: { - editor: Editor; - directory: HostDirectory | null; - model: ConnectedHostsModel; - canManage: boolean; - manageSources: boolean; - sourceId: string | undefined; - pending: boolean; -}) { - const compact = useIsCompactFormFactor(); - const detail = - directory?.hosts.find((host) => host.registrationId === editor.host?.registrationId) ?? - editor.host; - const editSource = !detail || manageSources || detail.source.id === sourceId; - const remove = useCallback(async () => { - if (!detail) return; - const confirmed = await confirmDialog({ - title: "Remove connected host?", - message: - detail.source.id === "owner" - ? "This removes the saved connection. The host's agents and workspaces keep running." - : `Managed by ${detail.source.id}. Its source can add it again. Disable the host to keep access off.`, - confirmLabel: "Remove", - destructive: true, - }); - if (confirmed) await model.remove(detail); - }, [detail, model]); - return ( - - {detail ? "Connected host details" : "Add host"} - {detail ? : null} - {canManage ? ( - - - {editSource ? ( - - ) : null} - {detail && editSource ? ( - - ) : null} - - ) : null} - {canManage && editSource ? ( - - - - ) : null} - - {canManage ? ( - - ) : null} - - {canManage && detail && editSource ? ( - - ) : null} - - - ); -} -const styles = StyleSheet.create((theme) => ({ - section: { gap: theme.spacing[3] }, - actions: { flexDirection: "row", flexWrap: "wrap", gap: theme.spacing[2] }, -})); - -function sourceLabel(sourceId: string): string { - if (sourceId === "owner") return "User"; - if (sourceId.startsWith("plugin:")) return `Plugin: ${sourceId.slice("plugin:".length)}`; - if (sourceId.startsWith("route:")) { - const components = sourceId.split(":"); - let producer: string; - try { - producer = decodeURIComponent(components[3] ?? ""); - } catch { - return "User (via host)"; - } - return producer.startsWith("plugin:") - ? `Plugin: ${producer.slice("plugin:".length)} (via host)` - : "User (via host)"; - } - return "User"; -} diff --git a/packages/app/src/connected-hosts/model.ts b/packages/app/src/connected-hosts/model.ts deleted file mode 100644 index 24fd41211f..0000000000 --- a/packages/app/src/connected-hosts/model.ts +++ /dev/null @@ -1,192 +0,0 @@ -import type { - PaseoApi, - HostSummary, - HostUpdate, - HostDirectory, - OwnedSubscription, -} from "@getpaseo/client"; - -export interface Editor { - key: number; - host: HostSummary | null; - connection: string; - label: string; - metadata: string; - replaceIdentity: boolean; -} -type Load = - | { status: "loading" } - | { status: "error"; error: string } - | { status: "loaded"; directory: HostDirectory }; -interface State { - load: Load; - editor: Editor | null; - pending: string | null; - error: string | null; -} - -/** Owns fetch/observation, form sequencing and mutation state outside React. */ -export class ConnectedHostsModel { - private state: State = { load: { status: "loading" }, editor: null, pending: null, error: null }; - private readonly listeners = new Set<() => void>(); - private observation?: OwnedSubscription; - private generation = 0; - private editorKey = 0; - private connected = false; - setConnected = (connected: boolean) => { - this.connected = connected; - if (connected && !this.observation) this.start(); - else if (!connected) - this.publish({ - load: { - status: "error", - error: - "Connection to this daemon was lost. Reconnect to continue; your edits are retained.", - }, - }); - }; - constructor(private readonly api: PaseoApi) {} - getState = () => this.state; - subscribe = (listener: () => void) => { - this.listeners.add(listener); - return () => this.listeners.delete(listener); - }; - private publish(patch: Partial) { - this.state = { ...this.state, ...patch }; - for (const listener of this.listeners) listener(); - } - start = () => { - if (!this.connected) { - this.publish({ - load: { status: "error", error: "Connect to this daemon to load its hosts." }, - }); - return; - } - const generation = ++this.generation; - void this.observation?.release(); - this.publish({ load: { status: "loading" } }); - try { - const observation = this.api.hosts.observe(); - this.observation = observation; - observation.subscribe({ - snapshot: (directory) => { - if (generation === this.generation) - this.publish({ load: { status: "loaded", directory } }); - }, - update: (message) => { - if (generation === this.generation && message.type === "hosts.changed") - this.publish({ load: { status: "loaded", directory: message.payload.directory } }); - }, - error: (error) => { - if (generation === this.generation) - this.publish({ load: { status: "error", error: describe(error) } }); - }, - }); - void observation.ready.catch((error) => { - if (generation === this.generation) - this.publish({ load: { status: "error", error: describe(error) } }); - }); - } catch (error) { - this.publish({ load: { status: "error", error: describe(error) } }); - } - }; - close = () => { - this.generation++; - void this.observation?.release(); - void this.api.dispose(); - this.listeners.clear(); - }; - edit = (host: HostSummary | null) => { - if (this.state.pending) return; - this.publish({ - error: null, - editor: { - key: ++this.editorKey, - host, - connection: "", - label: host?.label ?? "", - metadata: JSON.stringify(host?.metadata ?? {}, null, 2), - replaceIdentity: false, - }, - }); - }; - add = () => this.edit(null); - setLabel = (label: string) => this.set({ label }); - setConnection = (connection: string) => this.set({ connection }); - setMetadata = (metadata: string) => this.set({ metadata }); - setReplaceIdentity = (replaceIdentity: boolean) => this.set({ replaceIdentity }); - set = (patch: Partial>) => { - if (this.state.editor) this.publish({ editor: { ...this.state.editor, ...patch } }); - }; - cancel = () => { - if (!this.state.pending) this.publish({ editor: null, error: null }); - }; - dismissError = () => this.publish({ error: null }); - private async mutate(id: string, operation: () => Promise): Promise { - if (this.state.pending) return; - this.publish({ pending: id, error: null }); - try { - await operation(); - this.publish({ - load: { status: "loaded", directory: await this.api.hosts.list() }, - editor: null, - }); - } catch (error) { - this.publish({ error: describe(error) }); - } finally { - this.publish({ pending: null }); - } - } - save = () => - this.mutate("save", async () => { - const editor = this.state.editor; - if (!editor) return; - let metadata: Record; - try { - const value: unknown = JSON.parse(editor.metadata); - if ( - !value || - typeof value !== "object" || - Array.isArray(value) || - Object.values(value).some((entry) => typeof entry !== "string") - ) - throw new Error("invalid"); - metadata = value as Record; - } catch { - throw new Error("Metadata must be a JSON object with string values."); - } - if (editor.host) { - const patch: HostUpdate = { - label: editor.label.trim() || null, - replaceIdentity: editor.replaceIdentity, - }; - if (JSON.stringify(metadata) !== JSON.stringify(editor.host.metadata)) - patch.metadata = metadata; - if (editor.connection.trim()) { - patch.connection = editor.connection.trim(); - } - await this.api.hosts.update(editor.host.registrationId, patch); - } else - await this.api.hosts.add({ - connection: editor.connection.trim(), - label: editor.label.trim() || undefined, - metadata, - }); - }); - setEnabled = (host: HostSummary) => - this.mutate(host.registrationId, () => - this.api.hosts.update(host.registrationId, { enabled: !host.enabled }), - ); - remove = (host: HostSummary) => - this.mutate(host.registrationId, () => this.api.hosts.remove(host.registrationId)); -} -function describe(error: unknown): string { - if ( - error && - typeof error === "object" && - "userMessage" in error && - typeof error.userMessage === "string" - ) - return error.userMessage; - return error instanceof Error ? error.message : "Host operation failed. Retry when connected."; -} diff --git a/packages/app/src/screens/settings/host-page.tsx b/packages/app/src/screens/settings/host-page.tsx index 958c90f0d0..6b308547a1 100644 --- a/packages/app/src/screens/settings/host-page.tsx +++ b/packages/app/src/screens/settings/host-page.tsx @@ -1,4 +1,3 @@ -import { ConnectedHostsSection } from "@/connected-hosts"; import { ArrowDown, ArrowUp, @@ -375,7 +374,6 @@ export function HostSettingsPage({ - {isLocalDaemon ? : null}