fix(updater): improve release notes and banner spacing
This commit is contained in:
parent
435d0d4e1d
commit
948e00aedb
5 changed files with 210 additions and 15 deletions
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}`
|
||||
|
|
|
|||
88
src/shared/utils/releaseNotes.ts
Normal file
88
src/shared/utils/releaseNotes.ts
Normal 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;
|
||||
}
|
||||
86
test/shared/utils/releaseNotes.test.ts
Normal file
86
test/shared/utils/releaseNotes.test.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue