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
This commit is contained in:
l0ng-ai
2026-10-31 09:37:46 +08:00
committed by GitHub
parent a5fb30e04f
commit 28baa4a57a
5 changed files with 46 additions and 8 deletions
+14
View File
@@ -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
View File
@@ -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();
});
});
@@ -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
View File
@@ -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
View File
@@ -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)以启用更新检查。",