diff --git a/src/main/services/infrastructure/UpdaterService.ts b/src/main/services/infrastructure/UpdaterService.ts
index 495ba50d..da24fed6 100644
--- a/src/main/services/infrastructure/UpdaterService.ts
+++ b/src/main/services/infrastructure/UpdaterService.ts
@@ -13,6 +13,10 @@
import { safeSendToRenderer } from '@main/utils/safeWebContentsSend';
import { getErrorMessage } from '@shared/utils/errorHandling';
import { createLogger } from '@shared/utils/logger';
+import {
+ formatUpdaterReleaseNotes,
+ getUpdaterReleaseNoteForVersion,
+} from '@shared/utils/releaseNotes';
import { isVersionOlder, normalizeVersion } from '@shared/utils/version';
import { app, net } from 'electron';
import electronUpdater from 'electron-updater';
@@ -97,6 +101,7 @@ export class UpdaterService {
constructor() {
autoUpdater.autoDownload = false;
autoUpdater.autoInstallOnAppQuit = true;
+ autoUpdater.fullChangelog = true;
this.bindEvents();
}
@@ -241,11 +246,14 @@ export class UpdaterService {
return;
}
+ const latestReleaseNote = getUpdaterReleaseNoteForVersion(info.releaseNotes, info.version);
+ const releaseNotes = formatUpdaterReleaseNotes(info.releaseNotes);
+
if (
shouldSkipReleaseForUpdater({
tag_name: `v${info.version}`,
name: typeof info.releaseName === 'string' ? info.releaseName : undefined,
- body: typeof info.releaseNotes === 'string' ? info.releaseNotes : undefined,
+ body: latestReleaseNote,
})
) {
logger.warn(`Suppressing updater notification for locally marked release ${info.version}`);
@@ -278,7 +286,7 @@ export class UpdaterService {
this.sendStatus({
type: 'available',
version: info.version,
- releaseNotes: typeof info.releaseNotes === 'string' ? info.releaseNotes : undefined,
+ releaseNotes,
});
}
diff --git a/src/renderer/components/common/UpdateBanner.tsx b/src/renderer/components/common/UpdateBanner.tsx
index 62d6a0b2..c3234fe7 100644
--- a/src/renderer/components/common/UpdateBanner.tsx
+++ b/src/renderer/components/common/UpdateBanner.tsx
@@ -4,6 +4,7 @@
* Visible during download and after the update is ready to install.
*/
+import { isElectronMode } from '@renderer/api';
import { useStore } from '@renderer/store';
import { CheckCircle, Loader2, X } from 'lucide-react';
import { useShallow } from 'zustand/react/shallow';
@@ -34,14 +35,20 @@ export const UpdateBanner = (): React.JSX.Element | null => {
const isDownloading = updateStatus === 'downloading';
const percent = Math.round(downloadProgress);
const clampedPercent = Math.max(0, Math.min(percent, 100));
+ const isMacElectron =
+ isElectronMode() && window.navigator.userAgent.toLowerCase().includes('mac');
return (
{isDownloading ? (
@@ -79,10 +86,13 @@ export const UpdateBanner = (): React.JSX.Element | null => {
@@ -93,7 +103,12 @@ export const UpdateBanner = (): React.JSX.Element | null => {
diff --git a/src/renderer/components/common/UpdateDialog.tsx b/src/renderer/components/common/UpdateDialog.tsx
index 13138050..72fa533d 100644
--- a/src/renderer/components/common/UpdateDialog.tsx
+++ b/src/renderer/components/common/UpdateDialog.tsx
@@ -13,6 +13,7 @@ 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 { stripDownloadsSection } from '@shared/utils/releaseNotes';
import { ExternalLink, X } from 'lucide-react';
import remarkGfm from 'remark-gfm';
import { useShallow } from 'zustand/react/shallow';
@@ -94,10 +95,7 @@ export const UpdateDialog = (): React.JSX.Element | 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 filteredNotes = releaseNotes ? stripDownloadsSection(releaseNotes) : releaseNotes;
const releaseUrl = availableVersion
? `https://github.com/777genius/agent-teams-ai/releases/tag/v${availableVersion}`
diff --git a/src/shared/utils/releaseNotes.ts b/src/shared/utils/releaseNotes.ts
new file mode 100644
index 00000000..4bee0313
--- /dev/null
+++ b/src/shared/utils/releaseNotes.ts
@@ -0,0 +1,88 @@
+interface ReleaseNoteEntry {
+ readonly version?: unknown;
+ readonly note?: unknown;
+}
+
+function isDownloadsHeading(line: string): boolean {
+ const trimmed = line.trim();
+ if (!trimmed) {
+ return false;
+ }
+
+ const markdownHeading = /^#{1,6}\s*(.+?)\s*#*\s*$/.exec(trimmed);
+ const htmlHeading = /^
]*>\s*(.+?)\s*<\/h[1-6]>\s*$/i.exec(trimmed);
+ const headingText = markdownHeading?.[1] ?? htmlHeading?.[1];
+ if (!headingText) {
+ return false;
+ }
+
+ const words = headingText.toLowerCase().match(/[a-z0-9]+/g) ?? [];
+ return words.length > 0 && words.every((word) => word === 'download' || word === 'downloads');
+}
+
+export function stripDownloadsSection(markdown: string): string {
+ const lines = markdown.split(/\r?\n/u);
+ const downloadsHeadingIndex = lines.findIndex(isDownloadsHeading);
+ if (downloadsHeadingIndex === -1) {
+ return markdown.trimEnd();
+ }
+
+ return lines.slice(0, downloadsHeadingIndex).join('\n').trimEnd();
+}
+
+function normalizeReleaseNoteEntry(entry: unknown): { version: string; note: string } | null {
+ if (!entry || typeof entry !== 'object') {
+ return null;
+ }
+
+ const { version, note } = entry as ReleaseNoteEntry;
+ if (typeof version !== 'string' || version.trim() === '') {
+ return null;
+ }
+
+ return {
+ version: version.trim().replace(/^v/i, ''),
+ note: typeof note === 'string' ? note : '',
+ };
+}
+
+export function getUpdaterReleaseNoteForVersion(
+ releaseNotes: unknown,
+ version: string
+): string | undefined {
+ if (typeof releaseNotes === 'string') {
+ return releaseNotes;
+ }
+
+ if (!Array.isArray(releaseNotes)) {
+ return undefined;
+ }
+
+ const normalizedVersion = version.trim().replace(/^v/i, '');
+ return (
+ releaseNotes
+ .map(normalizeReleaseNoteEntry)
+ .find((entry) => entry?.version === normalizedVersion)?.note || undefined
+ );
+}
+
+export function formatUpdaterReleaseNotes(releaseNotes: unknown): string | undefined {
+ if (typeof releaseNotes === 'string') {
+ const stripped = stripDownloadsSection(releaseNotes);
+ return stripped || undefined;
+ }
+
+ if (!Array.isArray(releaseNotes)) {
+ return undefined;
+ }
+
+ const formattedNotes = releaseNotes
+ .map(normalizeReleaseNoteEntry)
+ .filter((entry): entry is { version: string; note: string } => entry !== null)
+ .map(({ version, note }) => {
+ const strippedNote = stripDownloadsSection(note);
+ return [`## v${version}`, strippedNote || '_No release notes provided._'].join('\n\n');
+ });
+
+ return formattedNotes.length > 0 ? formattedNotes.join('\n\n') : undefined;
+}
diff --git a/test/shared/utils/releaseNotes.test.ts b/test/shared/utils/releaseNotes.test.ts
new file mode 100644
index 00000000..395ae1db
--- /dev/null
+++ b/test/shared/utils/releaseNotes.test.ts
@@ -0,0 +1,86 @@
+import { describe, expect, it } from 'vitest';
+
+import {
+ formatUpdaterReleaseNotes,
+ getUpdaterReleaseNoteForVersion,
+ stripDownloadsSection,
+} from '../../../src/shared/utils/releaseNotes';
+
+describe('releaseNotes utilities', () => {
+ it('strips markdown Downloads sections even when they start the note', () => {
+ expect(
+ stripDownloadsSection(`### Downloads
+
+`)
+ ).toBe('');
+ });
+
+ it('strips Downloads sections without removing inline download mentions', () => {
+ const notes = `Patch release focused on smoother downloads.
+
+### Fixed
+
+- Download progress no longer gets stuck.
+
+### Downloads
+
+`;
+
+ expect(stripDownloadsSection(notes)).toBe(`Patch release focused on smoother downloads.
+
+### Fixed
+
+- Download progress no longer gets stuck.`);
+ });
+
+ it('strips html Downloads headings', () => {
+ expect(stripDownloadsSection('Fixed\n\nDownloads
\n')).toBe(
+ 'Fixed'
+ );
+ });
+
+ it('formats full-changelog updater notes as a version list', () => {
+ const notes = formatUpdaterReleaseNotes([
+ {
+ version: '2.0.2',
+ note: `Fixed launch reliability.
+
+### Downloads
+
+`,
+ },
+ {
+ version: '2.0.1',
+ note: 'Improved provider settings.',
+ },
+ ]);
+
+ expect(notes).toBe(`## v2.0.2
+
+Fixed launch reliability.
+
+## v2.0.1
+
+Improved provider settings.`);
+ });
+
+ it('strips Downloads from single-release string notes', () => {
+ expect(formatUpdaterReleaseNotes('Fixed\n\n### Downloads\n')).toBe('Fixed');
+ });
+
+ it('reads only the requested release note from full-changelog arrays', () => {
+ expect(
+ getUpdaterReleaseNoteForVersion(
+ [
+ { version: '2.0.2', note: 'Latest note' },
+ { version: '2.0.1', note: 'Older [skip-updater] note' },
+ ],
+ 'v2.0.2'
+ )
+ ).toBe('Latest note');
+ });
+});