fix(app): keep update progress visible on the welcome screen (#951)

Check for Updates on the welcome screen showed nothing while the request was out: the check ran only as a status-bar task, and the welcome screen has no status bar. Leaving a project also dropped every notification, including a running update download.

A check started with no project or LightEdit window open now shows its progress in a balloon (keyed like the result, so the result replaces it); project windows keep it in the status bar. A project switch clears only project notifications and keeps the app-wide update ones.

Co-authored-by: youngtseup <53266026+youngtseup@users.noreply.github.com>
This commit is contained in:
youngtseup
2026-11-03 14:38:19 +08:00
committed by GitHub
parent 843ff74d5d
commit c50d212a1d
5 changed files with 64 additions and 4 deletions
@@ -41,6 +41,7 @@ vi.mock("@/platform/ipc", async (orig) => {
const { checkForUpdates, installUpdate, notifyUpdate, progressText, CHECK_TASK_ID, INSTALL_TASK_ID, UPDATE_KEY } =
await import("../updates");
const { provideNotificationService } = await import("@/workbench/contributions");
const { useProject } = await import("@/platform/project");
const { useTasks } = await import("@/workbench/tasks");
type Note = Parameters<NotificationService["notify"]>[0];
@@ -59,6 +60,7 @@ beforeEach(() => {
check = deferred();
provideNotificationService({ notify: (n) => void notes.push(n), expire: (k) => void expired.push(k) });
useTasks.setState({ tasks: [] });
useProject.setState({ project: null, lightEdit: false });
});
afterEach(() => vi.clearAllMocks());
@@ -163,6 +165,23 @@ describe("Check for Updates", () => {
expect(notes.at(-1)?.title).toBe(title);
expect(task(CHECK_TASK_ID)?.done).toBe(true);
});
it("shows the check in a balloon where there is no status bar (the welcome screen)", async () => {
const done = checkForUpdates();
expect(notes[0]).toMatchObject({ level: "info", title: "Checking for updates…", key: UPDATE_KEY });
check.resolve(ok({ enabled: true, update: null }));
await done;
expect(notes.at(-1)).toMatchObject({ title: "OpenIDEA 0.1.21 is up to date", key: UPDATE_KEY });
});
it("leaves the check to the status bar in a project window", async () => {
useProject.setState({ project: { root: "/p", name: "p", trusted: true, isGit: false, settings: {} } as never });
const done = checkForUpdates();
expect(notes).toHaveLength(0);
check.resolve(ok({ enabled: true, update: null }));
await done;
expect(notes.at(-1)?.title).toBe("OpenIDEA 0.1.21 is up to date");
});
});
describe("progressText", () => {
+12 -2
View File
@@ -1,7 +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,
* which its balloon shows too (the welcome screen has no status bar).
* check while the request is out, the install with download progress and Cancel.
* Their balloons show the progress 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.
@@ -17,6 +17,7 @@ import {
type UpdateInfo,
type UpdateProgress,
} from "@/platform/ipc";
import { useProject } from "@/platform/project";
import { notifications } from "@/workbench/contributions";
import { upsertTask } from "@/workbench/tasks";
import { UpdateBalloonBody } from "./UpdateBalloonBody";
@@ -45,6 +46,15 @@ async function runCheck() {
let abandon = () => {};
const abandoned = new Promise<"cancelled">((resolve) => (abandon = () => resolve("cancelled")));
upsertTask({ id: CHECK_TASK_ID, title, message: null, fraction: null, done: false, cancel: () => abandon() });
// No status bar (the welcome screen): the balloon is the only place the check can show.
const { project, lightEdit } = useProject.getState();
if (!installing && !project && !lightEdit)
notifications().notify({
level: "info",
title,
body: <UpdateBalloonBody task={CHECK_TASK_ID} />,
key: UPDATE_KEY,
});
try {
const r = await Promise.race([call(commands.appCheckUpdate()), abandoned]);
if (r === "cancelled") return;
+2 -2
View File
@@ -15,7 +15,7 @@ import { installBrowserGuards } from "./browserGuards";
import { installDistractionFree } from "./distractionFree";
import { installFileDrop } from "./fileDrop";
import { installFocusTracking } from "./focus";
import { clearNotifications, installNotifyEvents, notificationServiceImpl } from "./notificationStore";
import { clearProjectNotifications, installNotifyEvents, notificationServiceImpl } from "./notificationStore";
import {
NOTIFICATIONS_TOOL_WINDOW,
NotificationsHeaderActions,
@@ -51,7 +51,7 @@ export function activateWorkbench(): () => void {
const unsubProject = useProject.subscribe((s, prev) => {
if (s.project?.root === prev.project?.root && s.lightEdit === prev.lightEdit) return;
// The previous project's notifications (and their actions) no longer apply.
if (prev.project) clearNotifications();
if (prev.project) clearProjectNotifications();
onProject(s.project?.root ?? null);
});
+5
View File
@@ -175,6 +175,11 @@ export function clearNotifications() {
useNotifications.setState({ items: [] });
}
/** A project switch drops the previous project's notifications; app-wide ones (updates) stay. */
export function clearProjectNotifications() {
useNotifications.setState((s) => ({ items: s.items.filter((x) => x.group === "updates") }));
}
/** The list was seen: everything is read and no longer shown as a balloon. */
export function markAllNotificationsRead() {
useNotifications.setState((s) => ({
@@ -0,0 +1,26 @@
import { afterEach, describe, expect, it } from "vitest";
import { useProject } from "@/platform/project";
import { activateWorkbench } from "./activate";
import { notify, useNotifications } from "./notificationStore";
afterEach(() => {
useNotifications.setState({ items: [] });
useProject.setState({ project: null, lightEdit: false });
});
describe("notifications across project changes", () => {
it("keeps app-level update notifications and drops project ones", () => {
useProject.setState({
project: { root: "/p", name: "p", trusted: true, isGit: false, settings: {} } as never,
});
const dispose = activateWorkbench();
try {
notify({ level: "info", title: "Downloading OpenIDEA 0.1.35", key: "app.update", sticky: true });
notify({ level: "info", title: "Git refresh failed", group: "vcs" });
useProject.setState({ project: null });
expect(useNotifications.getState().items.map((n) => n.title)).toEqual(["Downloading OpenIDEA 0.1.35"]);
} finally {
dispose();
}
});
});