diff --git a/desktop/src/app/AppShell.tsx b/desktop/src/app/AppShell.tsx index a32fe0e43..26facfc3d 100644 --- a/desktop/src/app/AppShell.tsx +++ b/desktop/src/app/AppShell.tsx @@ -447,6 +447,8 @@ export function AppShell() { > + +
handleOpenSettings("updates")} /> diff --git a/desktop/src/features/settings/UpdateIndicator.tsx b/desktop/src/features/settings/UpdateIndicator.tsx index 15697729b..7c9590053 100644 --- a/desktop/src/features/settings/UpdateIndicator.tsx +++ b/desktop/src/features/settings/UpdateIndicator.tsx @@ -3,9 +3,35 @@ import { Download, RefreshCw } from "lucide-react"; import { Button } from "@/shared/ui/button"; import { useUpdaterContext } from "./hooks/UpdaterProvider"; +import type { UpdateStatus } from "./hooks/use-updater"; const indicatorButtonClass = - "relative h-6 w-6 text-muted-foreground/70 hover:bg-muted/60 hover:text-foreground"; + "relative text-muted-foreground/70 hover:bg-muted/60 hover:text-foreground"; + +const iconClass = "h-3.5 w-3.5"; + +const variants: Record< + "available" | "ready", + { Icon: typeof Download; label: string; badgeColor: string } +> = { + available: { + Icon: Download, + label: "Update available", + badgeColor: "bg-primary", + }, + ready: { + Icon: RefreshCw, + label: "Restart to update", + badgeColor: "bg-emerald-500", + }, +}; + +function getVariant(state: UpdateStatus["state"]) { + if (state === "available" || state === "ready") { + return variants[state]; + } + return null; +} export function UpdateIndicator({ onOpenUpdates, @@ -13,36 +39,27 @@ export function UpdateIndicator({ onOpenUpdates: () => void; }) { const { status } = useUpdaterContext(); + const variant = getVariant(status.state); - if (status.state === "available") { - return ( - - ); + if (!variant) { + return null; } - if (status.state === "ready") { - return ( - - ); - } + const { Icon, label, badgeColor } = variant; - return null; + return ( + + ); } diff --git a/desktop/src/features/settings/hooks/use-updater.ts b/desktop/src/features/settings/hooks/use-updater.ts index 9e728ddcf..7e27986e4 100644 --- a/desktop/src/features/settings/hooks/use-updater.ts +++ b/desktop/src/features/settings/hooks/use-updater.ts @@ -13,14 +13,51 @@ export type UpdateStatus = | { state: "ready" } | { state: "error"; message: string }; +const TOAST_ID = "update-available"; + +function toErrorMessage(err: unknown): string { + return err instanceof Error ? err.message : String(err); +} + +function isUpdaterUnavailable(message: string): boolean { + return ( + message.includes("plugin updater not found") || + message.includes("not initialized") + ); +} + export function useUpdater() { const [status, setStatus] = useState({ state: "idle" }); const updateRef = useRef(null); const closeUpdate = useCallback(async () => { - if (updateRef.current) { - await updateRef.current.close(); + const current = updateRef.current; + if (current) { updateRef.current = null; + await current.close(); + } + }, []); + + const downloadAndInstall = useCallback(async () => { + try { + const update = updateRef.current; + if (!update) { + return; + } + + toast.dismiss(TOAST_ID); + setStatus({ state: "downloading" }); + + await update.downloadAndInstall((event) => { + if (event.event === "Finished") { + setStatus({ state: "installing" }); + } + }); + + updateRef.current = null; + setStatus({ state: "ready" }); + } catch (err) { + setStatus({ state: "error", message: toErrorMessage(err) }); } }, []); @@ -34,58 +71,32 @@ export function useUpdater() { updateRef.current = update; setStatus({ state: "available", version: update.version }); toast("Update Available", { - id: "update-available", + id: TOAST_ID, description: `Version ${update.version} is ready to download.`, + duration: Infinity, + action: { + label: "Download & install", + onClick: () => downloadAndInstall(), + }, }); } else { setStatus({ state: "up-to-date" }); } } catch (err) { - const msg = err instanceof Error ? err.message : String(err); - if ( - msg.includes("plugin updater not found") || - msg.includes("not initialized") - ) { + const message = toErrorMessage(err); + if (isUpdaterUnavailable(message)) { setStatus({ state: "idle" }); return; } - setStatus({ state: "error", message: msg }); + setStatus({ state: "error", message }); } - }, [closeUpdate]); - - const downloadAndInstall = useCallback(async () => { - try { - const update = updateRef.current; - if (!update) { - setStatus({ state: "up-to-date" }); - return; - } - - setStatus({ state: "downloading" }); - - await update.downloadAndInstall((event) => { - if (event.event === "Finished") { - setStatus({ state: "installing" }); - } - }); - - setStatus({ state: "ready" }); - } catch (err) { - setStatus({ - state: "error", - message: err instanceof Error ? err.message : String(err), - }); - } - }, []); + }, [closeUpdate, downloadAndInstall]); const handleRelaunch = useCallback(async () => { try { await relaunch(); } catch (err) { - setStatus({ - state: "error", - message: err instanceof Error ? err.message : String(err), - }); + setStatus({ state: "error", message: toErrorMessage(err) }); } }, []);