From 8212ed8b2c5d3c663449fb7324562a9285c4c3e4 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Wed, 6 May 2026 00:01:51 -0700 Subject: [PATCH 1/7] Add client-side changelog routing --- website/package.json | 2 +- website/public/_redirects | 1 + website/scripts/changelog-parser.js | 91 +++++++ website/scripts/changelog-parser.test.js | 54 ++++ website/scripts/generate-changelog.js | 14 ++ website/src/App.tsx | 8 + website/src/components/SiteHeader.tsx | 1 + website/src/data/changelog.json | 304 +++++++++++++++++++++++ website/src/pages/Changelog.tsx | 224 +++++++++++++++++ 9 files changed, 698 insertions(+), 1 deletion(-) create mode 100644 website/public/_redirects create mode 100644 website/scripts/changelog-parser.js create mode 100644 website/scripts/changelog-parser.test.js create mode 100644 website/scripts/generate-changelog.js create mode 100644 website/src/data/changelog.json create mode 100644 website/src/pages/Changelog.tsx diff --git a/website/package.json b/website/package.json index e4a5d78..6d2441d 100644 --- a/website/package.json +++ b/website/package.json @@ -6,7 +6,7 @@ "type": "module", "scripts": { "dev": "vite-react-ssg dev", - "prebuild": "node scripts/generate-deps.js", + "prebuild": "node scripts/generate-deps.js && node scripts/generate-changelog.js", "build": "vite-react-ssg build", "preview": "vite preview", "test": "vitest run" diff --git a/website/public/_redirects b/website/public/_redirects new file mode 100644 index 0000000..9b1d532 --- /dev/null +++ b/website/public/_redirects @@ -0,0 +1 @@ +/changelog/after/* /changelog 200 diff --git a/website/scripts/changelog-parser.js b/website/scripts/changelog-parser.js new file mode 100644 index 0000000..3ef6f1d --- /dev/null +++ b/website/scripts/changelog-parser.js @@ -0,0 +1,91 @@ +const RELEASE_HEADING_RE = /^## \[([^\]]+)\](?: - (\d{4}-\d{2}-\d{2}))?\s*$/; +const SECTION_HEADING_RE = /^###\s+(.+?)\s*$/; +const BULLET_RE = /^(\s*)-\s+(.*)$/; + +function normalizeVersion(version) { + return version.trim().replace(/^v/i, ""); +} + +function createRelease(rawVersion, date) { + const version = normalizeVersion(rawVersion); + return { + version, + tag: `v${version}`, + date: date ?? null, + sections: [], + }; +} + +function ensureSection(release, title = "Changes") { + if (!release.sections.length || release.sections[release.sections.length - 1].title !== title) { + release.sections.push({ title, items: [] }); + } + return release.sections[release.sections.length - 1]; +} + +function pushBullet(section, indent, text) { + const item = { text: text.trim(), children: [] }; + const isNested = indent.replace(/\t/g, " ").length >= 2; + const lastTopLevelItem = section.items[section.items.length - 1]; + + if (isNested && lastTopLevelItem) { + lastTopLevelItem.children.push(item); + return; + } + + section.items.push(item); +} + +function appendContinuation(section, text) { + const trimmed = text.trim(); + if (!trimmed) return; + + const lastTopLevelItem = section.items[section.items.length - 1]; + if (lastTopLevelItem) { + lastTopLevelItem.text = `${lastTopLevelItem.text} ${trimmed}`; + return; + } + + section.items.push({ text: trimmed, children: [] }); +} + +export function parseChangelog(markdown) { + const releases = []; + let currentRelease = null; + let currentSection = null; + + for (const line of markdown.replace(/\r\n?/g, "\n").split("\n")) { + const releaseHeading = line.match(RELEASE_HEADING_RE); + if (releaseHeading) { + currentRelease = createRelease(releaseHeading[1], releaseHeading[2]); + releases.push(currentRelease); + currentSection = null; + continue; + } + + if (!currentRelease) continue; + + const sectionHeading = line.match(SECTION_HEADING_RE); + if (sectionHeading) { + currentSection = ensureSection(currentRelease, sectionHeading[1].trim()); + continue; + } + + if (!line.trim()) continue; + + const bullet = line.match(BULLET_RE); + if (bullet) { + currentSection = currentSection ?? ensureSection(currentRelease); + pushBullet(currentSection, bullet[1], bullet[2]); + continue; + } + + currentSection = currentSection ?? ensureSection(currentRelease); + appendContinuation(currentSection, line); + } + + return { + generatedFrom: "CHANGELOG.md", + releases, + }; +} diff --git a/website/scripts/changelog-parser.test.js b/website/scripts/changelog-parser.test.js new file mode 100644 index 0000000..c832b68 --- /dev/null +++ b/website/scripts/changelog-parser.test.js @@ -0,0 +1,54 @@ +import { describe, expect, it } from "vitest"; +import { parseChangelog } from "./changelog-parser.js"; + +describe("parseChangelog", () => { + it("parses release headings, sections, and nested bullets", () => { + const parsed = parseChangelog(`# Changelog + +## [0.2.0] - 2026-01-02 +### Added +- First item. + - Nested detail. + +### Fixed +- Second item. + +## [0.1.0] - 2026-01-01 +- Initial release. +`); + + expect(parsed.releases).toEqual([ + { + version: "0.2.0", + tag: "v0.2.0", + date: "2026-01-02", + sections: [ + { + title: "Added", + items: [ + { + text: "First item.", + children: [{ text: "Nested detail.", children: [] }], + }, + ], + }, + { + title: "Fixed", + items: [{ text: "Second item.", children: [] }], + }, + ], + }, + { + version: "0.1.0", + tag: "v0.1.0", + date: "2026-01-01", + sections: [ + { + title: "Changes", + items: [{ text: "Initial release.", children: [] }], + }, + ], + }, + ]); + }); +}); diff --git a/website/scripts/generate-changelog.js b/website/scripts/generate-changelog.js new file mode 100644 index 0000000..a9eaece --- /dev/null +++ b/website/scripts/generate-changelog.js @@ -0,0 +1,14 @@ +import { readFileSync, writeFileSync } from "node:fs"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { parseChangelog } from "./changelog-parser.js"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const repoRoot = resolve(__dirname, "../.."); +const changelogPath = resolve(repoRoot, "CHANGELOG.md"); +const outPath = resolve(__dirname, "../src/data/changelog.json"); + +const changelog = parseChangelog(readFileSync(changelogPath, "utf-8")); + +writeFileSync(outPath, JSON.stringify(changelog, null, 2) + "\n"); +console.log(`Wrote ${changelog.releases.length} changelog releases to src/data/changelog.json`); diff --git a/website/src/App.tsx b/website/src/App.tsx index 1063ad9..c24edf0 100644 --- a/website/src/App.tsx +++ b/website/src/App.tsx @@ -9,6 +9,14 @@ export const routes: RouteRecord[] = [ path: "/playground", lazy: () => import("./pages/Playground"), }, + { + path: "/changelog", + lazy: () => import("./pages/Changelog"), + }, + { + path: "/changelog/after/:version", + lazy: () => import("./pages/Changelog"), + }, { path: "/dependencies", lazy: () => import("./pages/Dependencies"), diff --git a/website/src/components/SiteHeader.tsx b/website/src/components/SiteHeader.tsx index f9731c3..511f345 100644 --- a/website/src/components/SiteHeader.tsx +++ b/website/src/components/SiteHeader.tsx @@ -2,6 +2,7 @@ import { forwardRef } from "react"; const NAV_LINKS: readonly { href: string; label: string; external?: boolean; hideOnMobile?: boolean }[] = [ { href: "/playground", label: "Playground", hideOnMobile: true }, + { href: "/changelog", label: "Changelog", hideOnMobile: true }, { href: "/#download", label: "Download", hideOnMobile: true }, { href: "https://github.com/diffplug/mouseterm", label: "GitHub", external: true }, ]; diff --git a/website/src/data/changelog.json b/website/src/data/changelog.json new file mode 100644 index 0000000..12f6910 --- /dev/null +++ b/website/src/data/changelog.json @@ -0,0 +1,304 @@ +{ + "generatedFrom": "CHANGELOG.md", + "releases": [ + { + "version": "0.9.1", + "tag": "v0.9.1", + "date": "2026-05-01", + "sections": [ + { + "title": "Changed", + "items": [ + { + "text": "🖥️ Drop-to-paste from the OS file explorer is temporarily inert on standalone while we wait on upstream Tauri ([tauri#14373](https://github.com/tauri-apps/tauri/issues/14373)) to allow native drag-drop without blocking HTML5 drag events ([#39](https://github.com/diffplug/mouseterm/pull/39)).", + "children": [] + } + ] + }, + { + "title": "Fixed", + "items": [ + { + "text": "The mouse-override banner now renders inline in the terminal pane body and no longer stacks with the action-button tooltip ([#43](https://github.com/diffplug/mouseterm/pull/43)).", + "children": [] + }, + { + "text": "Themes with translucent selection backgrounds (e.g. Selenized Dark) no longer bleed through MouseTerm's solid AppBar and tab fills ([#37](https://github.com/diffplug/mouseterm/pull/37)).", + "children": [] + }, + { + "text": "🖥️ Force-closing the standalone host now reliably kills the Node sidecar tree via a Windows Job Object / Unix process group, so subsequent builds no longer hit orphan `node.exe` processes locking files ([#41](https://github.com/diffplug/mouseterm/pull/41)).", + "children": [] + }, + { + "text": "🖥️ Standalone macOS terminals run zsh as a login shell when no args are provided, so `~/.zprofile` runs and Homebrew/asdf land on `PATH` ([#40](https://github.com/diffplug/mouseterm/pull/40)).", + "children": [] + }, + { + "text": "🖥️ Pane drag-and-drop reordering works again on standalone ([#39](https://github.com/diffplug/mouseterm/pull/39)).", + "children": [] + } + ] + } + ] + }, + { + "version": "0.9.0", + "tag": "v0.9.0", + "date": "2026-04-30", + "sections": [ + { + "title": "Added", + "items": [ + { + "text": "🖥️ Debug dialog for failed auto-updates — surfaces the error and copies a pre-filled bug report (version, platform, last ~10 KB of `mouseterm.log`) ([#35](https://github.com/diffplug/mouseterm/pull/35)).", + "children": [] + } + ] + }, + { + "title": "Fixed", + "items": [ + { + "text": "Terminals auto-spawned from a blank workspace now respect the selected shell ([#33](https://github.com/diffplug/mouseterm/pull/33)).", + "children": [] + }, + { + "text": "🖥️ Polish app bar header to align with pane chrome and shared design tokens ([#34](https://github.com/diffplug/mouseterm/pull/34)).", + "children": [] + }, + { + "text": "🖥️ macOS auto-update — strip AppleDouble (`._*`) sidecars from the signed tarball that were breaking every v0.7.x → v0.8.0 install ([#35](https://github.com/diffplug/mouseterm/pull/35)).", + "children": [] + } + ] + } + ] + }, + { + "version": "0.8.0", + "tag": "v0.8.0", + "date": "2026-04-29", + "sections": [ + { + "title": "Changes", + "items": [ + { + "text": "Add intuitive shortcuts alongside the tmux shortcuts.", + "children": [] + }, + { + "text": "Simplify the TODO behavior to clear when ENTER pressed within a session, got rid of the \"soft TODO\" system.", + "children": [] + }, + { + "text": "Improve VS Code theme translation.", + "children": [ + { + "text": "Added a \"Theme debugger\" to assist with this.", + "children": [] + } + ] + }, + { + "text": "Fix terminal selection on Windows.", + "children": [] + } + ] + } + ] + }, + { + "version": "0.7.0", + "tag": "v0.7.0", + "date": "2026-04-22", + "sections": [ + { + "title": "Changes", + "items": [ + { + "text": "Overhaul the theming system.", + "children": [] + }, + { + "text": "Overhaul mouse and clipboard handling.", + "children": [] + }, + { + "text": "Overhaul alerting system.", + "children": [] + } + ] + } + ] + }, + { + "version": "0.6.2", + "tag": "v0.6.2", + "date": "2026-04-13", + "sections": [ + { + "title": "Changes", + "items": [ + { + "text": "Fix issues with deployed Tauri on Win and Mac (Linux is working great!)", + "children": [] + } + ] + } + ] + }, + { + "version": "0.6.1", + "tag": "v0.6.1", + "date": "2026-04-13", + "sections": [ + { + "title": "Changes", + "items": [ + { + "text": "Fix missing Tauri update permissions.", + "children": [] + } + ] + } + ] + }, + { + "version": "0.6.0", + "tag": "v0.6.0", + "date": "2026-04-13", + "sections": [ + { + "title": "Changes", + "items": [ + { + "text": "Standalone: fix some issues with node sidecar.", + "children": [] + }, + { + "text": "Standalone: app-rendered title bar.", + "children": [] + } + ] + } + ] + }, + { + "version": "0.5.2", + "tag": "v0.5.2", + "date": "2026-04-10", + "sections": [ + { + "title": "Changes", + "items": [ + { + "text": "Codex fixes.", + "children": [] + } + ] + } + ] + }, + { + "version": "0.5.1", + "tag": "v0.5.1", + "date": "2026-04-10", + "sections": [ + { + "title": "Changes", + "items": [ + { + "text": "Fix uploading glob.", + "children": [] + } + ] + } + ] + }, + { + "version": "0.5.0", + "tag": "v0.5.0", + "date": "2026-04-10", + "sections": [ + { + "title": "Changes", + "items": [ + { + "text": "Get ready to test auto-update for the standalone apps.", + "children": [] + }, + { + "text": "Add icons to the standalone apps.", + "children": [] + } + ] + } + ] + }, + { + "version": "0.4.0", + "tag": "v0.4.0", + "date": "2026-04-10", + "sections": [ + { + "title": "Changes", + "items": [ + { + "text": "Yet yet another initial release to test publishing.", + "children": [] + } + ] + } + ] + }, + { + "version": "0.3.0", + "tag": "v0.3.0", + "date": "2026-04-10", + "sections": [ + { + "title": "Changes", + "items": [ + { + "text": "Yet another initial release to test publishing.", + "children": [] + } + ] + } + ] + }, + { + "version": "0.2.0", + "tag": "v0.2.0", + "date": "2026-04-09", + "sections": [ + { + "title": "Changes", + "items": [ + { + "text": "Another initial release to test publishing.", + "children": [] + } + ] + } + ] + }, + { + "version": "0.1.0", + "tag": "v0.1.0", + "date": "2026-04-09", + "sections": [ + { + "title": "Changes", + "items": [ + { + "text": "Initial release to test publishing.", + "children": [] + } + ] + } + ] + } + ] +} diff --git a/website/src/pages/Changelog.tsx b/website/src/pages/Changelog.tsx new file mode 100644 index 0000000..2f7dfc1 --- /dev/null +++ b/website/src/pages/Changelog.tsx @@ -0,0 +1,224 @@ +import type { ReactNode } from "react"; +import { Link, useParams } from "react-router-dom"; +import SiteHeader from "../components/SiteHeader"; +import changelog from "../data/changelog.json"; + +interface ChangelogItem { + text: string; + children: ChangelogItem[]; +} + +interface ChangelogSection { + title: string; + items: ChangelogItem[]; +} + +interface ChangelogRelease { + version: string; + tag: string; + date: string | null; + sections: ChangelogSection[]; +} + +interface ChangelogData { + releases: ChangelogRelease[]; +} + +const CHANGELOG = changelog as ChangelogData; +const RELEASES = CHANGELOG.releases; +const RELEASE_VERSION_SET = new Set(RELEASES.map((release) => release.version)); +const HEADER_STYLE = { + background: "rgba(10, 10, 10, 0.85)", + backdropFilter: "blur(12px)", +}; + +function normalizeVersionParam(version: string) { + const normalized = version.trim().replace(/^v/i, ""); + return /^\d+\.\d+\.\d+$/.test(normalized) ? normalized : null; +} + +function compareVersions(left: string, right: string) { + const leftParts = left.split(".").map(Number); + const rightParts = right.split(".").map(Number); + + for (let index = 0; index < 3; index += 1) { + const difference = leftParts[index] - rightParts[index]; + if (difference !== 0) return difference; + } + + return 0; +} + +function formatDate(date: string | null) { + if (!date) return null; + + return new Intl.DateTimeFormat("en", { + month: "long", + day: "numeric", + year: "numeric", + timeZone: "UTC", + }).format(new Date(`${date}T00:00:00Z`)); +} + +function renderInlineMarkdown(text: string) { + const nodes: ReactNode[] = []; + const inlineToken = /`([^`]+)`|\[([^\]]+)\]\(([^)]+)\)/g; + let cursor = 0; + let key = 0; + + for (const match of text.matchAll(inlineToken)) { + if (match.index > cursor) { + nodes.push(text.slice(cursor, match.index)); + } + + if (match[1]) { + nodes.push( + + {match[1]} + , + ); + } else if (match[2] && match[3]) { + nodes.push( + + {match[2]} + , + ); + } + + cursor = match.index + match[0].length; + key += 1; + } + + if (cursor < text.length) nodes.push(text.slice(cursor)); + return nodes; +} + +function ChangelogListItem({ item }: { item: ChangelogItem }) { + return ( +
  • + {renderInlineMarkdown(item.text)} + {item.children.length > 0 ? ( + + ) : null} +
  • + ); +} + +function ReleaseSection({ section }: { section: ChangelogSection }) { + return ( +
    +

    + {section.title} +

    + +
    + ); +} + +function ReleaseArticle({ release }: { release: ChangelogRelease }) { + const date = formatDate(release.date); + + return ( +
    +
    +
    +

    + {release.tag} +

    + {date ? ( + + ) : null} +
    + + Newer than this + +
    + + {release.sections.map((section) => ( + + ))} +
    + ); +} + +export function Component() { + const { version: versionParam } = useParams(); + const requestedVersion = versionParam ? normalizeVersionParam(versionParam) : null; + const baselineVersion = + requestedVersion && RELEASE_VERSION_SET.has(requestedVersion) ? requestedVersion : null; + const visibleReleases = baselineVersion + ? RELEASES.filter((release) => compareVersions(release.version, baselineVersion) > 0) + : RELEASES; + const hasInvalidFilter = Boolean(versionParam && !baselineVersion); + + return ( + <> + + +
    +
    +
    +

    + Changelog +

    +

    + Release notes for MouseTerm. +

    +
    + + {baselineVersion ? ( +
    + Showing releases newer than v{baselineVersion}.{" "} + + Show all releases. + +
    + ) : null} + + {hasInvalidFilter ? ( +
    + Could not filter after "{versionParam}"; showing all releases.{" "} + + Reset the changelog. + +
    + ) : null} + + {visibleReleases.length > 0 ? ( +
    + {visibleReleases.map((release) => ( + + ))} +
    + ) : ( +
    + No releases newer than v{baselineVersion}. +
    + )} +
    +
    + + ); +} From 9a1c5368e5acb44b04f040cf351dab9ca67560b4 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Wed, 6 May 2026 00:07:38 -0700 Subject: [PATCH 2/7] Refine invalid changelog filter copy --- website/src/pages/Changelog.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/src/pages/Changelog.tsx b/website/src/pages/Changelog.tsx index 2f7dfc1..f810127 100644 --- a/website/src/pages/Changelog.tsx +++ b/website/src/pages/Changelog.tsx @@ -199,9 +199,9 @@ export function Component() { {hasInvalidFilter ? (
    - Could not filter after "{versionParam}"; showing all releases.{" "} + No such release "{versionParam}".{" "} - Reset the changelog. + Show all releases.
    ) : null} From a9a6564aea8618bbcf5b0e44547d1ae7ae0272f6 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Wed, 6 May 2026 00:08:09 -0700 Subject: [PATCH 3/7] Remove changelog from top nav --- website/src/components/SiteHeader.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/website/src/components/SiteHeader.tsx b/website/src/components/SiteHeader.tsx index 511f345..f9731c3 100644 --- a/website/src/components/SiteHeader.tsx +++ b/website/src/components/SiteHeader.tsx @@ -2,7 +2,6 @@ import { forwardRef } from "react"; const NAV_LINKS: readonly { href: string; label: string; external?: boolean; hideOnMobile?: boolean }[] = [ { href: "/playground", label: "Playground", hideOnMobile: true }, - { href: "/changelog", label: "Changelog", hideOnMobile: true }, { href: "/#download", label: "Download", hideOnMobile: true }, { href: "https://github.com/diffplug/mouseterm", label: "GitHub", external: true }, ]; From 97cd48f14305185b36299373af80d9610132b834 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Wed, 6 May 2026 00:08:45 -0700 Subject: [PATCH 4/7] Link changelog releases to GitHub --- website/src/pages/Changelog.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/website/src/pages/Changelog.tsx b/website/src/pages/Changelog.tsx index f810127..74b2afa 100644 --- a/website/src/pages/Changelog.tsx +++ b/website/src/pages/Changelog.tsx @@ -148,12 +148,14 @@ function ReleaseArticle({ release }: { release: ChangelogRelease }) { ) : null} - - Newer than this - + Download from GitHub + {release.sections.map((section) => ( From c43e727509e6cc70f166e8a2af3c7afda424fc95 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Wed, 6 May 2026 00:14:27 -0700 Subject: [PATCH 5/7] Hide releases for invalid changelog filters --- website/src/pages/Changelog.tsx | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/website/src/pages/Changelog.tsx b/website/src/pages/Changelog.tsx index 74b2afa..133c2dd 100644 --- a/website/src/pages/Changelog.tsx +++ b/website/src/pages/Changelog.tsx @@ -170,10 +170,15 @@ export function Component() { const requestedVersion = versionParam ? normalizeVersionParam(versionParam) : null; const baselineVersion = requestedVersion && RELEASE_VERSION_SET.has(requestedVersion) ? requestedVersion : null; - const visibleReleases = baselineVersion - ? RELEASES.filter((release) => compareVersions(release.version, baselineVersion) > 0) - : RELEASES; const hasInvalidFilter = Boolean(versionParam && !baselineVersion); + let visibleReleases: ChangelogRelease[]; + if (hasInvalidFilter) { + visibleReleases = []; + } else if (baselineVersion) { + visibleReleases = RELEASES.filter((release) => compareVersions(release.version, baselineVersion) > 0); + } else { + visibleReleases = RELEASES; + } return ( <> @@ -214,7 +219,7 @@ export function Component() { ))} - ) : ( + ) : hasInvalidFilter ? null : (
    No releases newer than v{baselineVersion}.
    From 87af1d684de3c193b1c5174d4dfe758e38c32845 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Wed, 6 May 2026 00:26:47 -0700 Subject: [PATCH 6/7] Treat changelog JSON as generated --- .claude/commands/release-notes.md | 16 +- .gitignore | 1 + docs/specs/deploy.md | 22 ++- website/package.json | 2 + website/src/data/changelog.json | 304 ------------------------------ 5 files changed, 27 insertions(+), 318 deletions(-) delete mode 100644 website/src/data/changelog.json diff --git a/.claude/commands/release-notes.md b/.claude/commands/release-notes.md index 122f84e..7f2a22d 100644 --- a/.claude/commands/release-notes.md +++ b/.claude/commands/release-notes.md @@ -1,10 +1,10 @@ --- name: release-notes -description: Draft release notes and recommend a version bump for the next mouseterm release by analyzing all merge commits and squash-merged PRs since the last release tag. Outputs a Keep a Changelog section ready to paste into CHANGELOG.md. Used as step 2 of the release checklist in docs/specs/deploy.md. +description: Draft release notes, recommend and apply a version bump, and update CHANGELOG.md for the next mouseterm release by analyzing all merge commits and squash-merged PRs since the last release tag. Used as step 2 of the release checklist in docs/specs/deploy.md. user-invocable: true --- -You are drafting release notes and recommending a version bump for the next mouseterm release. +You are drafting release notes, recommending and applying a version bump, and updating CHANGELOG.md for the next mouseterm release. ## 1. Gather context @@ -34,7 +34,11 @@ mouseterm uses **breaking.added.bugfix** semantics (semver-shaped, but named for Pick the highest-severity bump that any single change requires. -## 3. Edit `CHANGELOG.md` +## 3. Run the version bump + +Run `./scripts/bump-version.sh X.Y.Z` with the recommended version before editing the changelog. Show the script's output so the user can review the diff stat. + +## 4. Edit `CHANGELOG.md` Edit `CHANGELOG.md` directly — insert a new section above the most recent existing release, in this exact shape: @@ -65,8 +69,10 @@ Rules for the entries: Do not ask the user to paste it themselves — make the edit. The earlier flat-bullet entries (0.8.0 and below) are legacy; do not reformat them. -## 4. Run the version bump +## 5. Finish + +Do not run `website/scripts/generate-changelog.js` as part of the release-notes command. `website/src/data/changelog.json` is a gitignored generated artifact, and the website `prebuild`, `predev`, and `pretest` lifecycle scripts regenerate it from `CHANGELOG.md`. -After saving the changelog edit, run `./scripts/bump-version.sh X.Y.Z` with the recommended version. Show the script's output so the user can review the diff stat. Then remind the user of the next step: +Then remind the user of the next step: > Review the diff, then `git commit -am 'Release vX.Y.Z'` and `git tag vX.Y.Z`. diff --git a/.gitignore b/.gitignore index aff2428..4f9729e 100644 --- a/.gitignore +++ b/.gitignore @@ -29,6 +29,7 @@ standalone/node_modules/ # Website website/dist/ website/node_modules/ +website/src/data/changelog.json # OS .DS_Store diff --git a/docs/specs/deploy.md b/docs/specs/deploy.md index e788597..cce12a9 100644 --- a/docs/specs/deploy.md +++ b/docs/specs/deploy.md @@ -16,15 +16,13 @@ Every release produces three artifact groups under one version and changelog: Human-driven steps, in order: 1. **Update dependencies page** — run `node website/scripts/generate-deps.js` and review the diff in `website/src/data/dependencies.json`. Commit if changed. -2. **Draft release notes and pick version** — run `/release-notes` in Claude Code at the repo root. The slash command (defined in [.claude/commands/release-notes.md](../../.claude/commands/release-notes.md)) walks the merge commits and squash-merged PRs since the last tag, drafts a Keep a Changelog block, and recommends a `breaking.added.bugfix` version bump. Review and edit the output, paste it into `CHANGELOG.md` replacing the `[Unreleased]` section, and use the recommended `X.Y.Z` in the next step. - -3. **Bump versions** — `./scripts/bump-version.sh X.Y.Z`. Edits the four version files in lockstep and runs `cargo check` so `Cargo.lock` follows along. -4. **Commit and tag** — `git commit -m "Release vX.Y.Z"` then `git tag vX.Y.Z`. -5. **Push** — `git push && git push origin vX.Y.Z`. This triggers CI (Stage 1). -6. **Set environment variables** — copy the relevant secrets into the terminal from your password manager (see [Environment / secrets](#environment--secrets) for the list). -7. **Run local signing** — plug in the PIV USB key, then `./scripts/sign-and-deploy.sh all X.Y.Z`. The script waits for CI, downloads unsigned artifacts, signs macOS + Windows, generates the Tauri update manifest into `website/public/standalone-latest.json`, and creates the GitHub Release. Run `./scripts/sign-and-deploy.sh --help` for resume-after-failure subcommands. -8. **Deploy website** — commit the updated `website/public/standalone-latest.json` and deploy mouseterm.com so the updater endpoint is live. -9. **Verify the release** +2. **Draft release notes and bump version** — run `/release-notes` in Claude Code at the repo root. The slash command (defined in [.claude/commands/release-notes.md](../../.claude/commands/release-notes.md)) walks the merge commits and squash-merged PRs since the last tag, recommends a `breaking.added.bugfix` version bump, runs `./scripts/bump-version.sh X.Y.Z`, and edits `CHANGELOG.md` for the same version. Review and edit the resulting diff if needed. +3. **Commit and tag** — `git commit -am "Release vX.Y.Z"` then `git tag vX.Y.Z`. +4. **Push** — `git push && git push origin vX.Y.Z`. This triggers CI (Stage 1). +5. **Set environment variables** — copy the relevant secrets into the terminal from your password manager (see [Environment / secrets](#environment--secrets) for the list). +6. **Run local signing** — plug in the PIV USB key, then `./scripts/sign-and-deploy.sh all X.Y.Z`. The script waits for CI, downloads unsigned artifacts, signs macOS + Windows, generates the Tauri update manifest into `website/public/standalone-latest.json`, and creates the GitHub Release. Run `./scripts/sign-and-deploy.sh --help` for resume-after-failure subcommands. +7. **Deploy website** — commit the updated `website/public/standalone-latest.json` and deploy mouseterm.com so the updater endpoint is live. +8. **Verify the release** - Check GitHub Release assets are correct - On a Mac: extract the `.tar.gz`, open the `.app`, confirm no Gatekeeper warnings - On Windows: run the `.exe` installer, confirm no SmartScreen warnings @@ -204,6 +202,12 @@ Note: the update manifest URLs include the version in the *path* (`/v0.1.0/`) bu A single `CHANGELOG.md` at the repo root, following [Keep a Changelog](https://keepachangelog.com/) format. The `[Unreleased]` section is promoted to `[X.Y.Z]` at release time. The release notes include both standalone and VSCode changes in one entry. +The website changelog page imports generated data from `website/src/data/changelog.json`, but `CHANGELOG.md` is the source of truth and the JSON is gitignored. You do not normally run `website/scripts/generate-changelog.js` by hand: +- `pnpm --filter mouseterm-website build` runs it through the website `prebuild` script before Vite bundles the static site. +- `pnpm --filter mouseterm-website dev` and `pnpm --filter mouseterm-website test` also regenerate it through lifecycle scripts so clean checkouts work locally. + +If you edit `CHANGELOG.md` manually outside `/release-notes` and want to preview the generated data immediately, run `node website/scripts/generate-changelog.js`. Do not commit `website/src/data/changelog.json`. + ## Environment / secrets | Secret | Where | Purpose | diff --git a/website/package.json b/website/package.json index 6d2441d..d8704ac 100644 --- a/website/package.json +++ b/website/package.json @@ -6,9 +6,11 @@ "type": "module", "scripts": { "dev": "vite-react-ssg dev", + "predev": "node scripts/generate-changelog.js", "prebuild": "node scripts/generate-deps.js && node scripts/generate-changelog.js", "build": "vite-react-ssg build", "preview": "vite preview", + "pretest": "node scripts/generate-changelog.js", "test": "vitest run" }, "dependencies": { diff --git a/website/src/data/changelog.json b/website/src/data/changelog.json deleted file mode 100644 index 12f6910..0000000 --- a/website/src/data/changelog.json +++ /dev/null @@ -1,304 +0,0 @@ -{ - "generatedFrom": "CHANGELOG.md", - "releases": [ - { - "version": "0.9.1", - "tag": "v0.9.1", - "date": "2026-05-01", - "sections": [ - { - "title": "Changed", - "items": [ - { - "text": "🖥️ Drop-to-paste from the OS file explorer is temporarily inert on standalone while we wait on upstream Tauri ([tauri#14373](https://github.com/tauri-apps/tauri/issues/14373)) to allow native drag-drop without blocking HTML5 drag events ([#39](https://github.com/diffplug/mouseterm/pull/39)).", - "children": [] - } - ] - }, - { - "title": "Fixed", - "items": [ - { - "text": "The mouse-override banner now renders inline in the terminal pane body and no longer stacks with the action-button tooltip ([#43](https://github.com/diffplug/mouseterm/pull/43)).", - "children": [] - }, - { - "text": "Themes with translucent selection backgrounds (e.g. Selenized Dark) no longer bleed through MouseTerm's solid AppBar and tab fills ([#37](https://github.com/diffplug/mouseterm/pull/37)).", - "children": [] - }, - { - "text": "🖥️ Force-closing the standalone host now reliably kills the Node sidecar tree via a Windows Job Object / Unix process group, so subsequent builds no longer hit orphan `node.exe` processes locking files ([#41](https://github.com/diffplug/mouseterm/pull/41)).", - "children": [] - }, - { - "text": "🖥️ Standalone macOS terminals run zsh as a login shell when no args are provided, so `~/.zprofile` runs and Homebrew/asdf land on `PATH` ([#40](https://github.com/diffplug/mouseterm/pull/40)).", - "children": [] - }, - { - "text": "🖥️ Pane drag-and-drop reordering works again on standalone ([#39](https://github.com/diffplug/mouseterm/pull/39)).", - "children": [] - } - ] - } - ] - }, - { - "version": "0.9.0", - "tag": "v0.9.0", - "date": "2026-04-30", - "sections": [ - { - "title": "Added", - "items": [ - { - "text": "🖥️ Debug dialog for failed auto-updates — surfaces the error and copies a pre-filled bug report (version, platform, last ~10 KB of `mouseterm.log`) ([#35](https://github.com/diffplug/mouseterm/pull/35)).", - "children": [] - } - ] - }, - { - "title": "Fixed", - "items": [ - { - "text": "Terminals auto-spawned from a blank workspace now respect the selected shell ([#33](https://github.com/diffplug/mouseterm/pull/33)).", - "children": [] - }, - { - "text": "🖥️ Polish app bar header to align with pane chrome and shared design tokens ([#34](https://github.com/diffplug/mouseterm/pull/34)).", - "children": [] - }, - { - "text": "🖥️ macOS auto-update — strip AppleDouble (`._*`) sidecars from the signed tarball that were breaking every v0.7.x → v0.8.0 install ([#35](https://github.com/diffplug/mouseterm/pull/35)).", - "children": [] - } - ] - } - ] - }, - { - "version": "0.8.0", - "tag": "v0.8.0", - "date": "2026-04-29", - "sections": [ - { - "title": "Changes", - "items": [ - { - "text": "Add intuitive shortcuts alongside the tmux shortcuts.", - "children": [] - }, - { - "text": "Simplify the TODO behavior to clear when ENTER pressed within a session, got rid of the \"soft TODO\" system.", - "children": [] - }, - { - "text": "Improve VS Code theme translation.", - "children": [ - { - "text": "Added a \"Theme debugger\" to assist with this.", - "children": [] - } - ] - }, - { - "text": "Fix terminal selection on Windows.", - "children": [] - } - ] - } - ] - }, - { - "version": "0.7.0", - "tag": "v0.7.0", - "date": "2026-04-22", - "sections": [ - { - "title": "Changes", - "items": [ - { - "text": "Overhaul the theming system.", - "children": [] - }, - { - "text": "Overhaul mouse and clipboard handling.", - "children": [] - }, - { - "text": "Overhaul alerting system.", - "children": [] - } - ] - } - ] - }, - { - "version": "0.6.2", - "tag": "v0.6.2", - "date": "2026-04-13", - "sections": [ - { - "title": "Changes", - "items": [ - { - "text": "Fix issues with deployed Tauri on Win and Mac (Linux is working great!)", - "children": [] - } - ] - } - ] - }, - { - "version": "0.6.1", - "tag": "v0.6.1", - "date": "2026-04-13", - "sections": [ - { - "title": "Changes", - "items": [ - { - "text": "Fix missing Tauri update permissions.", - "children": [] - } - ] - } - ] - }, - { - "version": "0.6.0", - "tag": "v0.6.0", - "date": "2026-04-13", - "sections": [ - { - "title": "Changes", - "items": [ - { - "text": "Standalone: fix some issues with node sidecar.", - "children": [] - }, - { - "text": "Standalone: app-rendered title bar.", - "children": [] - } - ] - } - ] - }, - { - "version": "0.5.2", - "tag": "v0.5.2", - "date": "2026-04-10", - "sections": [ - { - "title": "Changes", - "items": [ - { - "text": "Codex fixes.", - "children": [] - } - ] - } - ] - }, - { - "version": "0.5.1", - "tag": "v0.5.1", - "date": "2026-04-10", - "sections": [ - { - "title": "Changes", - "items": [ - { - "text": "Fix uploading glob.", - "children": [] - } - ] - } - ] - }, - { - "version": "0.5.0", - "tag": "v0.5.0", - "date": "2026-04-10", - "sections": [ - { - "title": "Changes", - "items": [ - { - "text": "Get ready to test auto-update for the standalone apps.", - "children": [] - }, - { - "text": "Add icons to the standalone apps.", - "children": [] - } - ] - } - ] - }, - { - "version": "0.4.0", - "tag": "v0.4.0", - "date": "2026-04-10", - "sections": [ - { - "title": "Changes", - "items": [ - { - "text": "Yet yet another initial release to test publishing.", - "children": [] - } - ] - } - ] - }, - { - "version": "0.3.0", - "tag": "v0.3.0", - "date": "2026-04-10", - "sections": [ - { - "title": "Changes", - "items": [ - { - "text": "Yet another initial release to test publishing.", - "children": [] - } - ] - } - ] - }, - { - "version": "0.2.0", - "tag": "v0.2.0", - "date": "2026-04-09", - "sections": [ - { - "title": "Changes", - "items": [ - { - "text": "Another initial release to test publishing.", - "children": [] - } - ] - } - ] - }, - { - "version": "0.1.0", - "tag": "v0.1.0", - "date": "2026-04-09", - "sections": [ - { - "title": "Changes", - "items": [ - { - "text": "Initial release to test publishing.", - "children": [] - } - ] - } - ] - } - ] -} From 01cef30acf9ca4f95bfa3463dc5c3dcc09eb83f8 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Wed, 6 May 2026 00:42:57 -0700 Subject: [PATCH 7/7] Simplify changelog page rendering --- website/scripts/changelog-parser.js | 5 +- website/src/components/SiteHeader.tsx | 5 ++ website/src/pages/Changelog.tsx | 108 +++++++++++--------------- website/src/pages/Dependencies.tsx | 7 +- 4 files changed, 52 insertions(+), 73 deletions(-) diff --git a/website/scripts/changelog-parser.js b/website/scripts/changelog-parser.js index 3ef6f1d..676f766 100644 --- a/website/scripts/changelog-parser.js +++ b/website/scripts/changelog-parser.js @@ -84,8 +84,5 @@ export function parseChangelog(markdown) { appendContinuation(currentSection, line); } - return { - generatedFrom: "CHANGELOG.md", - releases, - }; + return { releases }; } diff --git a/website/src/components/SiteHeader.tsx b/website/src/components/SiteHeader.tsx index f9731c3..9cd28fa 100644 --- a/website/src/components/SiteHeader.tsx +++ b/website/src/components/SiteHeader.tsx @@ -1,5 +1,10 @@ import { forwardRef } from "react"; +export const STATIC_PAGE_HEADER_STYLE: React.CSSProperties = { + background: "rgba(10, 10, 10, 0.85)", + backdropFilter: "blur(12px)", +}; + const NAV_LINKS: readonly { href: string; label: string; external?: boolean; hideOnMobile?: boolean }[] = [ { href: "/playground", label: "Playground", hideOnMobile: true }, { href: "/#download", label: "Download", hideOnMobile: true }, diff --git a/website/src/pages/Changelog.tsx b/website/src/pages/Changelog.tsx index 133c2dd..a56a93c 100644 --- a/website/src/pages/Changelog.tsx +++ b/website/src/pages/Changelog.tsx @@ -1,6 +1,6 @@ import type { ReactNode } from "react"; import { Link, useParams } from "react-router-dom"; -import SiteHeader from "../components/SiteHeader"; +import SiteHeader, { STATIC_PAGE_HEADER_STYLE } from "../components/SiteHeader"; import changelog from "../data/changelog.json"; interface ChangelogItem { @@ -24,49 +24,31 @@ interface ChangelogData { releases: ChangelogRelease[]; } -const CHANGELOG = changelog as ChangelogData; -const RELEASES = CHANGELOG.releases; -const RELEASE_VERSION_SET = new Set(RELEASES.map((release) => release.version)); -const HEADER_STYLE = { - background: "rgba(10, 10, 10, 0.85)", - backdropFilter: "blur(12px)", -}; +const RELEASES = (changelog as ChangelogData).releases; +const INLINE_TOKEN_RE = /`([^`]+)`|\[([^\]]+)\]\(([^)]+)\)/g; +const DATE_FORMATTER = new Intl.DateTimeFormat("en", { + month: "long", + day: "numeric", + year: "numeric", + timeZone: "UTC", +}); function normalizeVersionParam(version: string) { const normalized = version.trim().replace(/^v/i, ""); return /^\d+\.\d+\.\d+$/.test(normalized) ? normalized : null; } -function compareVersions(left: string, right: string) { - const leftParts = left.split(".").map(Number); - const rightParts = right.split(".").map(Number); - - for (let index = 0; index < 3; index += 1) { - const difference = leftParts[index] - rightParts[index]; - if (difference !== 0) return difference; - } - - return 0; -} - function formatDate(date: string | null) { if (!date) return null; - - return new Intl.DateTimeFormat("en", { - month: "long", - day: "numeric", - year: "numeric", - timeZone: "UTC", - }).format(new Date(`${date}T00:00:00Z`)); + return DATE_FORMATTER.format(new Date(`${date}T00:00:00Z`)); } function renderInlineMarkdown(text: string) { const nodes: ReactNode[] = []; - const inlineToken = /`([^`]+)`|\[([^\]]+)\]\(([^)]+)\)/g; let cursor = 0; let key = 0; - for (const match of text.matchAll(inlineToken)) { + for (const match of text.matchAll(INLINE_TOKEN_RE)) { if (match.index > cursor) { nodes.push(text.slice(cursor, match.index)); } @@ -75,7 +57,7 @@ function renderInlineMarkdown(text: string) { nodes.push( {match[1]} , @@ -165,24 +147,34 @@ function ReleaseArticle({ release }: { release: ChangelogRelease }) { ); } +function FilterNotice({ children }: { children: ReactNode }) { + return ( +
    + {children}{" "} + + Show all releases. + +
    + ); +} + export function Component() { const { version: versionParam } = useParams(); const requestedVersion = versionParam ? normalizeVersionParam(versionParam) : null; - const baselineVersion = - requestedVersion && RELEASE_VERSION_SET.has(requestedVersion) ? requestedVersion : null; - const hasInvalidFilter = Boolean(versionParam && !baselineVersion); - let visibleReleases: ChangelogRelease[]; - if (hasInvalidFilter) { - visibleReleases = []; - } else if (baselineVersion) { - visibleReleases = RELEASES.filter((release) => compareVersions(release.version, baselineVersion) > 0); - } else { - visibleReleases = RELEASES; - } + const baselineIndex = requestedVersion + ? RELEASES.findIndex((release) => release.version === requestedVersion) + : -1; + const baselineVersion = baselineIndex >= 0 ? requestedVersion : null; + const hasInvalidFilter = Boolean(versionParam) && !baselineVersion; + const visibleReleases = hasInvalidFilter + ? [] + : baselineVersion + ? RELEASES.slice(0, baselineIndex) + : RELEASES; return ( <> - +
    @@ -195,35 +187,23 @@ export function Component() {

    - {baselineVersion ? ( -
    - Showing releases newer than v{baselineVersion}.{" "} - - Show all releases. - -
    + {hasInvalidFilter ? ( + No such release "{versionParam}". ) : null} - {hasInvalidFilter ? ( -
    - No such release "{versionParam}".{" "} - - Show all releases. - -
    + {baselineVersion ? ( + Showing releases newer than v{baselineVersion}. ) : null} - {visibleReleases.length > 0 ? ( -
    - {visibleReleases.map((release) => ( - - ))} -
    - ) : hasInvalidFilter ? null : ( + {visibleReleases.map((release) => ( + + ))} + + {baselineVersion && visibleReleases.length === 0 ? (
    No releases newer than v{baselineVersion}.
    - )} + ) : null}
    diff --git a/website/src/pages/Dependencies.tsx b/website/src/pages/Dependencies.tsx index b3a22d7..1a69227 100644 --- a/website/src/pages/Dependencies.tsx +++ b/website/src/pages/Dependencies.tsx @@ -1,13 +1,10 @@ import deps from "../data/dependencies.json"; -import SiteHeader from "../components/SiteHeader"; - -const HEADER_BG = "rgba(10, 10, 10, 0.85)"; -const HEADER_STYLE = { background: HEADER_BG, backdropFilter: "blur(12px)" }; +import SiteHeader, { STATIC_PAGE_HEADER_STYLE } from "../components/SiteHeader"; export function Component() { return ( <> - +