fix(coding-agent): tighten startup changelog contracts

(cherry picked from commit e5490279ca0b400b143514e7ef3e38be0686bf31)
This commit is contained in:
Wolfgang Schoenberger
2026-07-27 22:31:30 -07:00
committed by can1357
parent a4dc5a094a
commit 757b0938ce
5 changed files with 95 additions and 52 deletions
+1 -1
View File
@@ -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
@@ -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,
+6 -35
View File
@@ -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;
@@ -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<StartupChangelogSelection | undefined> {
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";
@@ -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", () => {