feat(coding-agent): show update notes (#4724)
This commit is contained in:
@@ -91,7 +91,7 @@ import { getCwdRelativePath } from "../../utils/paths.js";
|
|||||||
import { getPiUserAgent } from "../../utils/pi-user-agent.js";
|
import { getPiUserAgent } from "../../utils/pi-user-agent.js";
|
||||||
import { killTrackedDetachedChildren } from "../../utils/shell.js";
|
import { killTrackedDetachedChildren } from "../../utils/shell.js";
|
||||||
import { ensureTool } from "../../utils/tools-manager.js";
|
import { ensureTool } from "../../utils/tools-manager.js";
|
||||||
import { checkForNewPiVersion } from "../../utils/version-check.js";
|
import { checkForNewPiVersion, type LatestPiRelease } from "../../utils/version-check.js";
|
||||||
import { ArminComponent } from "./components/armin.js";
|
import { ArminComponent } from "./components/armin.js";
|
||||||
import { AssistantMessageComponent } from "./components/assistant-message.js";
|
import { AssistantMessageComponent } from "./components/assistant-message.js";
|
||||||
import { BashExecutionComponent } from "./components/bash-execution.js";
|
import { BashExecutionComponent } from "./components/bash-execution.js";
|
||||||
@@ -711,9 +711,9 @@ export class InteractiveMode {
|
|||||||
await this.init();
|
await this.init();
|
||||||
|
|
||||||
// Start version check asynchronously
|
// Start version check asynchronously
|
||||||
checkForNewPiVersion(this.version).then((newVersion) => {
|
checkForNewPiVersion(this.version).then((newRelease) => {
|
||||||
if (newVersion) {
|
if (newRelease) {
|
||||||
this.showNewVersionNotification(newVersion);
|
this.showNewVersionNotification(newRelease);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -3575,24 +3575,31 @@ export class InteractiveMode {
|
|||||||
this.ui.requestRender();
|
this.ui.requestRender();
|
||||||
}
|
}
|
||||||
|
|
||||||
showNewVersionNotification(newVersion: string): void {
|
showNewVersionNotification(release: LatestPiRelease): void {
|
||||||
const action = theme.fg("accent", `${APP_NAME} update`);
|
const action = theme.fg("accent", `${APP_NAME} update`);
|
||||||
const updateInstruction = theme.fg("muted", `New version ${newVersion} is available. Run `) + action;
|
const updateInstruction = theme.fg("muted", `New version ${release.version} is available. Run `) + action;
|
||||||
const changelogUrl = "https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/CHANGELOG.md";
|
const changelogUrl = "https://pi.dev/changelog";
|
||||||
const changelogLink = getCapabilities().hyperlinks
|
const changelogLink = getCapabilities().hyperlinks
|
||||||
? hyperlink(theme.fg("accent", "open changelog"), changelogUrl)
|
? hyperlink(theme.fg("accent", "open changelog"), changelogUrl)
|
||||||
: theme.fg("accent", changelogUrl);
|
: theme.fg("accent", changelogUrl);
|
||||||
const changelogLine = theme.fg("muted", "Changelog: ") + changelogLink;
|
const changelogLine = theme.fg("muted", "Changelog: ") + changelogLink;
|
||||||
|
const note = release.note?.trim();
|
||||||
|
|
||||||
this.chatContainer.addChild(new Spacer(1));
|
this.chatContainer.addChild(new Spacer(1));
|
||||||
this.chatContainer.addChild(new DynamicBorder((text) => theme.fg("warning", text)));
|
this.chatContainer.addChild(new DynamicBorder((text) => theme.fg("warning", text)));
|
||||||
this.chatContainer.addChild(
|
this.chatContainer.addChild(
|
||||||
new Text(
|
new Text(`${theme.bold(theme.fg("warning", "Update Available"))}\n${updateInstruction}`, 1, 0),
|
||||||
`${theme.bold(theme.fg("warning", "Update Available"))}\n${updateInstruction}\n${changelogLine}`,
|
|
||||||
1,
|
|
||||||
0,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
if (note) {
|
||||||
|
this.chatContainer.addChild(new Spacer(1));
|
||||||
|
this.chatContainer.addChild(
|
||||||
|
new Markdown(note, 1, 0, this.getMarkdownThemeWithSettings(), {
|
||||||
|
color: (text) => theme.fg("muted", text),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
this.chatContainer.addChild(new Spacer(1));
|
||||||
|
}
|
||||||
|
this.chatContainer.addChild(new Text(changelogLine, 1, 0));
|
||||||
this.chatContainer.addChild(new DynamicBorder((text) => theme.fg("warning", text)));
|
this.chatContainer.addChild(new DynamicBorder((text) => theme.fg("warning", text)));
|
||||||
this.ui.requestRender();
|
this.ui.requestRender();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { Markdown, type MarkdownTheme } from "@earendil-works/pi-tui";
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
import { selectConfig } from "./cli/config-selector.js";
|
import { selectConfig } from "./cli/config-selector.js";
|
||||||
import {
|
import {
|
||||||
@@ -24,6 +25,23 @@ export type PackageCommand = "install" | "remove" | "update" | "list";
|
|||||||
|
|
||||||
type UpdateTarget = { type: "all" } | { type: "self" } | { type: "extensions"; source?: string };
|
type UpdateTarget = { type: "all" } | { type: "self" } | { type: "extensions"; source?: string };
|
||||||
|
|
||||||
|
const SELF_UPDATE_NOTE_MARKDOWN_THEME: MarkdownTheme = {
|
||||||
|
heading: (text) => chalk.bold(chalk.yellow(text)),
|
||||||
|
link: (text) => chalk.cyan(text),
|
||||||
|
linkUrl: (text) => chalk.dim(text),
|
||||||
|
code: (text) => chalk.yellow(text),
|
||||||
|
codeBlock: (text) => chalk.dim(text),
|
||||||
|
codeBlockBorder: (text) => chalk.dim(text),
|
||||||
|
quote: (text) => chalk.dim(text),
|
||||||
|
quoteBorder: (text) => chalk.dim(text),
|
||||||
|
hr: (text) => chalk.dim(text),
|
||||||
|
listBullet: (text) => chalk.yellow(text),
|
||||||
|
bold: (text) => chalk.bold(text),
|
||||||
|
italic: (text) => chalk.italic(text),
|
||||||
|
strikethrough: (text) => chalk.strikethrough(text),
|
||||||
|
underline: (text) => chalk.underline(text),
|
||||||
|
};
|
||||||
|
|
||||||
interface PackageCommandOptions {
|
interface PackageCommandOptions {
|
||||||
command: PackageCommand;
|
command: PackageCommand;
|
||||||
source?: string;
|
source?: string;
|
||||||
@@ -293,9 +311,30 @@ function printSelfUpdateFallback(command: SelfUpdateCommand): void {
|
|||||||
console.error(chalk.dim(`If this keeps failing, run this command yourself: ${command.display}`));
|
console.error(chalk.dim(`If this keeps failing, run this command yourself: ${command.display}`));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function printSelfUpdateNote(note: string): void {
|
||||||
|
const trimmedNote = note.trim();
|
||||||
|
if (!trimmedNote) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log();
|
||||||
|
console.log(chalk.bold(chalk.yellow("Update note")));
|
||||||
|
try {
|
||||||
|
const width = Math.max(20, process.stdout.columns ?? 80);
|
||||||
|
const renderedLines = new Markdown(trimmedNote, 0, 0, SELF_UPDATE_NOTE_MARKDOWN_THEME)
|
||||||
|
.render(width)
|
||||||
|
.map((line) => line.trimEnd());
|
||||||
|
console.log(renderedLines.join("\n"));
|
||||||
|
} catch {
|
||||||
|
console.log(trimmedNote);
|
||||||
|
}
|
||||||
|
console.log();
|
||||||
|
}
|
||||||
|
|
||||||
interface SelfUpdatePlan {
|
interface SelfUpdatePlan {
|
||||||
packageName: string;
|
packageName: string;
|
||||||
shouldRun: boolean;
|
shouldRun: boolean;
|
||||||
|
note?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getSelfUpdatePlan(force: boolean): Promise<SelfUpdatePlan> {
|
async function getSelfUpdatePlan(force: boolean): Promise<SelfUpdatePlan> {
|
||||||
@@ -307,7 +346,7 @@ async function getSelfUpdatePlan(force: boolean): Promise<SelfUpdatePlan> {
|
|||||||
const latestRelease = await getLatestPiRelease(VERSION);
|
const latestRelease = await getLatestPiRelease(VERSION);
|
||||||
const packageName = latestRelease?.packageName ?? PACKAGE_NAME;
|
const packageName = latestRelease?.packageName ?? PACKAGE_NAME;
|
||||||
if (!latestRelease || packageName !== PACKAGE_NAME || isNewerPackageVersion(latestRelease.version, VERSION)) {
|
if (!latestRelease || packageName !== PACKAGE_NAME || isNewerPackageVersion(latestRelease.version, VERSION)) {
|
||||||
return { packageName, shouldRun: true };
|
return { packageName, shouldRun: true, ...(latestRelease?.note ? { note: latestRelease.note } : {}) };
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
return { packageName: PACKAGE_NAME, shouldRun: true };
|
return { packageName: PACKAGE_NAME, shouldRun: true };
|
||||||
@@ -522,6 +561,9 @@ export async function handlePackageCommand(args: string[]): Promise<boolean> {
|
|||||||
process.exitCode = 1;
|
process.exitCode = 1;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (selfUpdatePlan.note) {
|
||||||
|
printSelfUpdateNote(selfUpdatePlan.note);
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
if (installMethod === "npm") {
|
if (installMethod === "npm") {
|
||||||
prepareWindowsNpmSelfUpdate();
|
prepareWindowsNpmSelfUpdate();
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ const DEFAULT_VERSION_CHECK_TIMEOUT_MS = 10000;
|
|||||||
export interface LatestPiRelease {
|
export interface LatestPiRelease {
|
||||||
version: string;
|
version: string;
|
||||||
packageName?: string;
|
packageName?: string;
|
||||||
|
note?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ParsedVersion {
|
interface ParsedVersion {
|
||||||
@@ -67,13 +68,22 @@ export async function getLatestPiRelease(
|
|||||||
});
|
});
|
||||||
if (!response.ok) return undefined;
|
if (!response.ok) return undefined;
|
||||||
|
|
||||||
const data = (await response.json()) as { packageName?: unknown; version?: unknown };
|
const data = (await response.json()) as {
|
||||||
|
packageName?: unknown;
|
||||||
|
version?: unknown;
|
||||||
|
note?: unknown;
|
||||||
|
};
|
||||||
if (typeof data.version !== "string" || !data.version.trim()) {
|
if (typeof data.version !== "string" || !data.version.trim()) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
const packageName =
|
const packageName =
|
||||||
typeof data.packageName === "string" && data.packageName.trim() ? data.packageName.trim() : undefined;
|
typeof data.packageName === "string" && data.packageName.trim() ? data.packageName.trim() : undefined;
|
||||||
return { version: data.version.trim(), packageName };
|
const note = typeof data.note === "string" && data.note.trim() ? data.note.trim() : undefined;
|
||||||
|
return {
|
||||||
|
version: data.version.trim(),
|
||||||
|
packageName,
|
||||||
|
...(note ? { note } : {}),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getLatestPiVersion(
|
export async function getLatestPiVersion(
|
||||||
@@ -83,11 +93,11 @@ export async function getLatestPiVersion(
|
|||||||
return (await getLatestPiRelease(currentVersion, options))?.version;
|
return (await getLatestPiRelease(currentVersion, options))?.version;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function checkForNewPiVersion(currentVersion: string): Promise<string | undefined> {
|
export async function checkForNewPiVersion(currentVersion: string): Promise<LatestPiRelease | undefined> {
|
||||||
try {
|
try {
|
||||||
const latestVersion = await getLatestPiVersion(currentVersion);
|
const latestRelease = await getLatestPiRelease(currentVersion);
|
||||||
if (latestVersion && isNewerPackageVersion(latestVersion, currentVersion)) {
|
if (latestRelease && isNewerPackageVersion(latestRelease.version, currentVersion)) {
|
||||||
return latestVersion;
|
return latestRelease;
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ describe("version checks", () => {
|
|||||||
vi.stubGlobal("fetch", fetchMock);
|
vi.stubGlobal("fetch", fetchMock);
|
||||||
|
|
||||||
await expect(checkForNewPiVersion("1.2.3")).resolves.toBeUndefined();
|
await expect(checkForNewPiVersion("1.2.3")).resolves.toBeUndefined();
|
||||||
await expect(checkForNewPiVersion("1.2.2")).resolves.toBe("1.2.3");
|
await expect(checkForNewPiVersion("1.2.2")).resolves.toEqual({ version: "1.2.3" });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("uses the pi.dev version check api with a pi user agent", async () => {
|
it("uses the pi.dev version check api with a pi user agent", async () => {
|
||||||
@@ -57,11 +57,26 @@ describe("version checks", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns the active package name from the version check api", async () => {
|
it("returns the active package metadata from the version check api", async () => {
|
||||||
const fetchMock = vi.fn(async () => Response.json({ packageName: "@new-scope/pi", version: "1.2.4" }));
|
const fetchMock = vi.fn(async () =>
|
||||||
|
Response.json({
|
||||||
|
packageName: "@new-scope/pi",
|
||||||
|
version: "1.2.4",
|
||||||
|
}),
|
||||||
|
);
|
||||||
vi.stubGlobal("fetch", fetchMock);
|
vi.stubGlobal("fetch", fetchMock);
|
||||||
|
|
||||||
await expect(getLatestPiRelease("1.2.3")).resolves.toEqual({ packageName: "@new-scope/pi", version: "1.2.4" });
|
await expect(getLatestPiRelease("1.2.3")).resolves.toEqual({
|
||||||
|
packageName: "@new-scope/pi",
|
||||||
|
version: "1.2.4",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns update notes from the version check api", async () => {
|
||||||
|
const fetchMock = vi.fn(async () => Response.json({ note: " **Read this** ", version: "1.2.4" }));
|
||||||
|
vi.stubGlobal("fetch", fetchMock);
|
||||||
|
|
||||||
|
await expect(getLatestPiRelease("1.2.3")).resolves.toEqual({ note: "**Read this**", version: "1.2.4" });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("skips api calls when version checks are disabled", async () => {
|
it("skips api calls when version checks are disabled", async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user