mirror of
https://github.com/earendil-works/pi.git
synced 2026-06-18 15:54:04 +08:00
feat(coding-agent): show update notes (#4724)
This commit is contained in:
committed by
GitHub
Unverified
parent
2787b601d7
commit
f4f0ac7ada
@@ -91,7 +91,7 @@ import { getCwdRelativePath } from "../../utils/paths.js";
|
||||
import { getPiUserAgent } from "../../utils/pi-user-agent.js";
|
||||
import { killTrackedDetachedChildren } from "../../utils/shell.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 { AssistantMessageComponent } from "./components/assistant-message.js";
|
||||
import { BashExecutionComponent } from "./components/bash-execution.js";
|
||||
@@ -711,9 +711,9 @@ export class InteractiveMode {
|
||||
await this.init();
|
||||
|
||||
// Start version check asynchronously
|
||||
checkForNewPiVersion(this.version).then((newVersion) => {
|
||||
if (newVersion) {
|
||||
this.showNewVersionNotification(newVersion);
|
||||
checkForNewPiVersion(this.version).then((newRelease) => {
|
||||
if (newRelease) {
|
||||
this.showNewVersionNotification(newRelease);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -3575,24 +3575,31 @@ export class InteractiveMode {
|
||||
this.ui.requestRender();
|
||||
}
|
||||
|
||||
showNewVersionNotification(newVersion: string): void {
|
||||
showNewVersionNotification(release: LatestPiRelease): void {
|
||||
const action = theme.fg("accent", `${APP_NAME} update`);
|
||||
const updateInstruction = theme.fg("muted", `New version ${newVersion} is available. Run `) + action;
|
||||
const changelogUrl = "https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/CHANGELOG.md";
|
||||
const updateInstruction = theme.fg("muted", `New version ${release.version} is available. Run `) + action;
|
||||
const changelogUrl = "https://pi.dev/changelog";
|
||||
const changelogLink = getCapabilities().hyperlinks
|
||||
? hyperlink(theme.fg("accent", "open changelog"), changelogUrl)
|
||||
: theme.fg("accent", changelogUrl);
|
||||
const changelogLine = theme.fg("muted", "Changelog: ") + changelogLink;
|
||||
const note = release.note?.trim();
|
||||
|
||||
this.chatContainer.addChild(new Spacer(1));
|
||||
this.chatContainer.addChild(new DynamicBorder((text) => theme.fg("warning", text)));
|
||||
this.chatContainer.addChild(
|
||||
new Text(
|
||||
`${theme.bold(theme.fg("warning", "Update Available"))}\n${updateInstruction}\n${changelogLine}`,
|
||||
1,
|
||||
0,
|
||||
),
|
||||
new Text(`${theme.bold(theme.fg("warning", "Update Available"))}\n${updateInstruction}`, 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.ui.requestRender();
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Markdown, type MarkdownTheme } from "@earendil-works/pi-tui";
|
||||
import chalk from "chalk";
|
||||
import { selectConfig } from "./cli/config-selector.js";
|
||||
import {
|
||||
@@ -24,6 +25,23 @@ export type PackageCommand = "install" | "remove" | "update" | "list";
|
||||
|
||||
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 {
|
||||
command: PackageCommand;
|
||||
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}`));
|
||||
}
|
||||
|
||||
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 {
|
||||
packageName: string;
|
||||
shouldRun: boolean;
|
||||
note?: string;
|
||||
}
|
||||
|
||||
async function getSelfUpdatePlan(force: boolean): Promise<SelfUpdatePlan> {
|
||||
@@ -307,7 +346,7 @@ async function getSelfUpdatePlan(force: boolean): Promise<SelfUpdatePlan> {
|
||||
const latestRelease = await getLatestPiRelease(VERSION);
|
||||
const packageName = latestRelease?.packageName ?? PACKAGE_NAME;
|
||||
if (!latestRelease || packageName !== PACKAGE_NAME || isNewerPackageVersion(latestRelease.version, VERSION)) {
|
||||
return { packageName, shouldRun: true };
|
||||
return { packageName, shouldRun: true, ...(latestRelease?.note ? { note: latestRelease.note } : {}) };
|
||||
}
|
||||
} catch {
|
||||
return { packageName: PACKAGE_NAME, shouldRun: true };
|
||||
@@ -522,6 +561,9 @@ export async function handlePackageCommand(args: string[]): Promise<boolean> {
|
||||
process.exitCode = 1;
|
||||
return true;
|
||||
}
|
||||
if (selfUpdatePlan.note) {
|
||||
printSelfUpdateNote(selfUpdatePlan.note);
|
||||
}
|
||||
try {
|
||||
if (installMethod === "npm") {
|
||||
prepareWindowsNpmSelfUpdate();
|
||||
|
||||
@@ -6,6 +6,7 @@ const DEFAULT_VERSION_CHECK_TIMEOUT_MS = 10000;
|
||||
export interface LatestPiRelease {
|
||||
version: string;
|
||||
packageName?: string;
|
||||
note?: string;
|
||||
}
|
||||
|
||||
interface ParsedVersion {
|
||||
@@ -67,13 +68,22 @@ export async function getLatestPiRelease(
|
||||
});
|
||||
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()) {
|
||||
return undefined;
|
||||
}
|
||||
const packageName =
|
||||
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(
|
||||
@@ -83,11 +93,11 @@ export async function getLatestPiVersion(
|
||||
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 {
|
||||
const latestVersion = await getLatestPiVersion(currentVersion);
|
||||
if (latestVersion && isNewerPackageVersion(latestVersion, currentVersion)) {
|
||||
return latestVersion;
|
||||
const latestRelease = await getLatestPiRelease(currentVersion);
|
||||
if (latestRelease && isNewerPackageVersion(latestRelease.version, currentVersion)) {
|
||||
return latestRelease;
|
||||
}
|
||||
return undefined;
|
||||
} catch {
|
||||
|
||||
@@ -38,7 +38,7 @@ describe("version checks", () => {
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
|
||||
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 () => {
|
||||
@@ -57,11 +57,26 @@ describe("version checks", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("returns the active package name from the version check api", async () => {
|
||||
const fetchMock = vi.fn(async () => Response.json({ packageName: "@new-scope/pi", version: "1.2.4" }));
|
||||
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",
|
||||
}),
|
||||
);
|
||||
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 () => {
|
||||
|
||||
Reference in New Issue
Block a user