diff --git a/src/main/services/infrastructure/UpdaterService.ts b/src/main/services/infrastructure/UpdaterService.ts index 9fb74ee4..51cc4f63 100644 --- a/src/main/services/infrastructure/UpdaterService.ts +++ b/src/main/services/infrastructure/UpdaterService.ts @@ -18,6 +18,7 @@ const logger = createLogger('UpdaterService'); export class UpdaterService { private mainWindow: BrowserWindow | null = null; + private periodicTimer: ReturnType | null = null; constructor() { autoUpdater.autoDownload = false; @@ -66,6 +67,27 @@ export class UpdaterService { autoUpdater.quitAndInstall(true, true); } + /** + * Start periodic update checks at the given interval (default: 1 hour). + * Uses unref() so the timer does not prevent process exit. + */ + startPeriodicCheck(intervalMs: number = 3_600_000): void { + this.stopPeriodicCheck(); + this.periodicTimer = setInterval(() => void this.checkForUpdates(), intervalMs); + this.periodicTimer.unref(); + logger.info(`Periodic update check started (interval: ${Math.round(intervalMs / 60_000)}min)`); + } + + /** + * Stop periodic update checks. + */ + stopPeriodicCheck(): void { + if (this.periodicTimer !== null) { + clearInterval(this.periodicTimer); + this.periodicTimer = null; + } + } + private sendStatus(status: UpdaterStatus): void { if (this.mainWindow && !this.mainWindow.isDestroyed()) { this.mainWindow.webContents.send('updater:status', status); diff --git a/src/renderer/components/common/UpdateDialog.tsx b/src/renderer/components/common/UpdateDialog.tsx index 8cad8199..f2f09656 100644 --- a/src/renderer/components/common/UpdateDialog.tsx +++ b/src/renderer/components/common/UpdateDialog.tsx @@ -3,22 +3,26 @@ * * Prompts the user to download the update or dismiss it. * Release notes (markdown from GitHub) are rendered with ReactMarkdown. + * Shows "Restart now" when the update has already been downloaded. */ import { useEffect, useRef } from 'react'; import ReactMarkdown from 'react-markdown'; +import { isElectronMode } from '@renderer/api'; import { markdownComponents } from '@renderer/components/chat/markdownComponents'; import { useStore } from '@renderer/store'; import { REHYPE_PLUGINS } from '@renderer/utils/markdownPlugins'; -import { X } from 'lucide-react'; +import { ExternalLink, X } from 'lucide-react'; import remarkGfm from 'remark-gfm'; export const UpdateDialog = (): React.JSX.Element | null => { const showUpdateDialog = useStore((s) => s.showUpdateDialog); + const updateStatus = useStore((s) => s.updateStatus); const availableVersion = useStore((s) => s.availableVersion); const releaseNotes = useStore((s) => s.releaseNotes); const downloadUpdate = useStore((s) => s.downloadUpdate); + const installUpdate = useStore((s) => s.installUpdate); const dismissUpdateDialog = useStore((s) => s.dismissUpdateDialog); const dialogRef = useRef(null); @@ -75,6 +79,21 @@ export const UpdateDialog = (): React.JSX.Element | null => { if (!showUpdateDialog) return null; + const isDownloaded = updateStatus === 'downloaded'; + + const releaseUrl = availableVersion + ? `https://github.com/777genius/claude_agent_teams_ui/releases/tag/v${availableVersion}` + : null; + + const openReleaseOnGitHub = (): void => { + if (!releaseUrl) return; + if (isElectronMode()) { + void window.electronAPI.openExternal(releaseUrl); + } else { + window.open(releaseUrl, '_blank'); + } + }; + return (
{/* Backdrop */} @@ -87,7 +106,7 @@ export const UpdateDialog = (): React.JSX.Element | null => { />
{

- Update Available + {isDownloaded ? 'Update Ready' : 'Update Available'}

{availableVersion && ( -
+
v{availableVersion}
)}
{/* Release notes */} - {releaseNotes && ( -
+
+ {releaseNotes ? ( { > {releaseNotes} -
- )} + ) : ( +

+ No release notes available. +

+ )} +
{/* Actions */} -
+
+ {releaseUrl && ( + + )} +
- + {isDownloaded ? ( + + ) : ( + + )}
diff --git a/src/renderer/components/dashboard/DashboardUpdateBanner.tsx b/src/renderer/components/dashboard/DashboardUpdateBanner.tsx new file mode 100644 index 00000000..598049fd --- /dev/null +++ b/src/renderer/components/dashboard/DashboardUpdateBanner.tsx @@ -0,0 +1,84 @@ +/** + * DashboardUpdateBanner - Compact banner on the dashboard when a new app version is available. + * + * Single-line banner: icon + "New version available vX.Y.Z" + action button + dismiss X. + * Dismissible with localStorage persistence keyed by version — + * dismissed for v1.3.0 won't suppress the banner when v1.4.0 arrives. + */ + +import { useEffect, useState } from 'react'; + +import { useStore } from '@renderer/store'; +import { ArrowUpCircle, X } from 'lucide-react'; +import { useShallow } from 'zustand/react/shallow'; + +const DISMISSED_KEY = 'update:dashboard-dismissed-version'; + +export const DashboardUpdateBanner = (): React.JSX.Element | null => { + const { updateStatus, availableVersion, openUpdateDialog, installUpdate } = useStore( + useShallow((s) => ({ + updateStatus: s.updateStatus, + availableVersion: s.availableVersion, + openUpdateDialog: s.openUpdateDialog, + installUpdate: s.installUpdate, + })) + ); + + const [dismissed, setDismissed] = useState(() => { + const saved = localStorage.getItem(DISMISSED_KEY); + return saved === availableVersion; + }); + + // Reset dismissed state when a new version becomes available + useEffect(() => { + const saved = localStorage.getItem(DISMISSED_KEY); + setDismissed(saved === availableVersion); + }, [availableVersion]); + + if (dismissed) return null; + if (updateStatus !== 'available' && updateStatus !== 'downloaded') return null; + + const handleDismiss = (): void => { + if (availableVersion) { + localStorage.setItem(DISMISSED_KEY, availableVersion); + } + setDismissed(true); + }; + + const isDownloaded = updateStatus === 'downloaded'; + + return ( +
+ + + New version available{' '} + {availableVersion && ( + v{availableVersion} + )} + + + +
+ ); +}; diff --git a/src/renderer/components/dashboard/DashboardView.tsx b/src/renderer/components/dashboard/DashboardView.tsx index 80b8c0a7..ce2bfd9e 100644 --- a/src/renderer/components/dashboard/DashboardView.tsx +++ b/src/renderer/components/dashboard/DashboardView.tsx @@ -30,6 +30,7 @@ import { formatDistanceToNow } from 'date-fns'; import { Command, FolderGit2, FolderOpen, GitBranch, GitFork, Search, Users } from 'lucide-react'; import { CliStatusBanner } from './CliStatusBanner'; +import { DashboardUpdateBanner } from './DashboardUpdateBanner'; import type { RepositoryGroup } from '@renderer/types/data'; @@ -747,6 +748,9 @@ export const DashboardView = (): React.JSX.Element => { {/* Content */}
+ {/* App update banner */} + + {/* CLI Status Banner */} diff --git a/src/renderer/components/layout/TabBarActions.tsx b/src/renderer/components/layout/TabBarActions.tsx index 1d4aaa77..82924d9d 100644 --- a/src/renderer/components/layout/TabBarActions.tsx +++ b/src/renderer/components/layout/TabBarActions.tsx @@ -7,6 +7,7 @@ import { useMemo, useState } from 'react'; import { isElectronMode } from '@renderer/api'; +import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip'; import { useStore } from '@renderer/store'; import { Bell, PanelRight, Puzzle, Settings, Users } from 'lucide-react'; import { useShallow } from 'zustand/react/shallow'; @@ -25,6 +26,8 @@ export const TabBarActions = (): React.JSX.Element => { tabSessionData, sidebarCollapsed, toggleSidebar, + updateStatus, + openUpdateDialog, } = useStore( useShallow((s) => ({ unreadCount: s.unreadCount, @@ -37,6 +40,8 @@ export const TabBarActions = (): React.JSX.Element => { tabSessionData: s.tabSessionData, sidebarCollapsed: s.sidebarCollapsed, toggleSidebar: s.toggleSidebar, + updateStatus: s.updateStatus, + openUpdateDialog: s.openUpdateDialog, })) ); @@ -47,6 +52,7 @@ export const TabBarActions = (): React.JSX.Element => { const [githubHover, setGithubHover] = useState(false); const [settingsHover, setSettingsHover] = useState(false); const [expandHover, setExpandHover] = useState(false); + const [updateHover, setUpdateHover] = useState(false); // Derive active tab and session detail for MoreMenu const activeTab = useMemo( @@ -62,6 +68,31 @@ export const TabBarActions = (): React.JSX.Element => { className="ml-2 flex shrink-0 items-center gap-1" style={{ WebkitAppRegion: 'no-drag' } as React.CSSProperties} > + {/* Update app button — only visible when update available or downloaded */} + {(updateStatus === 'available' || updateStatus === 'downloaded') && ( + + + + + + {updateStatus === 'downloaded' + ? 'Update downloaded, restart to apply' + : 'New version available'} + + + )} + {/* Notifications bell icon */}