feat(electron): clarify desktop update prompts (#5160)
## Related issue
N/A
## Summary
- Show the installed and available Omnigent Desktop versions in the shell-owned update prompt, with session-safety copy on its own line and unclipped card chrome.
- Carry the effective desktop version through updater status events and keep the native up-to-date dialog consistent.
- Add a development-only semantic-version override that checks the production update feed, making the real update flow reproducible without changing packaged behavior.
ELI5: development can pretend the installed desktop app is older, compare it with the production feed, and pass that same version into the update card.
```text
OMNIGENT_DESKTOP_VERSION_OVERRIDE
|
v
electron-updater baseline ---> production update feed
|
v
update status + current version ---> shell overlay
```
## Test Plan
- `node --test web/electron/test/desktop_updater.test.js`
- `node --test web/electron/test/update-main.test.js`
- `pnpm --filter web exec vitest run src/components/UpdateBanner.test.tsx`
- `pnpm --filter web type-check`
- Targeted `oxlint` over the changed Electron and web sources/tests.
- `pnpm --filter web build:overlay`
- Development flow: `OMNIGENT_DESKTOP_VERSION_OVERRIDE=0.9.0 pnpm start`, then **Server → Check for Updates…** against the production feed.
## Demo
N/A — the UI is rendered in an Electron-owned transparent child window; the focused component assertions and overlay production build cover the final copy and layout contract.
## Type of change
- [ ] Bug fix
- [x] Feature
- [x] UI / frontend change
- [ ] Refactor / chore
- [ ] Docs
- [ ] Test / CI
- [ ] Breaking change
## Test coverage
- [x] Unit tests added / updated
- [ ] Integration tests added / updated
- [ ] E2E tests added / updated
- [ ] Manual verification completed
- [x] Existing tests cover this change
- [ ] Not applicable
## Coverage notes
The updater unit harness covers version propagation, development override isolation, and production-feed configuration. The UpdateBanner test covers available, downloading, and downloaded copy.
## Changelog
Desktop update prompts now show the installed and available versions with clearer copy and polished overlay spacing.
Signed-off-by: Zeyi (Rice) Fan <zeyi.f@databricks.com>
This commit is contained in:
@@ -387,6 +387,22 @@ server (see below), Connect, and you're in.
|
||||
> **not** run the Vite dev server. To develop the web UI itself with hot
|
||||
> reload, run `pnpm run dev` (plain Vite in a browser) from `web/` as usual.
|
||||
|
||||
### Test desktop updates
|
||||
|
||||
To override the current version used by development update checks, launch the
|
||||
unpackaged app with a valid semantic version:
|
||||
|
||||
```bash
|
||||
OMNIGENT_DESKTOP_VERSION_OVERRIDE=0.9.0 pnpm start
|
||||
```
|
||||
|
||||
The override controls both the **Current version** shown in update prompts and
|
||||
the baseline `electron-updater` uses to decide whether a production release is
|
||||
newer. It does not change Electron's real app/package version. Packaged builds
|
||||
ignore it. `pnpm start` rebuilds the shell-owned update overlay before launching
|
||||
it. Unpackaged runs read `dev-app-update.yml`, which intentionally checks the
|
||||
same production HTTPS update server as packaged builds.
|
||||
|
||||
## Build a distributable
|
||||
|
||||
From `web/electron/`:
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
provider: generic
|
||||
url: http://127.0.0.1:8765/
|
||||
url: https://omnigent.ai/_desktop/updates/
|
||||
updaterCacheDirName: omnigent-desktop-electron-updater-dev
|
||||
|
||||
@@ -80,8 +80,10 @@ function isUpdateSecurityError(message) {
|
||||
* @param {(win: Electron.BrowserWindow | null | undefined) => string | null} deps.pinnedOrigin
|
||||
* The origin a window is pinned to (used for the consent dialog copy).
|
||||
* @param {string} deps.iconPath Absolute path to the app icon PNG.
|
||||
* @param {boolean} [deps.forceDevUpdateConfig] Force the dev feed on in an
|
||||
* unpackaged build (main.js sets this from !app.isPackaged).
|
||||
* @param {boolean} [deps.forceDevUpdateConfig] Enable the development update
|
||||
* config in an unpackaged build (main.js sets this from !app.isPackaged).
|
||||
* @param {() => string} [deps.getCurrentVersion] Version shown in update UI.
|
||||
* Defaults to Electron's real app version.
|
||||
* @returns {{
|
||||
* getConfig: () => { mode: string, autoInstall: boolean, skippedVersion: string | null },
|
||||
* setConfig: (patch?: object) => { mode: string, autoInstall: boolean, skippedVersion: string | null },
|
||||
@@ -107,6 +109,7 @@ function createDesktopUpdater({
|
||||
pinnedOrigin,
|
||||
iconPath,
|
||||
forceDevUpdateConfig = false,
|
||||
getCurrentVersion = () => app.getVersion(),
|
||||
}) {
|
||||
let updateCheckTimer = null;
|
||||
let currentUpdateStatus = { state: "idle" };
|
||||
@@ -195,7 +198,7 @@ function createDesktopUpdater({
|
||||
autoUpdater.on("checking-for-update", () => broadcast({ state: "checking" }));
|
||||
autoUpdater.on("update-available", (info) => {
|
||||
manualCheckInFlight = false;
|
||||
broadcast({ state: "available", info });
|
||||
broadcast({ state: "available", currentVersion: getCurrentVersion(), info });
|
||||
});
|
||||
autoUpdater.on("update-not-available", () => {
|
||||
manualCheckInFlight = false;
|
||||
@@ -204,7 +207,9 @@ function createDesktopUpdater({
|
||||
autoUpdater.on("download-progress", (progress) =>
|
||||
broadcast({ state: "downloading", progress }),
|
||||
);
|
||||
autoUpdater.on("update-downloaded", (info) => broadcast({ state: "downloaded", info }));
|
||||
autoUpdater.on("update-downloaded", (info) =>
|
||||
broadcast({ state: "downloaded", currentVersion: getCurrentVersion(), info }),
|
||||
);
|
||||
autoUpdater.on("error", (err) => {
|
||||
const msg = String(err?.message ?? err);
|
||||
const isSecurity = isUpdateSecurityError(msg);
|
||||
|
||||
@@ -682,6 +682,33 @@ function activeWindow() {
|
||||
return windows.keys().next().value ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Effective version for development update checks and UI. Packaged builds
|
||||
* always use Electron's real app version.
|
||||
*/
|
||||
function configureDesktopVersion() {
|
||||
const override = !app.isPackaged
|
||||
? process.env.OMNIGENT_DESKTOP_VERSION_OVERRIDE?.trim()
|
||||
: undefined;
|
||||
if (!override) return app.getVersion();
|
||||
|
||||
try {
|
||||
// electron-updater stores a SemVer instance here and reads it when deciding
|
||||
// eligibility. Reuse its constructor so comparisons keep the expected type.
|
||||
const Version = autoUpdater.currentVersion.constructor;
|
||||
const version = new Version(override);
|
||||
autoUpdater.currentVersion = version;
|
||||
return version.version;
|
||||
} catch (err) {
|
||||
throw new Error(
|
||||
`OMNIGENT_DESKTOP_VERSION_OVERRIDE must be a valid semantic version (received ${JSON.stringify(override)})`,
|
||||
{ cause: err },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const currentDesktopVersion = configureDesktopVersion();
|
||||
|
||||
// Desktop auto-update orchestration lives in its own module; the main process
|
||||
// only composes it with its main-process dependencies and wires the four thin
|
||||
// seams below (startup init, the Updates menu, the update IPC surface, and the
|
||||
@@ -700,12 +727,11 @@ const updater = createDesktopUpdater({
|
||||
isPinnedOriginSender,
|
||||
pinnedOrigin,
|
||||
iconPath: ICON_PNG,
|
||||
// Dev builds always use the local dev feed (dev-app-update.yml ->
|
||||
// 127.0.0.1:8765); packaged builds always use the baked app-update.yml.
|
||||
// Tying this to !app.isPackaged — not an env var — closes a redirect attack:
|
||||
// an OMNIGENT_FORCE_DEV_UPDATE_CONFIG-style env var could otherwise point a
|
||||
// packaged (production) app at an untrusted HTTP local feed and push a
|
||||
// malicious update. A packaged build can never be redirected to the dev feed.
|
||||
getCurrentVersion: () => currentDesktopVersion,
|
||||
// Dev builds use dev-app-update.yml, which mirrors the production HTTPS
|
||||
// endpoint; packaged builds always use their baked app-update.yml. Tying
|
||||
// this to !app.isPackaged — not an env var — ensures a packaged app can
|
||||
// never be redirected to a repository-local update configuration.
|
||||
forceDevUpdateConfig: !app.isPackaged,
|
||||
});
|
||||
|
||||
@@ -1870,9 +1896,9 @@ function buildMenu() {
|
||||
if (status.state === "none") {
|
||||
await dialog.showMessageBox(activeWindow(), {
|
||||
type: "info",
|
||||
title: "Omnigent",
|
||||
title: "Omnigent Desktop",
|
||||
message: "You're up to date!",
|
||||
detail: `Omnigent ${app.getVersion()} is the latest version.`,
|
||||
detail: `Omnigent Desktop ${currentDesktopVersion} is the latest version.`,
|
||||
buttons: ["OK"],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -10,6 +10,9 @@
|
||||
const { describe, it } = require("node:test");
|
||||
const assert = require("node:assert/strict");
|
||||
const { EventEmitter } = require("node:events");
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
const yaml = require("js-yaml");
|
||||
|
||||
const {
|
||||
createDesktopUpdater,
|
||||
@@ -27,9 +30,10 @@ const PINNED_ORIGIN = "https://server.example";
|
||||
* @param {object} [opts]
|
||||
* @param {Record<string, unknown>} [opts.settings] Initial persisted settings.
|
||||
* @param {boolean} [opts.isPackaged] Simulate a packaged build.
|
||||
* @param {boolean} [opts.forceDevUpdateConfig] Force the dev feed on.
|
||||
* @param {boolean} [opts.forceDevUpdateConfig] Enable the development update config.
|
||||
* @param {boolean} [opts.pinnedSender] Whether IPC calls count as trusted.
|
||||
* @param {Array<{response: number}>} [opts.dialogResponses] Queued dialog answers.
|
||||
* @param {() => string} [opts.getCurrentVersion] Display-version override.
|
||||
*/
|
||||
function makeUpdater({
|
||||
settings = {},
|
||||
@@ -37,6 +41,7 @@ function makeUpdater({
|
||||
forceDevUpdateConfig = false,
|
||||
pinnedSender = true,
|
||||
dialogResponses = [{ response: 1 }],
|
||||
getCurrentVersion,
|
||||
} = {}) {
|
||||
let store = { ...settings };
|
||||
const calls = {
|
||||
@@ -75,6 +80,7 @@ function makeUpdater({
|
||||
const deps = {
|
||||
app: {
|
||||
isPackaged,
|
||||
getVersion: () => "0.3.0",
|
||||
quit: () => {
|
||||
calls.appQuit += 1;
|
||||
},
|
||||
@@ -104,6 +110,7 @@ function makeUpdater({
|
||||
pinnedOrigin: () => PINNED_ORIGIN,
|
||||
iconPath: "/icons/icon.png",
|
||||
forceDevUpdateConfig,
|
||||
getCurrentVersion,
|
||||
};
|
||||
|
||||
const updater = createDesktopUpdater(deps);
|
||||
@@ -145,6 +152,16 @@ describe("desktop_updater — pure helpers", () => {
|
||||
assert.match(UPDATES_UNAVAILABLE_IN_DEV, /unavailable in development/);
|
||||
});
|
||||
|
||||
it("uses the production update endpoint in development", () => {
|
||||
const devConfig = yaml.load(
|
||||
fs.readFileSync(path.join(__dirname, "../dev-app-update.yml"), "utf8"),
|
||||
);
|
||||
const packageConfig = require("../package.json");
|
||||
|
||||
assert.equal(devConfig.url, packageConfig.build.publish[0].url);
|
||||
assert.match(devConfig.url, /^https:\/\//);
|
||||
});
|
||||
|
||||
it("classifies signing/integrity errors as security errors", () => {
|
||||
assert.equal(isUpdateSecurityError("sha512 mismatch"), true);
|
||||
assert.equal(isUpdateSecurityError("app is not signed"), true);
|
||||
@@ -197,14 +214,41 @@ describe("desktop_updater — event wiring + broadcast", () => {
|
||||
assert.equal(h.autoUpdater.forceDevUpdateConfig, true);
|
||||
assert.deepEqual(plain(h.updater.getStatus()), {
|
||||
state: "available",
|
||||
currentVersion: "0.3.0",
|
||||
info: { version: "0.4.0" },
|
||||
});
|
||||
assert.deepEqual(plain(h.calls.sent), [
|
||||
{
|
||||
channel: "omnigent:update-status",
|
||||
payload: { state: "available", info: { version: "0.4.0" } },
|
||||
payload: {
|
||||
state: "available",
|
||||
currentVersion: "0.3.0",
|
||||
info: { version: "0.4.0" },
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
h.autoUpdater.emit("update-downloaded", { version: "0.4.0" });
|
||||
assert.deepEqual(plain(h.updater.getStatus()), {
|
||||
state: "downloaded",
|
||||
currentVersion: "0.3.0",
|
||||
info: { version: "0.4.0" },
|
||||
});
|
||||
});
|
||||
|
||||
it("uses an injected display version for update prompts", () => {
|
||||
const h = makeUpdater({
|
||||
forceDevUpdateConfig: true,
|
||||
settings: { update_mode: "manual" },
|
||||
getCurrentVersion: () => "0.2.0",
|
||||
});
|
||||
h.updater.init();
|
||||
|
||||
h.autoUpdater.emit("update-available", { version: "0.4.0" });
|
||||
assert.equal(h.updater.getStatus().currentVersion, "0.2.0");
|
||||
|
||||
h.autoUpdater.emit("update-downloaded", { version: "0.4.0" });
|
||||
assert.equal(h.updater.getStatus().currentVersion, "0.2.0");
|
||||
});
|
||||
|
||||
it("start mode kicks off a check with no lingering periodic timer", () => {
|
||||
@@ -248,7 +292,7 @@ describe("desktop_updater — manual check errors", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("desktop_updater — dev feed gating", () => {
|
||||
describe("desktop_updater — development update config gating", () => {
|
||||
it("blocks manual paths when the feed is unavailable in development", async () => {
|
||||
const h = makeUpdater({ settings: { update_mode: "manual" } });
|
||||
h.updater.registerIpc();
|
||||
|
||||
@@ -15,6 +15,7 @@ function loadMainHarness({
|
||||
isPackaged = false,
|
||||
platform = process.platform,
|
||||
developerMode = false,
|
||||
desktopVersionOverride,
|
||||
} = {}) {
|
||||
const userData = fs.mkdtempSync(path.join(os.tmpdir(), "omnigent-update-test-"));
|
||||
fs.writeFileSync(path.join(userData, "settings.json"), JSON.stringify(settings), "utf8");
|
||||
@@ -46,7 +47,21 @@ function loadMainHarness({
|
||||
focus: () => {},
|
||||
};
|
||||
|
||||
class FakeSemVer {
|
||||
constructor(version) {
|
||||
if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(version)) {
|
||||
throw new Error(`Invalid version: ${version}`);
|
||||
}
|
||||
this.version = version;
|
||||
}
|
||||
|
||||
format() {
|
||||
return this.version;
|
||||
}
|
||||
}
|
||||
|
||||
const autoUpdater = new EventEmitter();
|
||||
autoUpdater.currentVersion = new FakeSemVer("0.3.0");
|
||||
autoUpdater.autoDownload = true;
|
||||
autoUpdater.autoInstallOnAppQuit = true;
|
||||
autoUpdater.forceDevUpdateConfig = forceDevUpdateConfig;
|
||||
@@ -66,6 +81,7 @@ function loadMainHarness({
|
||||
app: {
|
||||
isPackaged,
|
||||
getPath: (name) => (name === "userData" ? userData : userData),
|
||||
getVersion: () => "0.3.0",
|
||||
setName: () => {},
|
||||
requestSingleInstanceLock: () => true,
|
||||
on: (name, listener) => appEvents.set(name, listener),
|
||||
@@ -164,6 +180,7 @@ function loadMainHarness({
|
||||
platform,
|
||||
env: {
|
||||
...process.env,
|
||||
OMNIGENT_DESKTOP_VERSION_OVERRIDE: desktopVersionOverride,
|
||||
// No OMNIGENT_FORCE_DEV_UPDATE_CONFIG injection: main.js now derives
|
||||
// forceDevUpdateConfig from !app.isPackaged (always true in this
|
||||
// harness), not an env var. The harness still controls the
|
||||
@@ -632,6 +649,41 @@ describe("auto-update main-process wiring", () => {
|
||||
assert.equal(harness.api.updater.installPending, false);
|
||||
});
|
||||
|
||||
it("allows only development builds to override the effective desktop version", async (t) => {
|
||||
const development = loadMainHarness({
|
||||
settings: { update_mode: "manual" },
|
||||
desktopVersionOverride: " 0.2.0 ",
|
||||
});
|
||||
t.after(development.cleanup);
|
||||
assert.equal(development.autoUpdater.currentVersion.version, "0.2.0");
|
||||
assert.equal(development.autoUpdater.currentVersion.format(), "0.2.0");
|
||||
|
||||
development.api.updater.init();
|
||||
development.autoUpdater.emit("update-available", { version: "0.4.0" });
|
||||
assert.equal(development.api.updater.getStatus().currentVersion, "0.2.0");
|
||||
|
||||
development.autoUpdater.emit("update-not-available");
|
||||
development.api.buildMenu();
|
||||
await findMenuItem(development.calls.setApplicationMenu.at(-1), "check_for_updates").click();
|
||||
assert.equal(development.calls.showMessageBox.at(-1).options.title, "Omnigent Desktop");
|
||||
assert.equal(
|
||||
development.calls.showMessageBox.at(-1).options.detail,
|
||||
"Omnigent Desktop 0.2.0 is the latest version.",
|
||||
);
|
||||
|
||||
const packaged = loadMainHarness({
|
||||
isPackaged: true,
|
||||
settings: { update_mode: "manual" },
|
||||
desktopVersionOverride: "0.2.0",
|
||||
});
|
||||
t.after(packaged.cleanup);
|
||||
assert.equal(packaged.autoUpdater.currentVersion.version, "0.3.0");
|
||||
|
||||
packaged.api.updater.init();
|
||||
packaged.autoUpdater.emit("update-available", { version: "0.4.0" });
|
||||
assert.equal(packaged.api.updater.getStatus().currentVersion, "0.3.0");
|
||||
});
|
||||
|
||||
it("supports forceDevUpdateConfig and broadcasts updater events", (t) => {
|
||||
const harness = loadMainHarness({
|
||||
forceDevUpdateConfig: true,
|
||||
@@ -645,12 +697,17 @@ describe("auto-update main-process wiring", () => {
|
||||
assert.equal(harness.autoUpdater.forceDevUpdateConfig, true);
|
||||
assert.deepEqual(plain(harness.api.updater.getStatus()), {
|
||||
state: "available",
|
||||
currentVersion: "0.3.0",
|
||||
info: { version: "0.4.0" },
|
||||
});
|
||||
assert.deepEqual(plain(harness.calls.sent), [
|
||||
{
|
||||
channel: "omnigent:update-status",
|
||||
payload: { state: "available", info: { version: "0.4.0" } },
|
||||
payload: {
|
||||
state: "available",
|
||||
currentVersion: "0.3.0",
|
||||
info: { version: "0.4.0" },
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -55,11 +55,14 @@ describe("UpdateBanner", () => {
|
||||
it("renders the correct controls for available, downloading, and downloaded states", async () => {
|
||||
const { bridge, emit } = installBridge({
|
||||
state: "available",
|
||||
currentVersion: "0.3.0",
|
||||
info: { version: "0.4.0", releaseNotes: "Fixes and polish." },
|
||||
});
|
||||
render(<UpdateBanner />);
|
||||
|
||||
expect(await screen.findByText("Omnigent 0.4.0 is available")).toBeInTheDocument();
|
||||
expect(await screen.findByText("Omnigent Desktop 0.4.0 is available")).toBeInTheDocument();
|
||||
expect(screen.getByText("Current version: 0.3.0.")).toBeInTheDocument();
|
||||
expect(screen.getByText("Updating won’t interrupt existing sessions.")).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: "Update now" })).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: "Skip this version" })).toBeInTheDocument();
|
||||
expect(screen.getByText("Release notes")).toBeInTheDocument();
|
||||
@@ -68,12 +71,16 @@ describe("UpdateBanner", () => {
|
||||
);
|
||||
|
||||
emit({ state: "downloading", progress: { percent: 42 } });
|
||||
expect(await screen.findByText("Downloading Omnigent update… 42%")).toBeInTheDocument();
|
||||
expect(await screen.findByText("Downloading Omnigent Desktop update… 42%")).toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: "Update now" })).toBeNull();
|
||||
expect(screen.queryByRole("button", { name: "Skip this version" })).toBeNull();
|
||||
|
||||
emit({ state: "downloaded", info: { version: "0.4.0" } });
|
||||
expect(await screen.findByText("Omnigent 0.4.0 is ready to install")).toBeInTheDocument();
|
||||
emit({ state: "downloaded", currentVersion: "0.3.0", info: { version: "0.4.0" } });
|
||||
expect(
|
||||
await screen.findByText("Omnigent Desktop 0.4.0 is ready to install"),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText("Current version: 0.3.0.")).toBeInTheDocument();
|
||||
expect(screen.getByText("Updating won’t interrupt existing sessions.")).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: "Restart to update" })).toBeInTheDocument();
|
||||
expect(screen.getByText("Installs automatically on next quit.")).toBeInTheDocument();
|
||||
});
|
||||
@@ -82,6 +89,7 @@ describe("UpdateBanner", () => {
|
||||
installBridge(
|
||||
{
|
||||
state: "downloaded",
|
||||
currentVersion: "0.3.0",
|
||||
info: { version: "0.4.0" },
|
||||
},
|
||||
{ ...DEFAULT_CONFIG, autoInstall: false },
|
||||
@@ -89,7 +97,9 @@ describe("UpdateBanner", () => {
|
||||
|
||||
render(<UpdateBanner />);
|
||||
|
||||
expect(await screen.findByText("Omnigent 0.4.0 is ready to install")).toBeInTheDocument();
|
||||
expect(
|
||||
await screen.findByText("Omnigent Desktop 0.4.0 is ready to install"),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: "Restart to update" })).toBeInTheDocument();
|
||||
expect(screen.queryByText("Installs automatically on next quit.")).toBeNull();
|
||||
});
|
||||
@@ -101,7 +111,7 @@ describe("UpdateBanner", () => {
|
||||
});
|
||||
|
||||
const { unmount } = render(<UpdateBanner />);
|
||||
expect(await screen.findByText("Omnigent 0.4.0 is available")).toBeInTheDocument();
|
||||
expect(await screen.findByText("Omnigent Desktop 0.4.0 is available")).toBeInTheDocument();
|
||||
|
||||
unmount();
|
||||
|
||||
@@ -120,12 +130,12 @@ describe("UpdateBanner", () => {
|
||||
vi.mocked(bridge.setConfig).mockResolvedValueOnce(skippedConfig);
|
||||
|
||||
render(<UpdateBanner />);
|
||||
expect(await screen.findByText("Omnigent 0.4.0 is available")).toBeInTheDocument();
|
||||
expect(await screen.findByText("Omnigent Desktop 0.4.0 is available")).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "Skip this version" }));
|
||||
await waitFor(() => {
|
||||
expect(bridge.setConfig).toHaveBeenCalledWith({ skippedVersion: "0.4.0" });
|
||||
expect(screen.queryByText("Omnigent 0.4.0 is available")).toBeNull();
|
||||
expect(screen.queryByText("Omnigent Desktop 0.4.0 is available")).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -151,14 +151,24 @@ export function UpdateBanner({ variant = "floating" }: { variant?: "floating" |
|
||||
|
||||
<div className="min-w-0 flex-1">
|
||||
{visibleStatus.state === "available" && (
|
||||
<p className="font-medium text-foreground">
|
||||
Omnigent {visibleStatus.info?.version ?? "update"} is available
|
||||
</p>
|
||||
<>
|
||||
<p className="font-medium text-foreground">
|
||||
Omnigent Desktop {visibleStatus.info?.version ?? "update"} is available
|
||||
</p>
|
||||
{visibleStatus.currentVersion && (
|
||||
<p className="mt-0.5 text-sm text-muted-foreground">
|
||||
Current version: {visibleStatus.currentVersion}.
|
||||
</p>
|
||||
)}
|
||||
<p className="mt-0.5 text-sm text-muted-foreground">
|
||||
Updating won’t interrupt existing sessions.
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
{visibleStatus.state === "downloading" && (
|
||||
<>
|
||||
<p className="font-medium text-foreground">
|
||||
Downloading Omnigent update… {progress}%
|
||||
Downloading Omnigent Desktop update… {progress}%
|
||||
</p>
|
||||
<Progress
|
||||
value={progress}
|
||||
@@ -170,7 +180,15 @@ export function UpdateBanner({ variant = "floating" }: { variant?: "floating" |
|
||||
{visibleStatus.state === "downloaded" && (
|
||||
<>
|
||||
<p className="font-medium text-foreground">
|
||||
Omnigent {visibleStatus.info?.version ?? "update"} is ready to install
|
||||
Omnigent Desktop {visibleStatus.info?.version ?? "update"} is ready to install
|
||||
</p>
|
||||
{visibleStatus.currentVersion && (
|
||||
<p className="mt-0.5 text-sm text-muted-foreground">
|
||||
Current version: {visibleStatus.currentVersion}.
|
||||
</p>
|
||||
)}
|
||||
<p className="mt-0.5 text-sm text-muted-foreground">
|
||||
Updating won’t interrupt existing sessions.
|
||||
</p>
|
||||
{autoInstall && (
|
||||
<p className="mt-0.5 text-sm text-muted-foreground">
|
||||
|
||||
@@ -240,7 +240,10 @@ export interface UpdateConfig {
|
||||
skippedVersion: string | null;
|
||||
}
|
||||
|
||||
export type UpdateStatus =
|
||||
export type UpdateStatus = {
|
||||
/** Installed Electron app version; absent on older desktop shells. */
|
||||
currentVersion?: string;
|
||||
} & (
|
||||
| {
|
||||
state: "idle" | "checking" | "none";
|
||||
info?: undefined;
|
||||
@@ -264,7 +267,8 @@ export type UpdateStatus =
|
||||
info?: { version: string; releaseNotes?: string };
|
||||
progress?: undefined;
|
||||
lastError?: string;
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
export interface ElectronUpdateBridge {
|
||||
getConfig: () => Promise<UpdateConfig>;
|
||||
|
||||
@@ -14,6 +14,12 @@
|
||||
background: transparent;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Keep the card and its shadow off the transparent window boundary.
|
||||
Empty overlays stay at zero height so the shell remains click-through. */
|
||||
#update-overlay-root:not(:empty) {
|
||||
padding: 12px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
Reference in New Issue
Block a user