diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index 77135ee28..b105eb1f1 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -104,11 +104,11 @@ import type { TruncationResult } from "../../core/tools/truncate.ts"; import { hasTrustRequiringProjectResources, ProjectTrustStore } from "../../core/trust-manager.ts"; import { getUsageCostBreakdown } from "../../core/usage-totals.ts"; import { + formatStartupChangelogPreviewLines, getChangelogPath, - getNewEntries, - getStartupChangelogPreviewLines, normalizeChangelogLinks, parseChangelog, + selectStartupChangelogEntries, } from "../../utils/changelog.ts"; import { copyToClipboard, readClipboardText } from "../../utils/clipboard.ts"; import { extensionForImageMimeType, readClipboardImage } from "../../utils/clipboard-image.ts"; @@ -1188,7 +1188,11 @@ export class InteractiveMode { } private getStartupWhatsNewLines(): string[] { - const previewLines = getStartupChangelogPreviewLines(getChangelogPath()); + if (!this.changelogMarkdown) { + return []; + } + + const previewLines = formatStartupChangelogPreviewLines(this.changelogMarkdown); if (previewLines.length === 0) { return []; } @@ -1215,22 +1219,16 @@ export class InteractiveMode { const lastVersion = this.settingsManager.getLastChangelogVersion(); const changelogPath = getChangelogPath(); const entries = parseChangelog(changelogPath); + const selection = selectStartupChangelogEntries(entries, lastVersion); - if (!lastVersion) { - // Fresh install - record the version, send telemetry, don't show changelog - this.settingsManager.setLastChangelogVersion(VERSION); + if (selection.checkpointVersion && selection.checkpointVersion !== lastVersion) { + this.settingsManager.setLastChangelogVersion(selection.checkpointVersion); this.reportInstallTelemetry(VERSION); - return undefined; } - const newEntries = getNewEntries(entries, lastVersion); - if (newEntries.length > 0) { - this.settingsManager.setLastChangelogVersion(VERSION); - this.reportInstallTelemetry(VERSION); - return newEntries.map((e) => normalizeChangelogLinks(e.content, e)).join("\n\n"); - } - - return undefined; + return selection.entries.length > 0 + ? selection.entries.map((entry) => normalizeChangelogLinks(entry.content, entry)).join("\n\n") + : undefined; } private reportInstallTelemetry(version: string): void { diff --git a/packages/coding-agent/src/utils/changelog.ts b/packages/coding-agent/src/utils/changelog.ts index c4bb825d0..2bb87f800 100644 --- a/packages/coding-agent/src/utils/changelog.ts +++ b/packages/coding-agent/src/utils/changelog.ts @@ -26,10 +26,44 @@ export interface StartupChangelogPreviewOptions { maxWidth?: number; } +export interface StartupChangelogSelection { + checkpointVersion?: string; + entries: ChangelogEntry[]; +} + function entryVersion(entry: ChangelogEntry): string { return `${entry.major}.${entry.minor}.${entry.patch}`; } +/** + * Select changelog entries that appear before the last acknowledged entry. + * + * Startup tracking follows document order instead of semver so repositories + * can reset their package version without replaying inherited release notes. + * Unknown markers are treated as a migration baseline rather than as a reason + * to display the complete changelog. + */ +export function selectStartupChangelogEntries( + entries: ChangelogEntry[], + lastVersion: string | undefined, +): StartupChangelogSelection { + const checkpointVersion = entries[0] ? entryVersion(entries[0]) : undefined; + if (!checkpointVersion || !lastVersion) { + return { checkpointVersion, entries: [] }; + } + + const normalizedLastVersion = lastVersion.trim().replace(/^v/i, ""); + const lastSeenIndex = entries.findIndex((entry) => entryVersion(entry) === normalizedLastVersion); + if (lastSeenIndex <= 0) { + return { checkpointVersion, entries: [] }; + } + + return { + checkpointVersion, + entries: entries.slice(0, lastSeenIndex), + }; +} + function normalizeTag(version: string | ChangelogEntry): string { const versionString = typeof version === "string" ? version : entryVersion(version); return versionString.startsWith("v") ? versionString : `v${versionString}`; diff --git a/packages/coding-agent/test/changelog.test.ts b/packages/coding-agent/test/changelog.test.ts index bad2bfd5d..34387c593 100644 --- a/packages/coding-agent/test/changelog.test.ts +++ b/packages/coding-agent/test/changelog.test.ts @@ -3,6 +3,7 @@ import { type ChangelogEntry, formatStartupChangelogPreviewLines, normalizeChangelogLinks, + selectStartupChangelogEntries, } from "../src/utils/changelog.ts"; const entry: ChangelogEntry = { @@ -80,3 +81,40 @@ describe("formatStartupChangelogPreviewLines", () => { ]); }); }); + +describe("selectStartupChangelogEntries", () => { + const changelogEntries: ChangelogEntry[] = [ + { major: 0, minor: 3, patch: 0, content: "## [0.3.0]\n\n- Current Diffex release" }, + { major: 0, minor: 84, patch: 0, content: "## [0.84.0]\n\n- Inherited release" }, + { major: 0, minor: 83, patch: 0, content: "## [0.83.0]\n\n- Older inherited release" }, + ]; + + test("uses changelog order when package versions have been reset", () => { + expect(selectStartupChangelogEntries(changelogEntries, "0.84.0")).toEqual({ + checkpointVersion: "0.3.0", + entries: [changelogEntries[0]], + }); + }); + + test("acknowledges fresh installs and legacy markers without replaying history", () => { + expect(selectStartupChangelogEntries(changelogEntries, undefined)).toEqual({ + checkpointVersion: "0.3.0", + entries: [], + }); + expect(selectStartupChangelogEntries(changelogEntries, "0.2.0")).toEqual({ + checkpointVersion: "0.3.0", + entries: [], + }); + }); + + test("returns no startup entries after the current checkpoint is seen", () => { + expect(selectStartupChangelogEntries(changelogEntries, "v0.3.0")).toEqual({ + checkpointVersion: "0.3.0", + entries: [], + }); + expect(selectStartupChangelogEntries([], "0.3.0")).toEqual({ + checkpointVersion: undefined, + entries: [], + }); + }); +});