import React, { type ReactElement, useCallback, useMemo, useState } from "react"; import { ActivityIndicator, Alert, Text, View } from "react-native"; import * as Clipboard from "expo-clipboard"; import { StyleSheet, useUnistyles } from "react-native-unistyles"; import { settingsStyles } from "@/styles/settings"; import { SettingsSection } from "@/screens/settings/settings-section"; import { ArrowUpRight, Copy, FileText, Activity } from "lucide-react-native"; import { AdaptiveModalSheet, type SheetHeader } from "@/components/adaptive-modal-sheet"; import { Button } from "@/components/ui/button"; import { Switch } from "@/components/ui/switch"; import { openExternalUrl } from "@/utils/open-external-url"; import { isVersionMismatch } from "@/desktop/updates/desktop-updates"; import { getCliDaemonStatus, shouldUseDesktopDaemon } from "@/desktop/daemon/desktop-daemon"; import { useBuiltInDaemonManagement } from "@/desktop/hooks/use-built-in-daemon-management"; import { useDaemonStatus } from "@/desktop/hooks/use-daemon-status"; import { useDesktopSettings, type DesktopSettings } from "@/desktop/settings/desktop-settings"; import { resolveAppVersion } from "@/utils/app-version"; type DesktopDaemonSettings = DesktopSettings["daemon"]; function useKeepRunningAfterQuitToggle(args: { settings: DesktopDaemonSettings; updateSettings: (next: Partial) => Promise; }) { const { settings, updateSettings } = args; const [isUpdatingKeepRunningAfterQuit, setIsUpdatingKeepRunningAfterQuit] = useState(false); const handleToggleKeepRunningAfterQuit = useCallback(() => { setIsUpdatingKeepRunningAfterQuit(true); void updateSettings({ keepRunningAfterQuit: !settings.keepRunningAfterQuit }) .catch(() => { // useDesktopSettings owns the user-visible IPC error. }) .finally(() => { setIsUpdatingKeepRunningAfterQuit(false); }); }, [settings.keepRunningAfterQuit, updateSettings]); return { isUpdatingKeepRunningAfterQuit, handleToggleKeepRunningAfterQuit }; } function useDaemonCliStatusModal() { const [cliStatusOutput, setCliStatusOutput] = useState(null); const [isCliStatusModalOpen, setIsCliStatusModalOpen] = useState(false); const [isLoadingCliStatus, setIsLoadingCliStatus] = useState(false); const handleOpenCliStatus = useCallback(async () => { setIsLoadingCliStatus(true); try { setCliStatusOutput(await getCliDaemonStatus()); setIsCliStatusModalOpen(true); } catch (error) { const message = error instanceof Error ? error.message : String(error); setCliStatusOutput(`Failed to fetch daemon status: ${message}`); setIsCliStatusModalOpen(true); } finally { setIsLoadingCliStatus(false); } }, []); const handleCopyCliStatus = useCallback(() => { if (!cliStatusOutput) { return; } void Clipboard.setStringAsync(cliStatusOutput) .then(() => { Alert.alert("Copied", "Status copied to clipboard."); return; }) .catch((error) => { console.error("[Settings] Failed to copy daemon status", error); }); }, [cliStatusOutput]); const handleCloseCliStatusModal = useCallback(() => setIsCliStatusModalOpen(false), []); return { cliStatusOutput, isCliStatusModalOpen, isLoadingCliStatus, handleCopyCliStatus, handleOpenCliStatus, handleCloseCliStatusModal, }; } function useDaemonLogsModal(daemonLogs: { logPath?: string } | null) { const [isLogsModalOpen, setIsLogsModalOpen] = useState(false); const handleCopyLogPath = useCallback(() => { const logPath = daemonLogs?.logPath; if (!logPath) { return; } void Clipboard.setStringAsync(logPath) .then(() => { Alert.alert("Copied", "Log path copied."); return; }) .catch((error) => { console.error("[Settings] Failed to copy log path", error); Alert.alert("Error", "Unable to copy log path."); }); }, [daemonLogs?.logPath]); const handleOpenLogs = useCallback(() => { if (!daemonLogs) { return; } setIsLogsModalOpen(true); }, [daemonLogs]); const handleCloseLogsModal = useCallback(() => setIsLogsModalOpen(false), []); return { isLogsModalOpen, handleCopyLogPath, handleOpenLogs, handleCloseLogsModal }; } interface DaemonLogsModalProps { visible: boolean; onClose: () => void; daemonLogs: { logPath?: string; contents?: string } | null; } function DaemonLogsModal({ visible, onClose, daemonLogs }: DaemonLogsModalProps) { return ( {daemonLogs?.logPath ?? "Log path unavailable"} {daemonLogs?.contents?.length ? daemonLogs.contents : "(log file is empty)"} ); } interface DaemonCliStatusModalProps { visible: boolean; onClose: () => void; cliStatusOutput: string | null; onCopy: () => void; } function DaemonCliStatusModal({ visible, onClose, cliStatusOutput, onCopy, }: DaemonCliStatusModalProps) { return ( {cliStatusOutput ?? ""} ); } interface DaemonInfoCardProps { daemonStatusStateText: string; daemonStatusDetailText: string; isDaemonManagementPaused: boolean; copyIcon: ReactElement; fileTextIcon: ReactElement; activityIcon: ReactElement; handleToggleDaemonManagement: () => void; isUpdatingDaemonManagement: boolean; keepRunningAfterQuit: boolean; handleToggleKeepRunningAfterQuit: () => void; isUpdatingKeepRunningAfterQuit: boolean; daemonLogs: { logPath?: string } | null; handleCopyLogPath: () => void; handleOpenLogs: () => void; handleRunCliStatus: () => void; isLoadingCliStatus: boolean; } function DaemonInfoCard(props: DaemonInfoCardProps) { const { daemonStatusStateText, daemonStatusDetailText, isDaemonManagementPaused, copyIcon, fileTextIcon, activityIcon, handleToggleDaemonManagement, isUpdatingDaemonManagement, keepRunningAfterQuit, handleToggleKeepRunningAfterQuit, isUpdatingKeepRunningAfterQuit, daemonLogs, handleCopyLogPath, handleOpenLogs, handleRunCliStatus, isLoadingCliStatus, } = props; return ( Status Only the built-in desktop daemon is shown here {daemonStatusStateText} {daemonStatusDetailText} Manage built-in daemon Let Paseo start and stop the built-in daemon Keep daemon running after quit Daemon keeps running when you quit Paseo Log file {daemonLogs?.logPath ?? "Log path unavailable"} {daemonLogs?.logPath ? ( ) : null} Full status Runs `paseo daemon status` and shows the output ); } export function LocalDaemonSection() { const { theme } = useUnistyles(); const showSection = shouldUseDesktopDaemon(); const appVersion = resolveAppVersion(); const { settings, updateSettings, isLoading: isLoadingSettings } = useDesktopSettings(); const daemonSettings = settings.daemon; const updateDaemonSettings = useCallback( (updates: Partial) => updateSettings({ daemon: updates }), [updateSettings], ); const { data, isLoading, error: statusError, setStatus, refetch } = useDaemonStatus(); const daemonStatus = data?.status ?? null; const daemonLogs = data?.logs ?? null; const daemonVersion = daemonStatus?.version ?? null; const daemonVersionMismatch = isVersionMismatch(appVersion, daemonVersion); const daemonStatusStateText = statusError ?? (daemonStatus?.status === "running" ? daemonStatus.status : "not running"); const daemonStatusDetailText = `PID ${daemonStatus?.pid ? daemonStatus.pid : "—"}`; const isDaemonManagementPaused = !daemonSettings.manageBuiltInDaemon; const { isUpdating: isUpdatingDaemonManagement, toggle: handleToggleDaemonManagement } = useBuiltInDaemonManagement({ daemonStatus, settings: daemonSettings, updateSettings: updateDaemonSettings, setStatus, refreshStatus: refetch, }); const { isUpdatingKeepRunningAfterQuit, handleToggleKeepRunningAfterQuit } = useKeepRunningAfterQuitToggle({ settings: daemonSettings, updateSettings: updateDaemonSettings, }); const { isLogsModalOpen, handleCopyLogPath, handleOpenLogs, handleCloseLogsModal } = useDaemonLogsModal(daemonLogs); const { cliStatusOutput, isCliStatusModalOpen, isLoadingCliStatus, handleCopyCliStatus, handleOpenCliStatus, handleCloseCliStatusModal, } = useDaemonCliStatusModal(); const handleRunCliStatus = useCallback(() => { void handleOpenCliStatus(); }, [handleOpenCliStatus]); const handleOpenAdvancedSettings = useCallback( () => void openExternalUrl(ADVANCED_DAEMON_SETTINGS_URL), [], ); const advancedSettingsIcon = useMemo( () => , [theme.iconSize.sm, theme.colors.foregroundMuted], ); const copyIcon = useMemo( () => , [theme.iconSize.sm, theme.colors.foreground], ); const fileTextIcon = useMemo( () => , [theme.iconSize.sm, theme.colors.foreground], ); const activityIcon = useMemo( () => , [theme.iconSize.sm, theme.colors.foreground], ); const advancedSettingsButton = useMemo( () => ( ), [advancedSettingsIcon, handleOpenAdvancedSettings], ); if (!showSection) { return null; } return ( {isLoading || isLoadingSettings ? ( ) : ( <> {daemonVersionMismatch ? ( { "App and daemon versions don't match. Update both to the same version for the best experience." } ) : null} )} ); } const ADVANCED_DAEMON_SETTINGS_URL = "https://paseo.sh/docs/configuration"; const styles = StyleSheet.create((theme) => ({ actionGroup: { flexDirection: "row", gap: theme.spacing[2], flexWrap: "wrap", justifyContent: "flex-end", }, loadingCard: { alignItems: "center", justifyContent: "center", paddingVertical: theme.spacing[6], }, statusValueGroup: { alignItems: "flex-end", gap: 2, }, valueText: { color: theme.colors.foregroundMuted, fontSize: theme.fontSize.sm, }, valueSubtext: { color: theme.colors.foregroundMuted, fontSize: theme.fontSize.xs, }, warningCard: { marginTop: theme.spacing[3], borderRadius: theme.borderRadius.lg, borderWidth: 1, borderColor: theme.colors.palette.amber[500], backgroundColor: "rgba(245, 158, 11, 0.12)", paddingVertical: theme.spacing[2], paddingHorizontal: theme.spacing[3], }, warningText: { color: theme.colors.palette.amber[500], fontSize: theme.fontSize.xs, }, modalBody: { gap: theme.spacing[3], paddingBottom: theme.spacing[2], }, logOutput: { color: theme.colors.foregroundMuted, fontSize: theme.fontSize.xs, fontFamily: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace", lineHeight: 18, }, modalActions: { flexDirection: "row", justifyContent: "flex-end", gap: theme.spacing[2], }, })); const LOADING_CARD_STYLE = [settingsStyles.card, styles.loadingCard]; const ROW_WITH_BORDER_STYLE = [settingsStyles.row, settingsStyles.rowBorder]; const LOGS_MODAL_SNAP_POINTS = ["70%", "92%"]; const CLI_STATUS_MODAL_SNAP_POINTS = ["60%", "85%"]; const DAEMON_LOGS_HEADER: SheetHeader = { title: "Daemon logs" }; const DAEMON_STATUS_HEADER: SheetHeader = { title: "Daemon status" };