diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index c6e270fe0..69a5d0f8d 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -79,7 +79,7 @@ - Fixed numeric GitHub issue and pull request autocomplete being suppressed inside skill slash-command arguments. ### Changed -- Startup release notes now default to a compact change-count summary. Use `startup.changelogMode` (`summary` | `expanded` | `hidden`) to control them; legacy `collapseChangelog` choices migrate automatically. +- Startup release notes now default to a compact change-count summary. Use `startup.changelogMode` (`summary` | `expanded` | `hidden`) to control them; legacy `collapseChangelog` choices migrate automatically ([#6771](https://github.com/can1357/oh-my-pi/issues/6771)). ## [17.1.7] - 2026-07-27 diff --git a/packages/coding-agent/src/config/settings-schema.ts b/packages/coding-agent/src/config/settings-schema.ts index 12ac2fbd6..81d80cb2e 100644 --- a/packages/coding-agent/src/config/settings-schema.ts +++ b/packages/coding-agent/src/config/settings-schema.ts @@ -1840,13 +1840,6 @@ export const SETTINGS_SCHEMA = { }, }, - // Legacy startup changelog boolean kept only as migration input for - // startup.changelogMode; it is intentionally hidden from the settings UI. - collapseChangelog: { - type: "boolean", - default: undefined, - }, - "magicKeywords.enabled": { type: "boolean", default: true, diff --git a/packages/coding-agent/src/main.ts b/packages/coding-agent/src/main.ts index 2bf71da19..27221afe6 100644 --- a/packages/coding-agent/src/main.ts +++ b/packages/coding-agent/src/main.ts @@ -90,16 +90,7 @@ import { createPersistedSubagentReviverFactory } from "./task/persisted-revive"; import { createTelemetryExportConfig, initTelemetryExport, isTelemetryExportEnabled } from "./telemetry-export"; import { concreteThinkingLevel, parseConfiguredThinkingLevel } from "./thinking"; import type { LspStartupServerInfo } from "./tools"; -import { - compareVersions, - getChangelogPath, - parseChangelog, - parseChangelogVersion, - readLastChangelogVersion, - type StartupChangelogSelection, - selectStartupChangelog, - writeLastChangelogVersion, -} from "./utils/changelog"; +import { getChangelogPath, resolveStartupChangelogForDisplay, type StartupChangelogSelection } from "./utils/changelog"; import { EventBus } from "./utils/event-bus"; import { withTimeoutSignal } from "./utils/fetch-timeout"; @@ -689,31 +680,11 @@ async function getChangelogForDisplay( return undefined; } - const lastVersion = await readLastChangelogVersion(); - const parsedLastVersion = parseChangelogVersion(lastVersion); - if (!parsedLastVersion) { - await writeLastChangelogVersion(VERSION); - return undefined; - } - if (lastVersion === VERSION) { - // Steady state: user already saw the current version's changelog. Skip the file read + parse. - return undefined; - } - if (mode === "hidden") { - const currentVersion = parseChangelogVersion(VERSION); - if (currentVersion && compareVersions(currentVersion, parsedLastVersion) > 0) { - await writeLastChangelogVersion(VERSION); - } - return undefined; - } - - const changelogPath = getChangelogPath(); - const entries = await parseChangelog(changelogPath); - const startupChangelog = selectStartupChangelog(entries, lastVersion, VERSION); - if (startupChangelog.persistCurrentVersion) { - await writeLastChangelogVersion(VERSION); - } - return startupChangelog.markdown ? startupChangelog : undefined; + return resolveStartupChangelogForDisplay({ + mode, + currentVersion: VERSION, + changelogPath: getChangelogPath(), + }); } const SESSION_ID_ARG_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; diff --git a/packages/coding-agent/src/utils/changelog.ts b/packages/coding-agent/src/utils/changelog.ts index 86735df7c..456fc23f2 100644 --- a/packages/coding-agent/src/utils/changelog.ts +++ b/packages/coding-agent/src/utils/changelog.ts @@ -1,6 +1,7 @@ import * as path from "node:path"; import { getLastChangelogVersionPath, isEnoent, logger } from "@oh-my-pi/pi-utils"; import bundledChangelogPath from "../../CHANGELOG.md" with { type: "file" }; +import type { SettingValue } from "../config/settings"; export interface ChangelogEntry { major: number; @@ -303,6 +304,44 @@ export function selectStartupChangelog( }; } +/** + * Resolve and persist the automatic startup changelog decision. + * + * Hidden mode advances the marker only for an upgrade, so downgrades do not + * erase knowledge of a newer version the user has already seen. + */ +export async function resolveStartupChangelogForDisplay(options: { + mode: SettingValue<"startup.changelogMode">; + currentVersion: string; + changelogPath?: string; + agentDir?: string; +}): Promise { + const lastVersion = await readLastChangelogVersion(options.agentDir); + const parsedLastVersion = parseChangelogVersion(lastVersion); + if (!parsedLastVersion) { + await writeLastChangelogVersion(options.currentVersion, options.agentDir); + return undefined; + } + if (lastVersion === options.currentVersion) { + // Steady state: skip the changelog file read and parse. + return undefined; + } + if (options.mode === "hidden") { + const currentVersion = parseChangelogVersion(options.currentVersion); + if (currentVersion && compareVersions(currentVersion, parsedLastVersion) > 0) { + await writeLastChangelogVersion(options.currentVersion, options.agentDir); + } + return undefined; + } + + const entries = await parseChangelog(options.changelogPath); + const startupChangelog = selectStartupChangelog(entries, lastVersion, options.currentVersion); + if (startupChangelog.persistCurrentVersion) { + await writeLastChangelogVersion(options.currentVersion, options.agentDir); + } + return startupChangelog.markdown ? startupChangelog : undefined; +} + // Re-export getChangelogPath from paths.ts for convenience export { getChangelogPath } from "../config"; diff --git a/packages/coding-agent/test/utils/changelog.test.ts b/packages/coding-agent/test/utils/changelog.test.ts index 3d333064a..a7d64d5f1 100644 --- a/packages/coding-agent/test/utils/changelog.test.ts +++ b/packages/coding-agent/test/utils/changelog.test.ts @@ -15,7 +15,7 @@ import * as fs from "node:fs/promises"; import * as os from "node:os"; import * as path from "node:path"; import { removeWithRetries, VERSION } from "@oh-my-pi/pi-utils"; -import { Settings } from "../../src/config/settings"; +import { SETTINGS_SCHEMA, Settings } from "../../src/config/settings"; import { type ChangelogEntry, formatStartupChangelogSummary, @@ -24,6 +24,7 @@ import { RECENT_CHANGELOG_ENTRY_LIMIT, readLastChangelogVersion, renderChangelogEntries, + resolveStartupChangelogForDisplay, STARTUP_CHANGELOG_FULL_HINT, STARTUP_CHANGELOG_MAX_BYTES, selectStartupChangelog, @@ -51,17 +52,31 @@ describe("startup changelog mode settings", () => { expect(Settings.isolated().get("startup.changelogMode")).toBe("summary"); }); - test("preserves legacy collapsed and expanded choices", () => { - expect(Settings.isolated({ collapseChangelog: true }).get("startup.changelogMode")).toBe("summary"); - expect(Settings.isolated({ collapseChangelog: false }).get("startup.changelogMode")).toBe("expanded"); + test("keeps the legacy key out of the public schema while migrating raw config", async () => { + expect(Object.hasOwn(SETTINGS_SCHEMA, "collapseChangelog")).toBe(false); + + await withTempAgentDir(async agentDir => { + const configPath = path.join(agentDir, "config.yml"); + for (const [legacyValue, expectedMode] of [ + [true, "summary"], + [false, "expanded"], + ] as const) { + await Bun.write(configPath, `collapseChangelog: ${legacyValue}\n`); + const settings = await Settings.loadReadOnly({ cwd: agentDir, agentDir }); + expect(settings.get("startup.changelogMode")).toBe(expectedMode); + } + }); }); - test("an explicit new mode wins over the legacy boolean", () => { - const settings = Settings.isolated({ - collapseChangelog: false, - "startup.changelogMode": "hidden", + test("lets an explicit new mode win over the legacy raw config key", async () => { + await withTempAgentDir(async agentDir => { + await Bun.write( + path.join(agentDir, "config.yml"), + "collapseChangelog: false\nstartup:\n changelogMode: hidden\n", + ); + const settings = await Settings.loadReadOnly({ cwd: agentDir, agentDir }); + expect(settings.get("startup.changelogMode")).toBe("hidden"); }); - expect(settings.get("startup.changelogMode")).toBe("hidden"); }); }); @@ -258,6 +273,31 @@ describe("last changelog marker", () => { expect(await Bun.file(path.join(agentDir, "last-changelog-version")).text()).toBe(CURRENT_VERSION); }); }); + + test("hidden mode suppresses display and advances the marker only for upgrades", async () => { + await withTempAgentDir(async agentDir => { + await writeLastChangelogVersion("1.0.0", agentDir); + + const upgradeDisplay = await resolveStartupChangelogForDisplay({ + mode: "hidden", + currentVersion: CURRENT_VERSION, + agentDir, + }); + + expect(upgradeDisplay).toBeUndefined(); + expect(await readLastChangelogVersion(agentDir)).toBe(CURRENT_VERSION); + + await writeLastChangelogVersion("3.0.0", agentDir); + const downgradeDisplay = await resolveStartupChangelogForDisplay({ + mode: "hidden", + currentVersion: CURRENT_VERSION, + agentDir, + }); + + expect(downgradeDisplay).toBeUndefined(); + expect(await readLastChangelogVersion(agentDir)).toBe("3.0.0"); + }); + }); }); describe.skipIf(!hasPtyHarness)("interactive startup changelog PTY smoke", () => {