Skip to content

Commit 96a8239

Browse files
authored
Merge pull request #122 from beNative/codex/add-auto-update-feature-with-notifications
Add auto-update toast with progress tracking
2 parents f5b46db + fc10cf6 commit 96a8239

5 files changed

Lines changed: 403 additions & 38 deletions

File tree

App.tsx

Lines changed: 164 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import ConfirmModal from './components/ConfirmModal';
2828
import FatalError from './components/FatalError';
2929
import ContextMenu, { MenuItem } from './components/ContextMenu';
3030
import 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';
3232
import { IconProvider } from './contexts/IconContext';
3333
import { storageService } from './services/storageService';
3434
import { 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+
103117
const 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

Comments
 (0)