Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 19 additions & 5 deletions src/main/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5247,14 +5247,16 @@ interface GitHubRelease {
body: string;
html_url: string;
published_at: string;
prerelease: boolean;
assets: Array<{ name: string; browser_download_url: string }>;
}

// Check for updates from GitHub releases
ipcMain.handle('updates:checkForUpdate', async () => {
try {
// Fetch release list so we can skip non-semver tags (e.g. the "assets" release)
const response = await fetch(
`https://api.github.com/repos/${GITHUB_REPO_OWNER}/${GITHUB_REPO_NAME}/releases/latest`,
`https://api.github.com/repos/${GITHUB_REPO_OWNER}/${GITHUB_REPO_NAME}/releases?per_page=10`,
{
headers: {
Accept: 'application/vnd.github.v3+json',
Expand All @@ -5270,7 +5272,15 @@ ipcMain.handle('updates:checkForUpdate', async () => {
throw new Error(`GitHub API error: ${response.status}`);
}

const release = (await response.json()) as GitHubRelease;
const releases = (await response.json()) as GitHubRelease[];
// Find the first non-prerelease release with a semver tag
const semverRegex = /^v?\d+\.\d+\.\d+$/;
const release = releases.find((r) => !r.prerelease && semverRegex.test(r.tag_name));

if (!release) {
return { hasUpdate: false, error: 'No semver releases found' };
}

const latestVersion = release.tag_name.replace(/^v/, '');

// Get current version from package.json
Expand All @@ -5297,15 +5307,19 @@ ipcMain.handle('updates:checkForUpdate', async () => {
// Compare versions (simple comparison, assumes semver)
const hasUpdate = compareVersions(latestVersion, currentVersion) > 0;

// Pick a platform-appropriate download asset
const isWindows = process.platform === 'win32';
const installerAsset = release.assets.find((a) =>
isWindows ? a.name.endsWith('.exe') : a.name.endsWith('.dmg')
);

return {
hasUpdate: hasUpdate && latestVersion !== (store.get('dismissedUpdateVersion', '') as string),
currentVersion,
latestVersion,
releaseNotes: release.body || '',
releaseUrl: release.html_url,
downloadUrl:
release.assets.find((a) => a.name.endsWith('.dmg'))?.browser_download_url ||
release.html_url,
downloadUrl: installerAsset?.browser_download_url || release.html_url,
};
} catch (error) {
console.error('Failed to check for updates:', error);
Expand Down
20 changes: 20 additions & 0 deletions src/renderer/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -713,6 +713,26 @@ const App: React.FC = () => {
return () => clearTimeout(timer);
}, []);

// Debug helper: run window.showUpdateModal() in DevTools console to test the modal
useEffect(() => {
(window as any).showUpdateModal = (info?: {
currentVersion?: string;
latestVersion?: string;
downloadUrl?: string;
}) => {
const defaults = {
currentVersion: buildInfo.baseVersion,
latestVersion: '99.0.0',
downloadUrl: 'https://github.com/CooperAgent/cooper/releases',
};
setUpdateInfo({ ...defaults, ...info });
setShowUpdateModal(true);
};
return () => {
delete (window as any).showUpdateModal;
};
}, []);

// Check if user has seen welcome wizard on startup
useEffect(() => {
const checkWelcomeWizard = async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ export const UpdateAvailableModal: React.FC<UpdateAvailableModalProps> = ({
};

const handleOpenReleases = () => {
window.electronAPI.updates.openDownloadUrl('https://github.com/CooperAgent/cooper');
window.electronAPI.updates.openDownloadUrl(
'https://github.com/CooperAgent/cooper/releases/latest'
);
};

return (
Expand Down Expand Up @@ -69,23 +71,8 @@ export const UpdateAvailableModal: React.FC<UpdateAvailableModalProps> = ({
</div>

<p className="text-copilot-text-muted text-xs text-center">
Pull the latest version and rebuild to upgrade.
Visit the releases page to download the latest version.
</p>

<div className="space-y-3 text-xs">
<div>
<div className="text-copilot-text-muted mb-1">macOS</div>
<pre className="bg-copilot-background rounded p-2 overflow-auto whitespace-pre-wrap">
{`cd cooper\ngit pull\nnpm install\nnpm run dist\nopen release/Cooper-*-arm64.dmg`}
</pre>
</div>
<div>
<div className="text-copilot-text-muted mb-1">Windows (PowerShell)</div>
<pre className="bg-copilot-background rounded p-2 overflow-auto whitespace-pre-wrap">
{`cd cooper\ngit pull\npwsh -NoProfile -File .\\scripts\\setup-windows.ps1\nnpm run dist:win`}
</pre>
</div>
</div>
</div>
</Modal.Body>
<Modal.Footer className="p-4 border-t border-copilot-border">
Expand All @@ -95,7 +82,7 @@ export const UpdateAvailableModal: React.FC<UpdateAvailableModalProps> = ({
Later
</Button>
<Button variant="primary" onClick={handleOpenReleases}>
Open Repository
Open Releases
</Button>
</div>
<div className="flex justify-center">
Expand Down
10 changes: 6 additions & 4 deletions tests/components/UpdateModals.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,17 +64,19 @@ describe('UpdateAvailableModal', () => {
expect(defaultProps.onClose).toHaveBeenCalled();
});

it('opens repository page when Open Repository button is clicked', async () => {
it('opens releases page when Open Releases button is clicked', async () => {
render(<UpdateAvailableModal {...defaultProps} />);

await waitFor(() => {
expect(screen.getByText('Open Repository')).toBeInTheDocument();
expect(screen.getByText('Open Releases')).toBeInTheDocument();
});

fireEvent.click(screen.getByText('Open Repository'));
fireEvent.click(screen.getByText('Open Releases'));

await waitFor(() => {
expect(mockOpenDownloadUrl).toHaveBeenCalled();
expect(mockOpenDownloadUrl).toHaveBeenCalledWith(
'https://github.com/CooperAgent/cooper/releases/latest'
);
});
});
});
Expand Down