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) });
}
}, []);