Skip to content
10 changes: 9 additions & 1 deletion ui/desktop/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -136,11 +136,19 @@ const PairRouteWrapper = ({
const SettingsRoute = () => {
const location = useLocation();
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const setView = useNavigation();

// Get viewOptions from location.state or history.state
// Get viewOptions from location.state, history.state, or URL search params
const viewOptions =
(location.state as SettingsViewOptions) || (window.history.state as SettingsViewOptions) || {};

// If section is provided via URL search params, add it to viewOptions
const sectionFromUrl = searchParams.get('section');
if (sectionFromUrl) {
viewOptions.section = sectionFromUrl;
}

return <SettingsView onClose={() => navigate('/')} setView={setView} viewOptions={viewOptions} />;
};

Expand Down
131 changes: 88 additions & 43 deletions ui/desktop/src/components/settings/app/UpdateSection.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState, useEffect } from 'react';
import React, { useState, useEffect } from 'react';
import { Button } from '../../ui/button';
import { Loader2, Download, CheckCircle, AlertCircle } from 'lucide-react';

Expand Down Expand Up @@ -29,6 +29,9 @@ export default function UpdateSection() {
currentVersion: '',
});
const [progress, setProgress] = useState<number>(0);
const [isUsingGitHubFallback, setIsUsingGitHubFallback] = useState<boolean>(false);
const progressTimeoutRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
const lastProgressRef = React.useRef<number>(0); // Track last progress to prevent backward jumps

useEffect(() => {
// Get current version on mount
Expand All @@ -47,6 +50,11 @@ export default function UpdateSection() {
}
});

// Check if using GitHub fallback
window.electron.isUsingGitHubFallback().then((isGitHub) => {
setIsUsingGitHubFallback(isGitHub);
});

// Listen for updater events
window.electron.onUpdaterEvent((event) => {
console.log('Updater event:', event);
Expand All @@ -63,6 +71,10 @@ export default function UpdateSection() {
latestVersion: (event.data as UpdateEventData)?.version,
isUpdateAvailable: true,
}));
// Check if GitHub fallback is being used
window.electron.isUsingGitHubFallback().then((isGitHub) => {
setIsUsingGitHubFallback(isGitHub);
});
break;

case 'update-not-available':
Expand All @@ -73,10 +85,29 @@ export default function UpdateSection() {
}));
break;

case 'download-progress':
case 'download-progress': {
setUpdateStatus('downloading');
setProgress((event.data as UpdateEventData)?.percent || 0);

// Get the new progress value (ensure it's a valid number)
const rawPercent = (event.data as UpdateEventData)?.percent;
const newProgress = typeof rawPercent === 'number' ? Math.round(rawPercent) : 0;

// Only update if progress increased (prevents backward jumps from out-of-order events)
if (newProgress > lastProgressRef.current) {
lastProgressRef.current = newProgress;

// Cancel any pending update
if (progressTimeoutRef.current) {
clearTimeout(progressTimeoutRef.current);
}

// Use a small delay to batch rapid updates
progressTimeoutRef.current = setTimeout(() => {
setProgress(newProgress);
}, 50); // 50ms delay for smoother batching
}
break;
}

case 'update-downloaded':
setUpdateStatus('ready');
Expand All @@ -93,11 +124,19 @@ export default function UpdateSection() {
break;
}
});

// Cleanup timeout on unmount
return () => {
if (progressTimeoutRef.current) {
clearTimeout(progressTimeoutRef.current);
}
};
}, []);

const checkForUpdates = async () => {
setUpdateStatus('checking');
setProgress(0);
lastProgressRef.current = 0; // Reset progress tracking for new download

try {
const result = await window.electron.checkForUpdates();
Expand All @@ -123,29 +162,6 @@ export default function UpdateSection() {
}
};

const downloadAndInstallUpdate = async () => {
setUpdateStatus('downloading');
setProgress(0);

try {
const result = await window.electron.downloadUpdate();

if (!result.success) {
throw new Error(result.error || 'Failed to download update');
}

// The download progress and completion will be handled by updater events
} catch (error) {
console.error('Error downloading update:', error);
setUpdateInfo((prev) => ({
...prev,
error: error instanceof Error ? error.message : 'Failed to download update',
}));
setUpdateStatus('error');
setTimeout(() => setUpdateStatus('idle'), 5000);
}
};

const installUpdate = () => {
window.electron.installUpdate();
};
Expand Down Expand Up @@ -216,13 +232,6 @@ export default function UpdateSection() {
Check for Updates
</Button>

{updateInfo.isUpdateAvailable && updateStatus === 'idle' && (
<Button onClick={downloadAndInstallUpdate} variant="secondary" size="sm">
<Download className="w-3 h-3 mr-1" />
Download Update
</Button>
)}

{updateStatus === 'ready' && (
<Button onClick={installUpdate} variant="default" size="sm">
Install & Restart
Expand All @@ -238,21 +247,57 @@ export default function UpdateSection() {
)}

{updateStatus === 'downloading' && (
<div className="w-full bg-gray-200 rounded-full h-1.5">
<div
className="bg-blue-500 h-1.5 rounded-full transition-all duration-300"
style={{ width: `${progress}%` }}
/>
<div className="w-full mt-2">
<div className="flex justify-between text-xs text-text-muted mb-1">
<span>Downloading update...</span>
<span>{progress}%</span>
</div>
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2 overflow-hidden">
<div
className="bg-blue-500 h-2 rounded-full transition-[width] duration-150 ease-out"
style={{ width: `${Math.max(progress, 0)}%`, minWidth: progress > 0 ? '8px' : '0' }}
/>
</div>
</div>
)}

{/* Update information */}
{updateInfo.isUpdateAvailable && (
{updateInfo.isUpdateAvailable && updateStatus === 'idle' && (
<div className="text-xs text-text-muted mt-4 space-y-1">
<p>Update will be downloaded automatically in the background.</p>
{isUsingGitHubFallback ? (
<p className="text-xs text-amber-600">
After download, you'll need to manually install the update.
</p>
) : (
<p className="text-xs text-green-600">
The update will be installed automatically when you quit the app.
</p>
)}
</div>
)}

{updateStatus === 'ready' && (
<div className="text-xs text-text-muted mt-4 space-y-1">
<p>Update will be downloaded to your Downloads folder.</p>
<p className="text-xs text-amber-600">
Note: After downloading, you'll need to close the app and manually install the update.
</p>
{isUsingGitHubFallback ? (
<>
<p className="text-xs text-green-600">
✓ Update is ready! Click "Install & Restart" for installation instructions.
</p>
<p className="text-xs text-text-muted">
Manual installation required for this update method.
</p>
</>
) : (
<>
<p className="text-xs text-green-600">
✓ Update is ready! It will be installed when you quit Goose.
</p>
<p className="text-xs text-text-muted">
Or click "Install & Restart" to update now.
</p>
</>
)}
</div>
)}
</div>
Expand Down
4 changes: 4 additions & 0 deletions ui/desktop/src/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ type ElectronAPI = {
restartApp: () => void;
onUpdaterEvent: (callback: (event: UpdaterEvent) => void) => void;
getUpdateState: () => Promise<{ updateAvailable: boolean; latestVersion?: string } | null>;
isUsingGitHubFallback: () => Promise<boolean>;
// Recipe warning functions
closeWindow: () => void;
hasAcceptedRecipeBefore: (recipe: Recipe) => Promise<boolean>;
Expand Down Expand Up @@ -242,6 +243,9 @@ const electronAPI: ElectronAPI = {
getUpdateState: (): Promise<{ updateAvailable: boolean; latestVersion?: string } | null> => {
return ipcRenderer.invoke('get-update-state');
},
isUsingGitHubFallback: (): Promise<boolean> => {
return ipcRenderer.invoke('is-using-github-fallback');
},
closeWindow: () => ipcRenderer.send('close-window'),
hasAcceptedRecipeBefore: (recipe: Recipe) =>
ipcRenderer.invoke('has-accepted-recipe-before', recipe),
Expand Down
Loading