feat(x): scrollable release-notes box on the update card, notes on Windows too

The notes pane was a bare max-h clip: nothing signalled that more content
sat below the fold, and Squirrel.Windows never supplies release notes at
all, so Windows users only ever saw the static fallback line. Squirrel.Mac
is no better in the edit-after-publish window — it snapshots the release
body at download time, which is how v0.7.7 shipped a card showing two
paragraphs of a ten-item release.

- Render the notes in a bordered, inset scroll box (max-h + overflow-y)
  so the frame and scrollbar make the overflow visible on both platforms.
- Backfill the notes from the GitHub release body after update-downloaded:
  gives Windows notes for the first time and replaces macOS's stale
  snapshot with the current body. On fetch failure the snapshot (or the
  fallback line) stands.
- Strip the release body's leading "What's new" heading, which duplicated
  the card's own label.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Harshvardhan Vatsa 2026-07-20 17:51:14 +05:30
parent 63a99e5fe4
commit 60165979f0
2 changed files with 42 additions and 4 deletions

View file

@ -1,4 +1,4 @@
import { app, autoUpdater, nativeImage, BrowserWindow } from "electron";
import { app, autoUpdater, net, nativeImage, BrowserWindow } from "electron";
import { capture } from "@x/core/dist/analytics/posthog.js";
import type { ipc } from "@x/shared";
@ -90,6 +90,12 @@ export function initUpdater(): void {
releaseNotes: releaseNotes || undefined,
});
showReadyBadge();
// Squirrel.Windows never carries notes, and Squirrel.Mac's copy is a
// snapshot from download time — stale when the release body is edited
// after publish (update.electronjs.org caching widens that window).
// Refresh from the GitHub API; on failure the snapshot (or the card's
// static fallback line) stands.
void backfillReleaseNotes(releaseName || undefined);
});
autoUpdater.on("error", (err) => {
setStatus({ state: "error", error: err.message, lastCheckedAt: status.lastCheckedAt });
@ -110,6 +116,30 @@ export function initUpdater(): void {
setInterval(checkForUpdates, CHECK_INTERVAL_MS);
}
/**
* Replace the staged update's release notes with the current GitHub release
* body. releaseName is "0.7.7" from Squirrel.Windows and "v0.7.7" from
* Squirrel.Mac normalize to the tag form.
*/
async function backfillReleaseNotes(releaseName: string | undefined): Promise<void> {
if (!releaseName) return;
try {
const tag = `v${releaseName.replace(/^v/, "")}`;
const res = await net.fetch(`https://api.github.com/repos/${REPO}/releases/tags/${tag}`, {
headers: { Accept: "application/vnd.github+json", "User-Agent": "Rowboat" },
});
if (!res.ok) return;
const { body } = (await res.json()) as { body?: string };
const notes = body?.trim();
// Re-check the state: the fetch raced user actions (quitAndInstall).
if (notes && status.state === "ready" && status.newVersion === releaseName) {
setStatus({ ...status, releaseNotes: notes });
}
} catch {
// Offline or rate-limited — the Squirrel snapshot / fallback line stands.
}
}
/**
* Manual "Check for updates". Only meaningful when idle or errored;
* checking/downloading are already in flight and ready is already staged.

View file

@ -55,6 +55,11 @@ export function UpdateCard() {
if (!visible || status?.state !== "ready") return null
const releaseUrl = version ? `${RELEASES_URL}/tag/v${version}` : `${RELEASES_URL}/latest`
// Release bodies usually open with their own "What's new" heading, which
// would duplicate the card's label above the box — drop it.
const releaseNotes = status.releaseNotes
?.replace(/^\s*#{1,6}[ \t]*what[']?s new[ \t]*\r?\n+/i, "")
.trim()
return (
<div
@ -81,10 +86,13 @@ export function UpdateCard() {
</p>
<div className="mt-3">
<h5 className="text-xs font-semibold">What&apos;s new</h5>
{status.releaseNotes ? (
<div className="mt-1.5 max-h-56 overflow-y-auto">
{releaseNotes ? (
// A bordered, fixed-height scroll box (not a bare max-h clip): the
// frame and inset scrollbar signal there is more content below the
// fold on every platform.
<div className="mt-1.5 max-h-56 overflow-y-auto overscroll-contain rounded-md border border-border/60 bg-muted/30 p-2.5">
<Streamdown className="prose prose-sm dark:prose-invert max-w-none text-xs [&>*:first-child]:mt-0 [&>*:last-child]:mb-0">
{status.releaseNotes}
{releaseNotes}
</Streamdown>
</div>
) : (