fix(coding-agent): tighten startup changelog contracts
(cherry picked from commit e5490279ca0b400b143514e7ef3e38be0686bf31)
This commit is contained in:
committed by
can1357
parent
a4dc5a094a
commit
757b0938ce
@@ -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,
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
Reference in New Issue
Block a user