@@ -28,7 +28,7 @@ import ConfirmModal from './components/ConfirmModal';
2828import FatalError from './components/FatalError' ;
2929import ContextMenu , { MenuItem } from './components/ContextMenu' ;
3030import NewCodeFileModal from './components/NewCodeFileModal' ;
31- import type { DocumentOrFolder , Command , LogMessage , DiscoveredLLMModel , DiscoveredLLMService , Settings , DocumentTemplate , ViewMode , DocType , DraggedNodeTransfer } from './types' ;
31+ import type { DocumentOrFolder , Command , LogMessage , DiscoveredLLMModel , DiscoveredLLMService , Settings , DocumentTemplate , ViewMode , DocType , DraggedNodeTransfer , UpdateAvailableInfo } from './types' ;
3232import { IconProvider } from './contexts/IconContext' ;
3333import { storageService } from './services/storageService' ;
3434import { llmDiscoveryService } from './services/llmDiscoveryService' ;
@@ -100,6 +100,20 @@ interface DatabaseStatusState {
100100 tone : DatabaseStatusTone ;
101101}
102102
103+ type UpdateStatus = 'idle' | 'downloading' | 'downloaded' | 'error' ;
104+
105+ interface UpdateToastState {
106+ status : UpdateStatus ;
107+ version : string | null ;
108+ releaseName : string | null ;
109+ progress : number ;
110+ bytesTransferred : number | null ;
111+ bytesTotal : number | null ;
112+ visible : boolean ;
113+ snoozed : boolean ;
114+ errorMessage : string | null ;
115+ }
116+
103117const MainApp : React . FC = ( ) => {
104118 const { settings, saveSettings, loaded : settingsLoaded } = useSettings ( ) ;
105119 const { items, addDocument, addFolder, updateItem, commitVersion, deleteItems, moveItems, getDescendantIds, duplicateItems, addDocumentsFromFiles, importNodesFromTransfer, isLoading : areDocumentsLoading } = useDocuments ( ) ;
@@ -129,7 +143,17 @@ const MainApp: React.FC = () => {
129143 const [ discoveredServices , setDiscoveredServices ] = useState < DiscoveredLLMService [ ] > ( [ ] ) ;
130144 const [ isDetecting , setIsDetecting ] = useState ( false ) ;
131145 const [ appVersion , setAppVersion ] = useState ( '' ) ;
132- const [ updateInfo , setUpdateInfo ] = useState < { ready : boolean ; version : string | null } > ( { ready : false , version : null } ) ;
146+ const [ updateToast , setUpdateToast ] = useState < UpdateToastState > ( {
147+ status : 'idle' ,
148+ version : null ,
149+ releaseName : null ,
150+ progress : 0 ,
151+ bytesTransferred : null ,
152+ bytesTotal : null ,
153+ visible : false ,
154+ snoozed : false ,
155+ errorMessage : null ,
156+ } ) ;
133157 const [ confirmAction , setConfirmAction ] = useState < { title : string ; message : React . ReactNode ; onConfirm : ( ) => void ; } | null > ( null ) ;
134158 const [ searchTerm , setSearchTerm ] = useState ( '' ) ;
135159 const [ contextMenu , setContextMenu ] = useState < { isOpen : boolean ; position : { x : number , y : number } , items : MenuItem [ ] } > ( { isOpen : false , position : { x : 0 , y : 0 } , items : [ ] } ) ;
@@ -911,13 +935,104 @@ const MainApp: React.FC = () => {
911935 } , [ ] ) ;
912936
913937 useEffect ( ( ) => {
914- if ( window . electronAPI ?. onUpdateDownloaded ) {
915- const cleanup = window . electronAPI . onUpdateDownloaded ( ( version ) => {
916- addLog ( 'INFO' , `Update version ${ version } is ready to be installed.` ) ;
917- setUpdateInfo ( { ready : true , version } ) ;
918- } ) ;
919- return cleanup ;
938+ if ( ! window . electronAPI ) {
939+ return ;
940+ }
941+
942+ const cleanups : ( ( ) => void ) [ ] = [ ] ;
943+ const {
944+ onUpdateAvailable,
945+ onUpdateDownloadProgress,
946+ onUpdateDownloaded,
947+ onUpdateError,
948+ } = window . electronAPI ;
949+
950+ if ( onUpdateAvailable ) {
951+ cleanups . push ( onUpdateAvailable ( ( info ) => {
952+ const versionLabel = info . version ?? info . releaseName ?? 'latest' ;
953+ addLog ( 'INFO' , `Update ${ versionLabel } detected. Downloading in the background.` ) ;
954+ setUpdateToast ( prev => ( {
955+ ...prev ,
956+ status : 'downloading' ,
957+ version : info . version ?? prev . version ?? null ,
958+ releaseName : info . releaseName ?? prev . releaseName ?? null ,
959+ progress : 0 ,
960+ bytesTransferred : null ,
961+ bytesTotal : null ,
962+ visible : true ,
963+ snoozed : false ,
964+ errorMessage : null ,
965+ } ) ) ;
966+ } ) ) ;
967+ }
968+
969+ if ( onUpdateDownloadProgress ) {
970+ cleanups . push ( onUpdateDownloadProgress ( ( progress ) => {
971+ setUpdateToast ( prev => ( {
972+ ...prev ,
973+ status : 'downloading' ,
974+ progress : Number . isFinite ( progress . percent ) ? progress . percent : prev . progress ,
975+ bytesTransferred : Number . isFinite ( progress . transferred ) ? progress . transferred : prev . bytesTransferred ,
976+ bytesTotal : Number . isFinite ( progress . total ) ? progress . total : prev . bytesTotal ,
977+ visible : prev . snoozed ? prev . visible : true ,
978+ snoozed : prev . snoozed ,
979+ errorMessage : null ,
980+ } ) ) ;
981+ } ) ) ;
982+ }
983+
984+ if ( onUpdateDownloaded ) {
985+ cleanups . push ( onUpdateDownloaded ( ( payload : string | UpdateAvailableInfo ) => {
986+ const versionLabel = typeof payload === 'string'
987+ ? payload
988+ : payload . version ?? payload . releaseName ?? 'latest' ;
989+ addLog ( 'INFO' , `Update version ${ versionLabel } is ready to be installed.` ) ;
990+ setUpdateToast ( prev => {
991+ const version = typeof payload === 'string'
992+ ? payload
993+ : payload . version ?? prev . version ?? payload . releaseName ?? prev . releaseName ?? null ;
994+ const releaseName = typeof payload === 'string'
995+ ? prev . releaseName
996+ : payload . releaseName ?? prev . releaseName ?? null ;
997+
998+ return {
999+ ...prev ,
1000+ status : 'downloaded' ,
1001+ version,
1002+ releaseName,
1003+ progress : 100 ,
1004+ bytesTransferred : prev . bytesTotal ?? prev . bytesTransferred ?? null ,
1005+ bytesTotal : prev . bytesTotal ?? prev . bytesTransferred ?? null ,
1006+ visible : true ,
1007+ snoozed : false ,
1008+ errorMessage : null ,
1009+ } ;
1010+ } ) ;
1011+ } ) ) ;
1012+ }
1013+
1014+ if ( onUpdateError ) {
1015+ cleanups . push ( onUpdateError ( ( message ) => {
1016+ addLog ( 'ERROR' , `Auto-update error: ${ message } ` ) ;
1017+ setUpdateToast ( prev => ( {
1018+ ...prev ,
1019+ status : 'error' ,
1020+ visible : true ,
1021+ snoozed : false ,
1022+ errorMessage : message ,
1023+ } ) ) ;
1024+ } ) ) ;
9201025 }
1026+
1027+ return ( ) => {
1028+ cleanups . forEach ( dispose => {
1029+ try {
1030+ dispose ( ) ;
1031+ } catch ( error ) {
1032+ console . error ( 'Failed to cleanup update listener' , error ) ;
1033+ }
1034+ } ) ;
1035+ } ;
9211036 } , [ addLog ] ) ;
9221037
9231038
@@ -1762,6 +1877,30 @@ const MainApp: React.FC = () => {
17621877 setIsAboutModalOpen ( false ) ;
17631878 } , [ addLog ] ) ;
17641879
1880+ const handleUpdateToastClose = useCallback ( ( ) => {
1881+ setUpdateToast ( prev => {
1882+ if ( prev . status === 'error' ) {
1883+ return {
1884+ status : 'idle' ,
1885+ version : prev . version ,
1886+ releaseName : prev . releaseName ,
1887+ progress : 0 ,
1888+ bytesTransferred : null ,
1889+ bytesTotal : null ,
1890+ visible : false ,
1891+ snoozed : false ,
1892+ errorMessage : null ,
1893+ } ;
1894+ }
1895+
1896+ return {
1897+ ...prev ,
1898+ visible : false ,
1899+ snoozed : true ,
1900+ } ;
1901+ } ) ;
1902+ } , [ ] ) ;
1903+
17651904 const handleFormatDocument = useCallback ( ( ) => {
17661905 const activeDoc = items . find ( p => p . id === activeNodeId ) ;
17671906 if ( activeDoc && activeDoc . type === 'document' && view === 'editor' ) {
@@ -2280,6 +2419,12 @@ const MainApp: React.FC = () => {
22802419 commands : enrichedCommands ,
22812420 } ;
22822421
2422+ const shouldShowUpdateToast = updateToast . visible && updateToast . status !== 'idle' ;
2423+ const updateVersionLabel = updateToast . version ?? updateToast . releaseName ?? 'latest' ;
2424+ const updateToastStatus : 'downloading' | 'downloaded' | 'error' = updateToast . status === 'idle'
2425+ ? 'downloading'
2426+ : updateToast . status ;
2427+
22832428 return (
22842429 < IconProvider value = { { iconSet : getSupportedIconSet ( settings . iconSet ) } } >
22852430 < div className = "flex flex-col h-full font-sans bg-background text-text-main antialiased overflow-hidden" >
@@ -2443,11 +2588,18 @@ const MainApp: React.FC = () => {
24432588 < AboutModal onClose = { handleCloseAbout } />
24442589 ) }
24452590
2446- { updateInfo . ready && window . electronAPI ?. quitAndInstallUpdate && (
2591+ { shouldShowUpdateToast && (
24472592 < UpdateNotification
2448- version = { updateInfo . version ! }
2449- onInstall = { ( ) => window . electronAPI ! . quitAndInstallUpdate ! ( ) }
2450- onClose = { ( ) => setUpdateInfo ( { ready : false , version : null } ) }
2593+ status = { updateToastStatus }
2594+ versionLabel = { updateVersionLabel }
2595+ progress = { updateToast . progress }
2596+ bytesTransferred = { updateToast . bytesTransferred ?? undefined }
2597+ bytesTotal = { updateToast . bytesTotal ?? undefined }
2598+ errorMessage = { updateToast . errorMessage ?? undefined }
2599+ onInstall = { updateToast . status === 'downloaded' && window . electronAPI ?. quitAndInstallUpdate
2600+ ? ( ) => window . electronAPI ! . quitAndInstallUpdate ! ( )
2601+ : undefined }
2602+ onClose = { handleUpdateToastClose }
24512603 />
24522604 ) }
24532605
0 commit comments