From 8ee9e7274dd185d98e50e1a311b52edbaed8483a Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Sun, 22 Feb 2026 13:28:14 +0700 Subject: [PATCH] Polish desktop settings update/local-daemon sections --- .../components/desktop-updates-section.tsx | 226 +++++------------- .../desktop/updates/desktop-updates.test.ts | 89 +++---- .../src/desktop/updates/desktop-updates.ts | 57 ++--- packages/app/src/screens/settings-screen.tsx | 123 +++++++++- packages/desktop/src-tauri/src/lib.rs | 61 +++++ 5 files changed, 297 insertions(+), 259 deletions(-) diff --git a/packages/app/src/desktop/components/desktop-updates-section.tsx b/packages/app/src/desktop/components/desktop-updates-section.tsx index ea0ebb408..848366d9c 100644 --- a/packages/app/src/desktop/components/desktop-updates-section.tsx +++ b/packages/app/src/desktop/components/desktop-updates-section.tsx @@ -1,114 +1,53 @@ -import { useCallback, useMemo, useState } from "react"; +import { useCallback, useState } from "react"; import { Alert, Text, View } from "react-native"; import * as Clipboard from "expo-clipboard"; import { useFocusEffect } from "@react-navigation/native"; import { StyleSheet } from "react-native-unistyles"; -import { useDaemonRegistry } from "@/contexts/daemon-registry-context"; import { Button } from "@/components/ui/button"; import { confirmDialog } from "@/utils/confirm-dialog"; -import { useSessionStore } from "@/stores/session-store"; -import { useDesktopAppUpdater } from "@/desktop/updates/use-desktop-app-updater"; import { buildDaemonUpdateDiagnostics, - findLikelyLocalDaemonHost, formatVersionWithPrefix, - isLikelyLocalHost, + getLocalDaemonVersion, isVersionMismatch, runLocalDaemonUpdate, shouldShowDesktopUpdateSection, } from "@/desktop/updates/desktop-updates"; -export interface DesktopUpdatesSectionProps { +export interface LocalDaemonSectionProps { appVersion: string | null; } -export function DesktopUpdatesSection({ appVersion }: DesktopUpdatesSectionProps) { - const showDesktopUpdatesSection = shouldShowDesktopUpdateSection(); - const { daemons } = useDaemonRegistry(); +export function LocalDaemonSection({ appVersion }: LocalDaemonSectionProps) { + const showSection = shouldShowDesktopUpdateSection(); + const [localDaemonVersion, setLocalDaemonVersion] = useState(null); + const [localDaemonVersionError, setLocalDaemonVersionError] = useState(null); const [isUpdatingLocalDaemon, setIsUpdatingLocalDaemon] = useState(false); const [localDaemonUpdateMessage, setLocalDaemonUpdateMessage] = useState(null); const [localDaemonUpdateDiagnostics, setLocalDaemonUpdateDiagnostics] = useState( null ); - const appVersionText = formatVersionWithPrefix(appVersion); - const localDaemonHost = useMemo(() => findLikelyLocalDaemonHost(daemons), [daemons]); - const hasLikelyLocalDaemon = isLikelyLocalHost(localDaemonHost); - const localDaemonServerId = localDaemonHost?.serverId ?? null; - const localDaemonVersion = useSessionStore( - useCallback( - (state) => - localDaemonServerId - ? (state.sessions[localDaemonServerId]?.serverInfo?.version ?? null) - : null, - [localDaemonServerId] - ) - ); - const localDaemonVersionText = formatVersionWithPrefix(localDaemonVersion); - const daemonVersionMismatch = isVersionMismatch(appVersion, localDaemonVersion); - const daemonVersionTitle = hasLikelyLocalDaemon - ? "Local daemon version" - : "Connected daemon version"; - const daemonHostHint = localDaemonHost - ? hasLikelyLocalDaemon - ? `Connected host: ${localDaemonHost.label}` - : `No local daemon detected. Showing connected host: ${localDaemonHost.label}.` - : "No local daemon detected yet."; - - const { - isDesktop: isDesktopUpdaterAvailable, - statusText: appUpdateStatusText, - availableUpdate, - errorMessage: appUpdateError, - isChecking: isCheckingAppUpdate, - isInstalling: isInstallingAppUpdate, - checkForUpdates, - installUpdate, - } = useDesktopAppUpdater(); - useFocusEffect( useCallback(() => { - if (!showDesktopUpdatesSection || !isDesktopUpdaterAvailable) { + if (!showSection) { return undefined; } - void checkForUpdates({ silent: true }); + void getLocalDaemonVersion().then((result) => { + setLocalDaemonVersion(result.version); + setLocalDaemonVersionError(result.error); + }); return undefined; - }, [checkForUpdates, isDesktopUpdaterAvailable, showDesktopUpdatesSection]) + }, [showSection]) ); - const handleCheckForAppUpdates = useCallback(() => { - if (!showDesktopUpdatesSection || !isDesktopUpdaterAvailable) { - return; - } - void checkForUpdates(); - }, [checkForUpdates, isDesktopUpdaterAvailable, showDesktopUpdatesSection]); - - const handleInstallAppUpdate = useCallback(() => { - if (!showDesktopUpdatesSection || !isDesktopUpdaterAvailable) { - 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, isDesktopUpdaterAvailable, showDesktopUpdatesSection]); + const localDaemonVersionText = formatVersionWithPrefix(localDaemonVersion); + const daemonVersionMismatch = isVersionMismatch(appVersion, localDaemonVersion); + const daemonVersionHint = localDaemonVersionError ?? "Daemon installed on this computer."; const handleUpdateLocalDaemon = useCallback(() => { - if (!showDesktopUpdatesSection || !isDesktopUpdaterAvailable) { + if (!showSection) { return; } if (isUpdatingLocalDaemon) { @@ -148,6 +87,11 @@ export function DesktopUpdatesSection({ appVersion }: DesktopUpdatesSectionProps if (result.stdout.trim().length > 0 || result.stderr.trim().length > 0) { setLocalDaemonUpdateDiagnostics(diagnostics); } + + void getLocalDaemonVersion().then((versionResult) => { + setLocalDaemonVersion(versionResult.version); + setLocalDaemonVersionError(versionResult.error); + }); }) .catch((error) => { console.error("[Settings] Failed to update local daemon", error); @@ -171,7 +115,7 @@ export function DesktopUpdatesSection({ appVersion }: DesktopUpdatesSectionProps console.error("[Settings] Failed to open daemon update confirmation", error); Alert.alert("Error", "Unable to open the daemon update confirmation dialog."); }); - }, [isDesktopUpdaterAvailable, isUpdatingLocalDaemon, showDesktopUpdatesSection]); + }, [isUpdatingLocalDaemon, showSection]); const handleCopyDaemonDiagnostics = useCallback(() => { if (!localDaemonUpdateDiagnostics) { @@ -188,82 +132,36 @@ export function DesktopUpdatesSection({ appVersion }: DesktopUpdatesSectionProps }); }, [localDaemonUpdateDiagnostics]); - if (!showDesktopUpdatesSection) { + if (!showSection) { return null; } return ( - Updates - - - - Desktop app version - Paseo installed on this computer. + Local daemon + + + + Version + {daemonVersionHint} - {appVersionText} + {localDaemonVersionText} - - - {daemonVersionTitle} - {daemonHostHint} - - {localDaemonVersionText} - - - - App update status - {appUpdateStatusText} - {availableUpdate?.latestVersion ? ( - - New version available: {formatVersionWithPrefix(availableUpdate.latestVersion)} - - ) : null} - {appUpdateError ? {appUpdateError} : null} - - - - - - - - - Update local daemon - + + + Update daemon + Updates the daemon on this computer only. Requires a restart. {localDaemonUpdateMessage ? ( - {localDaemonUpdateMessage} + {localDaemonUpdateMessage} ) : null} @@ -271,8 +169,8 @@ export function DesktopUpdatesSection({ appVersion }: DesktopUpdatesSectionProps {daemonVersionMismatch ? ( - - + + Desktop app and local daemon versions differ. Keep both on the same version to avoid stability issues or breaking changes. @@ -280,14 +178,14 @@ export function DesktopUpdatesSection({ appVersion }: DesktopUpdatesSectionProps ) : null} {localDaemonUpdateDiagnostics ? ( - - - Daemon update diagnostics + + + Daemon update diagnostics - + {localDaemonUpdateDiagnostics} @@ -307,58 +205,48 @@ const styles = StyleSheet.create((theme) => ({ marginBottom: theme.spacing[3], marginLeft: theme.spacing[1], }, - audioCard: { + card: { backgroundColor: theme.colors.surface2, borderRadius: theme.borderRadius.lg, borderWidth: 1, borderColor: theme.colors.border, overflow: "hidden", }, - audioRow: { + row: { flexDirection: "row", alignItems: "center", justifyContent: "space-between", paddingVertical: theme.spacing[4], paddingHorizontal: theme.spacing[4], }, - audioRowBorder: { + rowBorder: { borderTopWidth: 1, borderTopColor: theme.colors.border, }, - audioRowContent: { + rowContent: { flex: 1, marginRight: theme.spacing[3], }, - audioRowTitle: { + rowTitle: { color: theme.colors.foreground, fontSize: theme.fontSize.base, }, - aboutValue: { + valueText: { color: theme.colors.foregroundMuted, fontSize: theme.fontSize.sm, fontWeight: theme.fontWeight.normal, }, - updateHintText: { + hintText: { color: theme.colors.foregroundMuted, fontSize: theme.fontSize.xs, marginTop: 2, }, - updateStatusText: { + statusText: { color: theme.colors.foregroundMuted, fontSize: theme.fontSize.xs, marginTop: theme.spacing[1], }, - updateActions: { - flexDirection: "row", - alignItems: "center", - gap: theme.spacing[2], - }, - updateErrorText: { - color: theme.colors.palette.red[300], - fontSize: theme.fontSize.xs, - marginTop: theme.spacing[1], - }, - updateWarningCard: { + warningCard: { marginTop: theme.spacing[3], borderRadius: theme.borderRadius.lg, borderWidth: 1, @@ -367,11 +255,11 @@ const styles = StyleSheet.create((theme) => ({ paddingVertical: theme.spacing[2], paddingHorizontal: theme.spacing[3], }, - updateWarningText: { + warningText: { color: theme.colors.palette.amber[500], fontSize: theme.fontSize.xs, }, - updateDiagnosticsCard: { + diagnosticsCard: { marginTop: theme.spacing[3], borderRadius: theme.borderRadius.lg, borderWidth: 1, @@ -380,17 +268,17 @@ const styles = StyleSheet.create((theme) => ({ padding: theme.spacing[3], gap: theme.spacing[2], }, - updateDiagnosticsHeader: { + diagnosticsHeader: { flexDirection: "row", alignItems: "center", justifyContent: "space-between", gap: theme.spacing[2], }, - updateDiagnosticsTitle: { + diagnosticsTitle: { color: theme.colors.foreground, fontSize: theme.fontSize.sm, }, - updateDiagnosticsText: { + diagnosticsText: { color: theme.colors.foregroundMuted, fontSize: theme.fontSize.xs, fontFamily: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace", diff --git a/packages/app/src/desktop/updates/desktop-updates.test.ts b/packages/app/src/desktop/updates/desktop-updates.test.ts index 0ac433a9e..de69ac57e 100644 --- a/packages/app/src/desktop/updates/desktop-updates.test.ts +++ b/packages/app/src/desktop/updates/desktop-updates.test.ts @@ -1,5 +1,4 @@ import { afterEach, describe, expect, it, vi } from 'vitest' -import type { HostProfile } from '@/contexts/daemon-registry-context' async function loadModuleForPlatform(platform: 'web' | 'ios' | 'android') { vi.resetModules() @@ -7,47 +6,6 @@ async function loadModuleForPlatform(platform: 'web' | 'ios' | 'android') { return import('./desktop-updates') } -function createHost(input: { - serverId: string - endpoint: string - type?: 'direct' | 'relay' -}): HostProfile { - const now = new Date(0).toISOString() - - if (input.type === 'relay') { - return { - serverId: input.serverId, - label: input.serverId, - connections: [ - { - id: `${input.serverId}-relay`, - type: 'relay', - relayEndpoint: input.endpoint, - daemonPublicKeyB64: 'test-key', - }, - ], - preferredConnectionId: `${input.serverId}-relay`, - createdAt: now, - updatedAt: now, - } - } - - return { - serverId: input.serverId, - label: input.serverId, - connections: [ - { - id: `${input.serverId}-direct`, - type: 'direct', - endpoint: input.endpoint, - }, - ], - preferredConnectionId: `${input.serverId}-direct`, - createdAt: now, - updatedAt: now, - } -} - describe('desktop-updates helpers', () => { afterEach(() => { vi.doUnmock('react-native') @@ -79,20 +37,47 @@ describe('desktop-updates helpers', () => { expect(formatVersionWithPrefix(null)).toBe('\u2014') }) - it('prefers localhost direct host when selecting local daemon', async () => { - const { findLikelyLocalDaemonHost } = await loadModuleForPlatform('web') - const remote = createHost({ serverId: 'remote', endpoint: '10.0.0.2:6767' }) - const local = createHost({ serverId: 'local', endpoint: 'localhost:6767' }) + it('parses valid local daemon version result', async () => { + const { parseLocalDaemonVersionResult } = await loadModuleForPlatform('web') - expect(findLikelyLocalDaemonHost([remote, local])?.serverId).toBe('local') + expect(parseLocalDaemonVersionResult({ version: '0.1.15', error: null })).toEqual({ + version: '0.1.15', + error: null, + }) }) - it('falls back to first host when no localhost endpoint exists', async () => { - const { findLikelyLocalDaemonHost } = await loadModuleForPlatform('web') - const relay = createHost({ serverId: 'relay', endpoint: 'relay.paseo.sh:443', type: 'relay' }) - const remote = createHost({ serverId: 'remote', endpoint: '10.0.0.2:6767' }) + it('parses local daemon version error result', async () => { + const { parseLocalDaemonVersionResult } = await loadModuleForPlatform('web') - expect(findLikelyLocalDaemonHost([relay, remote])?.serverId).toBe('relay') + expect( + parseLocalDaemonVersionResult({ version: null, error: 'paseo command not found in PATH' }) + ).toEqual({ + version: null, + error: 'paseo command not found in PATH', + }) + }) + + it('parses unexpected local daemon version result', async () => { + const { parseLocalDaemonVersionResult } = await loadModuleForPlatform('web') + + expect(parseLocalDaemonVersionResult(null)).toEqual({ + version: null, + error: 'Unexpected response from version check.', + }) + + expect(parseLocalDaemonVersionResult('not an object')).toEqual({ + version: null, + error: 'Unexpected response from version check.', + }) + }) + + it('trims whitespace in parsed version', async () => { + const { parseLocalDaemonVersionResult } = await loadModuleForPlatform('web') + + expect(parseLocalDaemonVersionResult({ version: ' 0.1.15 ', error: null })).toEqual({ + version: '0.1.15', + error: null, + }) }) it('builds copyable daemon update diagnostics', async () => { diff --git a/packages/app/src/desktop/updates/desktop-updates.ts b/packages/app/src/desktop/updates/desktop-updates.ts index 0610fd672..28546c68f 100644 --- a/packages/app/src/desktop/updates/desktop-updates.ts +++ b/packages/app/src/desktop/updates/desktop-updates.ts @@ -1,5 +1,4 @@ import { Platform } from 'react-native' -import type { HostProfile } from '@/contexts/daemon-registry-context' import { getTauri } from '@/utils/tauri' import { invokeDesktopCommand } from '@/desktop/tauri/invoke-desktop-command' @@ -23,6 +22,11 @@ export interface LocalDaemonUpdateResult { stderr: string } +export interface LocalDaemonVersionResult { + version: string | null + error: string | null +} + function isRecord(value: unknown): value is Record { return typeof value === 'object' && value !== null } @@ -44,24 +48,26 @@ function toNumberOr(defaultValue: number, value: unknown): number { return typeof value === 'number' && Number.isFinite(value) ? value : defaultValue } -function isLikelyLocalDirectEndpoint(endpoint: string): boolean { - const normalized = endpoint.trim().toLowerCase() - return ( - normalized === 'localhost' || - normalized.startsWith('localhost:') || - normalized === '127.0.0.1' || - normalized.startsWith('127.0.0.1:') || - normalized === '::1' || - normalized.startsWith('::1:') || - normalized === '[::1]' || - normalized.startsWith('[::1]:') - ) -} - export function shouldShowDesktopUpdateSection(): boolean { return Platform.OS === 'web' && getTauri() !== null } +export function parseLocalDaemonVersionResult(raw: unknown): LocalDaemonVersionResult { + if (!isRecord(raw)) { + return { version: null, error: 'Unexpected response from version check.' } + } + + return { + version: toStringOrNull(raw.version), + error: toStringOrNull(raw.error), + } +} + +export async function getLocalDaemonVersion(): Promise { + const result = await invokeDesktopCommand('get_local_daemon_version') + return parseLocalDaemonVersionResult(result) +} + export async function checkDesktopAppUpdate(): Promise { const result = await invokeDesktopCommand('check_app_update') if (!isRecord(result)) { @@ -135,27 +141,6 @@ export function formatVersionWithPrefix(version: string | null | undefined): str return value.startsWith('v') ? value : `v${value}` } -export function findLikelyLocalDaemonHost(daemons: HostProfile[]): HostProfile | null { - if (daemons.length === 0) { - return null - } - - const localhostDaemon = daemons.find((daemon) => isLikelyLocalHost(daemon)) - - return localhostDaemon ?? daemons[0] ?? null -} - -export function isLikelyLocalHost(host: HostProfile | null | undefined): boolean { - if (!host) { - return false - } - - return host.connections.some( - (connection) => - connection.type === 'direct' && isLikelyLocalDirectEndpoint(connection.endpoint) - ) -} - export function buildDaemonUpdateDiagnostics(result: LocalDaemonUpdateResult): string { const stdout = result.stdout.length > 0 ? result.stdout : '(empty)' const stderr = result.stderr.length > 0 ? result.stderr : '(empty)' diff --git a/packages/app/src/screens/settings-screen.tsx b/packages/app/src/screens/settings-screen.tsx index c61a5cae6..d35052baf 100644 --- a/packages/app/src/screens/settings-screen.tsx +++ b/packages/app/src/screens/settings-screen.tsx @@ -9,7 +9,9 @@ import { Platform, } from "react-native"; import { router, useLocalSearchParams } from "expo-router"; +import { useFocusEffect } from "@react-navigation/native"; import Constants from "expo-constants"; +import appPackage from "../../package.json"; 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"; @@ -38,7 +40,9 @@ import { } from "@/components/ui/dropdown-menu"; import { AdaptiveModalSheet, AdaptiveTextInput } from "@/components/adaptive-modal-sheet"; import { DesktopPermissionsSection } from "@/desktop/components/desktop-permissions-section"; -import { DesktopUpdatesSection } from "@/desktop/components/desktop-updates-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"; const delay = (ms: number) => new Promise((resolve) => { @@ -319,6 +323,21 @@ const styles = StyleSheet.create((theme) => ({ 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: { backgroundColor: theme.colors.surface2, @@ -367,6 +386,11 @@ const styles = StyleSheet.create((theme) => ({ })); function resolveAppVersion(): string | null { + const packageVersion = appPackage?.version; + if (typeof packageVersion === "string" && packageVersion.trim().length > 0) { + return packageVersion.trim(); + } + const expoVersion = Constants.expoConfig?.version; if (typeof expoVersion === "string" && expoVersion.trim().length > 0) { return expoVersion.trim(); @@ -404,6 +428,100 @@ function formatVersionForDisplay(version: string | null | undefined): string { return `v${value}`; } +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(); @@ -791,7 +909,7 @@ export default function SettingsScreen() { {isDesktop ? : null} - {isDesktop ? : null} + {isDesktop ? : null} {/* About */} @@ -803,6 +921,7 @@ export default function SettingsScreen() { {appVersionText} + {isDesktop ? : null} diff --git a/packages/desktop/src-tauri/src/lib.rs b/packages/desktop/src-tauri/src/lib.rs index f583be8e5..c11d82b16 100644 --- a/packages/desktop/src-tauri/src/lib.rs +++ b/packages/desktop/src-tauri/src/lib.rs @@ -46,6 +46,13 @@ struct AppUpdateInstallResult { message: String, } +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +struct LocalDaemonVersionResult { + version: Option, + error: Option, +} + fn resolve_login_shell() -> String { std::env::var("SHELL") .ok() @@ -54,6 +61,48 @@ fn resolve_login_shell() -> String { .unwrap_or_else(|| "/bin/zsh".to_string()) } +fn execute_local_daemon_version(shell: &str) -> LocalDaemonVersionResult { + let script = r#"if command -v paseo >/dev/null 2>&1; then + paseo --version +else + echo "paseo command not found in PATH" >&2 + exit 127 +fi"#; + + match Command::new(shell).arg("-lc").arg(script).output() { + Ok(output) => { + if output.status.success() { + let version = String::from_utf8_lossy(&output.stdout).trim().to_string(); + if version.is_empty() { + LocalDaemonVersionResult { + version: None, + error: Some("paseo --version returned empty output".to_string()), + } + } else { + LocalDaemonVersionResult { + version: Some(version), + error: None, + } + } + } else { + let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string(); + LocalDaemonVersionResult { + version: None, + error: Some(if stderr.is_empty() { + format!("paseo --version exited with code {}", output.status.code().unwrap_or(1)) + } else { + stderr + }), + } + } + } + Err(error) => LocalDaemonVersionResult { + version: None, + error: Some(format!("Failed to run version check: {error}")), + }, + } +} + fn execute_local_daemon_update(shell: &str) -> DaemonUpdateCommandResult { let script = r#"if command -v paseo >/dev/null 2>&1; then paseo daemon update @@ -76,6 +125,17 @@ fi"#; } } +#[tauri::command] +async fn get_local_daemon_version() -> LocalDaemonVersionResult { + let shell = resolve_login_shell(); + tauri::async_runtime::spawn_blocking(move || execute_local_daemon_version(&shell)) + .await + .unwrap_or_else(|error| LocalDaemonVersionResult { + version: None, + error: Some(format!("Version check task failed: {error}")), + }) +} + #[tauri::command] async fn run_local_daemon_update() -> DaemonUpdateCommandResult { let shell = resolve_login_shell(); @@ -158,6 +218,7 @@ pub fn run() { .plugin(tauri_plugin_updater::Builder::new().build()) .plugin(tauri_plugin_websocket::init()) .invoke_handler(tauri::generate_handler![ + get_local_daemon_version, run_local_daemon_update, check_app_update, install_app_update