* Wrap desktop IPC calls in shared hooks * Unslop desktop IPC hook callsites * Redesign desktop IPC hooks around domains
This commit is contained in:
@@ -8,87 +8,16 @@ import { ArrowUpRight, Copy, FileText, Activity } from "lucide-react-native";
|
|||||||
import { AdaptiveModalSheet } from "@/components/adaptive-modal-sheet";
|
import { AdaptiveModalSheet } from "@/components/adaptive-modal-sheet";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
import { confirmDialog } from "@/utils/confirm-dialog";
|
|
||||||
import { openExternalUrl } from "@/utils/open-external-url";
|
import { openExternalUrl } from "@/utils/open-external-url";
|
||||||
import { isVersionMismatch } from "@/desktop/updates/desktop-updates";
|
import { isVersionMismatch } from "@/desktop/updates/desktop-updates";
|
||||||
import {
|
import { getCliDaemonStatus, shouldUseDesktopDaemon } from "@/desktop/daemon/desktop-daemon";
|
||||||
getCliDaemonStatus,
|
import { useBuiltInDaemonManagement } from "@/desktop/hooks/use-built-in-daemon-management";
|
||||||
shouldUseDesktopDaemon,
|
|
||||||
startDesktopDaemon,
|
|
||||||
stopDesktopDaemon,
|
|
||||||
} from "@/desktop/daemon/desktop-daemon";
|
|
||||||
import { executeDaemonManagementToggle } from "@/desktop/daemon/daemon-management-toggle";
|
|
||||||
import { useDaemonStatus } from "@/desktop/hooks/use-daemon-status";
|
import { useDaemonStatus } from "@/desktop/hooks/use-daemon-status";
|
||||||
import { useDesktopSettings, type DesktopSettings } from "@/desktop/settings/desktop-settings";
|
import { useDesktopSettings, type DesktopSettings } from "@/desktop/settings/desktop-settings";
|
||||||
import type { DesktopDaemonStatus } from "@/desktop/daemon/desktop-daemon";
|
|
||||||
import { resolveAppVersion } from "@/utils/app-version";
|
import { resolveAppVersion } from "@/utils/app-version";
|
||||||
|
|
||||||
type DesktopDaemonSettings = DesktopSettings["daemon"];
|
type DesktopDaemonSettings = DesktopSettings["daemon"];
|
||||||
|
|
||||||
function useDaemonManagementToggle(args: {
|
|
||||||
daemonStatus: DesktopDaemonStatus | null;
|
|
||||||
settings: DesktopDaemonSettings;
|
|
||||||
updateSettings: (next: Partial<DesktopDaemonSettings>) => Promise<unknown>;
|
|
||||||
setStatus: (status: DesktopDaemonStatus) => void;
|
|
||||||
refetch: () => void;
|
|
||||||
}) {
|
|
||||||
const { daemonStatus, settings, updateSettings, setStatus, refetch } = args;
|
|
||||||
const [isUpdatingDaemonManagement, setIsUpdatingDaemonManagement] = useState(false);
|
|
||||||
|
|
||||||
const handleToggleDaemonManagement = useCallback(() => {
|
|
||||||
if (isUpdatingDaemonManagement) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsUpdatingDaemonManagement(true);
|
|
||||||
void executeDaemonManagementToggle(settings.manageBuiltInDaemon, daemonStatus, {
|
|
||||||
confirm: () =>
|
|
||||||
confirmDialog({
|
|
||||||
title: "Pause built-in daemon",
|
|
||||||
message:
|
|
||||||
"This will stop the built-in daemon immediately. Running agents and terminals connected to the built-in daemon will be stopped.",
|
|
||||||
confirmLabel: "Pause and stop",
|
|
||||||
cancelLabel: "Cancel",
|
|
||||||
destructive: true,
|
|
||||||
}),
|
|
||||||
persistSettings: (next) => updateSettings(next) as Promise<void>,
|
|
||||||
startDaemon: startDesktopDaemon,
|
|
||||||
stopDaemon: stopDesktopDaemon,
|
|
||||||
})
|
|
||||||
.then((result) => {
|
|
||||||
if (result.kind === "cancelled") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (result.newStatus) {
|
|
||||||
setStatus(result.newStatus);
|
|
||||||
}
|
|
||||||
refetch();
|
|
||||||
return;
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error("[Settings] Failed to update built-in daemon management", error);
|
|
||||||
Alert.alert(
|
|
||||||
"Error",
|
|
||||||
settings.manageBuiltInDaemon
|
|
||||||
? "Built-in daemon management was paused, but Paseo could not stop the daemon."
|
|
||||||
: "Unable to update built-in daemon management.",
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setIsUpdatingDaemonManagement(false);
|
|
||||||
});
|
|
||||||
}, [
|
|
||||||
daemonStatus,
|
|
||||||
isUpdatingDaemonManagement,
|
|
||||||
refetch,
|
|
||||||
setStatus,
|
|
||||||
settings.manageBuiltInDaemon,
|
|
||||||
updateSettings,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return { isUpdatingDaemonManagement, handleToggleDaemonManagement };
|
|
||||||
}
|
|
||||||
|
|
||||||
function useKeepRunningAfterQuitToggle(args: {
|
function useKeepRunningAfterQuitToggle(args: {
|
||||||
settings: DesktopDaemonSettings;
|
settings: DesktopDaemonSettings;
|
||||||
updateSettings: (next: Partial<DesktopDaemonSettings>) => Promise<unknown>;
|
updateSettings: (next: Partial<DesktopDaemonSettings>) => Promise<unknown>;
|
||||||
@@ -99,9 +28,8 @@ function useKeepRunningAfterQuitToggle(args: {
|
|||||||
const handleToggleKeepRunningAfterQuit = useCallback(() => {
|
const handleToggleKeepRunningAfterQuit = useCallback(() => {
|
||||||
setIsUpdatingKeepRunningAfterQuit(true);
|
setIsUpdatingKeepRunningAfterQuit(true);
|
||||||
void updateSettings({ keepRunningAfterQuit: !settings.keepRunningAfterQuit })
|
void updateSettings({ keepRunningAfterQuit: !settings.keepRunningAfterQuit })
|
||||||
.catch((error) => {
|
.catch(() => {
|
||||||
console.error("[Settings] Failed to update desktop quit daemon behavior", error);
|
// useDesktopSettings owns the user-visible IPC error.
|
||||||
Alert.alert("Error", "Unable to update the desktop quit daemon behavior.");
|
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
setIsUpdatingKeepRunningAfterQuit(false);
|
setIsUpdatingKeepRunningAfterQuit(false);
|
||||||
@@ -393,13 +321,14 @@ export function LocalDaemonSection() {
|
|||||||
const daemonStatusDetailText = `PID ${daemonStatus?.pid ? daemonStatus.pid : "—"}`;
|
const daemonStatusDetailText = `PID ${daemonStatus?.pid ? daemonStatus.pid : "—"}`;
|
||||||
const isDaemonManagementPaused = !daemonSettings.manageBuiltInDaemon;
|
const isDaemonManagementPaused = !daemonSettings.manageBuiltInDaemon;
|
||||||
|
|
||||||
const { isUpdatingDaemonManagement, handleToggleDaemonManagement } = useDaemonManagementToggle({
|
const { isUpdating: isUpdatingDaemonManagement, toggle: handleToggleDaemonManagement } =
|
||||||
daemonStatus,
|
useBuiltInDaemonManagement({
|
||||||
settings: daemonSettings,
|
daemonStatus,
|
||||||
updateSettings: updateDaemonSettings,
|
settings: daemonSettings,
|
||||||
setStatus,
|
updateSettings: updateDaemonSettings,
|
||||||
refetch,
|
setStatus,
|
||||||
});
|
refreshStatus: refetch,
|
||||||
|
});
|
||||||
const { isUpdatingKeepRunningAfterQuit, handleToggleKeepRunningAfterQuit } =
|
const { isUpdatingKeepRunningAfterQuit, handleToggleKeepRunningAfterQuit } =
|
||||||
useKeepRunningAfterQuitToggle({
|
useKeepRunningAfterQuitToggle({
|
||||||
settings: daemonSettings,
|
settings: daemonSettings,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useMemo, useState } from "react";
|
import { useCallback, useMemo } from "react";
|
||||||
import { Text, View } from "react-native";
|
import { Text, View } from "react-native";
|
||||||
import { useFocusEffect } from "@react-navigation/native";
|
import { useFocusEffect } from "@react-navigation/native";
|
||||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||||
@@ -7,14 +7,8 @@ import { settingsStyles } from "@/styles/settings";
|
|||||||
import { SettingsSection } from "@/screens/settings/settings-section";
|
import { SettingsSection } from "@/screens/settings/settings-section";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { openExternalUrl } from "@/utils/open-external-url";
|
import { openExternalUrl } from "@/utils/open-external-url";
|
||||||
import {
|
import { shouldUseDesktopDaemon } from "@/desktop/daemon/desktop-daemon";
|
||||||
shouldUseDesktopDaemon,
|
import { useCliInstall, useSkillsInstall } from "@/desktop/hooks/use-install-status";
|
||||||
getCliInstallStatus,
|
|
||||||
installCli,
|
|
||||||
getSkillsInstallStatus,
|
|
||||||
installSkills,
|
|
||||||
type InstallStatus,
|
|
||||||
} from "@/desktop/daemon/desktop-daemon";
|
|
||||||
|
|
||||||
const CLI_DOCS_URL = "https://paseo.sh/docs/cli";
|
const CLI_DOCS_URL = "https://paseo.sh/docs/cli";
|
||||||
const SKILLS_DOCS_URL = "https://paseo.sh/docs/skills";
|
const SKILLS_DOCS_URL = "https://paseo.sh/docs/skills";
|
||||||
@@ -23,59 +17,37 @@ const ROW_WITH_BORDER_STYLE = [settingsStyles.row, settingsStyles.rowBorder];
|
|||||||
export function IntegrationsSection() {
|
export function IntegrationsSection() {
|
||||||
const { theme } = useUnistyles();
|
const { theme } = useUnistyles();
|
||||||
const showSection = shouldUseDesktopDaemon();
|
const showSection = shouldUseDesktopDaemon();
|
||||||
|
const {
|
||||||
const [cliStatus, setCliStatus] = useState<InstallStatus | null>(null);
|
status: cliStatus,
|
||||||
const [skillsStatus, setSkillsStatus] = useState<InstallStatus | null>(null);
|
isInstalling: isInstallingCli,
|
||||||
const [isInstallingCli, setIsInstallingCli] = useState(false);
|
install: installCli,
|
||||||
const [isInstallingSkills, setIsInstallingSkills] = useState(false);
|
refresh: refreshCliStatus,
|
||||||
|
} = useCliInstall();
|
||||||
const loadStatus = useCallback(() => {
|
const {
|
||||||
if (!showSection) return;
|
status: skillsStatus,
|
||||||
void getCliInstallStatus()
|
isInstalling: isInstallingSkills,
|
||||||
.then(setCliStatus)
|
install: installSkills,
|
||||||
.catch((error) => {
|
refresh: refreshSkillsStatus,
|
||||||
console.error("[Integrations] Failed to load CLI status", error);
|
} = useSkillsInstall();
|
||||||
});
|
|
||||||
void getSkillsInstallStatus()
|
|
||||||
.then(setSkillsStatus)
|
|
||||||
.catch((error) => {
|
|
||||||
console.error("[Integrations] Failed to load skills status", error);
|
|
||||||
});
|
|
||||||
}, [showSection]);
|
|
||||||
|
|
||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
if (!showSection) return undefined;
|
if (!showSection) return undefined;
|
||||||
loadStatus();
|
refreshCliStatus();
|
||||||
|
refreshSkillsStatus();
|
||||||
return undefined;
|
return undefined;
|
||||||
}, [loadStatus, showSection]),
|
}, [refreshCliStatus, refreshSkillsStatus, showSection]),
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleInstallCli = useCallback(() => {
|
const handleInstallCli = useCallback(() => {
|
||||||
if (isInstallingCli) return;
|
if (isInstallingCli) return;
|
||||||
setIsInstallingCli(true);
|
installCli();
|
||||||
void installCli()
|
}, [installCli, isInstallingCli]);
|
||||||
.then(setCliStatus)
|
|
||||||
.catch((error) => {
|
|
||||||
console.error("[Integrations] Failed to install CLI", error);
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setIsInstallingCli(false);
|
|
||||||
});
|
|
||||||
}, [isInstallingCli]);
|
|
||||||
|
|
||||||
const handleInstallSkills = useCallback(() => {
|
const handleInstallSkills = useCallback(() => {
|
||||||
if (isInstallingSkills) return;
|
if (isInstallingSkills) return;
|
||||||
setIsInstallingSkills(true);
|
installSkills();
|
||||||
void installSkills()
|
}, [installSkills, isInstallingSkills]);
|
||||||
.then(setSkillsStatus)
|
|
||||||
.catch((error) => {
|
|
||||||
console.error("[Integrations] Failed to install skills", error);
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setIsInstallingSkills(false);
|
|
||||||
});
|
|
||||||
}, [isInstallingSkills]);
|
|
||||||
|
|
||||||
const handleOpenCliDocs = useCallback(() => {
|
const handleOpenCliDocs = useCallback(() => {
|
||||||
void openExternalUrl(CLI_DOCS_URL);
|
void openExternalUrl(CLI_DOCS_URL);
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import { useCallback, useEffect, useRef } from "react";
|
||||||
|
import type { ToastApi } from "@/components/toast-host";
|
||||||
|
import { useToast } from "@/contexts/toast-context";
|
||||||
|
|
||||||
|
interface DesktopIpcErrorReport {
|
||||||
|
toast: ToastApi;
|
||||||
|
logLabel: string;
|
||||||
|
message: string;
|
||||||
|
error: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DesktopIpcQueryErrorToastOptions {
|
||||||
|
error: Error | null;
|
||||||
|
logLabel: string;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DesktopIpcErrorReporterInput {
|
||||||
|
logLabel: string;
|
||||||
|
message: string;
|
||||||
|
error: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function reportDesktopIpcError(input: DesktopIpcErrorReport): void {
|
||||||
|
console.error(input.logLabel, input.error);
|
||||||
|
input.toast.error(input.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDesktopIpcErrorReporter(): (input: DesktopIpcErrorReporterInput) => void {
|
||||||
|
const toast = useToast();
|
||||||
|
return useCallback(
|
||||||
|
(input: DesktopIpcErrorReporterInput) => {
|
||||||
|
reportDesktopIpcError({ ...input, toast });
|
||||||
|
},
|
||||||
|
[toast],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDesktopIpcQueryErrorToast(options: DesktopIpcQueryErrorToastOptions): void {
|
||||||
|
const toast = useToast();
|
||||||
|
const lastReportedErrorRef = useRef<Error | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!options.error || options.error === lastReportedErrorRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lastReportedErrorRef.current = options.error;
|
||||||
|
reportDesktopIpcError({
|
||||||
|
toast,
|
||||||
|
logLabel: options.logLabel,
|
||||||
|
message: options.message,
|
||||||
|
error: options.error,
|
||||||
|
});
|
||||||
|
}, [options.error, options.logLabel, options.message, toast]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
import { useCallback } from "react";
|
||||||
|
import { useMutation } from "@tanstack/react-query";
|
||||||
|
import {
|
||||||
|
type DesktopDaemonStatus,
|
||||||
|
startDesktopDaemon,
|
||||||
|
stopDesktopDaemon,
|
||||||
|
} from "@/desktop/daemon/desktop-daemon";
|
||||||
|
import {
|
||||||
|
executeDaemonManagementToggle,
|
||||||
|
type DaemonManagementToggleResult,
|
||||||
|
} from "@/desktop/daemon/daemon-management-toggle";
|
||||||
|
import { useDesktopIpcErrorReporter } from "@/desktop/hooks/desktop-ipc-error";
|
||||||
|
import type { DesktopSettings } from "@/desktop/settings/desktop-settings";
|
||||||
|
import { confirmDialog } from "@/utils/confirm-dialog";
|
||||||
|
|
||||||
|
type DesktopDaemonSettings = DesktopSettings["daemon"];
|
||||||
|
|
||||||
|
interface UseBuiltInDaemonManagementInput {
|
||||||
|
daemonStatus: DesktopDaemonStatus | null;
|
||||||
|
settings: DesktopDaemonSettings;
|
||||||
|
updateSettings: (next: Partial<DesktopDaemonSettings>) => Promise<unknown>;
|
||||||
|
setStatus: (status: DesktopDaemonStatus) => void;
|
||||||
|
refreshStatus: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UseBuiltInDaemonManagementResult {
|
||||||
|
isUpdating: boolean;
|
||||||
|
toggle: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useBuiltInDaemonManagement(
|
||||||
|
input: UseBuiltInDaemonManagementInput,
|
||||||
|
): UseBuiltInDaemonManagementResult {
|
||||||
|
const { daemonStatus, settings, updateSettings, setStatus, refreshStatus } = input;
|
||||||
|
const reportError = useDesktopIpcErrorReporter();
|
||||||
|
const { mutate: toggleDaemonManagement, isPending: isUpdating } = useMutation<
|
||||||
|
DaemonManagementToggleResult,
|
||||||
|
Error
|
||||||
|
>({
|
||||||
|
mutationFn: () =>
|
||||||
|
executeDaemonManagementToggle(settings.manageBuiltInDaemon, daemonStatus, {
|
||||||
|
confirm: () =>
|
||||||
|
confirmDialog({
|
||||||
|
title: "Pause built-in daemon",
|
||||||
|
message:
|
||||||
|
"This will stop the built-in daemon immediately. Running agents and terminals connected to the built-in daemon will be stopped.",
|
||||||
|
confirmLabel: "Pause and stop",
|
||||||
|
cancelLabel: "Cancel",
|
||||||
|
destructive: true,
|
||||||
|
}),
|
||||||
|
persistSettings: (next) => updateSettings(next) as Promise<void>,
|
||||||
|
startDaemon: startDesktopDaemon,
|
||||||
|
stopDaemon: stopDesktopDaemon,
|
||||||
|
}),
|
||||||
|
onError: (error) => {
|
||||||
|
reportError({
|
||||||
|
error,
|
||||||
|
message: settings.manageBuiltInDaemon
|
||||||
|
? "Built-in daemon management was paused, but Paseo could not stop the daemon."
|
||||||
|
: "Unable to update built-in daemon management.",
|
||||||
|
logLabel: "[Settings] Failed to update built-in daemon management",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onSuccess: (result) => {
|
||||||
|
if (result.kind === "cancelled") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (result.newStatus) {
|
||||||
|
setStatus(result.newStatus);
|
||||||
|
}
|
||||||
|
refreshStatus();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const toggle = useCallback(() => {
|
||||||
|
if (isUpdating) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleDaemonManagement();
|
||||||
|
}, [isUpdating, toggleDaemonManagement]);
|
||||||
|
|
||||||
|
return { isUpdating, toggle };
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
type DesktopDaemonLogs,
|
type DesktopDaemonLogs,
|
||||||
type DesktopDaemonStatus,
|
type DesktopDaemonStatus,
|
||||||
} from "@/desktop/daemon/desktop-daemon";
|
} from "@/desktop/daemon/desktop-daemon";
|
||||||
|
import { useDesktopIpcQueryErrorToast } from "@/desktop/hooks/desktop-ipc-error";
|
||||||
|
|
||||||
const DAEMON_STATUS_QUERY_KEY = ["desktopDaemonStatus"] as const;
|
const DAEMON_STATUS_QUERY_KEY = ["desktopDaemonStatus"] as const;
|
||||||
|
|
||||||
@@ -19,16 +20,22 @@ export function useDaemonStatus() {
|
|||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const enabled = shouldUseDesktopDaemon();
|
const enabled = shouldUseDesktopDaemon();
|
||||||
|
|
||||||
const query = useQuery<DaemonStatusData>({
|
const query = useQuery<DaemonStatusData, Error>({
|
||||||
queryKey: DAEMON_STATUS_QUERY_KEY,
|
queryKey: DAEMON_STATUS_QUERY_KEY,
|
||||||
enabled,
|
enabled,
|
||||||
staleTime: 30_000,
|
staleTime: 30_000,
|
||||||
refetchOnMount: "always",
|
refetchOnMount: "always",
|
||||||
|
retry: false,
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const [status, logs] = await Promise.all([getDesktopDaemonStatus(), getDesktopDaemonLogs()]);
|
const [status, logs] = await Promise.all([getDesktopDaemonStatus(), getDesktopDaemonLogs()]);
|
||||||
return { status, logs };
|
return { status, logs };
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
useDesktopIpcQueryErrorToast({
|
||||||
|
error: query.error,
|
||||||
|
message: "Unable to load desktop daemon status.",
|
||||||
|
logLabel: "[DesktopDaemon] Failed to load daemon status",
|
||||||
|
});
|
||||||
|
|
||||||
const setStatus = useCallback(
|
const setStatus = useCallback(
|
||||||
(status: DesktopDaemonStatus) => {
|
(status: DesktopDaemonStatus) => {
|
||||||
|
|||||||
@@ -0,0 +1,135 @@
|
|||||||
|
/**
|
||||||
|
* @vitest-environment jsdom
|
||||||
|
*/
|
||||||
|
import React from "react";
|
||||||
|
import { act, renderHook, waitFor } from "@testing-library/react";
|
||||||
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import { useCliInstall, useSkillsInstall } from "./use-install-status";
|
||||||
|
|
||||||
|
const toast = vi.hoisted(() => ({
|
||||||
|
error: vi.fn(),
|
||||||
|
show: vi.fn(),
|
||||||
|
copied: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const desktopDaemon = vi.hoisted(() => ({
|
||||||
|
getCliInstallStatus: vi.fn(),
|
||||||
|
getSkillsInstallStatus: vi.fn(),
|
||||||
|
installCli: vi.fn(),
|
||||||
|
installSkills: vi.fn(),
|
||||||
|
shouldUseDesktopDaemon: vi.fn(() => true),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/contexts/toast-context", () => ({
|
||||||
|
useToast: () => toast,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/desktop/daemon/desktop-daemon", () => desktopDaemon);
|
||||||
|
|
||||||
|
function createQueryClient(): QueryClient {
|
||||||
|
return new QueryClient({
|
||||||
|
defaultOptions: {
|
||||||
|
queries: { retry: false },
|
||||||
|
mutations: { retry: false },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderDesktopHook<TResult>(callback: () => TResult) {
|
||||||
|
const queryClient = createQueryClient();
|
||||||
|
const wrapper = ({ children }: { children: React.ReactNode }) =>
|
||||||
|
React.createElement(QueryClientProvider, { client: queryClient }, children);
|
||||||
|
|
||||||
|
return renderHook(callback, { wrapper });
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("useCliInstall", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.spyOn(console, "error").mockImplementation(() => {});
|
||||||
|
desktopDaemon.getCliInstallStatus.mockResolvedValue({ installed: true });
|
||||||
|
desktopDaemon.installCli.mockResolvedValue({ installed: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("loads CLI install status", async () => {
|
||||||
|
const { result } = renderDesktopHook(() => useCliInstall());
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.current.status).toEqual({ installed: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(toast.error).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("toasts and exposes CLI install errors", async () => {
|
||||||
|
const error = new Error("Missing IPC handler");
|
||||||
|
desktopDaemon.getCliInstallStatus.mockResolvedValue({ installed: false });
|
||||||
|
desktopDaemon.installCli.mockRejectedValue(error);
|
||||||
|
const { result } = renderDesktopHook(() => useCliInstall());
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.current.status).toEqual({ installed: false });
|
||||||
|
});
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.install();
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.current.error).toBe(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(toast.error).toHaveBeenCalledWith("Unable to install the Paseo CLI.");
|
||||||
|
expect(console.error).toHaveBeenCalledWith("[Integrations] Failed to install CLI", error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("useSkillsInstall", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.spyOn(console, "error").mockImplementation(() => {});
|
||||||
|
desktopDaemon.getSkillsInstallStatus.mockResolvedValue({ installed: true });
|
||||||
|
desktopDaemon.installSkills.mockResolvedValue({ installed: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("loads skills install status", async () => {
|
||||||
|
const { result } = renderDesktopHook(() => useSkillsInstall());
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.current.status).toEqual({ installed: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(toast.error).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("toasts and exposes skills install errors", async () => {
|
||||||
|
const error = new Error("Missing IPC handler");
|
||||||
|
desktopDaemon.getSkillsInstallStatus.mockResolvedValue({ installed: false });
|
||||||
|
desktopDaemon.installSkills.mockRejectedValue(error);
|
||||||
|
const { result } = renderDesktopHook(() => useSkillsInstall());
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.current.status).toEqual({ installed: false });
|
||||||
|
});
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.install();
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.current.error).toBe(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(toast.error).toHaveBeenCalledWith("Unable to install orchestration skills.");
|
||||||
|
expect(console.error).toHaveBeenCalledWith("[Integrations] Failed to install skills", error);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
import { useCallback } from "react";
|
||||||
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import {
|
||||||
|
getCliInstallStatus,
|
||||||
|
getSkillsInstallStatus,
|
||||||
|
installCli,
|
||||||
|
installSkills,
|
||||||
|
shouldUseDesktopDaemon,
|
||||||
|
type InstallStatus,
|
||||||
|
} from "@/desktop/daemon/desktop-daemon";
|
||||||
|
import {
|
||||||
|
useDesktopIpcErrorReporter,
|
||||||
|
useDesktopIpcQueryErrorToast,
|
||||||
|
} from "@/desktop/hooks/desktop-ipc-error";
|
||||||
|
|
||||||
|
const CLI_INSTALL_STATUS_QUERY_KEY = ["desktop", "integrations", "cli-install-status"] as const;
|
||||||
|
const SKILLS_INSTALL_STATUS_QUERY_KEY = [
|
||||||
|
"desktop",
|
||||||
|
"integrations",
|
||||||
|
"skills-install-status",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
interface DesktopInstallHookResult {
|
||||||
|
status: InstallStatus | null;
|
||||||
|
isLoading: boolean;
|
||||||
|
isInstalling: boolean;
|
||||||
|
error: Error | null;
|
||||||
|
install: () => void;
|
||||||
|
refresh: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useCliInstall(): DesktopInstallHookResult {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const reportError = useDesktopIpcErrorReporter();
|
||||||
|
const enabled = shouldUseDesktopDaemon();
|
||||||
|
|
||||||
|
const statusQuery = useQuery<InstallStatus, Error>({
|
||||||
|
queryKey: CLI_INSTALL_STATUS_QUERY_KEY,
|
||||||
|
queryFn: getCliInstallStatus,
|
||||||
|
enabled,
|
||||||
|
retry: false,
|
||||||
|
});
|
||||||
|
const { data: installStatus, error: statusError, isLoading, refetch } = statusQuery;
|
||||||
|
useDesktopIpcQueryErrorToast({
|
||||||
|
error: statusQuery.error,
|
||||||
|
message: "Unable to check CLI install status.",
|
||||||
|
logLabel: "[Integrations] Failed to load CLI status",
|
||||||
|
});
|
||||||
|
|
||||||
|
const installMutation = useMutation<InstallStatus, Error>({
|
||||||
|
mutationFn: installCli,
|
||||||
|
onError: (error) => {
|
||||||
|
reportError({
|
||||||
|
error,
|
||||||
|
message: "Unable to install the Paseo CLI.",
|
||||||
|
logLabel: "[Integrations] Failed to install CLI",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onSuccess: (nextStatus) => {
|
||||||
|
queryClient.setQueryData<InstallStatus>(CLI_INSTALL_STATUS_QUERY_KEY, nextStatus);
|
||||||
|
void queryClient.invalidateQueries({ queryKey: CLI_INSTALL_STATUS_QUERY_KEY });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const { error: installError, isPending: isInstalling, mutate: install } = installMutation;
|
||||||
|
|
||||||
|
const refresh = useCallback(() => {
|
||||||
|
void refetch();
|
||||||
|
}, [refetch]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: installStatus ?? null,
|
||||||
|
isLoading,
|
||||||
|
isInstalling,
|
||||||
|
error: statusError ?? installError ?? null,
|
||||||
|
install,
|
||||||
|
refresh,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSkillsInstall(): DesktopInstallHookResult {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const reportError = useDesktopIpcErrorReporter();
|
||||||
|
const enabled = shouldUseDesktopDaemon();
|
||||||
|
|
||||||
|
const statusQuery = useQuery<InstallStatus, Error>({
|
||||||
|
queryKey: SKILLS_INSTALL_STATUS_QUERY_KEY,
|
||||||
|
queryFn: getSkillsInstallStatus,
|
||||||
|
enabled,
|
||||||
|
retry: false,
|
||||||
|
});
|
||||||
|
const { data: installStatus, error: statusError, isLoading, refetch } = statusQuery;
|
||||||
|
useDesktopIpcQueryErrorToast({
|
||||||
|
error: statusQuery.error,
|
||||||
|
message: "Unable to check orchestration skills install status.",
|
||||||
|
logLabel: "[Integrations] Failed to load skills status",
|
||||||
|
});
|
||||||
|
|
||||||
|
const installMutation = useMutation<InstallStatus, Error>({
|
||||||
|
mutationFn: installSkills,
|
||||||
|
onError: (error) => {
|
||||||
|
reportError({
|
||||||
|
error,
|
||||||
|
message: "Unable to install orchestration skills.",
|
||||||
|
logLabel: "[Integrations] Failed to install skills",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onSuccess: (nextStatus) => {
|
||||||
|
queryClient.setQueryData<InstallStatus>(SKILLS_INSTALL_STATUS_QUERY_KEY, nextStatus);
|
||||||
|
void queryClient.invalidateQueries({ queryKey: SKILLS_INSTALL_STATUS_QUERY_KEY });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const { error: installError, isPending: isInstalling, mutate: install } = installMutation;
|
||||||
|
|
||||||
|
const refresh = useCallback(() => {
|
||||||
|
void refetch();
|
||||||
|
}, [refetch]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: installStatus ?? null,
|
||||||
|
isLoading,
|
||||||
|
isInstalling,
|
||||||
|
error: statusError ?? installError ?? null,
|
||||||
|
install,
|
||||||
|
refresh,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,10 +1,14 @@
|
|||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { getIsElectron } from "@/constants/platform";
|
import { getIsElectron } from "@/constants/platform";
|
||||||
import { invokeDesktopCommand } from "@/desktop/electron/invoke";
|
import { invokeDesktopCommand } from "@/desktop/electron/invoke";
|
||||||
|
import {
|
||||||
|
useDesktopIpcErrorReporter,
|
||||||
|
useDesktopIpcQueryErrorToast,
|
||||||
|
} from "@/desktop/hooks/desktop-ipc-error";
|
||||||
import type { ReleaseChannel } from "@/hooks/use-settings";
|
import type { ReleaseChannel } from "@/hooks/use-settings";
|
||||||
|
|
||||||
export const DESKTOP_SETTINGS_QUERY_KEY = ["desktop-settings"] as const;
|
const DESKTOP_SETTINGS_QUERY_KEY = ["desktop-settings"] as const;
|
||||||
|
|
||||||
export interface DesktopSettings {
|
export interface DesktopSettings {
|
||||||
releaseChannel: ReleaseChannel;
|
releaseChannel: ReleaseChannel;
|
||||||
@@ -30,15 +34,59 @@ export const DEFAULT_DESKTOP_SETTINGS: DesktopSettings = {
|
|||||||
export function useDesktopSettings(): {
|
export function useDesktopSettings(): {
|
||||||
settings: DesktopSettings;
|
settings: DesktopSettings;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
|
isSaving: boolean;
|
||||||
error: unknown;
|
error: unknown;
|
||||||
updateSettings: (updates: DesktopSettingsPatch) => Promise<void>;
|
updateSettings: (updates: DesktopSettingsPatch) => Promise<void>;
|
||||||
} {
|
} {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const { data, isPending, error } = useQuery({
|
const reportError = useDesktopIpcErrorReporter();
|
||||||
|
const {
|
||||||
|
data,
|
||||||
|
isPending,
|
||||||
|
error: loadError,
|
||||||
|
} = useQuery<DesktopSettings, Error>({
|
||||||
queryKey: DESKTOP_SETTINGS_QUERY_KEY,
|
queryKey: DESKTOP_SETTINGS_QUERY_KEY,
|
||||||
queryFn: loadDesktopSettings,
|
queryFn: loadDesktopSettings,
|
||||||
staleTime: Infinity,
|
staleTime: Infinity,
|
||||||
gcTime: Infinity,
|
gcTime: Infinity,
|
||||||
|
retry: false,
|
||||||
|
});
|
||||||
|
useDesktopIpcQueryErrorToast({
|
||||||
|
error: loadError,
|
||||||
|
message: "Unable to load desktop settings.",
|
||||||
|
logLabel: "[DesktopSettings] Failed to load settings",
|
||||||
|
});
|
||||||
|
|
||||||
|
const { mutateAsync: saveDesktopSettings, isPending: isSaving } = useMutation<
|
||||||
|
DesktopSettings,
|
||||||
|
Error,
|
||||||
|
DesktopSettingsPatch,
|
||||||
|
DesktopSettingsMutationContext
|
||||||
|
>({
|
||||||
|
mutationFn: updatePersistedDesktopSettings,
|
||||||
|
onMutate: (updates) => {
|
||||||
|
const previous =
|
||||||
|
queryClient.getQueryData<DesktopSettings>(DESKTOP_SETTINGS_QUERY_KEY) ??
|
||||||
|
DEFAULT_DESKTOP_SETTINGS;
|
||||||
|
queryClient.setQueryData<DesktopSettings>(
|
||||||
|
DESKTOP_SETTINGS_QUERY_KEY,
|
||||||
|
mergeDesktopSettings(previous, updates),
|
||||||
|
);
|
||||||
|
return { previous };
|
||||||
|
},
|
||||||
|
onSuccess: (persisted) => {
|
||||||
|
queryClient.setQueryData<DesktopSettings>(DESKTOP_SETTINGS_QUERY_KEY, persisted);
|
||||||
|
},
|
||||||
|
onError: (saveError, _updates, context) => {
|
||||||
|
if (context) {
|
||||||
|
queryClient.setQueryData<DesktopSettings>(DESKTOP_SETTINGS_QUERY_KEY, context.previous);
|
||||||
|
}
|
||||||
|
reportError({
|
||||||
|
error: saveError,
|
||||||
|
message: "Unable to save desktop settings.",
|
||||||
|
logLabel: "[DesktopSettings] Failed to save settings",
|
||||||
|
});
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const updateSettings = useCallback(
|
const updateSettings = useCallback(
|
||||||
@@ -47,25 +95,24 @@ export function useDesktopSettings(): {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const previous =
|
await saveDesktopSettings(updates);
|
||||||
queryClient.getQueryData<DesktopSettings>(DESKTOP_SETTINGS_QUERY_KEY) ??
|
|
||||||
DEFAULT_DESKTOP_SETTINGS;
|
|
||||||
const next = mergeDesktopSettings(previous, updates);
|
|
||||||
queryClient.setQueryData<DesktopSettings>(DESKTOP_SETTINGS_QUERY_KEY, next);
|
|
||||||
const persisted = await updatePersistedDesktopSettings(updates);
|
|
||||||
queryClient.setQueryData<DesktopSettings>(DESKTOP_SETTINGS_QUERY_KEY, persisted);
|
|
||||||
},
|
},
|
||||||
[queryClient],
|
[saveDesktopSettings],
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
settings: data ?? DEFAULT_DESKTOP_SETTINGS,
|
settings: data ?? DEFAULT_DESKTOP_SETTINGS,
|
||||||
isLoading: isPending,
|
isLoading: isPending,
|
||||||
error: error ?? null,
|
isSaving,
|
||||||
|
error: loadError ?? null,
|
||||||
updateSettings,
|
updateSettings,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface DesktopSettingsMutationContext {
|
||||||
|
previous: DesktopSettings;
|
||||||
|
}
|
||||||
|
|
||||||
export async function loadDesktopSettings(): Promise<DesktopSettings> {
|
export async function loadDesktopSettings(): Promise<DesktopSettings> {
|
||||||
if (!getIsElectron()) {
|
if (!getIsElectron()) {
|
||||||
return DEFAULT_DESKTOP_SETTINGS;
|
return DEFAULT_DESKTOP_SETTINGS;
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
/**
|
/**
|
||||||
* @vitest-environment jsdom
|
* @vitest-environment jsdom
|
||||||
*/
|
*/
|
||||||
|
import React from "react";
|
||||||
import { act, renderHook, waitFor } from "@testing-library/react";
|
import { act, renderHook, waitFor } from "@testing-library/react";
|
||||||
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
import { useDesktopAppUpdater } from "./use-desktop-app-updater";
|
import { useDesktopAppUpdater } from "./use-desktop-app-updater";
|
||||||
|
|
||||||
@@ -37,6 +39,27 @@ vi.mock("@/desktop/settings/desktop-settings", () => ({
|
|||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/contexts/toast-context", () => ({
|
||||||
|
useToast: () => ({ error: vi.fn(), show: vi.fn(), copied: vi.fn() }),
|
||||||
|
}));
|
||||||
|
|
||||||
|
function createQueryClient(): QueryClient {
|
||||||
|
return new QueryClient({
|
||||||
|
defaultOptions: {
|
||||||
|
queries: { retry: false },
|
||||||
|
mutations: { retry: false },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderUpdaterHook() {
|
||||||
|
const queryClient = createQueryClient();
|
||||||
|
const wrapper = ({ children }: { children: React.ReactNode }) =>
|
||||||
|
React.createElement(QueryClientProvider, { client: queryClient }, children);
|
||||||
|
|
||||||
|
return renderHook(() => useDesktopAppUpdater(), { wrapper });
|
||||||
|
}
|
||||||
|
|
||||||
describe("useDesktopAppUpdater", () => {
|
describe("useDesktopAppUpdater", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
settingsState.releaseChannel = "stable";
|
settingsState.releaseChannel = "stable";
|
||||||
@@ -47,7 +70,7 @@ describe("useDesktopAppUpdater", () => {
|
|||||||
it("uses the effective desktop release channel when checking for updates", async () => {
|
it("uses the effective desktop release channel when checking for updates", async () => {
|
||||||
settingsState.releaseChannel = "beta";
|
settingsState.releaseChannel = "beta";
|
||||||
|
|
||||||
renderHook(() => useDesktopAppUpdater());
|
renderUpdaterHook();
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(desktopUpdatesMock.checkDesktopAppUpdate).toHaveBeenCalledWith({
|
expect(desktopUpdatesMock.checkDesktopAppUpdate).toHaveBeenCalledWith({
|
||||||
@@ -58,7 +81,7 @@ describe("useDesktopAppUpdater", () => {
|
|||||||
|
|
||||||
it("uses the effective desktop release channel when installing updates", async () => {
|
it("uses the effective desktop release channel when installing updates", async () => {
|
||||||
settingsState.releaseChannel = "beta";
|
settingsState.releaseChannel = "beta";
|
||||||
const { result } = renderHook(() => useDesktopAppUpdater());
|
const { result } = renderUpdaterHook();
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
await result.current.installUpdate();
|
await result.current.installUpdate();
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
import { useMutation } from "@tanstack/react-query";
|
||||||
import {
|
import {
|
||||||
checkDesktopAppUpdate,
|
checkDesktopAppUpdate,
|
||||||
formatVersionWithPrefix,
|
formatVersionWithPrefix,
|
||||||
@@ -8,6 +9,7 @@ import {
|
|||||||
type DesktopAppUpdateInstallResult,
|
type DesktopAppUpdateInstallResult,
|
||||||
} from "@/desktop/updates/desktop-updates";
|
} from "@/desktop/updates/desktop-updates";
|
||||||
import { useDesktopSettings } from "@/desktop/settings/desktop-settings";
|
import { useDesktopSettings } from "@/desktop/settings/desktop-settings";
|
||||||
|
import { useDesktopIpcErrorReporter } from "@/desktop/hooks/desktop-ipc-error";
|
||||||
|
|
||||||
export type DesktopAppUpdateStatus =
|
export type DesktopAppUpdateStatus =
|
||||||
| "idle"
|
| "idle"
|
||||||
@@ -86,12 +88,26 @@ export function useDesktopAppUpdater(): UseDesktopAppUpdaterReturn {
|
|||||||
const isDesktopApp = shouldShowDesktopUpdateSection();
|
const isDesktopApp = shouldShowDesktopUpdateSection();
|
||||||
const { settings: desktopSettings } = useDesktopSettings();
|
const { settings: desktopSettings } = useDesktopSettings();
|
||||||
const releaseChannel = desktopSettings.releaseChannel;
|
const releaseChannel = desktopSettings.releaseChannel;
|
||||||
|
const reportError = useDesktopIpcErrorReporter();
|
||||||
const requestVersionRef = useRef(0);
|
const requestVersionRef = useRef(0);
|
||||||
const [status, setStatus] = useState<DesktopAppUpdateStatus>("idle");
|
const [status, setStatus] = useState<DesktopAppUpdateStatus>("idle");
|
||||||
const [availableUpdate, setAvailableUpdate] = useState<DesktopAppUpdateCheckResult | null>(null);
|
const [availableUpdate, setAvailableUpdate] = useState<DesktopAppUpdateCheckResult | null>(null);
|
||||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||||
const [installMessage, setInstallMessage] = useState<string | null>(null);
|
const [installMessage, setInstallMessage] = useState<string | null>(null);
|
||||||
const [lastCheckedAt, setLastCheckedAt] = useState<number | null>(null);
|
const [lastCheckedAt, setLastCheckedAt] = useState<number | null>(null);
|
||||||
|
const { mutateAsync: installAppUpdate, isPending: isInstallingAppUpdate } = useMutation<
|
||||||
|
DesktopAppUpdateInstallResult,
|
||||||
|
Error
|
||||||
|
>({
|
||||||
|
mutationFn: () => installDesktopAppUpdate({ releaseChannel }),
|
||||||
|
onError: (error) => {
|
||||||
|
reportError({
|
||||||
|
error,
|
||||||
|
message: "Unable to install the desktop app update.",
|
||||||
|
logLabel: "[DesktopUpdater] Failed to install app update",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const checkForUpdates = useCallback(
|
const checkForUpdates = useCallback(
|
||||||
async (options: { silent?: boolean } = {}) => {
|
async (options: { silent?: boolean } = {}) => {
|
||||||
@@ -177,7 +193,7 @@ export function useDesktopAppUpdater(): UseDesktopAppUpdaterReturn {
|
|||||||
setErrorMessage(null);
|
setErrorMessage(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await installDesktopAppUpdate({ releaseChannel });
|
const result = await installAppUpdate();
|
||||||
setLastCheckedAt(Date.now());
|
setLastCheckedAt(Date.now());
|
||||||
|
|
||||||
if (result.installed) {
|
if (result.installed) {
|
||||||
@@ -197,7 +213,7 @@ export function useDesktopAppUpdater(): UseDesktopAppUpdaterReturn {
|
|||||||
setErrorMessage(message);
|
setErrorMessage(message);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}, [isDesktopApp, releaseChannel]);
|
}, [installAppUpdate, isDesktopApp]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isDesktopApp,
|
isDesktopApp,
|
||||||
@@ -211,7 +227,7 @@ export function useDesktopAppUpdater(): UseDesktopAppUpdaterReturn {
|
|||||||
errorMessage,
|
errorMessage,
|
||||||
lastCheckedAt,
|
lastCheckedAt,
|
||||||
isChecking: status === "checking",
|
isChecking: status === "checking",
|
||||||
isInstalling: status === "installing",
|
isInstalling: status === "installing" || isInstallingAppUpdate,
|
||||||
checkForUpdates,
|
checkForUpdates,
|
||||||
installUpdate,
|
installUpdate,
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user