import { ArrowDown, ArrowUp, ArrowUpToLine, ChevronRight, Globe, Monitor, Pencil, Plus, RotateCw, SquareTerminal, Trash2, } from "lucide-react-native"; import type { TFunction } from "i18next"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { Alert, Pressable, Text, View } from "react-native"; import { StyleSheet, useUnistyles, withUnistyles } from "react-native-unistyles"; import type { TerminalProfile } from "@getpaseo/protocol/messages"; import { getTerminalProfileIcon, resolveTerminalProfiles, } from "@getpaseo/protocol/terminal-profiles"; import { AdaptiveModalSheet, type SheetHeader } from "@/components/adaptive-modal-sheet"; 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 { ProfileDraft, TerminalProfileEditModal, } from "@/screens/settings/terminal-profile-edit-modal"; 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"; import { getHostRuntimeStore, isHostRuntimeConnected, useHostMutations, useHostRuntimeClient, useHostRuntimeIsConnected, useHostRuntimeSnapshot, useHosts, } from "@/runtime/host-runtime"; import { ProvidersSection } from "@/screens/settings/providers-section"; import { ProviderUsageSettingsSection } from "@/provider-usage/settings-section"; import { useProviderUsage } from "@/provider-usage/use-provider-usage"; import { SettingsSection } from "@/screens/settings/settings-section"; import { useSessionStore } from "@/stores/session-store"; import { settingsStyles } from "@/styles/settings"; import type { HostConnection, HostProfile } from "@/types/host-connection"; import { confirmDialog } from "@/utils/confirm-dialog"; import { isVersionMismatch } from "@/desktop/updates/desktop-updates"; import { resolveAppVersion } from "@/utils/app-version"; import { formatConnectionStatus, getConnectionStatusTone } from "@/utils/daemons"; import { formatLatency } from "@/utils/latency"; import { ICON_SIZE } from "@/styles/theme"; import type { Theme } from "@/styles/theme"; import { getProviderIcon } from "@/components/provider-icons"; const ThemedArrowUp = withUnistyles(ArrowUp); const ThemedArrowDown = withUnistyles(ArrowDown); const ThemedProfilePencil = withUnistyles(Pencil); const ThemedTrash2 = withUnistyles(Trash2); const ThemedProfileSquareTerminal = withUnistyles(SquareTerminal); const ThemedPlus = withUnistyles(Plus); interface DynamicProviderIconProps { iconKey: string; size: number; color?: string; } function DynamicProviderIcon({ iconKey, size, color = "" }: DynamicProviderIconProps) { const Icon = getProviderIcon(iconKey); return ; } const ThemedDynamicProviderIcon = withUnistyles(DynamicProviderIcon); const mutedColorMapping = (theme: Theme) => ({ color: theme.colors.foregroundMuted }); const destructiveColorMapping = (theme: Theme) => ({ color: theme.colors.destructive }); const moveUpIcon = ; const moveDownIcon = ; const editProfileIcon = ; const removeProfileIcon = ; const addProfileIcon = ; function formatHostConnectionLabel(connection: HostConnection, t: TFunction): string { if (connection.type === "relay") { return `${t("settings.host.badges.relay")} (${connection.relayEndpoint})`; } if (connection.type === "directSocket" || connection.type === "directPipe") { return `${t("settings.host.badges.local")} (${connection.path})`; } return `TCP (${connection.endpoint})`; } function formatActiveConnectionBadge( activeConnection: { type: HostConnection["type"]; display: string } | null, theme: ReturnType["theme"], t: TFunction, ): { icon: React.ReactNode; text: string } | null { if (!activeConnection) return null; if (activeConnection.type === "relay") { return { icon: , text: t("settings.host.badges.relay"), }; } if (activeConnection.type === "directSocket" || activeConnection.type === "directPipe") { return { icon: , text: t("settings.host.badges.local"), }; } return { icon: , text: activeConnection.display, }; } function formatDaemonVersionBadge(version: string | null): string | null { const trimmed = version?.trim(); if (!trimmed) return null; return trimmed.startsWith("v") ? trimmed : `v${trimmed}`; } function useHostProfile(serverId: string): HostProfile | null { const daemons = useHosts(); return daemons.find((entry) => entry.serverId === serverId) ?? null; } function HostNotFound() { const { t } = useTranslation(); return ( {t("settings.host.notFound")} ); } function HostStatusBadges({ serverId }: { serverId: string }) { const { t } = useTranslation(); const { theme } = useUnistyles(); const snapshot = useHostRuntimeSnapshot(serverId); const daemonVersion = useSessionStore( (state) => state.sessions[serverId]?.serverInfo?.version ?? null, ); const connectionStatus = snapshot?.connectionStatus ?? "connecting"; const activeConnection = snapshot?.activeConnection ?? null; const statusLabel = formatConnectionStatus(connectionStatus); const statusTone = getConnectionStatusTone(connectionStatus); let statusColor: string; if (statusTone === "success") { statusColor = theme.colors.palette.green[400]; } else if (statusTone === "warning") { statusColor = theme.colors.palette.amber[500]; } else if (statusTone === "error") { statusColor = theme.colors.destructive; } else { statusColor = theme.colors.foregroundMuted; } let statusPillBg: string; if (statusTone === "success") { statusPillBg = "rgba(74, 222, 128, 0.1)"; } else if (statusTone === "warning") { statusPillBg = "rgba(245, 158, 11, 0.1)"; } else if (statusTone === "error") { statusPillBg = "rgba(248, 113, 113, 0.1)"; } else { statusPillBg = "rgba(161, 161, 170, 0.1)"; } const connectionBadge = formatActiveConnectionBadge(activeConnection, theme, t); const versionBadgeText = formatDaemonVersionBadge(daemonVersion); const statusPillStyle = useMemo( () => [styles.statusPill, { backgroundColor: statusPillBg }], [statusPillBg], ); const statusDotStyle = useMemo( () => [styles.statusDot, { backgroundColor: statusColor }], [statusColor], ); const statusTextStyle = useMemo(() => [styles.statusText, { color: statusColor }], [statusColor]); return ( {statusLabel} {connectionBadge ? ( {connectionBadge.icon} {connectionBadge.text} ) : null} {versionBadgeText ? ( {versionBadgeText} ) : null} ); } function HostConnectionError({ serverId }: { serverId: string }) { const snapshot = useHostRuntimeSnapshot(serverId); const lastError = snapshot?.lastError ?? null; const connectionError = typeof lastError === "string" && lastError.trim().length > 0 ? lastError.trim() : null; if (!connectionError) return null; return {connectionError}; } export function HostConnectionsPage({ serverId }: { serverId: string }) { const { t } = useTranslation(); const host = useHostProfile(serverId); const isLocalDaemon = useIsLocalDaemon(serverId); if (!host) { return ; } return ( {isLocalDaemon ? ( ) : null} ); } export function HostAgentsPage({ serverId }: { serverId: string }) { const { t } = useTranslation(); const host = useHostProfile(serverId); const isConnected = useHostRuntimeIsConnected(serverId); if (!host) { return ; } return ( {isConnected ? ( ) : ( {t("settings.host.agents.unavailable")} )} ); } export function HostWorkspacesPage({ serverId }: { serverId: string }) { const { t } = useTranslation(); const host = useHostProfile(serverId); const isConnected = useHostRuntimeIsConnected(serverId); if (!host) { return ; } return ( {isConnected ? ( ) : ( {t("settings.host.workspaces.unavailable")} )} ); } export function HostProvidersPage({ serverId }: { serverId: string }) { const host = useHostProfile(serverId); if (!host) { return ; } return ( ); } export function HostUsagePage({ serverId }: { serverId: string }) { const host = useHostProfile(serverId); const { view: providerUsageView, refresh: refreshProviderUsage } = useProviderUsage(serverId); const handleRefresh = useCallback(() => { void refreshProviderUsage(); }, [refreshProviderUsage]); if (!host) { return ; } return ( ); } export function HostSettingsPage({ serverId, onHostRemoved, }: { serverId: string; onHostRemoved?: () => void; }) { const host = useHostProfile(serverId); const isLocalDaemon = useIsLocalDaemon(serverId); if (!host) { return ; } return ( {host.label} {isLocalDaemon ? : null} {!isLocalDaemon ? : null} ); } export function HostRenameButton({ host }: { host: HostProfile }) { const { t } = useTranslation(); const { theme } = useUnistyles(); const { renameHost } = useHostMutations(); const [isEditing, setIsEditing] = useState(false); const handleSubmit = useCallback( async (value: string) => { const nextLabel = value.trim(); if (nextLabel === host.label.trim()) return; await renameHost(host.serverId, nextLabel); }, [host.label, host.serverId, renameHost], ); const openEditor = useCallback(() => setIsEditing(true), []); const closeEditor = useCallback(() => setIsEditing(false), []); return ( <> ); } function ConnectionsSection({ host }: { host: HostProfile }) { const { t } = useTranslation(); const { removeConnection } = useHostMutations(); const snapshot = useHostRuntimeSnapshot(host.serverId); const probeByConnectionId = snapshot?.probeByConnectionId ?? new Map(); const [pendingRemoveConnection, setPendingRemoveConnection] = useState<{ connectionId: string; title: string; } | null>(null); const [isRemovingConnection, setIsRemovingConnection] = useState(false); const removeConnectionHeader = useMemo( () => ({ title: t("settings.host.connections.removeTitle") }), [t], ); const handleRequestRemove = useCallback( (connection: HostConnection) => { setPendingRemoveConnection({ connectionId: connection.id, title: formatHostConnectionLabel(connection, t), }); }, [t], ); const handleCloseConfirm = useCallback(() => { if (isRemovingConnection) return; setPendingRemoveConnection(null); }, [isRemovingConnection]); const handleCancelConfirm = useCallback(() => { setPendingRemoveConnection(null); }, []); const handleConfirmRemove = useCallback(() => { if (!pendingRemoveConnection) return; const { connectionId } = pendingRemoveConnection; setIsRemovingConnection(true); void removeConnection(host.serverId, connectionId) .then(() => setPendingRemoveConnection(null)) .catch((error) => { console.error("[HostPage] Failed to remove connection", error); Alert.alert( t("settings.host.connections.removeErrorTitle"), t("settings.host.connections.removeErrorMessage"), ); }) .finally(() => setIsRemovingConnection(false)); }, [pendingRemoveConnection, removeConnection, host.serverId, t]); return ( {host.connections.map((conn, index) => { const probe = probeByConnectionId.get(conn.id); return ( 0} latencyMs={probe?.status === "available" ? probe.latencyMs : undefined} latencyLoading={!probe || probe.status === "pending"} latencyError={probe?.status === "unavailable"} onRemove={handleRequestRemove} /> ); })} {pendingRemoveConnection ? ( {t("settings.host.connections.removeMessage", { name: pendingRemoveConnection.title, })} ) : null} ); } function ConnectionRow({ connection, showBorder, latencyMs, latencyLoading, latencyError, onRemove, }: { connection: HostConnection; showBorder: boolean; latencyMs: number | null | undefined; latencyLoading: boolean; latencyError: boolean; onRemove: (connection: HostConnection) => void; }) { const { t } = useTranslation(); const { theme } = useUnistyles(); const title = formatHostConnectionLabel(connection, t); const latencyText = (() => { if (latencyLoading) return "..."; if (latencyError) return t("settings.host.connections.timeout"); if (latencyMs != null) return formatLatency(latencyMs); return "—"; })(); const latencyColor = latencyError ? theme.colors.palette.red[300] : theme.colors.foregroundMuted; const handlePressRemove = useCallback(() => { onRemove(connection); }, [onRemove, connection]); const rowStyle = useMemo( () => [settingsStyles.row, showBorder && settingsStyles.rowBorder], [showBorder], ); const latencyTextStyle = useMemo( () => [styles.connectionLatency, { color: latencyColor }], [latencyColor], ); const destructiveTextStyle = useMemo( () => ({ color: theme.colors.destructive }), [theme.colors.destructive], ); return ( {title} {latencyText} ); } const delay = (ms: number) => new Promise((resolve) => { setTimeout(resolve, ms); }); function RestartDaemonCard({ host }: { host: HostProfile }) { const { t } = useTranslation(); const { theme } = useUnistyles(); const daemonClient = useHostRuntimeClient(host.serverId); const isConnected = useHostRuntimeIsConnected(host.serverId); const runtime = getHostRuntimeStore(); const [isRestarting, setIsRestarting] = useState(false); const isMountedRef = useRef(true); useEffect(() => { return () => { isMountedRef.current = false; }; }, []); const isHostConnected = useCallback( () => isHostRuntimeConnected(runtime.getSnapshot(host.serverId)), [host.serverId, runtime], ); 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 waitForDaemonRestart = useCallback(async () => { const disconnectTimeoutMs = 7000; const reconnectTimeoutMs = 30000; if (isHostConnected()) { await waitForCondition(() => !isHostConnected(), disconnectTimeoutMs); } const reconnected = await waitForCondition(() => isHostConnected(), reconnectTimeoutMs); if (isMountedRef.current) { setIsRestarting(false); if (!reconnected) { Alert.alert( t("settings.host.daemon.restart.unableToReconnectTitle"), t("settings.host.daemon.restart.unableToReconnectMessage", { name: host.label }), ); } } }, [host.label, isHostConnected, t, waitForCondition]); const handleRestart = useCallback(() => { if (!daemonClient) { Alert.alert( t("settings.host.daemon.restart.unavailableTitle"), t("settings.host.daemon.restart.unavailableMessage"), ); return; } if (!isHostConnected()) { Alert.alert( t("settings.host.daemon.restart.offlineTitle"), t("settings.host.daemon.restart.offlineMessage"), ); return; } void confirmDialog({ title: t("settings.host.daemon.restart.confirmTitle", { name: host.label }), message: t("settings.host.daemon.restart.confirmMessage"), confirmLabel: t("settings.host.daemon.restart.confirm"), cancelLabel: t("common.actions.cancel"), destructive: true, }) .then((confirmed) => { if (!confirmed) return; setIsRestarting(true); void daemonClient .restartServer(`settings_daemon_restart_${host.serverId}`) .catch((error) => { console.error(`[HostPage] Failed to restart daemon ${host.label}`, error); if (!isMountedRef.current) return; setIsRestarting(false); Alert.alert( t("settings.host.daemon.restart.requestFailedTitle"), t("settings.host.daemon.restart.requestFailedMessage"), ); }); void waitForDaemonRestart(); return; }) .catch((error) => { console.error(`[HostPage] Failed to open restart confirmation for ${host.label}`, error); Alert.alert( t("settings.host.daemon.restart.requestFailedTitle"), t("settings.host.daemon.restart.dialogFailedMessage"), ); }); }, [daemonClient, host.label, host.serverId, isHostConnected, t, waitForDaemonRestart]); const restartIcon = useMemo( () => , [theme.iconSize.sm, theme.colors.foreground], ); return ( {t("settings.host.daemon.restart.title")} {t("settings.host.daemon.restart.hint")} ); } function UpdateDaemonCard({ host }: { host: HostProfile }) { const { t } = useTranslation(); const { theme } = useUnistyles(); const daemonClient = useHostRuntimeClient(host.serverId); const isConnected = useHostRuntimeIsConnected(host.serverId); const runtime = getHostRuntimeStore(); const [isUpdating, setIsUpdating] = useState(false); const [progressPhase, setProgressPhase] = useState(null); const isMountedRef = useRef(true); const unsubscribeRef = useRef<(() => void) | null>(null); const daemonVersion = useSessionStore( (state) => state.sessions[host.serverId]?.serverInfo?.version ?? null, ); const supportsSelfUpdate = useSessionStore( (state) => state.sessions[host.serverId]?.serverInfo?.features?.daemonSelfUpdate === true, ); const appVersion = resolveAppVersion(); const hasVersionMismatch = isVersionMismatch(appVersion, daemonVersion); useEffect(() => { return () => { isMountedRef.current = false; unsubscribeRef.current?.(); }; }, []); const isHostConnected = useCallback( () => isHostRuntimeConnected(runtime.getSnapshot(host.serverId)), [host.serverId, runtime], ); const hasReconnectedAfter = useCallback( (startGeneration: number | null) => { const snapshot = runtime.getSnapshot(host.serverId); if (!snapshot || !isHostRuntimeConnected(snapshot)) return false; return startGeneration === null || snapshot.clientGeneration !== startGeneration; }, [host.serverId, runtime], ); 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 waitForDaemonRestart = useCallback( async (startGeneration: number | null) => { const disconnectTimeoutMs = 15000; const reconnectTimeoutMs = 120000; // 2 minutes — npm update + restart can take a while if (!hasReconnectedAfter(startGeneration) && isHostConnected()) { await waitForCondition( () => !isHostConnected() || hasReconnectedAfter(startGeneration), disconnectTimeoutMs, ); } const reconnected = hasReconnectedAfter(startGeneration) || (await waitForCondition(() => hasReconnectedAfter(startGeneration), reconnectTimeoutMs)); if (isMountedRef.current) { setIsUpdating(false); setProgressPhase(null); if (!reconnected) { Alert.alert( t("settings.host.daemon.update.unableToReconnectTitle"), t("settings.host.daemon.update.unableToReconnectMessage", { name: host.label }), ); } } }, [hasReconnectedAfter, host.label, isHostConnected, t, waitForCondition], ); const handleUpdate = useCallback(() => { if (!daemonClient) { Alert.alert( t("settings.host.daemon.update.unavailableTitle"), t("settings.host.daemon.update.unavailableMessage"), ); return; } if (!isHostConnected()) { Alert.alert( t("settings.host.daemon.update.offlineTitle"), t("settings.host.daemon.update.offlineMessage"), ); return; } void confirmDialog({ title: t("settings.host.daemon.update.confirmTitle", { name: host.label }), message: t("settings.host.daemon.update.confirmMessage"), confirmLabel: t("settings.host.daemon.update.confirm"), cancelLabel: t("common.actions.cancel"), destructive: false, }) .then((confirmed) => { if (!confirmed) return; const startGeneration = runtime.getSnapshot(host.serverId)?.clientGeneration ?? null; setIsUpdating(true); setProgressPhase(t("settings.host.daemon.update.phaseStarting")); const requestId = `settings_daemon_update_${host.serverId}`; const unsubscribe = daemonClient.on("daemon.update.progress", (message) => { if (message.payload.requestId !== requestId) return; if (!isMountedRef.current) return; const { phase } = message.payload; if (phase === "starting") setProgressPhase(t("settings.host.daemon.update.phaseStarting")); else if (phase === "downloading") setProgressPhase(t("settings.host.daemon.update.phaseDownloading")); else if (phase === "installing") setProgressPhase(t("settings.host.daemon.update.phaseInstalling")); else if (phase === "complete") setProgressPhase(t("settings.host.daemon.update.phaseComplete")); }); unsubscribeRef.current = unsubscribe; void daemonClient .updateDaemon(requestId) .then((response) => { unsubscribeRef.current = null; unsubscribe(); if (!response.success) { if (!isMountedRef.current) return undefined; setIsUpdating(false); setProgressPhase(null); Alert.alert( t("settings.host.daemon.update.requestFailedTitle"), t("settings.host.daemon.update.requestFailedMessage", { error: response.error ?? "Unknown error", }), ); return undefined; } // Update succeeded — wait for daemon to restart and reconnect void waitForDaemonRestart(startGeneration); return undefined; }) .catch((error) => { unsubscribeRef.current = null; unsubscribe(); console.error(`[HostPage] Failed to update daemon ${host.label}`, error); if (!isMountedRef.current) return; setIsUpdating(false); setProgressPhase(null); Alert.alert( t("settings.host.daemon.update.requestFailedTitle"), t("settings.host.daemon.update.requestFailedMessage", { error: error instanceof Error ? error.message : "Unknown error", }), ); }); return; }) .catch((error) => { console.error(`[HostPage] Failed to open update confirmation for ${host.label}`, error); Alert.alert( t("settings.host.daemon.update.requestFailedTitle"), t("settings.host.daemon.update.dialogFailedMessage"), ); }); }, [daemonClient, host.label, host.serverId, isHostConnected, runtime, t, waitForDaemonRestart]); const updateIcon = useMemo( () => , [theme.iconSize.sm, theme.colors.foreground], ); // Don't show if the daemon doesn't support self-update or versions match if (!supportsSelfUpdate || !hasVersionMismatch) { return null; } const buttonLabel = isUpdating ? (progressPhase ?? t("settings.host.daemon.update.updating")) : t("settings.host.daemon.update.confirm"); return ( {t("settings.host.daemon.update.title")} {t("settings.host.daemon.update.hint")} ); } function InjectPaseoToolsCard({ serverId }: { serverId: string }) { const { t } = useTranslation(); const isConnected = useHostRuntimeIsConnected(serverId); const { config, patchConfig } = useDaemonConfig(serverId); const handleValueChange = useCallback( (next: boolean) => { void patchConfig({ mcp: { injectIntoAgents: next, }, }); }, [patchConfig], ); if (!isConnected) return null; return ( {t("settings.host.orchestration.enableTools.title")} {t("settings.host.orchestration.enableTools.hint")} ); } function AutoArchiveMergedWorkspacesCard({ serverId }: { serverId: string }) { const isConnected = useHostRuntimeIsConnected(serverId); const { config, patchConfig } = useDaemonConfig(serverId); const handleValueChange = useCallback( (next: boolean) => { void patchConfig({ autoArchiveAfterMerge: next }).catch((error) => { console.error("[HostPage] Failed to update auto-archive after merge", error); Alert.alert( "Unable to update workspaces", error instanceof Error ? error.message : String(error), ); }); }, [patchConfig], ); if (!isConnected) return null; return ( Archive merged PR workspaces Automatically archive clean Paseo workspaces after their pull request is merged ); } function EnableTerminalAgentHooksCard({ serverId }: { serverId: string }) { const isConnected = useHostRuntimeIsConnected(serverId); const { config, patchConfig } = useDaemonConfig(serverId); const handleValueChange = useCallback( (next: boolean) => { void patchConfig({ enableTerminalAgentHooks: next }).catch((error) => { console.error("[HostPage] Failed to update terminal agent hooks", error); Alert.alert( "Unable to update terminal agent hooks", error instanceof Error ? error.message : String(error), ); }); }, [patchConfig], ); if (!isConnected) return null; return ( Enable terminal agent hooks Get notifications and status from terminal agents. This installs hooks in your agent config files. ); } function AppendSystemPromptCard({ serverId }: { serverId: string }) { const { t } = useTranslation(); const isConnected = useHostRuntimeIsConnected(serverId); const { config, patchConfig } = useDaemonConfig(serverId); const persistedPrompt = config?.appendSystemPrompt ?? ""; const [draft, setDraft] = useState(persistedPrompt); const [isEditing, setIsEditing] = useState(false); const [isSaving, setIsSaving] = useState(false); const header = useMemo( () => ({ title: t("settings.host.orchestration.systemPrompt.sheetTitle") }), [t], ); useEffect(() => { setDraft(persistedPrompt); }, [persistedPrompt]); const hasChanges = draft !== persistedPrompt; const handleOpen = useCallback(() => { setDraft(persistedPrompt); setIsEditing(true); }, [persistedPrompt]); const handleClose = useCallback(() => { if (isSaving) return; setDraft(persistedPrompt); setIsEditing(false); }, [isSaving, persistedPrompt]); const handleSave = useCallback(() => { setIsSaving(true); void patchConfig({ appendSystemPrompt: draft }) .then(() => { setIsEditing(false); return; }) .catch((error) => { console.error("[HostPage] Failed to save append system prompt", error); }) .finally(() => setIsSaving(false)); }, [draft, patchConfig]); const handleReset = useCallback(() => { setDraft(persistedPrompt); }, [persistedPrompt]); if (!isConnected) return null; return ( <> {t("settings.host.orchestration.systemPrompt.title")} {t("settings.host.orchestration.systemPrompt.hint")} {isEditing ? ( ) : null} ); } function PairDeviceRow() { const { t } = useTranslation(); const { theme } = useUnistyles(); const [isModalOpen, setIsModalOpen] = useState(false); const handleOpen = useCallback(() => setIsModalOpen(true), []); const handleClose = useCallback(() => setIsModalOpen(false), []); return ( {t("settings.host.pairDevices.rowTitle")} {t("settings.host.pairDevices.rowHint")} ); } function RemoveHostSection({ host, isLocalDaemon, onRemoved, }: { host: HostProfile; isLocalDaemon: boolean; onRemoved?: () => void; }) { const { t } = useTranslation(); 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 removeHostHeader = useMemo( () => ({ title: isLocalDaemon ? t("settings.host.daemon.remove.localConfirmTitle") : t("settings.host.daemon.remove.title"), }), [isLocalDaemon, t], ); const destructiveTextStyle = useMemo( () => ({ color: theme.colors.destructive }), [theme.colors.destructive], ); const handleOpenConfirm = useCallback(() => setIsConfirming(true), []); const handleCloseConfirm = useCallback(() => { if (isRemoving) return; 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); 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?.(); return; }) .catch((error) => { console.error("[HostPage] Failed to remove host", error); Alert.alert( t("settings.host.daemon.remove.errorTitle"), isLocalDaemon ? t("settings.host.daemon.remove.localErrorMessage") : t("settings.host.daemon.remove.errorMessage"), ); }) .finally(() => setIsRemoving(false)); }, [ daemonStatus, host.serverId, isLocalDaemon, onRemoved, removeHost, rollbackLocalhostRemoval, setStatus, t, updateSettings, ]); const removeIcon = useMemo( () => , [theme.iconSize.sm, theme.colors.destructive], ); return ( {isLocalDaemon ? t("settings.host.daemon.remove.localTitle") : t("settings.host.daemon.remove.title")} {isLocalDaemon ? t("settings.host.daemon.remove.localHint") : t("settings.host.daemon.remove.hint")} {isConfirming ? ( {isLocalDaemon ? t("settings.host.daemon.remove.localConfirmMessage") : t("settings.host.daemon.remove.confirmMessage", { name: host.label })} ) : null} ); } // --------------------------------------------------------------------------- // Terminal Profiles // --------------------------------------------------------------------------- function generateProfileId(): string { return `profile_${Date.now().toString(36)}_${Math.random().toString(36).slice(2)}`; } function parseArgsString(raw: string): string[] | undefined { const trimmed = raw.trim(); if (!trimmed) return undefined; return trimmed .split(/\s+/) .map((s) => s.trim()) .filter(Boolean); } const EMPTY_PROFILE_DRAFT: ProfileDraft = { name: "", command: "", args: "" }; interface TerminalProfileRowProps { profile: TerminalProfile; isFirst: boolean; isLast: boolean; onEdit: (id: string) => void; onRemove: (id: string) => void; onMoveUp: (id: string) => void; onMoveDown: (id: string) => void; } function TerminalProfileRow({ profile, isFirst, isLast, onEdit, onRemove, onMoveUp, onMoveDown, }: TerminalProfileRowProps) { const { t } = useTranslation(); const handleEdit = useCallback(() => onEdit(profile.id), [onEdit, profile.id]); const handleRemove = useCallback(() => onRemove(profile.id), [onRemove, profile.id]); const handleMoveUp = useCallback(() => onMoveUp(profile.id), [onMoveUp, profile.id]); const handleMoveDown = useCallback(() => onMoveDown(profile.id), [onMoveDown, profile.id]); const commandText = profile.args && profile.args.length > 0 ? `${profile.command} ${profile.args.join(" ")}` : profile.command; const rowStyle = useMemo( () => [settingsStyles.row, !isFirst && settingsStyles.rowBorder, terminalProfileStyles.row], [isFirst], ); const icon = getTerminalProfileIcon(profile); return ( {icon ? ( ) : ( )} {profile.name} {commandText}