/** * UpdateDialog - Modal dialog shown when a new version is available. * * 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 { ExternalLink, X } from 'lucide-react'; import remarkGfm from 'remark-gfm'; import { useShallow } from 'zustand/react/shallow'; export const UpdateDialog = (): React.JSX.Element | null => { const { showUpdateDialog, updateStatus, availableVersion, releaseNotes, downloadUpdate, installUpdate, dismissUpdateDialog, } = useStore( useShallow((s) => ({ showUpdateDialog: s.showUpdateDialog, updateStatus: s.updateStatus, availableVersion: s.availableVersion, releaseNotes: s.releaseNotes, downloadUpdate: s.downloadUpdate, installUpdate: s.installUpdate, dismissUpdateDialog: s.dismissUpdateDialog, })) ); const dialogRef = useRef(null); // Handle ESC key to close dialog useEffect(() => { if (!showUpdateDialog) return; const handleEscape = (e: KeyboardEvent): void => { if (e.key === 'Escape') { dismissUpdateDialog(); } }; document.addEventListener('keydown', handleEscape); return () => document.removeEventListener('keydown', handleEscape); }, [showUpdateDialog, dismissUpdateDialog]); // Focus trap: keep focus within dialog useEffect(() => { if (!showUpdateDialog || !dialogRef.current) return; const dialog = dialogRef.current; const focusableElements = dialog.querySelectorAll( 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' ); const firstElement = focusableElements[0]; const lastElement = focusableElements[focusableElements.length - 1]; // Focus first element when dialog opens firstElement?.focus(); const handleTab = (e: KeyboardEvent): void => { if (e.key !== 'Tab') return; if (e.shiftKey) { // Shift+Tab: if on first element, go to last if (document.activeElement === firstElement) { e.preventDefault(); lastElement?.focus(); } } else { // Tab: if on last element, go to first if (document.activeElement === lastElement) { e.preventDefault(); firstElement?.focus(); } } }; dialog.addEventListener('keydown', handleTab); return () => dialog.removeEventListener('keydown', handleTab); }, [showUpdateDialog]); if (!showUpdateDialog) return null; const isDownloaded = updateStatus === 'downloaded'; // Strip "Downloads" section (and everything after it) from release notes const filteredNotes = releaseNotes ? releaseNotes.replace(/\n#{1,3}\s+Downloads[\s\S]*$/i, '').trimEnd() : releaseNotes; 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 */}

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

{availableVersion && (
v{availableVersion}
)}
{/* Release notes */}
{filteredNotes ? ( {filteredNotes} ) : (

No release notes available.

)}
{/* Actions */}
{releaseUrl && ( )}
{isDownloaded ? ( ) : ( )}
); };