From 5aac5dd3302fb093991374986de84917ea5b2d43 Mon Sep 17 00:00:00 2001 From: Jeffrey Tseng Date: Sat, 29 Aug 2026 21:46:10 -0400 Subject: [PATCH] fix(tui): stop replaying release notes Track startup release notes by changelog document order rather than package semver so version resets do not replay inherited history. Baseline fresh and legacy markers at the current changelog head and only preview entries that are actually unseen. --- .../src/modes/interactive/interactive-mode.ts | 28 +++++++------- packages/coding-agent/src/utils/changelog.ts | 34 +++++++++++++++++ packages/coding-agent/test/changelog.test.ts | 38 +++++++++++++++++++ 3 files changed, 85 insertions(+), 15 deletions(-) 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: [], + }); + }); +});