import { useState, useEffect, useRef, useCallback } from "react"; import type { MutableRefObject } from "react"; import { View, Text, ScrollView, Alert, Platform, } from "react-native"; import { router, useLocalSearchParams } from "expo-router"; import { useFocusEffect } from "@react-navigation/native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyles"; import { Sun, Moon, Monitor, Globe, Settings, RotateCw, Trash2 } from "lucide-react-native"; import { useAppSettings, type AppSettings } from "@/hooks/use-settings"; import { useDaemonRegistry, type HostProfile, type HostConnection } from "@/contexts/daemon-registry-context"; import { formatConnectionStatus, getConnectionStatusTone } from "@/utils/daemons"; import { confirmDialog } from "@/utils/confirm-dialog"; import { MenuHeader } from "@/components/headers/menu-header"; import { useSessionStore } from "@/stores/session-store"; import { getHostRuntimeStore, isHostRuntimeConnected, useHostRuntimeSession, } from "@/runtime/host-runtime"; import { AddHostMethodModal } from "@/components/add-host-method-modal"; import { AddHostModal } from "@/components/add-host-modal"; import { PairLinkModal } from "@/components/pair-link-modal"; import { NameHostModal } from "@/components/name-host-modal"; import { Button } from "@/components/ui/button"; import { SegmentedControl } from "@/components/ui/segmented-control"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { AdaptiveModalSheet, AdaptiveTextInput } from "@/components/adaptive-modal-sheet"; import { DesktopPermissionsSection } from "@/desktop/components/desktop-permissions-section"; import { LocalDaemonSection } from "@/desktop/components/desktop-updates-section"; import { useDesktopAppUpdater } from "@/desktop/updates/use-desktop-app-updater"; import { formatVersionWithPrefix } from "@/desktop/updates/desktop-updates"; import { resolveAppVersion } from "@/utils/app-version"; import { settingsStyles } from "@/styles/settings"; const delay = (ms: number) => new Promise((resolve) => { const timeout = setTimeout(() => { clearTimeout(timeout); resolve(); }, ms); }); function formatHostConnectionLabel(connection: HostConnection): string { if (connection.type === "relay") { return `Relay (${connection.relayEndpoint})`; } if (connection.type === "directSocket") { return `Local (${connection.path})`; } if (connection.type === "directPipe") { return `Local (${connection.path})`; } return `TCP (${connection.endpoint})`; } function formatActiveConnectionBadge(input: { activeConnection: { type: HostConnection["type"]; display: string } | null; theme: ReturnType["theme"]; }) { const { activeConnection, theme } = input; if (!activeConnection) { return null; } if (activeConnection.type === "relay") { return { icon: , text: "Relay" }; } if (activeConnection.type === "directSocket") { return { icon: , text: "Local" }; } if (activeConnection.type === "directPipe") { return { icon: , text: "Local" }; } return { icon: , text: activeConnection.display, }; } const styles = StyleSheet.create((theme) => ({ loadingContainer: { flex: 1, backgroundColor: theme.colors.surface0, alignItems: "center", justifyContent: "center", }, loadingText: { color: theme.colors.foreground, fontSize: theme.fontSize.lg, }, container: { flex: 1, backgroundColor: theme.colors.surface0, }, scrollView: { flex: 1, }, content: { padding: theme.spacing[4], paddingTop: theme.spacing[6], width: "100%", maxWidth: 720, alignSelf: "center", }, label: { color: theme.colors.foregroundMuted, fontSize: theme.fontSize.xs, fontWeight: theme.fontWeight.normal, marginBottom: theme.spacing[2], }, input: { backgroundColor: theme.colors.surface0, color: theme.colors.foreground, padding: theme.spacing[3], borderRadius: theme.borderRadius.md, borderWidth: 1, borderColor: theme.colors.border, fontSize: theme.fontSize.base, }, // Host card styles hostCard: { marginBottom: theme.spacing[3], overflow: "hidden", }, hostCardContent: { padding: theme.spacing[4], gap: theme.spacing[2], }, hostHeaderRow: { flexDirection: "row", alignItems: "center", gap: theme.spacing[3], }, hostHeaderRight: { flexDirection: "row", alignItems: "center", gap: theme.spacing[1], flexShrink: 0, }, hostLabel: { color: theme.colors.foreground, fontSize: theme.fontSize.base, fontWeight: theme.fontWeight.normal, flex: 1, flexShrink: 1, }, hostError: { color: theme.colors.palette.red[300], fontSize: theme.fontSize.xs, }, // Status pill statusPill: { flexDirection: "row", alignItems: "center", gap: 6, paddingHorizontal: theme.spacing[2], paddingVertical: 4, borderRadius: theme.borderRadius.full, }, statusPillMobile: { alignItems: "center", justifyContent: "center", width: 14, height: 14, borderRadius: 7, }, statusDot: { width: 6, height: 6, borderRadius: theme.borderRadius.full, }, statusText: { fontSize: theme.fontSize.xs, fontWeight: theme.fontWeight.normal, }, connectionPill: { flexDirection: "row", alignItems: "center", gap: 6, paddingHorizontal: theme.spacing[2], paddingVertical: 4, borderRadius: theme.borderRadius.full, borderWidth: 1, borderColor: theme.colors.border, backgroundColor: theme.colors.surface3, maxWidth: 170, }, connectionPillMobile: { flexDirection: "row", alignItems: "center", borderRadius: theme.borderRadius.full, }, connectionText: { fontSize: theme.fontSize.xs, fontWeight: theme.fontWeight.normal, color: theme.colors.foregroundMuted, flexShrink: 1, }, versionPill: { flexDirection: "row", alignItems: "center", paddingHorizontal: theme.spacing[2], paddingVertical: 4, borderRadius: theme.borderRadius.full, borderWidth: 1, borderColor: theme.colors.border, backgroundColor: theme.colors.surface3, maxWidth: 200, }, hostSettingsButton: { width: 28, height: 28, paddingVertical: 0, paddingHorizontal: 0, borderRadius: theme.borderRadius.md, gap: 0, marginLeft: theme.spacing[2], }, advancedTrigger: { flexDirection: "row", alignItems: "center", gap: theme.spacing[2], paddingVertical: theme.spacing[2], paddingHorizontal: theme.spacing[3], borderRadius: theme.borderRadius.md, borderWidth: 1, borderColor: theme.colors.border, backgroundColor: "transparent", }, advancedTriggerText: { color: theme.colors.foreground, fontSize: theme.fontSize.sm, fontWeight: theme.fontWeight.medium, }, disabled: { opacity: theme.opacity[50], }, testResultText: { fontSize: theme.fontSize.xs, }, // Add host button addButton: { borderStyle: "dashed", }, addButtonText: { color: theme.colors.foregroundMuted, }, // Add/Edit form formCard: { padding: theme.spacing[4], marginBottom: theme.spacing[3], gap: theme.spacing[4], }, formTitle: { color: theme.colors.foreground, fontSize: theme.fontSize.base, fontWeight: theme.fontWeight.normal, }, formField: { gap: theme.spacing[2], }, formActionsRow: { flexDirection: "row", justifyContent: "flex-end", gap: theme.spacing[2], }, formButton: { paddingVertical: theme.spacing[2], paddingHorizontal: theme.spacing[4], borderRadius: theme.borderRadius.md, borderWidth: 1, borderColor: theme.colors.border, }, formButtonPrimary: { backgroundColor: theme.colors.palette.blue[500], borderColor: theme.colors.palette.blue[500], }, formButtonText: { color: theme.colors.foreground, fontSize: theme.fontSize.sm, fontWeight: theme.fontWeight.normal, }, formButtonPrimaryText: { color: theme.colors.palette.white, }, // Audio settings card audioCard: { overflow: "hidden", }, audioRow: { flexDirection: "row", alignItems: "center", justifyContent: "space-between", paddingVertical: theme.spacing[4], paddingHorizontal: theme.spacing[4], }, audioRowBorder: { borderTopWidth: 1, borderTopColor: theme.colors.border, }, audioRowContent: { flex: 1, marginRight: theme.spacing[3], }, audioRowTitle: { color: theme.colors.foreground, fontSize: theme.fontSize.base, }, aboutValue: { color: theme.colors.foregroundMuted, fontSize: theme.fontSize.sm, fontWeight: theme.fontWeight.normal, }, aboutHintText: { color: theme.colors.foregroundMuted, fontSize: theme.fontSize.xs, marginTop: 2, }, aboutErrorText: { color: theme.colors.palette.red[300], fontSize: theme.fontSize.xs, marginTop: theme.spacing[1], }, aboutUpdateActions: { flexDirection: "row", alignItems: "center", gap: theme.spacing[2], }, // Empty state emptyCard: { padding: theme.spacing[4], marginBottom: theme.spacing[3], }, emptyText: { color: theme.colors.foregroundMuted, fontSize: theme.fontSize.sm, textAlign: "center", }, // Dev section devCard: { overflow: "hidden", }, devButton: { flexDirection: "row", alignItems: "center", gap: theme.spacing[3], paddingVertical: theme.spacing[4], paddingHorizontal: theme.spacing[4], }, devButtonBorder: { borderTopWidth: 1, borderTopColor: theme.colors.border, }, devButtonContent: { flex: 1, }, devButtonTitle: { color: theme.colors.foreground, fontSize: theme.fontSize.base, }, devButtonDescription: { color: theme.colors.foregroundMuted, fontSize: theme.fontSize.sm, marginTop: 2, }, })); function formatDaemonVersionBadge(version: string | null): string | null { const daemonVersion = version?.trim(); if (!daemonVersion) { return null; } if (daemonVersion.startsWith("v")) { return daemonVersion; } return `v${daemonVersion}`; } function DesktopAppUpdateRow() { const { isDesktop, statusText, availableUpdate, errorMessage, isChecking, isInstalling, checkForUpdates, installUpdate, } = useDesktopAppUpdater(); useFocusEffect( useCallback(() => { if (!isDesktop) { return undefined; } void checkForUpdates({ silent: true }); return undefined; }, [checkForUpdates, isDesktop]) ); const handleCheckForUpdates = useCallback(() => { if (!isDesktop) { return; } void checkForUpdates(); }, [checkForUpdates, isDesktop]); const handleInstallUpdate = useCallback(() => { if (!isDesktop) { return; } void confirmDialog({ title: "Install desktop update", message: "This updates Paseo on this computer.", confirmLabel: "Install update", cancelLabel: "Cancel", }) .then((confirmed) => { if (!confirmed) { return; } void installUpdate(); }) .catch((error) => { console.error("[Settings] Failed to open app update confirmation", error); Alert.alert("Error", "Unable to open the update confirmation dialog."); }); }, [installUpdate, isDesktop]); if (!isDesktop) { return null; } return ( App updates {statusText} {availableUpdate?.latestVersion ? ( New version available: {formatVersionWithPrefix(availableUpdate.latestVersion)} ) : null} {errorMessage ? {errorMessage} : null} ); } export default function SettingsScreen() { const { theme } = useUnistyles(); const insets = useSafeAreaInsets(); const params = useLocalSearchParams<{ editHost?: string; serverId?: string }>(); const routeServerId = typeof params.serverId === "string" ? params.serverId.trim() : ""; const { settings, isLoading: settingsLoading, updateSettings } = useAppSettings(); const { daemons, isLoading: daemonLoading, updateHost, removeHost, removeConnection, } = useDaemonRegistry(); const [isAddHostMethodVisible, setIsAddHostMethodVisible] = useState(false); const [isDirectHostVisible, setIsDirectHostVisible] = useState(false); const [isPasteLinkVisible, setIsPasteLinkVisible] = useState(false); const [addConnectionTargetServerId, setAddConnectionTargetServerId] = useState(null); const [pendingEditReopenServerId, setPendingEditReopenServerId] = useState(null); const [pendingNameHost, setPendingNameHost] = useState<{ serverId: string; hostname: string | null } | null>(null); const [pendingRemoveHost, setPendingRemoveHost] = useState(null); const [isRemovingHost, setIsRemovingHost] = useState(false); const [editingDaemon, setEditingDaemon] = useState(null); const [isSavingEdit, setIsSavingEdit] = useState(false); const isLoading = settingsLoading || daemonLoading; const isMountedRef = useRef(true); const lastHandledEditHostRef = useRef(null); const isDesktop = Platform.OS === "web"; const appVersion = resolveAppVersion(); const appVersionText = formatVersionWithPrefix(appVersion); const editingServerId = editingDaemon?.serverId ?? null; const editingDaemonLive = editingServerId ? daemons.find((daemon) => daemon.serverId === editingServerId) ?? null : null; const pendingNameHostname = useSessionStore( useCallback( (state) => { if (!pendingNameHost) return null; return state.sessions[pendingNameHost.serverId]?.serverInfo?.hostname ?? pendingNameHost.hostname ?? null; }, [pendingNameHost] ) ); useEffect(() => { return () => { isMountedRef.current = false; }; }, []); // Keep the edit modal bound to live registry state. useEffect(() => { if (!editingServerId) return; if (editingDaemonLive) return; setEditingDaemon(null); }, [editingDaemonLive, editingServerId]); const waitForCondition = useCallback( async (predicate: () => boolean, timeoutMs: number, intervalMs = 250) => { const deadline = Date.now() + timeoutMs; while (Date.now() < deadline) { if (!isMountedRef.current) { return false; } if (predicate()) { return true; } await delay(intervalMs); } return predicate(); }, [] ); const handleEditDaemon = useCallback((profile: HostProfile) => { setEditingDaemon(profile); }, []); const handleCloseEditDaemon = useCallback(() => { if (isSavingEdit) return; setEditingDaemon(null); }, [isSavingEdit]); const closeAddConnectionFlow = useCallback(() => { setIsAddHostMethodVisible(false); setIsDirectHostVisible(false); setIsPasteLinkVisible(false); setAddConnectionTargetServerId(null); }, []); const goBackToAddConnectionMethods = useCallback(() => { setIsDirectHostVisible(false); setIsPasteLinkVisible(false); setIsAddHostMethodVisible(true); }, []); useEffect(() => { const editHost = typeof params.editHost === "string" ? params.editHost.trim() : ""; if (!editHost) return; if (lastHandledEditHostRef.current === editHost) return; const profile = daemons.find((daemon) => daemon.serverId === editHost) ?? null; if (!profile) return; lastHandledEditHostRef.current = editHost; handleEditDaemon(profile); }, [daemons, handleEditDaemon, params.editHost]); useEffect(() => { if (!pendingEditReopenServerId) return; if (isAddHostMethodVisible || isDirectHostVisible || isPasteLinkVisible) return; const profile = daemons.find((daemon) => daemon.serverId === pendingEditReopenServerId) ?? null; setPendingEditReopenServerId(null); setAddConnectionTargetServerId(null); if (profile) { handleEditDaemon(profile); } }, [ daemons, handleEditDaemon, isAddHostMethodVisible, isDirectHostVisible, isPasteLinkVisible, pendingEditReopenServerId, ]); const handleSaveEditDaemon = useCallback(async (nextLabelRaw: string) => { if (!editingServerId) return; if (isSavingEdit) return; const nextLabel = nextLabelRaw.trim(); if (!nextLabel) { Alert.alert("Label required", "Enter a label for this host."); return; } try { setIsSavingEdit(true); await updateHost(editingServerId, { label: nextLabel }); handleCloseEditDaemon(); } catch (error) { console.error("[Settings] Failed to rename host", error); Alert.alert("Error", "Unable to save host"); } finally { setIsSavingEdit(false); } }, [editingServerId, handleCloseEditDaemon, isSavingEdit, updateHost]); const handleRemoveConnection = useCallback( async (serverId: string, connectionId: string) => { await removeConnection(serverId, connectionId); }, [removeConnection] ); const handleRemoveDaemon = useCallback((profile: HostProfile) => { setEditingDaemon(null); setPendingRemoveHost(profile); }, []); const handleAddConnectionFromModal = useCallback(() => { if (!editingServerId) return; const serverId = editingServerId; setEditingDaemon(null); setAddConnectionTargetServerId(serverId); setPendingEditReopenServerId(serverId); setIsAddHostMethodVisible(true); }, [editingServerId]); const handleThemeChange = useCallback( (newTheme: AppSettings["theme"]) => { void updateSettings({ theme: newTheme }); if (newTheme === "auto") { UnistylesRuntime.setAdaptiveThemes(true); } else { UnistylesRuntime.setAdaptiveThemes(false); UnistylesRuntime.setTheme(newTheme); } }, [updateSettings] ); const restartConfirmationMessage = "This will restart the daemon. The app will reconnect automatically."; if (isLoading) { return ( Loading settings... ); } return ( {/* Host Management */} Hosts {daemons.length === 0 ? ( No hosts configured ) : ( daemons.map((daemon) => { return ( ); }) )} { setIsAddHostMethodVisible(false); setIsDirectHostVisible(true); }} onPasteLink={() => { setIsAddHostMethodVisible(false); setIsPasteLinkVisible(true); }} onScanQr={() => { const targetServerId = addConnectionTargetServerId; const source = targetServerId ? "editHost" : "settings"; const sourceServerId = routeServerId || targetServerId || undefined; closeAddConnectionFlow(); router.push({ pathname: "/pair-scan", params: targetServerId ? { source, targetServerId, sourceServerId } : { source, sourceServerId }, }); }} /> { if (isNewHost) { setPendingNameHost({ serverId, hostname }); } }} /> { if (isNewHost) { setPendingNameHost({ serverId, hostname }); } }} /> {pendingNameHost ? ( setPendingNameHost(null)} onSave={(label) => { void updateHost(pendingNameHost.serverId, { label }).finally(() => { setPendingNameHost(null); }); }} /> ) : null} {pendingRemoveHost ? ( { if (isRemovingHost) return; setPendingRemoveHost(null); }} testID="remove-host-confirm-modal" > Remove {pendingRemoveHost.label}? This will delete its saved connections. ) : null} void handleSaveEditDaemon(label)} onRemoveConnection={handleRemoveConnection} onRemoveHost={handleRemoveDaemon} onAddConnection={handleAddConnectionFromModal} restartConfirmationMessage={restartConfirmationMessage} waitForCondition={waitForCondition} isScreenMountedRef={isMountedRef} /> {/* Appearance */} Appearance Theme , }, { value: "dark", label: "Dark", icon: ({ color, size }) => , }, { value: "auto", label: "System", icon: ({ color, size }) => , }, ]} /> {isDesktop ? : null} {isDesktop ? : null} {/* About */} About Version {appVersionText} {isDesktop ? : null} ); } interface HostDetailModalProps { visible: boolean; host: HostProfile | null; isSaving: boolean; onClose: () => void; onSave: (label: string) => void; onRemoveConnection: (serverId: string, connectionId: string) => Promise; onRemoveHost: (host: HostProfile) => void; onAddConnection: () => void; restartConfirmationMessage: string; waitForCondition: (predicate: () => boolean, timeoutMs: number, intervalMs?: number) => Promise; isScreenMountedRef: MutableRefObject; } function HostDetailModal({ visible, host, isSaving, onClose, onSave, onRemoveConnection, onRemoveHost, onAddConnection, restartConfirmationMessage, waitForCondition, isScreenMountedRef, }: HostDetailModalProps) { const { theme } = useUnistyles(); const [draftLabel, setDraftLabel] = useState(""); const [pendingRemoveConnection, setPendingRemoveConnection] = useState<{ serverId: string; connectionId: string; title: string } | null>(null); const [isRemovingConnection, setIsRemovingConnection] = useState(false); // Read per-connection probes from host runtime snapshots. const connections = host?.connections ?? []; // Restart logic (moved from DaemonCard) const { snapshot: runtimeSnapshot, client: runtimeClient, isConnected } = useHostRuntimeSession( host?.serverId ?? "" ); const runtime = getHostRuntimeStore(); const daemonClient = runtimeClient; const daemonVersion = useSessionStore((state) => host ? (state.sessions[host.serverId]?.serverInfo?.version ?? null) : null); const probeByConnectionId = runtimeSnapshot?.probeByConnectionId ?? new Map(); const connectionStatus = runtimeSnapshot?.connectionStatus ?? "connecting"; const activeConnection = runtimeSnapshot?.activeConnection ?? null; const lastError = runtimeSnapshot?.lastError ?? null; const [isRestarting, setIsRestarting] = useState(false); const isHostConnected = useCallback(() => { if (!host) { return false; } return isHostRuntimeConnected(runtime.getSnapshot(host.serverId)); }, [host, runtime]); const waitForDaemonRestart = useCallback(async () => { const disconnectTimeoutMs = 7000; const reconnectTimeoutMs = 30000; if (isHostConnected()) { await waitForCondition(() => !isHostConnected(), disconnectTimeoutMs); } const reconnected = await waitForCondition(() => isHostConnected(), reconnectTimeoutMs); if (isScreenMountedRef.current) { setIsRestarting(false); if (!reconnected && host) { Alert.alert( "Unable to reconnect", `${host.label} did not come back online. Please verify it restarted.` ); } } }, [host, isHostConnected, isScreenMountedRef, waitForCondition]); const beginServerRestart = useCallback(() => { if (!daemonClient || !host) return; if (!isHostConnected()) { Alert.alert( "Host offline", "This host is offline. Paseo reconnects automatically—wait until it's back online before restarting." ); return; } setIsRestarting(true); void daemonClient .restartServer(`settings_daemon_restart_${host.serverId}`) .catch((error) => { console.error(`[Settings] Failed to restart daemon ${host.label}`, error); if (!isScreenMountedRef.current) return; setIsRestarting(false); Alert.alert( "Error", "Failed to send the restart request. Paseo reconnects automatically—try again once the host shows as online." ); }); void waitForDaemonRestart(); }, [daemonClient, host, isHostConnected, isScreenMountedRef, waitForDaemonRestart]); const handleRestartPress = useCallback(() => { if (!daemonClient || !host) { Alert.alert( "Host unavailable", "This host is not connected. Wait for it to come online before restarting." ); return; } void confirmDialog({ title: `Restart ${host.label}`, message: restartConfirmationMessage, confirmLabel: "Restart", cancelLabel: "Cancel", destructive: true, }).then((confirmed) => { if (!confirmed) { return; } beginServerRestart(); }).catch((error) => { console.error(`[Settings] Failed to open restart confirmation for ${host.label}`, error); Alert.alert("Error", "Unable to open the restart confirmation dialog."); }); }, [beginServerRestart, daemonClient, host, restartConfirmationMessage]); // Status display const statusLabel = formatConnectionStatus(connectionStatus); const statusTone = getConnectionStatusTone(connectionStatus); const statusColor = statusTone === "success" ? theme.colors.palette.green[400] : statusTone === "warning" ? theme.colors.palette.amber[500] : statusTone === "error" ? theme.colors.destructive : theme.colors.foregroundMuted; const statusPillBg = statusTone === "success" ? "rgba(74, 222, 128, 0.1)" : statusTone === "warning" ? "rgba(245, 158, 11, 0.1)" : statusTone === "error" ? "rgba(248, 113, 113, 0.1)" : "rgba(161, 161, 170, 0.1)"; const connectionBadge = (() => { return formatActiveConnectionBadge({ activeConnection, theme, }); })(); const versionBadgeText = formatDaemonVersionBadge(daemonVersion); const connectionError = typeof lastError === "string" && lastError.trim().length > 0 ? lastError.trim() : null; const handleDraftLabelChange = useCallback((nextValue: string) => { setDraftLabel(nextValue); }, []); useEffect(() => { if (!visible || !host) return; // Initialize once per modal open / host switch; keep user edits fully local while typing. setDraftLabel(host.label ?? ""); }, [visible, host?.serverId]); useEffect(() => { if (!visible) { setIsRestarting(false); setDraftLabel(""); } }, [visible]); return ( <> {/* Status row */} {statusLabel} {connectionBadge ? ( {connectionBadge.icon} {connectionBadge.text} ) : null} {versionBadgeText ? ( {versionBadgeText} ) : null} {connectionError ? ( {connectionError} ) : null} {/* Label */} Label {/* Connections */} {host ? ( Connections {host.connections.map((conn) => { const probe = probeByConnectionId.get(conn.id); return ( { const title = formatHostConnectionLabel(conn); setPendingRemoveConnection({ serverId: host.serverId, connectionId: conn.id, title }); }} /> ); })} ) : null} {/* Save/Cancel + Advanced */} [ styles.advancedTrigger, pressed && { opacity: 0.85 }, ]} > Advanced } status={isRestarting ? "pending" : "idle"} pendingLabel="Restarting..." disabled={!daemonClient || !isConnected} > Restart daemon { if (host) onRemoveHost(host); }} leading={} > Remove host {/* Remove connection confirmation */} {pendingRemoveConnection ? ( { if (isRemovingConnection) return; setPendingRemoveConnection(null); }} testID="remove-connection-confirm-modal" > Remove {pendingRemoveConnection.title}? This cannot be undone. ) : null} ); } function ConnectionRow({ connection, latencyMs, latencyLoading, latencyError, onRemove, }: { connection: HostConnection; latencyMs: number | null | undefined; latencyLoading: boolean; latencyError: boolean; onRemove: () => void; }) { const { theme } = useUnistyles(); const title = formatHostConnectionLabel(connection); const latencyText = (() => { if (latencyLoading) return "..."; if (latencyError) return "Timeout"; if (latencyMs != null) return `${latencyMs}ms`; return "\u2014"; })(); const latencyColor = latencyError ? theme.colors.palette.red[300] : theme.colors.foregroundMuted; return ( {title} {latencyText} ); } interface DaemonCardProps { daemon: HostProfile; onOpenSettings: (daemon: HostProfile) => void; } function DaemonCard({ daemon, onOpenSettings, }: DaemonCardProps) { const { theme } = useUnistyles(); const { snapshot } = useHostRuntimeSession(daemon.serverId); const connectionStatus = snapshot?.connectionStatus ?? "connecting"; const activeConnection = snapshot?.activeConnection ?? null; const lastError = snapshot?.lastError ?? null; const daemonVersion = useSessionStore( useCallback( (state) => state.sessions[daemon.serverId]?.serverInfo?.version ?? null, [daemon.serverId] ) ); const statusLabel = formatConnectionStatus(connectionStatus); const statusTone = getConnectionStatusTone(connectionStatus); const statusColor = statusTone === "success" ? theme.colors.palette.green[400] : statusTone === "warning" ? theme.colors.palette.amber[500] : statusTone === "error" ? theme.colors.destructive : theme.colors.foregroundMuted; const badgeText = statusLabel; const connectionError = typeof lastError === "string" && lastError.trim().length > 0 ? lastError.trim() : null; const statusPillBg = statusTone === "success" ? "rgba(74, 222, 128, 0.1)" : statusTone === "warning" ? "rgba(245, 158, 11, 0.1)" : statusTone === "error" ? "rgba(248, 113, 113, 0.1)" : "rgba(161, 161, 170, 0.1)"; const connectionBadge = (() => { return formatActiveConnectionBadge({ activeConnection, theme, }); })(); const versionBadgeText = formatDaemonVersionBadge(daemonVersion); return ( {daemon.label} {Platform.OS === "web" ? ( {badgeText} ) : null} {connectionBadge ? ( {connectionBadge.icon} {Platform.OS === "web" ? ( {connectionBadge.text} ) : null} ) : null} {versionBadgeText ? ( {versionBadgeText} ) : null} {connectionError ? {connectionError} : null} ); }