import React, { type ReactElement, useCallback, useMemo, useState } from "react"; import { ActivityIndicator, Alert, Text, View } from "react-native"; import * as Clipboard from "expo-clipboard"; import { useTranslation } from "react-i18next"; 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"; import { CODE_SURFACE_DATASET } from "@/styles/code-surface"; 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 { t } = useTranslation(); 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(t("desktop.daemon.fullStatus.fetchFailed", { message })); setIsCliStatusModalOpen(true); } finally { setIsLoadingCliStatus(false); } }, [t]); const handleCopyCliStatus = useCallback(() => { if (!cliStatusOutput) { return; } void Clipboard.setStringAsync(cliStatusOutput) .then(() => { Alert.alert(t("common.states.copied"), t("desktop.daemon.fullStatus.copied")); return; }) .catch((error) => { console.error("[Settings] Failed to copy daemon status", error); }); }, [cliStatusOutput, t]); const handleCloseCliStatusModal = useCallback(() => setIsCliStatusModalOpen(false), []); return { cliStatusOutput, isCliStatusModalOpen, isLoadingCliStatus, handleCopyCliStatus, handleOpenCliStatus, handleCloseCliStatusModal, }; } function useDaemonLogsModal(daemonLogs: { logPath?: string } | null) { const { t } = useTranslation(); const [isLogsModalOpen, setIsLogsModalOpen] = useState(false); const handleCopyLogPath = useCallback(() => { const logPath = daemonLogs?.logPath; if (!logPath) { return; } void Clipboard.setStringAsync(logPath) .then(() => { Alert.alert(t("common.states.copied"), t("desktop.daemon.logs.copied")); return; }) .catch((error) => { console.error("[Settings] Failed to copy log path", error); Alert.alert(t("common.errors.error"), t("desktop.daemon.logs.copyFailed")); }); }, [daemonLogs?.logPath, t]); 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) { const { t } = useTranslation(); const header = useMemo(() => ({ title: t("desktop.daemon.logs.modalTitle") }), [t]); return ( {daemonLogs?.logPath ?? t("desktop.daemon.logs.unavailable")} {daemonLogs?.contents?.length ? daemonLogs.contents : t("desktop.daemon.logs.empty")} ); } interface DaemonCliStatusModalProps { visible: boolean; onClose: () => void; cliStatusOutput: string | null; onCopy: () => void; } function DaemonCliStatusModal({ visible, onClose, cliStatusOutput, onCopy, }: DaemonCliStatusModalProps) { const { t } = useTranslation(); const header = useMemo( () => ({ title: t("desktop.daemon.fullStatus.modalTitle") }), [t], ); 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 { t } = useTranslation(); const { daemonStatusStateText, daemonStatusDetailText, isDaemonManagementPaused, copyIcon, fileTextIcon, activityIcon, handleToggleDaemonManagement, isUpdatingDaemonManagement, keepRunningAfterQuit, handleToggleKeepRunningAfterQuit, isUpdatingKeepRunningAfterQuit, daemonLogs, handleCopyLogPath, handleOpenLogs, handleRunCliStatus, isLoadingCliStatus, } = props; return ( {t("desktop.daemon.status.title")} {t("desktop.daemon.status.builtInOnly")} {daemonStatusStateText} {daemonStatusDetailText} {t("desktop.daemon.management.title")} {t("desktop.daemon.management.hint")} {t("desktop.daemon.keepRunning.title")} {t("desktop.daemon.keepRunning.hint")} {t("desktop.daemon.logs.title")} {daemonLogs?.logPath ?? t("desktop.daemon.logs.unavailable")} {daemonLogs?.logPath ? ( ) : null} {t("desktop.daemon.fullStatus.title")} {t("desktop.daemon.fullStatus.hint")} ); } export function LocalDaemonSection() { const { t } = useTranslation(); 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" ? t("desktop.daemon.status.running") : t("desktop.daemon.status.notRunning")); const daemonStatusDetailText = t("desktop.daemon.status.pid", { 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, t], ); if (!showSection) { return null; } return ( {isLoading || isLoadingSettings ? ( ) : ( <> {daemonVersionMismatch ? ( {t("desktop.daemon.versionMismatch")} ) : 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 LOGS_MODAL_SNAP_POINTS = ["70%", "92%"]; const CLI_STATUS_MODAL_SNAP_POINTS = ["60%", "85%"];