diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index d051c8226..8718e766e 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -11,6 +11,7 @@ - A failed token refresh of an MCP connection no longer registers an OAuth client or replaces the PKCE verifier of a sign-in in progress, which made that sign-in fail. MCP sign-in now refreshes the stored tokens with the stored client even when the loopback callback port changed, instead of registering a new client and opening the browser. Ctrl+C in `/mcp` and the `--timeout` of `pi mcp login` now also stop a sign-in while it waits up to 25 seconds for another pi process's token refresh, and a refresh lock that fails for another reason than that wait now fails the sign-in instead of opening the browser. - Fixed RPC mode ignoring an extension's `ctx.shutdown()` while idle, for example from a timer: the process kept running until the client sent another command. pi now shuts down right away when idle, and a shutdown requested by an extension command waits until the command's `prompt` response is written ([#11599](https://github.com/earendil-works/pi/issues/11599)) +- Fixed changelog notes repeating on every fresh launch when the installed package version has a prerelease suffix, such as `0.1.5-20261020.7`: the saved version was read as `0.1.0`, so the `0.1.1` to `0.1.5` notes counted as new each time. A saved version without `major.minor.patch` now shows no notes and is replaced by the installed version, instead of showing the whole changelog ([#11703](https://github.com/earendil-works/pi/issues/11703)) ## [1.5.0] - 2026-11-02 diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index 6c6fbcf37..1af1ad6d3 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -1372,6 +1372,11 @@ export class InteractiveMode { } const newEntries = getNewEntries(entries, lastVersion); + if (!newEntries) { + // Unreadable saved version: show nothing and record the current one so later updates compare again + this.settingsManager.setLastChangelogVersion(VERSION); + return undefined; + } if (newEntries.length > 0) { this.settingsManager.setLastChangelogVersion(VERSION); this.reportInstallTelemetry(VERSION); @@ -1413,6 +1418,11 @@ export class InteractiveMode { } const newEntries = getNewEntries(parseChangelog(sourceInfo.changelogPath), lastVersion); + if (!newEntries) { + // Unreadable saved version: show nothing and record the current one so later updates compare again + this.settingsManager.setExtensionChangelogVersion(key, pkg.version); + continue; + } if (newEntries.length > 0) { this.settingsManager.setExtensionChangelogVersion(key, pkg.version); const name = pkg.name || sourceInfo.source; diff --git a/packages/coding-agent/src/utils/changelog.ts b/packages/coding-agent/src/utils/changelog.ts index 2c8ce4a60..be813eb3c 100644 --- a/packages/coding-agent/src/utils/changelog.ts +++ b/packages/coding-agent/src/utils/changelog.ts @@ -177,15 +177,20 @@ export function compareVersions(v1: ChangelogEntry, v2: ChangelogEntry): number } /** - * Get entries newer than lastVersion + * Get entries newer than lastVersion, or undefined when lastVersion has no major.minor.patch to compare against */ -export function getNewEntries(entries: ChangelogEntry[], lastVersion: string): ChangelogEntry[] { - // Parse lastVersion - const parts = lastVersion.split(".").map(Number); +export function getNewEntries(entries: ChangelogEntry[], lastVersion: string): ChangelogEntry[] | undefined { + // Read only the leading major.minor.patch, like the changelog headings, so a prerelease such as + // 0.1.5-20261020.7 counts as 0.1.5 and its notes are not shown again on every launch (#11703). + // Alternatives considered: plain string comparison orders 0.10.0 before 0.9.0; numeric localeCompare + // works by accident and is locale-dependent; strict semver orders 0.1.5-x before 0.1.5, which repeats + // the [0.1.5] notes forever. + const parts = lastVersion.trim().match(/^v?(\d+)\.(\d+)\.(\d+)/); + if (!parts) return undefined; const last: ChangelogEntry = { - major: parts[0] || 0, - minor: parts[1] || 0, - patch: parts[2] || 0, + major: Number(parts[1]), + minor: Number(parts[2]), + patch: Number(parts[3]), content: "", }; diff --git a/packages/coding-agent/test/changelog.test.ts b/packages/coding-agent/test/changelog.test.ts index 979e7cdf6..7a42ac95c 100644 --- a/packages/coding-agent/test/changelog.test.ts +++ b/packages/coding-agent/test/changelog.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "vitest"; -import { type ChangelogEntry, normalizeChangelogLinks } from "../src/utils/changelog.ts"; +import { type ChangelogEntry, getNewEntries, normalizeChangelogLinks } from "../src/utils/changelog.ts"; const entry: ChangelogEntry = { major: 0, @@ -47,3 +47,30 @@ describe("normalizeChangelogLinks", () => { ); }); }); + +describe("getNewEntries", () => { + const entries: ChangelogEntry[] = [ + { major: 0, minor: 1, patch: 5, content: "0.1.5" }, + { major: 0, minor: 1, patch: 4, content: "0.1.4" }, + ]; + const newContent = (lastVersion: string) => getNewEntries(entries, lastVersion)?.map((e) => e.content); + + // #11703 + test("ignores prerelease and build suffixes of the last seen version", () => { + expect(newContent("0.1.5-20261020.7")).toEqual([]); + expect(newContent("0.1.5+build.3")).toEqual([]); + expect(newContent("0.1.4-beta.1")).toEqual(["0.1.5"]); + }); + + test("compares release versions numerically", () => { + expect(newContent("0.1.5")).toEqual([]); + expect(newContent("0.1.3")).toEqual(["0.1.5", "0.1.4"]); + expect(getNewEntries([{ major: 0, minor: 10, patch: 0, content: "0.10.0" }], "0.9.0")?.length).toBe(1); + }); + + test("returns undefined for a last seen version without major.minor.patch", () => { + expect(newContent("latest")).toBeUndefined(); + expect(newContent("0.1")).toBeUndefined(); + expect(newContent("")).toBeUndefined(); + }); +}); diff --git a/packages/coding-agent/test/interactive-mode-extension-changelog.test.ts b/packages/coding-agent/test/interactive-mode-extension-changelog.test.ts index f588ba474..853be1ec8 100644 --- a/packages/coding-agent/test/interactive-mode-extension-changelog.test.ts +++ b/packages/coding-agent/test/interactive-mode-extension-changelog.test.ts @@ -117,6 +117,28 @@ describe("InteractiveMode extension changelog", () => { expect(display()).toEqual([]); }); + // #11703 + test("does not repeat entries for a prerelease version", () => { + writePackage("1.0.0"); + display(); + writePackage("1.2.0-20261020.7"); + + expect(display()).toHaveLength(1); + expect(display()).toEqual([]); + }); + + test("shows nothing for an unreadable last seen version and records the current one", () => { + const key = getSourcePackageKey(sourceInfo); + settingsManager.setExtensionChangelogVersion(key, "latest"); + writePackage("1.0.0"); + + expect(display()).toEqual([]); + expect(settingsManager.getExtensionChangelogVersion(key)).toBe("1.0.0"); + + writePackage("1.2.0"); + expect(display()).toHaveLength(1); + }); + test("skips resumed sessions", () => { writePackage("1.0.0"); display();