fix(app): show update progress in its notification

The welcome screen has no status bar, so clicking Install and Restart there showed nothing until the download ended. The notification now stays with the download progress and Cancel, and a cancelled update says so.

Fixes #491
Цей коміт міститься в:
l0ng-ai authored and GitHub committed 2026-10-31 09:37:46 +08:00
1 джерело a5fb30e04f
коміт 28baa4a57a
5 files changed
+46 -8

No files matched your search

+14
Переглянути файл
@@ -0,0 +1,14 @@
/** The running update's progress in its balloon, for windows without a status bar (the welcome screen). */
import { ProgressBar } from "@/ui";
import { useTasks } from "@/workbench/tasks";
export function UpdateBalloonBody({ task }: { task: string }) {
const current = useTasks((s) => s.tasks.find((t) => t.id === task));
if (!current || current.done) return null;
return (
<>
{current.message && <div>{current.message}</div>}
<ProgressBar value={current.fraction} aria-label={current.title} />
</>
);
}
+6 -4
Переглянути файл
@@ -66,13 +66,15 @@ afterEach(() => vi.clearAllMocks());
describe("Install and Restart", () => {
it("runs as a background task with download progress, then restarts", async () => {
const done = installUpdate("0.1.22");
expect(expired).toEqual([UPDATE_KEY]);
expect(notes.at(-1)).toMatchObject({ title: "Downloading OpenIDEA 0.1.22", key: UPDATE_KEY, sticky: true });
expect(notes.at(-1)!.actions![0]!.label).toBe("Cancel");
expect(task(INSTALL_TASK_ID)).toMatchObject({ title: "Downloading OpenIDEA 0.1.22", fraction: null, done: false });
expect(task(INSTALL_TASK_ID)?.cancel).toBeTypeOf("function");
channel!.onmessage({ type: "downloading", downloaded: 21 * 1024 * 1024, total: 84 * 1024 * 1024 });
expect(task(INSTALL_TASK_ID)).toMatchObject({ fraction: 0.25, message: "21.0 MB of 84.0 MB" });
channel!.onmessage({ type: "installing" });
expect(notes.at(-1)!.actions).toBeUndefined();
expect(task(INSTALL_TASK_ID)).toMatchObject({ fraction: null, message: "Installing…", cancel: undefined });
install.resolve(ok(null));
@@ -99,7 +101,7 @@ describe("Install and Restart", () => {
ok({ enabled: true, update: { version: "0.1.22", currentVersion: "0.1.21", notes: null, date: null } }),
);
await checked;
expect(notes).toHaveLength(1);
expect(notes.map((n) => n.title)).toEqual(["OpenIDEA 0.1.22 is available", "Downloading OpenIDEA 0.1.22"]);
install.resolve(ok(null));
await tick();
@@ -107,7 +109,7 @@ describe("Install and Restart", () => {
expect(appInstallUpdate).toHaveBeenCalledTimes(2);
});
it("reports a failure with Retry, and stays quiet when cancelled", async () => {
it("reports a failure with Retry, and a cancellation", async () => {
const failed = installUpdate("0.1.22");
install.resolve({ status: "error", error: { kind: "unavailable", message: "connection reset" } });
await failed;
@@ -127,7 +129,7 @@ describe("Install and Restart", () => {
expect(cancelRequest).toHaveBeenCalledWith(appInstallUpdate.mock.calls.at(-1)![0]);
install.resolve({ status: "error", error: { kind: "cancelled", message: "cancelled" } });
await cancelled;
expect(notes).toEqual([]);
expect(notes.at(-1)).toMatchObject({ level: "info", title: "Update cancelled", key: UPDATE_KEY });
expect(appRestart).not.toHaveBeenCalled();
});
});
+22 -4
Переглянути файл
@@ -1,6 +1,7 @@
/**
* Update check and install. Both run as background tasks in the status bar: the
* check while the request is out, the install with download progress and Cancel.
* check while the request is out, the install with download progress and Cancel,
* which its balloon shows too (the welcome screen has no status bar).
* Every entry point (Help menu, toolbar gear, welcome screen, Search Everywhere,
* the "is available" notification and its log entry) goes through these, so a
* repeated click joins the running check or install instead of starting another.
@@ -18,6 +19,7 @@ import {
} from "@/platform/ipc";
import { notifications } from "@/workbench/contributions";
import { upsertTask } from "@/workbench/tasks";
import { UpdateBalloonBody } from "./UpdateBalloonBody";
export const UPDATE_KEY = "app.update";
export const CHECK_TASK_ID = "app.update.check";
@@ -117,8 +119,22 @@ async function runInstall(version: string) {
const request = nextRequestId();
const cancel = () => void commands.cancelRequest(request).catch(() => {});
const channel = new Channel<UpdateProgress>();
const balloon = (actions?: Array<{ label: string; run: () => void }>) =>
notifications().notify({
level: "info",
title,
body: <UpdateBalloonBody task={INSTALL_TASK_ID} />,
sticky: true,
key: UPDATE_KEY,
actions,
});
let installingShown = false;
channel.onmessage = (p) => {
const { message, fraction } = progressText(p);
if (p.type === "installing" && !installingShown) {
installingShown = true;
balloon();
}
upsertTask({
id: INSTALL_TASK_ID,
title,
@@ -130,13 +146,15 @@ async function runInstall(version: string) {
uncancellable: p.type === "installing" ? t("app.update.installUncancellable") : undefined,
});
};
// The notification's action is spent: progress lives in the status bar from here.
notifications().expire?.(UPDATE_KEY);
upsertTask({ id: INSTALL_TASK_ID, title, message: null, fraction: null, done: false, cancel });
// Replaces the notification whose action is now spent.
balloon([{ label: t("app.update.cancel"), run: cancel }]);
try {
await call(commands.appInstallUpdate(request, channel));
} catch (e) {
if (!isCancelled(e)) {
if (isCancelled(e)) {
notifications().notify({ level: "info", title: t("app.update.cancelled"), key: UPDATE_KEY });
} else {
notifications().notify({
level: "error",
title: t("app.update.installFailed"),
+2
Переглянути файл
@@ -56,6 +56,8 @@
"app.unsaved.saveFailed": "Could not save all files",
"app.unsaved.title": "Unsaved Changes",
"app.update.available": "OpenIDEA {version} is available",
"app.update.cancel": "Cancel",
"app.update.cancelled": "Update cancelled",
"app.update.checking": "Checking for updates…",
"app.update.disabled": "Automatic updates are not configured",
"app.update.disabledBody": "Set an update endpoint (OPENIDEA_UPDATE_ENDPOINT or plugins.updater.endpoints) to enable update checks.",
+2
Переглянути файл
@@ -56,6 +56,8 @@
"app.unsaved.saveFailed": "无法保存所有文件",
"app.unsaved.title": "未保存的更改",
"app.update.available": "OpenIDEA {version} 可用",
"app.update.cancel": "取消",
"app.update.cancelled": "已取消更新",
"app.update.checking": "正在检查更新…",
"app.update.disabled": "未配置自动更新",
"app.update.disabledBody": "设置更新地址(OPENIDEA_UPDATE_ENDPOINT 或 plugins.updater.endpoints)以启用更新检查。",