fix(updater): improve release notes and banner spacing

This commit is contained in:
777genius 2026-05-20 12:30:06 +03:00
parent 435d0d4e1d
commit 948e00aedb
5 changed files with 210 additions and 15 deletions

View file

@ -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,
});
}

View file

@ -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 (
<div
className="relative border-b px-4 py-2.5"
style={{
backgroundColor: 'var(--color-surface)',
borderColor: 'var(--color-border)',
}}
style={
{
backgroundColor: 'var(--color-surface)',
borderColor: 'var(--color-border)',
paddingLeft: isMacElectron ? 'var(--macos-traffic-light-padding-left, 72px)' : undefined,
WebkitAppRegion: isMacElectron ? 'drag' : undefined,
} as React.CSSProperties
}
>
{isDownloading ? (
<div className="pr-8">
@ -79,10 +86,13 @@ export const UpdateBanner = (): React.JSX.Element | null => {
<button
onClick={installUpdate}
className="ml-auto rounded-md border px-2.5 py-1 text-xs font-medium transition-colors hover:bg-white/5"
style={{
borderColor: 'var(--color-border-emphasis)',
color: 'var(--color-text)',
}}
style={
{
borderColor: 'var(--color-border-emphasis)',
color: 'var(--color-text)',
WebkitAppRegion: isMacElectron ? 'no-drag' : undefined,
} as React.CSSProperties
}
>
Restart now
</button>
@ -93,7 +103,12 @@ export const UpdateBanner = (): React.JSX.Element | null => {
<button
onClick={dismissUpdateBanner}
className="absolute right-3 top-1/2 shrink-0 -translate-y-1/2 rounded p-0.5 transition-colors hover:bg-white/10"
style={{ color: 'var(--color-text-muted)' }}
style={
{
color: 'var(--color-text-muted)',
WebkitAppRegion: isMacElectron ? 'no-drag' : undefined,
} as React.CSSProperties
}
>
<X className="size-3.5" />
</button>

View file

@ -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}`

View file

@ -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 = /^<h[1-6][^>]*>\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;
}

View file

@ -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
<table>
<tr><td>installer links</td></tr>
</table>`)
).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
<table>
<tr><td>installer links</td></tr>
</table>`;
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\n<h2>Downloads</h2>\n<table>links</table>')).toBe(
'Fixed'
);
});
it('formats full-changelog updater notes as a version list', () => {
const notes = formatUpdaterReleaseNotes([
{
version: '2.0.2',
note: `Fixed launch reliability.
### Downloads
<table>links</table>`,
},
{
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<table>links</table>')).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');
});
});