From c1a614b871455ed1b3e60245387cb4921cb5327a Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Thu, 25 Dec 2025 09:09:42 -0800 Subject: [PATCH 01/19] feat(desktop): replace auto-update dialog with in-app toast notification MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the intrusive Electron dialog with a subtle toast notification in the bottom-right corner when updates are downloaded. The toast shows version info with "See changes" (opens release notes) and "Restart" buttons. - Add tRPC router for auto-update events and install action - Create UpdateToast component with dismiss button - Use EventEmitter to broadcast update events to renderer - Remove unused setMainWindow function 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../src/lib/trpc/routers/auto-update/index.ts | 41 ++++++++++++ apps/desktop/src/lib/trpc/routers/index.ts | 2 + apps/desktop/src/main/lib/auto-updater.ts | 62 +++++++------------ apps/desktop/src/main/windows/main.ts | 2 - .../components/UpdateToast/UpdateToast.tsx | 60 ++++++++++++++++++ .../renderer/components/UpdateToast/index.ts | 2 + .../UpdateToast/useUpdateListener.tsx | 45 ++++++++++++++ .../src/renderer/screens/main/index.tsx | 2 + 8 files changed, 175 insertions(+), 41 deletions(-) create mode 100644 apps/desktop/src/lib/trpc/routers/auto-update/index.ts create mode 100644 apps/desktop/src/renderer/components/UpdateToast/UpdateToast.tsx create mode 100644 apps/desktop/src/renderer/components/UpdateToast/index.ts create mode 100644 apps/desktop/src/renderer/components/UpdateToast/useUpdateListener.tsx diff --git a/apps/desktop/src/lib/trpc/routers/auto-update/index.ts b/apps/desktop/src/lib/trpc/routers/auto-update/index.ts new file mode 100644 index 00000000000..486c127b61b --- /dev/null +++ b/apps/desktop/src/lib/trpc/routers/auto-update/index.ts @@ -0,0 +1,41 @@ +import { observable } from "@trpc/server/observable"; +import { + autoUpdateEmitter, + installUpdate, + type UpdateDownloadedEvent, +} from "main/lib/auto-updater"; +import { publicProcedure, router } from "../.."; + +export type AutoUpdateEvent = + | { + type: "update-downloaded"; + data: UpdateDownloadedEvent; + } + | { type: "update-not-available" }; + +export const createAutoUpdateRouter = () => { + return router({ + subscribe: publicProcedure.subscription(() => { + return observable((emit) => { + const onUpdateDownloaded = (data: UpdateDownloadedEvent) => { + emit.next({ type: "update-downloaded", data }); + }; + + const onUpdateNotAvailable = () => { + emit.next({ type: "update-not-available" }); + }; + + autoUpdateEmitter.on("update-downloaded", onUpdateDownloaded); + autoUpdateEmitter.on("update-not-available", onUpdateNotAvailable); + + return () => { + autoUpdateEmitter.off("update-downloaded", onUpdateDownloaded); + autoUpdateEmitter.off("update-not-available", onUpdateNotAvailable); + }; + }); + }), + installUpdate: publicProcedure.mutation(() => { + installUpdate(); + }), + }); +}; diff --git a/apps/desktop/src/lib/trpc/routers/index.ts b/apps/desktop/src/lib/trpc/routers/index.ts index bb53dea8f41..dc0e9e21664 100644 --- a/apps/desktop/src/lib/trpc/routers/index.ts +++ b/apps/desktop/src/lib/trpc/routers/index.ts @@ -1,6 +1,7 @@ import type { BrowserWindow } from "electron"; import { router } from ".."; import { createAuthRouter } from "./auth"; +import { createAutoUpdateRouter } from "./auto-update"; import { createChangesRouter } from "./changes"; import { createConfigRouter } from "./config"; import { createExternalRouter } from "./external"; @@ -26,6 +27,7 @@ import { createWorkspacesRouter } from "./workspaces"; export const createAppRouter = (getWindow: () => BrowserWindow | null) => { return router({ auth: createAuthRouter(getWindow), + autoUpdate: createAutoUpdateRouter(), user: createUserRouter(), window: createWindowRouter(getWindow), projects: createProjectsRouter(getWindow), diff --git a/apps/desktop/src/main/lib/auto-updater.ts b/apps/desktop/src/main/lib/auto-updater.ts index ddd2867e126..b2b3212cf84 100644 --- a/apps/desktop/src/main/lib/auto-updater.ts +++ b/apps/desktop/src/main/lib/auto-updater.ts @@ -1,4 +1,5 @@ -import { app, type BrowserWindow, dialog } from "electron"; +import { EventEmitter } from "node:events"; +import { app, dialog } from "electron"; import { autoUpdater } from "electron-updater"; import { env } from "main/env.main"; import { PLATFORM } from "shared/constants"; @@ -6,12 +7,19 @@ import { PLATFORM } from "shared/constants"; const UPDATE_CHECK_INTERVAL_MS = 1000 * 60 * 60 * 4; // 4 hours const UPDATE_FEED_URL = "https://github.com/superset-sh/superset/releases/latest/download"; +const RELEASES_URL = "https://github.com/superset-sh/superset/releases"; -let mainWindow: BrowserWindow | null = null; -let isUpdateDialogOpen = false; +export interface UpdateDownloadedEvent { + version: string; + releaseUrl: string; +} + +export const autoUpdateEmitter = new EventEmitter(); + +let hasNotifiedUpdateDownloaded = false; -export function setMainWindow(window: BrowserWindow): void { - mainWindow = window; +export function installUpdate(): void { + autoUpdater.quitAndInstall(false, true); } export function checkForUpdates(): void { @@ -45,11 +53,7 @@ export function checkForUpdatesInteractive(): void { .checkForUpdates() .then((result) => { if (!result || !result.updateInfo) { - dialog.showMessageBox({ - type: "info", - title: "No Updates", - message: "You are running the latest version.", - }); + autoUpdateEmitter.emit("update-not-available"); } }) .catch((error) => { @@ -91,43 +95,23 @@ export function setupAutoUpdater(): void { }); autoUpdater.on("update-downloaded", (info) => { - if (isUpdateDialogOpen) { - console.info("[auto-updater] Update dialog already open, skipping"); + if (hasNotifiedUpdateDownloaded) { + console.info("[auto-updater] Already notified about update, skipping"); return; } console.info( - `[auto-updater] Update downloaded (${info.version}). Prompting user to restart.`, + `[auto-updater] Update downloaded (${info.version}). Notifying renderer.`, ); - isUpdateDialogOpen = true; - - const dialogOptions = { - type: "info" as const, - buttons: ["Restart Now", "Later"], - defaultId: 0, - cancelId: 1, - title: "Update Ready", - message: `Version ${info.version} is ready to install`, - detail: - "A new version has been downloaded. Restart the application to apply the update.", + hasNotifiedUpdateDownloaded = true; + + const event: UpdateDownloadedEvent = { + version: info.version, + releaseUrl: `${RELEASES_URL}/tag/v${info.version}`, }; - const showDialog = mainWindow - ? dialog.showMessageBox(mainWindow, dialogOptions) - : dialog.showMessageBox(dialogOptions); - - showDialog - .then((response) => { - isUpdateDialogOpen = false; - if (response.response === 0) { - autoUpdater.quitAndInstall(false, true); - } - }) - .catch((error) => { - isUpdateDialogOpen = false; - console.error("[auto-updater] Failed to show update dialog:", error); - }); + autoUpdateEmitter.emit("update-downloaded", event); }); const interval = setInterval(checkForUpdates, UPDATE_CHECK_INTERVAL_MS); diff --git a/apps/desktop/src/main/windows/main.ts b/apps/desktop/src/main/windows/main.ts index 65161e62783..c49659d112b 100644 --- a/apps/desktop/src/main/windows/main.ts +++ b/apps/desktop/src/main/windows/main.ts @@ -10,7 +10,6 @@ import { NOTIFICATION_EVENTS, PORTS } from "shared/constants"; import { createIPCHandler } from "trpc-electron/main"; import { productName } from "~/package.json"; import { appState } from "../lib/app-state"; -import { setMainWindow } from "../lib/auto-updater"; import { createApplicationMenu } from "../lib/menu"; import { playNotificationSound } from "../lib/notification-sound"; import { @@ -54,7 +53,6 @@ export async function MainWindow() { }, }); - setMainWindow(window); createApplicationMenu(); currentWindow = window; diff --git a/apps/desktop/src/renderer/components/UpdateToast/UpdateToast.tsx b/apps/desktop/src/renderer/components/UpdateToast/UpdateToast.tsx new file mode 100644 index 00000000000..0c8e4206cae --- /dev/null +++ b/apps/desktop/src/renderer/components/UpdateToast/UpdateToast.tsx @@ -0,0 +1,60 @@ +import { Button } from "@superset/ui/button"; +import { toast } from "@superset/ui/sonner"; +import { HiMiniXMark } from "react-icons/hi2"; +import { trpc } from "renderer/lib/trpc"; + +interface UpdateToastProps { + toastId: string | number; + version: string; + releaseUrl: string; +} + +export function UpdateToast({ + toastId, + version, + releaseUrl, +}: UpdateToastProps) { + const openUrl = trpc.external.openUrl.useMutation(); + const installUpdate = trpc.autoUpdate.installUpdate.useMutation(); + + const handleSeeChanges = () => { + openUrl.mutate(releaseUrl); + }; + + const handleRestart = () => { + installUpdate.mutate(); + }; + + const handleDismiss = () => { + toast.dismiss(toastId); + }; + + return ( +
+
+ + New update available + + + Restart to use v{version}. + +
+
+ + +
+ +
+ ); +} diff --git a/apps/desktop/src/renderer/components/UpdateToast/index.ts b/apps/desktop/src/renderer/components/UpdateToast/index.ts new file mode 100644 index 00000000000..002a8e0121b --- /dev/null +++ b/apps/desktop/src/renderer/components/UpdateToast/index.ts @@ -0,0 +1,2 @@ +export { UpdateToast } from "./UpdateToast"; +export { useUpdateListener } from "./useUpdateListener"; diff --git a/apps/desktop/src/renderer/components/UpdateToast/useUpdateListener.tsx b/apps/desktop/src/renderer/components/UpdateToast/useUpdateListener.tsx new file mode 100644 index 00000000000..239991e34ec --- /dev/null +++ b/apps/desktop/src/renderer/components/UpdateToast/useUpdateListener.tsx @@ -0,0 +1,45 @@ +import { toast } from "@superset/ui/sonner"; +import { useRef } from "react"; +import { trpc } from "renderer/lib/trpc"; +import { UpdateToast } from "./UpdateToast"; + +/** + * Hook that listens for auto-update events via tRPC subscription. + * Shows a toast notification when an update has been downloaded. + */ +export function useUpdateListener() { + const toastIdRef = useRef(null); + + trpc.autoUpdate.subscribe.useSubscription(undefined, { + onData: (event) => { + if (event.type === "update-downloaded" && event.data) { + // Don't show duplicate toasts + if (toastIdRef.current !== null) { + return; + } + + const { version, releaseUrl } = event.data; + + const toastId = toast.custom( + (id) => ( + + ), + { + duration: Number.POSITIVE_INFINITY, + position: "bottom-right", + }, + ); + + toastIdRef.current = toastId; + } else if (event.type === "update-not-available") { + toast.info("You're on the latest version", { + position: "bottom-right", + }); + } + }, + }); +} diff --git a/apps/desktop/src/renderer/screens/main/index.tsx b/apps/desktop/src/renderer/screens/main/index.tsx index d73c70db7f4..51f89669b64 100644 --- a/apps/desktop/src/renderer/screens/main/index.tsx +++ b/apps/desktop/src/renderer/screens/main/index.tsx @@ -5,6 +5,7 @@ import { useHotkeys } from "react-hotkeys-hook"; import { HiArrowPath } from "react-icons/hi2"; import { NewWorkspaceModal } from "renderer/components/NewWorkspaceModal"; import { SetupConfigModal } from "renderer/components/SetupConfigModal"; +import { useUpdateListener } from "renderer/components/UpdateToast"; import { trpc } from "renderer/lib/trpc"; import { SignInScreen } from "renderer/screens/sign-in"; import { useCurrentView, useOpenSettings } from "renderer/stores/app-state"; @@ -63,6 +64,7 @@ export function MainScreen() { const tabs = useTabsStore((s) => s.tabs); useAgentHookListener(); + useUpdateListener(); trpc.menu.subscribe.useSubscription(undefined, { onData: (event) => { From c4ec8255e9502e50df97eaa2780478799b758131 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Thu, 25 Dec 2025 09:28:59 -0800 Subject: [PATCH 02/19] fix(desktop): improve update toast styling and add dev menu MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix toast layout with proper container styling and min-width - Use unstyled toast to prevent sonner wrapper interference - Add onDismiss callback to allow re-triggering toast in dev - Add Dev menu (dev-only) with "Simulate Update Available" option 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- apps/desktop/src/main/lib/menu.ts | 22 ++++++++++++- .../components/UpdateToast/UpdateToast.tsx | 31 ++++++++++--------- .../UpdateToast/useUpdateListener.tsx | 8 ++++- 3 files changed, 44 insertions(+), 17 deletions(-) diff --git a/apps/desktop/src/main/lib/menu.ts b/apps/desktop/src/main/lib/menu.ts index 88cc9483fc2..99700b91f19 100644 --- a/apps/desktop/src/main/lib/menu.ts +++ b/apps/desktop/src/main/lib/menu.ts @@ -1,6 +1,7 @@ import { COMPANY } from "@superset/shared/constants"; import { app, Menu, shell } from "electron"; -import { checkForUpdatesInteractive } from "./auto-updater"; +import { env } from "main/env.main"; +import { autoUpdateEmitter, checkForUpdatesInteractive } from "./auto-updater"; import { menuEmitter } from "./menu-events"; export function createApplicationMenu() { @@ -72,6 +73,25 @@ export function createApplicationMenu() { }, ]; + // DEV ONLY: Add Dev menu + if (env.NODE_ENV === "development") { + template.push({ + label: "Dev", + submenu: [ + { + label: "Simulate Update Available", + click: () => { + autoUpdateEmitter.emit("update-downloaded", { + version: "99.0.0-test", + releaseUrl: + "https://github.com/superset-sh/superset/releases/tag/v99.0.0-test", + }); + }, + }, + ], + }); + } + if (process.platform === "darwin") { template.unshift({ label: app.name, diff --git a/apps/desktop/src/renderer/components/UpdateToast/UpdateToast.tsx b/apps/desktop/src/renderer/components/UpdateToast/UpdateToast.tsx index 0c8e4206cae..e7c72b9995d 100644 --- a/apps/desktop/src/renderer/components/UpdateToast/UpdateToast.tsx +++ b/apps/desktop/src/renderer/components/UpdateToast/UpdateToast.tsx @@ -7,12 +7,14 @@ interface UpdateToastProps { toastId: string | number; version: string; releaseUrl: string; + onDismiss?: () => void; } export function UpdateToast({ toastId, version, releaseUrl, + onDismiss, }: UpdateToastProps) { const openUrl = trpc.external.openUrl.useMutation(); const installUpdate = trpc.autoUpdate.installUpdate.useMutation(); @@ -27,34 +29,33 @@ export function UpdateToast({ const handleDismiss = () => { toast.dismiss(toastId); + onDismiss?.(); }; return ( -
-
- - New update available - +
+ +
+ New update available - Restart to use v{version}. + Restart to use the latest.
-
-
); } diff --git a/apps/desktop/src/renderer/components/UpdateToast/useUpdateListener.tsx b/apps/desktop/src/renderer/components/UpdateToast/useUpdateListener.tsx index 239991e34ec..248ca4666d1 100644 --- a/apps/desktop/src/renderer/components/UpdateToast/useUpdateListener.tsx +++ b/apps/desktop/src/renderer/components/UpdateToast/useUpdateListener.tsx @@ -1,5 +1,5 @@ import { toast } from "@superset/ui/sonner"; -import { useRef } from "react"; +import { useCallback, useRef } from "react"; import { trpc } from "renderer/lib/trpc"; import { UpdateToast } from "./UpdateToast"; @@ -10,6 +10,10 @@ import { UpdateToast } from "./UpdateToast"; export function useUpdateListener() { const toastIdRef = useRef(null); + const handleDismiss = useCallback(() => { + toastIdRef.current = null; + }, []); + trpc.autoUpdate.subscribe.useSubscription(undefined, { onData: (event) => { if (event.type === "update-downloaded" && event.data) { @@ -26,11 +30,13 @@ export function useUpdateListener() { toastId={id} version={version} releaseUrl={releaseUrl} + onDismiss={handleDismiss} /> ), { duration: Number.POSITIVE_INFINITY, position: "bottom-right", + unstyled: true, }, ); From ed768e97b2ec76d2b38a6e9981df49f2b4607ad3 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Thu, 25 Dec 2025 11:55:12 -0800 Subject: [PATCH 03/19] fix(desktop): link See changes to releases page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Change "See changes" button to open the main releases page instead of a specific release tag URL. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- apps/desktop/src/main/lib/auto-updater.ts | 8 +------- apps/desktop/src/main/lib/menu.ts | 2 -- .../src/renderer/components/UpdateToast/UpdateToast.tsx | 6 +++--- .../renderer/components/UpdateToast/useUpdateListener.tsx | 3 +-- 4 files changed, 5 insertions(+), 14 deletions(-) diff --git a/apps/desktop/src/main/lib/auto-updater.ts b/apps/desktop/src/main/lib/auto-updater.ts index b2b3212cf84..792e47df935 100644 --- a/apps/desktop/src/main/lib/auto-updater.ts +++ b/apps/desktop/src/main/lib/auto-updater.ts @@ -11,7 +11,6 @@ const RELEASES_URL = "https://github.com/superset-sh/superset/releases"; export interface UpdateDownloadedEvent { version: string; - releaseUrl: string; } export const autoUpdateEmitter = new EventEmitter(); @@ -106,12 +105,7 @@ export function setupAutoUpdater(): void { hasNotifiedUpdateDownloaded = true; - const event: UpdateDownloadedEvent = { - version: info.version, - releaseUrl: `${RELEASES_URL}/tag/v${info.version}`, - }; - - autoUpdateEmitter.emit("update-downloaded", event); + autoUpdateEmitter.emit("update-downloaded", { version: info.version }); }); const interval = setInterval(checkForUpdates, UPDATE_CHECK_INTERVAL_MS); diff --git a/apps/desktop/src/main/lib/menu.ts b/apps/desktop/src/main/lib/menu.ts index 99700b91f19..e5656e0f721 100644 --- a/apps/desktop/src/main/lib/menu.ts +++ b/apps/desktop/src/main/lib/menu.ts @@ -83,8 +83,6 @@ export function createApplicationMenu() { click: () => { autoUpdateEmitter.emit("update-downloaded", { version: "99.0.0-test", - releaseUrl: - "https://github.com/superset-sh/superset/releases/tag/v99.0.0-test", }); }, }, diff --git a/apps/desktop/src/renderer/components/UpdateToast/UpdateToast.tsx b/apps/desktop/src/renderer/components/UpdateToast/UpdateToast.tsx index e7c72b9995d..2d308b124ff 100644 --- a/apps/desktop/src/renderer/components/UpdateToast/UpdateToast.tsx +++ b/apps/desktop/src/renderer/components/UpdateToast/UpdateToast.tsx @@ -3,24 +3,24 @@ import { toast } from "@superset/ui/sonner"; import { HiMiniXMark } from "react-icons/hi2"; import { trpc } from "renderer/lib/trpc"; +const RELEASES_URL = "https://github.com/superset-sh/superset/releases"; + interface UpdateToastProps { toastId: string | number; version: string; - releaseUrl: string; onDismiss?: () => void; } export function UpdateToast({ toastId, version, - releaseUrl, onDismiss, }: UpdateToastProps) { const openUrl = trpc.external.openUrl.useMutation(); const installUpdate = trpc.autoUpdate.installUpdate.useMutation(); const handleSeeChanges = () => { - openUrl.mutate(releaseUrl); + openUrl.mutate(RELEASES_URL); }; const handleRestart = () => { diff --git a/apps/desktop/src/renderer/components/UpdateToast/useUpdateListener.tsx b/apps/desktop/src/renderer/components/UpdateToast/useUpdateListener.tsx index 248ca4666d1..9f19f01b9b6 100644 --- a/apps/desktop/src/renderer/components/UpdateToast/useUpdateListener.tsx +++ b/apps/desktop/src/renderer/components/UpdateToast/useUpdateListener.tsx @@ -22,14 +22,13 @@ export function useUpdateListener() { return; } - const { version, releaseUrl } = event.data; + const { version } = event.data; const toastId = toast.custom( (id) => ( ), From 2b1c66fc34ad80e0fc10dd2b0dbd65abef19c716 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Thu, 25 Dec 2025 13:28:56 -0800 Subject: [PATCH 04/19] refactor(desktop): implement status-based auto-update system MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add AUTO_UPDATE_STATUS constants (idle/checking/downloading/ready/error) - Show downloading state with version info in toast - Replace X button with "Later" button for dismissing - Change "Restart" to "Install" for clearer action - Add dismiss functionality that hides toast for current session - Emit current status immediately on subscription - Add separate Dev menu items for testing both states 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../src/lib/trpc/routers/auto-update/index.ts | 50 ++++++---- apps/desktop/src/main/lib/auto-updater.ts | 99 ++++++++++++++++--- apps/desktop/src/main/lib/menu.ts | 18 ++-- .../components/UpdateToast/UpdateToast.tsx | 81 ++++++++------- .../UpdateToast/useUpdateListener.tsx | 42 +++++--- apps/desktop/src/shared/auto-update.ts | 11 +++ 6 files changed, 208 insertions(+), 93 deletions(-) create mode 100644 apps/desktop/src/shared/auto-update.ts diff --git a/apps/desktop/src/lib/trpc/routers/auto-update/index.ts b/apps/desktop/src/lib/trpc/routers/auto-update/index.ts index 486c127b61b..12e47f7d782 100644 --- a/apps/desktop/src/lib/trpc/routers/auto-update/index.ts +++ b/apps/desktop/src/lib/trpc/routers/auto-update/index.ts @@ -1,41 +1,53 @@ import { observable } from "@trpc/server/observable"; import { + type AutoUpdateStatusEvent, autoUpdateEmitter, + dismissUpdate, + getUpdateStatus, installUpdate, - type UpdateDownloadedEvent, + simulateDownloading, + simulateUpdateReady, } from "main/lib/auto-updater"; import { publicProcedure, router } from "../.."; -export type AutoUpdateEvent = - | { - type: "update-downloaded"; - data: UpdateDownloadedEvent; - } - | { type: "update-not-available" }; - export const createAutoUpdateRouter = () => { return router({ subscribe: publicProcedure.subscription(() => { - return observable((emit) => { - const onUpdateDownloaded = (data: UpdateDownloadedEvent) => { - emit.next({ type: "update-downloaded", data }); - }; + return observable((emit) => { + // Emit current status immediately + emit.next(getUpdateStatus()); - const onUpdateNotAvailable = () => { - emit.next({ type: "update-not-available" }); + const onStatusChanged = (event: AutoUpdateStatusEvent) => { + emit.next(event); }; - autoUpdateEmitter.on("update-downloaded", onUpdateDownloaded); - autoUpdateEmitter.on("update-not-available", onUpdateNotAvailable); + autoUpdateEmitter.on("status-changed", onStatusChanged); return () => { - autoUpdateEmitter.off("update-downloaded", onUpdateDownloaded); - autoUpdateEmitter.off("update-not-available", onUpdateNotAvailable); + autoUpdateEmitter.off("status-changed", onStatusChanged); }; }); }), - installUpdate: publicProcedure.mutation(() => { + + getStatus: publicProcedure.query(() => { + return getUpdateStatus(); + }), + + install: publicProcedure.mutation(() => { installUpdate(); }), + + dismiss: publicProcedure.mutation(() => { + dismissUpdate(); + }), + + // DEV ONLY + simulateReady: publicProcedure.mutation(() => { + simulateUpdateReady(); + }), + + simulateDownloading: publicProcedure.mutation(() => { + simulateDownloading(); + }), }); }; diff --git a/apps/desktop/src/main/lib/auto-updater.ts b/apps/desktop/src/main/lib/auto-updater.ts index 792e47df935..9eaa5dc1baa 100644 --- a/apps/desktop/src/main/lib/auto-updater.ts +++ b/apps/desktop/src/main/lib/auto-updater.ts @@ -2,31 +2,67 @@ import { EventEmitter } from "node:events"; import { app, dialog } from "electron"; import { autoUpdater } from "electron-updater"; import { env } from "main/env.main"; +import { AUTO_UPDATE_STATUS, type AutoUpdateStatus } from "shared/auto-update"; import { PLATFORM } from "shared/constants"; const UPDATE_CHECK_INTERVAL_MS = 1000 * 60 * 60 * 4; // 4 hours const UPDATE_FEED_URL = "https://github.com/superset-sh/superset/releases/latest/download"; -const RELEASES_URL = "https://github.com/superset-sh/superset/releases"; -export interface UpdateDownloadedEvent { - version: string; +export interface AutoUpdateStatusEvent { + status: AutoUpdateStatus; + version?: string; + error?: string; } export const autoUpdateEmitter = new EventEmitter(); -let hasNotifiedUpdateDownloaded = false; +// Current state +let currentStatus: AutoUpdateStatus = AUTO_UPDATE_STATUS.IDLE; +let currentVersion: string | undefined; +let isDismissed = false; + +function emitStatus( + status: AutoUpdateStatus, + version?: string, + error?: string, +): void { + currentStatus = status; + currentVersion = version; + + // Don't emit if dismissed and status is ready + if (isDismissed && status === AUTO_UPDATE_STATUS.READY) { + return; + } + + autoUpdateEmitter.emit("status-changed", { status, version, error }); +} + +export function getUpdateStatus(): AutoUpdateStatusEvent { + if (isDismissed && currentStatus === AUTO_UPDATE_STATUS.READY) { + return { status: AUTO_UPDATE_STATUS.IDLE }; + } + return { status: currentStatus, version: currentVersion }; +} export function installUpdate(): void { autoUpdater.quitAndInstall(false, true); } +export function dismissUpdate(): void { + isDismissed = true; + autoUpdateEmitter.emit("status-changed", { status: AUTO_UPDATE_STATUS.IDLE }); +} + export function checkForUpdates(): void { if (env.NODE_ENV === "development" || !PLATFORM.IS_MAC) { return; } + isDismissed = false; + emitStatus(AUTO_UPDATE_STATUS.CHECKING); autoUpdater.checkForUpdates().catch((error) => { console.error("[auto-updater] Failed to check for updates:", error); + emitStatus(AUTO_UPDATE_STATUS.ERROR, undefined, error.message); }); } @@ -48,15 +84,27 @@ export function checkForUpdatesInteractive(): void { return; } + isDismissed = false; + emitStatus(AUTO_UPDATE_STATUS.CHECKING); + autoUpdater .checkForUpdates() .then((result) => { - if (!result || !result.updateInfo) { - autoUpdateEmitter.emit("update-not-available"); + if ( + !result?.updateInfo || + result.updateInfo.version === app.getVersion() + ) { + dialog.showMessageBox({ + type: "info", + title: "No Updates", + message: "You're up to date!", + detail: `Version ${app.getVersion()} is the latest version.`, + }); } }) .catch((error) => { console.error("[auto-updater] Failed to check for updates:", error); + emitStatus(AUTO_UPDATE_STATUS.ERROR, undefined, error.message); dialog.showMessageBox({ type: "error", title: "Update Error", @@ -65,6 +113,19 @@ export function checkForUpdatesInteractive(): void { }); } +// DEV ONLY: Simulate update for testing +export function simulateUpdateReady(): void { + if (env.NODE_ENV !== "development") return; + isDismissed = false; + emitStatus(AUTO_UPDATE_STATUS.READY, "99.0.0-test"); +} + +export function simulateDownloading(): void { + if (env.NODE_ENV !== "development") return; + isDismissed = false; + emitStatus(AUTO_UPDATE_STATUS.DOWNLOADING, "99.0.0-test"); +} + export function setupAutoUpdater(): void { if (env.NODE_ENV === "development" || !PLATFORM.IS_MAC) { return; @@ -81,31 +142,37 @@ export function setupAutoUpdater(): void { autoUpdater.on("error", (error) => { console.error("[auto-updater] Error during update check:", error); + emitStatus(AUTO_UPDATE_STATUS.ERROR, undefined, error.message); + }); + + autoUpdater.on("checking-for-update", () => { + console.info("[auto-updater] Checking for updates..."); + emitStatus(AUTO_UPDATE_STATUS.CHECKING); }); autoUpdater.on("update-available", (info) => { console.info( `[auto-updater] Update available: ${info.version}. Downloading...`, ); + emitStatus(AUTO_UPDATE_STATUS.DOWNLOADING, info.version); }); autoUpdater.on("update-not-available", () => { console.info("[auto-updater] No updates available"); + emitStatus(AUTO_UPDATE_STATUS.IDLE); }); - autoUpdater.on("update-downloaded", (info) => { - if (hasNotifiedUpdateDownloaded) { - console.info("[auto-updater] Already notified about update, skipping"); - return; - } - + autoUpdater.on("download-progress", (progress) => { console.info( - `[auto-updater] Update downloaded (${info.version}). Notifying renderer.`, + `[auto-updater] Download progress: ${progress.percent.toFixed(1)}%`, ); + }); - hasNotifiedUpdateDownloaded = true; - - autoUpdateEmitter.emit("update-downloaded", { version: info.version }); + autoUpdater.on("update-downloaded", (info) => { + console.info( + `[auto-updater] Update downloaded (${info.version}). Ready to install.`, + ); + emitStatus(AUTO_UPDATE_STATUS.READY, info.version); }); const interval = setInterval(checkForUpdates, UPDATE_CHECK_INTERVAL_MS); diff --git a/apps/desktop/src/main/lib/menu.ts b/apps/desktop/src/main/lib/menu.ts index e5656e0f721..d90504bf0e4 100644 --- a/apps/desktop/src/main/lib/menu.ts +++ b/apps/desktop/src/main/lib/menu.ts @@ -1,7 +1,11 @@ import { COMPANY } from "@superset/shared/constants"; import { app, Menu, shell } from "electron"; import { env } from "main/env.main"; -import { autoUpdateEmitter, checkForUpdatesInteractive } from "./auto-updater"; +import { + checkForUpdatesInteractive, + simulateDownloading, + simulateUpdateReady, +} from "./auto-updater"; import { menuEmitter } from "./menu-events"; export function createApplicationMenu() { @@ -79,12 +83,12 @@ export function createApplicationMenu() { label: "Dev", submenu: [ { - label: "Simulate Update Available", - click: () => { - autoUpdateEmitter.emit("update-downloaded", { - version: "99.0.0-test", - }); - }, + label: "Simulate Update Downloading", + click: () => simulateDownloading(), + }, + { + label: "Simulate Update Ready", + click: () => simulateUpdateReady(), }, ], }); diff --git a/apps/desktop/src/renderer/components/UpdateToast/UpdateToast.tsx b/apps/desktop/src/renderer/components/UpdateToast/UpdateToast.tsx index 2d308b124ff..b17d7c734ba 100644 --- a/apps/desktop/src/renderer/components/UpdateToast/UpdateToast.tsx +++ b/apps/desktop/src/renderer/components/UpdateToast/UpdateToast.tsx @@ -1,61 +1,72 @@ import { Button } from "@superset/ui/button"; import { toast } from "@superset/ui/sonner"; -import { HiMiniXMark } from "react-icons/hi2"; import { trpc } from "renderer/lib/trpc"; +import { AUTO_UPDATE_STATUS } from "shared/auto-update"; const RELEASES_URL = "https://github.com/superset-sh/superset/releases"; interface UpdateToastProps { toastId: string | number; - version: string; - onDismiss?: () => void; + status: "downloading" | "ready"; + version?: string; } -export function UpdateToast({ - toastId, - version, - onDismiss, -}: UpdateToastProps) { +export function UpdateToast({ toastId, status, version }: UpdateToastProps) { const openUrl = trpc.external.openUrl.useMutation(); - const installUpdate = trpc.autoUpdate.installUpdate.useMutation(); + const installMutation = trpc.autoUpdate.install.useMutation(); + const dismissMutation = trpc.autoUpdate.dismiss.useMutation({ + onSuccess: () => { + toast.dismiss(toastId); + }, + }); + + const isDownloading = status === AUTO_UPDATE_STATUS.DOWNLOADING; + const isReady = status === AUTO_UPDATE_STATUS.READY; const handleSeeChanges = () => { openUrl.mutate(RELEASES_URL); }; - const handleRestart = () => { - installUpdate.mutate(); + const handleInstall = () => { + installMutation.mutate(); }; - const handleDismiss = () => { - toast.dismiss(toastId); - onDismiss?.(); + const handleLater = () => { + dismissMutation.mutate(); }; return ( -
- +
- New update available - - Restart to use the latest. - -
-
- - + {isDownloading ? ( + <> + Downloading update... + + {version ? `Version ${version}` : "Please wait"} + + + ) : ( + <> + Update available + + {version ? `Version ${version} is ready to install` : "Ready to install"} + + + )}
+ {isReady && ( +
+ + + +
+ )}
); } diff --git a/apps/desktop/src/renderer/components/UpdateToast/useUpdateListener.tsx b/apps/desktop/src/renderer/components/UpdateToast/useUpdateListener.tsx index 9f19f01b9b6..cba9b99e898 100644 --- a/apps/desktop/src/renderer/components/UpdateToast/useUpdateListener.tsx +++ b/apps/desktop/src/renderer/components/UpdateToast/useUpdateListener.tsx @@ -1,35 +1,49 @@ import { toast } from "@superset/ui/sonner"; -import { useCallback, useRef } from "react"; +import { useRef } from "react"; import { trpc } from "renderer/lib/trpc"; +import { AUTO_UPDATE_STATUS } from "shared/auto-update"; import { UpdateToast } from "./UpdateToast"; /** - * Hook that listens for auto-update events via tRPC subscription. - * Shows a toast notification when an update has been downloaded. + * Hook that listens for auto-update status changes via tRPC subscription. + * Shows a toast notification when downloading or ready to install. */ export function useUpdateListener() { const toastIdRef = useRef(null); - const handleDismiss = useCallback(() => { - toastIdRef.current = null; - }, []); - trpc.autoUpdate.subscribe.useSubscription(undefined, { onData: (event) => { - if (event.type === "update-downloaded" && event.data) { - // Don't show duplicate toasts + const { status, version } = event; + + // Dismiss existing toast if status changed to idle/checking/error + if ( + status === AUTO_UPDATE_STATUS.IDLE || + status === AUTO_UPDATE_STATUS.CHECKING || + status === AUTO_UPDATE_STATUS.ERROR + ) { if (toastIdRef.current !== null) { - return; + toast.dismiss(toastIdRef.current); + toastIdRef.current = null; } + return; + } - const { version } = event.data; + // Show toast for downloading or ready states + if ( + status === AUTO_UPDATE_STATUS.DOWNLOADING || + status === AUTO_UPDATE_STATUS.READY + ) { + // Dismiss existing toast before showing new one + if (toastIdRef.current !== null) { + toast.dismiss(toastIdRef.current); + } const toastId = toast.custom( (id) => ( ), { @@ -40,10 +54,6 @@ export function useUpdateListener() { ); toastIdRef.current = toastId; - } else if (event.type === "update-not-available") { - toast.info("You're on the latest version", { - position: "bottom-right", - }); } }, }); diff --git a/apps/desktop/src/shared/auto-update.ts b/apps/desktop/src/shared/auto-update.ts new file mode 100644 index 00000000000..2d737babb3e --- /dev/null +++ b/apps/desktop/src/shared/auto-update.ts @@ -0,0 +1,11 @@ +// Auto-update status values +export const AUTO_UPDATE_STATUS = { + IDLE: "idle", + CHECKING: "checking", + DOWNLOADING: "downloading", + READY: "ready", + ERROR: "error", +} as const; + +export type AutoUpdateStatus = + (typeof AUTO_UPDATE_STATUS)[keyof typeof AUTO_UPDATE_STATUS]; From 4ae4e5cf52ca79621c395a5f599fb512311a54f7 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Thu, 25 Dec 2025 13:30:19 -0800 Subject: [PATCH 05/19] fix(desktop): stack update toast text above buttons MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../src/renderer/components/UpdateToast/UpdateToast.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/desktop/src/renderer/components/UpdateToast/UpdateToast.tsx b/apps/desktop/src/renderer/components/UpdateToast/UpdateToast.tsx index b17d7c734ba..bd1152a24b0 100644 --- a/apps/desktop/src/renderer/components/UpdateToast/UpdateToast.tsx +++ b/apps/desktop/src/renderer/components/UpdateToast/UpdateToast.tsx @@ -36,8 +36,8 @@ export function UpdateToast({ toastId, status, version }: UpdateToastProps) { }; return ( -
-
+
+
{isDownloading ? ( <> Downloading update... @@ -55,7 +55,7 @@ export function UpdateToast({ toastId, status, version }: UpdateToastProps) { )}
{isReady && ( -
+
From 86aae87da6201afdce929ae75a9010efbae08c28 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Thu, 25 Dec 2025 13:31:40 -0800 Subject: [PATCH 06/19] fix(desktop): disable sonner toast hover animation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- apps/desktop/src/renderer/globals.css | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/apps/desktop/src/renderer/globals.css b/apps/desktop/src/renderer/globals.css index aaeac15edbb..b2f373f2a65 100644 --- a/apps/desktop/src/renderer/globals.css +++ b/apps/desktop/src/renderer/globals.css @@ -251,4 +251,9 @@ .no-drag { -webkit-app-region: no-drag; } + + /* Disable sonner toast hover animation */ + [data-sonner-toast]:hover { + transform: none !important; + } } From ee837aecbbbeef3290b57e96a8ea300f41c594fc Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Thu, 25 Dec 2025 13:32:30 -0800 Subject: [PATCH 07/19] fix(desktop): scope hover animation fix to update toast only MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../src/renderer/components/UpdateToast/UpdateToast.tsx | 2 +- apps/desktop/src/renderer/globals.css | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/desktop/src/renderer/components/UpdateToast/UpdateToast.tsx b/apps/desktop/src/renderer/components/UpdateToast/UpdateToast.tsx index bd1152a24b0..bc31a3bca67 100644 --- a/apps/desktop/src/renderer/components/UpdateToast/UpdateToast.tsx +++ b/apps/desktop/src/renderer/components/UpdateToast/UpdateToast.tsx @@ -36,7 +36,7 @@ export function UpdateToast({ toastId, status, version }: UpdateToastProps) { }; return ( -
+
{isDownloading ? ( <> diff --git a/apps/desktop/src/renderer/globals.css b/apps/desktop/src/renderer/globals.css index b2f373f2a65..ac8966eebfe 100644 --- a/apps/desktop/src/renderer/globals.css +++ b/apps/desktop/src/renderer/globals.css @@ -252,8 +252,8 @@ -webkit-app-region: no-drag; } - /* Disable sonner toast hover animation */ - [data-sonner-toast]:hover { + /* Disable sonner toast hover animation for update toast only */ + [data-sonner-toast]:has(.update-toast):hover { transform: none !important; } } From 6c90eacd9040f5109e6d1b9c677025fbeac84cc3 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Thu, 25 Dec 2025 13:35:17 -0800 Subject: [PATCH 08/19] feat(desktop): add Later button to downloading toast state MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../components/UpdateToast/UpdateToast.tsx | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/apps/desktop/src/renderer/components/UpdateToast/UpdateToast.tsx b/apps/desktop/src/renderer/components/UpdateToast/UpdateToast.tsx index bc31a3bca67..ed0fedc8648 100644 --- a/apps/desktop/src/renderer/components/UpdateToast/UpdateToast.tsx +++ b/apps/desktop/src/renderer/components/UpdateToast/UpdateToast.tsx @@ -54,19 +54,21 @@ export function UpdateToast({ toastId, status, version }: UpdateToastProps) { )}
- {isReady && ( -
+
+ {isReady && ( - + )} + + {isReady && ( -
- )} + )} +
); } From d66a0eab45f187d033d220b9877c90a662259405 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Thu, 25 Dec 2025 13:36:27 -0800 Subject: [PATCH 09/19] fix(desktop): use X button for downloading, Later button for ready MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../components/UpdateToast/UpdateToast.tsx | 27 ++++++++++++------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/apps/desktop/src/renderer/components/UpdateToast/UpdateToast.tsx b/apps/desktop/src/renderer/components/UpdateToast/UpdateToast.tsx index ed0fedc8648..860714414de 100644 --- a/apps/desktop/src/renderer/components/UpdateToast/UpdateToast.tsx +++ b/apps/desktop/src/renderer/components/UpdateToast/UpdateToast.tsx @@ -1,5 +1,6 @@ import { Button } from "@superset/ui/button"; import { toast } from "@superset/ui/sonner"; +import { HiMiniXMark } from "react-icons/hi2"; import { trpc } from "renderer/lib/trpc"; import { AUTO_UPDATE_STATUS } from "shared/auto-update"; @@ -37,6 +38,16 @@ export function UpdateToast({ toastId, status, version }: UpdateToastProps) { return (
+ {isDownloading && ( + + )}
{isDownloading ? ( <> @@ -54,21 +65,19 @@ export function UpdateToast({ toastId, status, version }: UpdateToastProps) { )}
-
- {isReady && ( + {isReady && ( +
- )} - - {isReady && ( + - )} -
+
+ )}
); } From 79e3f965ba05c3730a2bf5b6225b26202acc55dd Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Thu, 25 Dec 2025 13:43:09 -0800 Subject: [PATCH 10/19] fix(desktop): remove transition from X button hover MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../desktop/src/renderer/components/UpdateToast/UpdateToast.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/desktop/src/renderer/components/UpdateToast/UpdateToast.tsx b/apps/desktop/src/renderer/components/UpdateToast/UpdateToast.tsx index 860714414de..e49c91f4dab 100644 --- a/apps/desktop/src/renderer/components/UpdateToast/UpdateToast.tsx +++ b/apps/desktop/src/renderer/components/UpdateToast/UpdateToast.tsx @@ -42,7 +42,7 @@ export function UpdateToast({ toastId, status, version }: UpdateToastProps) { - )} +
{isDownloading ? ( <> @@ -68,9 +66,6 @@ export function UpdateToast({ toastId, status, version }: UpdateToastProps) { - From 3ed3eab1bd3d6ecef3e242d52562347c8c9ce719 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Thu, 25 Dec 2025 13:52:07 -0800 Subject: [PATCH 14/19] remove slop comments --- apps/desktop/src/lib/trpc/routers/auto-update/index.ts | 2 -- apps/desktop/src/main/lib/auto-updater.ts | 3 --- apps/desktop/src/main/lib/local-db/index.ts | 1 + .../renderer/components/UpdateToast/useUpdateListener.tsx | 7 ------- apps/desktop/src/shared/auto-update.ts | 1 - 5 files changed, 1 insertion(+), 13 deletions(-) diff --git a/apps/desktop/src/lib/trpc/routers/auto-update/index.ts b/apps/desktop/src/lib/trpc/routers/auto-update/index.ts index 12e47f7d782..113e4016603 100644 --- a/apps/desktop/src/lib/trpc/routers/auto-update/index.ts +++ b/apps/desktop/src/lib/trpc/routers/auto-update/index.ts @@ -14,7 +14,6 @@ export const createAutoUpdateRouter = () => { return router({ subscribe: publicProcedure.subscription(() => { return observable((emit) => { - // Emit current status immediately emit.next(getUpdateStatus()); const onStatusChanged = (event: AutoUpdateStatusEvent) => { @@ -41,7 +40,6 @@ export const createAutoUpdateRouter = () => { dismissUpdate(); }), - // DEV ONLY simulateReady: publicProcedure.mutation(() => { simulateUpdateReady(); }), diff --git a/apps/desktop/src/main/lib/auto-updater.ts b/apps/desktop/src/main/lib/auto-updater.ts index 9eaa5dc1baa..6c43ce573d2 100644 --- a/apps/desktop/src/main/lib/auto-updater.ts +++ b/apps/desktop/src/main/lib/auto-updater.ts @@ -17,7 +17,6 @@ export interface AutoUpdateStatusEvent { export const autoUpdateEmitter = new EventEmitter(); -// Current state let currentStatus: AutoUpdateStatus = AUTO_UPDATE_STATUS.IDLE; let currentVersion: string | undefined; let isDismissed = false; @@ -30,7 +29,6 @@ function emitStatus( currentStatus = status; currentVersion = version; - // Don't emit if dismissed and status is ready if (isDismissed && status === AUTO_UPDATE_STATUS.READY) { return; } @@ -113,7 +111,6 @@ export function checkForUpdatesInteractive(): void { }); } -// DEV ONLY: Simulate update for testing export function simulateUpdateReady(): void { if (env.NODE_ENV !== "development") return; isDismissed = false; diff --git a/apps/desktop/src/main/lib/local-db/index.ts b/apps/desktop/src/main/lib/local-db/index.ts index d0f86055a5b..238eeed0207 100644 --- a/apps/desktop/src/main/lib/local-db/index.ts +++ b/apps/desktop/src/main/lib/local-db/index.ts @@ -19,6 +19,7 @@ function ensureAppHomeDirExists() { mkdirSync(SUPERSET_HOME_DIR, { recursive: true }); } ensureAppHomeDirExists(); + /** * Gets the migrations directory path. * diff --git a/apps/desktop/src/renderer/components/UpdateToast/useUpdateListener.tsx b/apps/desktop/src/renderer/components/UpdateToast/useUpdateListener.tsx index cba9b99e898..69abc323d64 100644 --- a/apps/desktop/src/renderer/components/UpdateToast/useUpdateListener.tsx +++ b/apps/desktop/src/renderer/components/UpdateToast/useUpdateListener.tsx @@ -4,10 +4,6 @@ import { trpc } from "renderer/lib/trpc"; import { AUTO_UPDATE_STATUS } from "shared/auto-update"; import { UpdateToast } from "./UpdateToast"; -/** - * Hook that listens for auto-update status changes via tRPC subscription. - * Shows a toast notification when downloading or ready to install. - */ export function useUpdateListener() { const toastIdRef = useRef(null); @@ -15,7 +11,6 @@ export function useUpdateListener() { onData: (event) => { const { status, version } = event; - // Dismiss existing toast if status changed to idle/checking/error if ( status === AUTO_UPDATE_STATUS.IDLE || status === AUTO_UPDATE_STATUS.CHECKING || @@ -28,12 +23,10 @@ export function useUpdateListener() { return; } - // Show toast for downloading or ready states if ( status === AUTO_UPDATE_STATUS.DOWNLOADING || status === AUTO_UPDATE_STATUS.READY ) { - // Dismiss existing toast before showing new one if (toastIdRef.current !== null) { toast.dismiss(toastIdRef.current); } diff --git a/apps/desktop/src/shared/auto-update.ts b/apps/desktop/src/shared/auto-update.ts index 0af0b0cf9fe..e9075c7c5fa 100644 --- a/apps/desktop/src/shared/auto-update.ts +++ b/apps/desktop/src/shared/auto-update.ts @@ -1,4 +1,3 @@ -// Auto-update status values export const AUTO_UPDATE_STATUS = { IDLE: "idle", CHECKING: "checking", From 20cdc029e9d31689a3a3c517018f40d6e76f6689 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Thu, 25 Dec 2025 13:54:25 -0800 Subject: [PATCH 15/19] fix lint --- .../renderer/components/UpdateToast/UpdateToast.tsx | 10 ++++++++-- .../components/UpdateToast/useUpdateListener.tsx | 6 +----- apps/desktop/src/renderer/globals.css | 2 +- apps/desktop/src/shared/constants.ts | 4 ---- 4 files changed, 10 insertions(+), 12 deletions(-) diff --git a/apps/desktop/src/renderer/components/UpdateToast/UpdateToast.tsx b/apps/desktop/src/renderer/components/UpdateToast/UpdateToast.tsx index 81c15f1e3fd..f15ae97aa17 100644 --- a/apps/desktop/src/renderer/components/UpdateToast/UpdateToast.tsx +++ b/apps/desktop/src/renderer/components/UpdateToast/UpdateToast.tsx @@ -56,7 +56,9 @@ export function UpdateToast({ toastId, status, version }: UpdateToastProps) { <> Update available - {version ? `Version ${version} is ready to install` : "Ready to install"} + {version + ? `Version ${version} is ready to install` + : "Ready to install"} )} @@ -66,7 +68,11 @@ export function UpdateToast({ toastId, status, version }: UpdateToastProps) { -
diff --git a/apps/desktop/src/renderer/components/UpdateToast/useUpdateListener.tsx b/apps/desktop/src/renderer/components/UpdateToast/useUpdateListener.tsx index 69abc323d64..2ac2cffe73c 100644 --- a/apps/desktop/src/renderer/components/UpdateToast/useUpdateListener.tsx +++ b/apps/desktop/src/renderer/components/UpdateToast/useUpdateListener.tsx @@ -33,11 +33,7 @@ export function useUpdateListener() { const toastId = toast.custom( (id) => ( - + ), { duration: Number.POSITIVE_INFINITY, diff --git a/apps/desktop/src/renderer/globals.css b/apps/desktop/src/renderer/globals.css index 6430cf29666..a70939dc814 100644 --- a/apps/desktop/src/renderer/globals.css +++ b/apps/desktop/src/renderer/globals.css @@ -254,6 +254,6 @@ /* Disable sonner toast hover animation for update toast only */ [data-sonner-toast]:has(.update-toast) { - transform: translateY(0) !important; + transform: translateY(0); } } diff --git a/apps/desktop/src/shared/constants.ts b/apps/desktop/src/shared/constants.ts index 6c93528bf10..7d8a43f569a 100644 --- a/apps/desktop/src/shared/constants.ts +++ b/apps/desktop/src/shared/constants.ts @@ -9,9 +9,7 @@ export const PLATFORM = { // Ports - different for dev vs prod to allow running both simultaneously export const PORTS = { - // Vite dev server port VITE_DEV_SERVER: env.NODE_ENV === "development" ? 5927 : 4927, - // Notification HTTP server port NOTIFICATIONS: env.NODE_ENV === "development" ? 31416 : 31415, }; @@ -34,13 +32,11 @@ export const PROJECT_SUPERSET_DIR_NAME = ".superset"; export const WORKTREES_DIR_NAME = "worktrees"; export const CONFIG_FILE_NAME = "config.json"; -// Config file template export const CONFIG_TEMPLATE = `{ "setup": [], "teardown": [] }`; -// Notification event types export const NOTIFICATION_EVENTS = { AGENT_COMPLETE: "agent-complete", FOCUS_TAB: "focus-tab", From 1d6bb986f6fc349c02126e5e404562404331c27f Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Thu, 25 Dec 2025 14:00:18 -0800 Subject: [PATCH 16/19] chore: add co-author Co-Authored-By: Avi Peltz From e316d3711c85170e65c639592ba7c30aaa6da82e Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Thu, 25 Dec 2025 21:53:45 -0800 Subject: [PATCH 17/19] fix(desktop): emit IDLE status when no updates available MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When checkForUpdatesInteractive finds no updates, emit IDLE status before showing the dialog. This prevents currentStatus from being stuck at CHECKING, which could cause inconsistent UI state. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- apps/desktop/src/main/lib/auto-updater.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/desktop/src/main/lib/auto-updater.ts b/apps/desktop/src/main/lib/auto-updater.ts index 6c43ce573d2..6471f1c9ded 100644 --- a/apps/desktop/src/main/lib/auto-updater.ts +++ b/apps/desktop/src/main/lib/auto-updater.ts @@ -92,6 +92,7 @@ export function checkForUpdatesInteractive(): void { !result?.updateInfo || result.updateInfo.version === app.getVersion() ) { + emitStatus(AUTO_UPDATE_STATUS.IDLE); dialog.showMessageBox({ type: "info", title: "No Updates", From 9ba59da1f6062eed5c2dbbf410a6906fd7684714 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Thu, 25 Dec 2025 21:56:11 -0800 Subject: [PATCH 18/19] fix(desktop): skip quitAndInstall in dev mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In dev mode, clicking Install on a simulated update would call quitAndInstall with no real update downloaded. Now it just logs a message and dismisses the toast. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- apps/desktop/src/main/lib/auto-updater.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/apps/desktop/src/main/lib/auto-updater.ts b/apps/desktop/src/main/lib/auto-updater.ts index 6471f1c9ded..98e7d0112b9 100644 --- a/apps/desktop/src/main/lib/auto-updater.ts +++ b/apps/desktop/src/main/lib/auto-updater.ts @@ -44,6 +44,11 @@ export function getUpdateStatus(): AutoUpdateStatusEvent { } export function installUpdate(): void { + if (env.NODE_ENV === "development") { + console.info("[auto-updater] Install skipped in dev mode"); + emitStatus(AUTO_UPDATE_STATUS.IDLE); + return; + } autoUpdater.quitAndInstall(false, true); } From 4bfd942efa49df618c04356e09b42386aa8ce30f Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Thu, 25 Dec 2025 22:18:34 -0800 Subject: [PATCH 19/19] feat(desktop): show error state in update toast MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add error status handling to UpdateToast component - Show "Update failed" with error message in toast UI - Add simulateError function for dev testing - Add "Simulate Update Error" option to Dev menu 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../src/lib/trpc/routers/auto-update/index.ts | 5 +++++ apps/desktop/src/main/lib/auto-updater.ts | 10 +++++++++ apps/desktop/src/main/lib/menu.ts | 5 +++++ .../components/UpdateToast/UpdateToast.tsx | 22 ++++++++++++++++--- .../UpdateToast/useUpdateListener.tsx | 15 ++++++++----- 5 files changed, 49 insertions(+), 8 deletions(-) diff --git a/apps/desktop/src/lib/trpc/routers/auto-update/index.ts b/apps/desktop/src/lib/trpc/routers/auto-update/index.ts index 113e4016603..003f69a7e7d 100644 --- a/apps/desktop/src/lib/trpc/routers/auto-update/index.ts +++ b/apps/desktop/src/lib/trpc/routers/auto-update/index.ts @@ -6,6 +6,7 @@ import { getUpdateStatus, installUpdate, simulateDownloading, + simulateError, simulateUpdateReady, } from "main/lib/auto-updater"; import { publicProcedure, router } from "../.."; @@ -47,5 +48,9 @@ export const createAutoUpdateRouter = () => { simulateDownloading: publicProcedure.mutation(() => { simulateDownloading(); }), + + simulateError: publicProcedure.mutation(() => { + simulateError(); + }), }); }; diff --git a/apps/desktop/src/main/lib/auto-updater.ts b/apps/desktop/src/main/lib/auto-updater.ts index 98e7d0112b9..0bc07423b84 100644 --- a/apps/desktop/src/main/lib/auto-updater.ts +++ b/apps/desktop/src/main/lib/auto-updater.ts @@ -129,6 +129,16 @@ export function simulateDownloading(): void { emitStatus(AUTO_UPDATE_STATUS.DOWNLOADING, "99.0.0-test"); } +export function simulateError(): void { + if (env.NODE_ENV !== "development") return; + isDismissed = false; + emitStatus( + AUTO_UPDATE_STATUS.ERROR, + undefined, + "Simulated error for testing", + ); +} + export function setupAutoUpdater(): void { if (env.NODE_ENV === "development" || !PLATFORM.IS_MAC) { return; diff --git a/apps/desktop/src/main/lib/menu.ts b/apps/desktop/src/main/lib/menu.ts index d90504bf0e4..8c7ccd1eb4a 100644 --- a/apps/desktop/src/main/lib/menu.ts +++ b/apps/desktop/src/main/lib/menu.ts @@ -4,6 +4,7 @@ import { env } from "main/env.main"; import { checkForUpdatesInteractive, simulateDownloading, + simulateError, simulateUpdateReady, } from "./auto-updater"; import { menuEmitter } from "./menu-events"; @@ -90,6 +91,10 @@ export function createApplicationMenu() { label: "Simulate Update Ready", click: () => simulateUpdateReady(), }, + { + label: "Simulate Update Error", + click: () => simulateError(), + }, ], }); } diff --git a/apps/desktop/src/renderer/components/UpdateToast/UpdateToast.tsx b/apps/desktop/src/renderer/components/UpdateToast/UpdateToast.tsx index f15ae97aa17..0d6f8946b3c 100644 --- a/apps/desktop/src/renderer/components/UpdateToast/UpdateToast.tsx +++ b/apps/desktop/src/renderer/components/UpdateToast/UpdateToast.tsx @@ -6,11 +6,17 @@ import { AUTO_UPDATE_STATUS, RELEASES_URL } from "shared/auto-update"; interface UpdateToastProps { toastId: string | number; - status: "downloading" | "ready"; + status: "downloading" | "ready" | "error"; version?: string; + error?: string; } -export function UpdateToast({ toastId, status, version }: UpdateToastProps) { +export function UpdateToast({ + toastId, + status, + version, + error, +}: UpdateToastProps) { const openUrl = trpc.external.openUrl.useMutation(); const installMutation = trpc.autoUpdate.install.useMutation(); const dismissMutation = trpc.autoUpdate.dismiss.useMutation({ @@ -21,6 +27,7 @@ export function UpdateToast({ toastId, status, version }: UpdateToastProps) { const isDownloading = status === AUTO_UPDATE_STATUS.DOWNLOADING; const isReady = status === AUTO_UPDATE_STATUS.READY; + const isError = status === AUTO_UPDATE_STATUS.ERROR; const handleSeeChanges = () => { openUrl.mutate(RELEASES_URL); @@ -45,7 +52,16 @@ export function UpdateToast({ toastId, status, version }: UpdateToastProps) {
- {isDownloading ? ( + {isError ? ( + <> + + Update failed + + + {error || "Please try again later"} + + + ) : isDownloading ? ( <> Downloading update... diff --git a/apps/desktop/src/renderer/components/UpdateToast/useUpdateListener.tsx b/apps/desktop/src/renderer/components/UpdateToast/useUpdateListener.tsx index 2ac2cffe73c..34c8584cee0 100644 --- a/apps/desktop/src/renderer/components/UpdateToast/useUpdateListener.tsx +++ b/apps/desktop/src/renderer/components/UpdateToast/useUpdateListener.tsx @@ -9,12 +9,11 @@ export function useUpdateListener() { trpc.autoUpdate.subscribe.useSubscription(undefined, { onData: (event) => { - const { status, version } = event; + const { status, version, error } = event; if ( status === AUTO_UPDATE_STATUS.IDLE || - status === AUTO_UPDATE_STATUS.CHECKING || - status === AUTO_UPDATE_STATUS.ERROR + status === AUTO_UPDATE_STATUS.CHECKING ) { if (toastIdRef.current !== null) { toast.dismiss(toastIdRef.current); @@ -25,7 +24,8 @@ export function useUpdateListener() { if ( status === AUTO_UPDATE_STATUS.DOWNLOADING || - status === AUTO_UPDATE_STATUS.READY + status === AUTO_UPDATE_STATUS.READY || + status === AUTO_UPDATE_STATUS.ERROR ) { if (toastIdRef.current !== null) { toast.dismiss(toastIdRef.current); @@ -33,7 +33,12 @@ export function useUpdateListener() { const toastId = toast.custom( (id) => ( - + ), { duration: Number.POSITIVE_INFINITY,