diff --git a/apps/desktop/src/App.vue b/apps/desktop/src/App.vue index 65c35c61ca..ac5fe323c8 100644 --- a/apps/desktop/src/App.vue +++ b/apps/desktop/src/App.vue @@ -28,7 +28,7 @@ import { canDownloadAndInstallUpdate, useAppUpdater } from "@/composables/useApp import { useMcpUpdateBadge } from "@/composables/useMcpUpdateBadge"; import { useComponentUpdates, type ComponentUpdateCategory } from "@/composables/useComponentUpdates"; import { driverStoreUpdateBadgeCount, showMcpUpdateBadge } from "@/lib/updates/updateBadges"; -import { markPendingComponentUpdatesAfterAppUpdate, resolveUpdateAllAction, runPendingComponentUpdatePlan, takePendingComponentUpdatesAfterAppRestart, type PendingComponentUpdatePlan } from "@/lib/updates/componentUpdateOrchestration"; +import { markPendingComponentUpdatesAfterAppUpdate, resolveUpdateAllAction, runPendingComponentUpdatePlan, shouldCloseUpdateCenterAfterComponentUpdate, takePendingComponentUpdatesAfterAppRestart, type PendingComponentUpdatePlan } from "@/lib/updates/componentUpdateOrchestration"; import { isUpdatePreviewMockEnabled } from "@/lib/updates/updatePreviewMock"; import { useExportTracker } from "@/composables/useExportTracker"; import { useFileDrop } from "@/composables/useFileDrop"; @@ -1215,9 +1215,22 @@ function handleToolbarUpdateClick() { function reportComponentUpdateResult(result: Awaited>) { const updatedComponents = [result.drivers > 0 ? t("settings.updateDrivers") : "", result.jdbc ? t("settings.updateJdbc") : "", result.mcp ? t("settings.updateMcp") : "", result.plugins > 0 ? t("settings.updatePlugins") : ""].filter(Boolean); + // Only a clean refresh is authoritative; a failed registry check must not clear the toolbar count. + if (result.failed.length === 0) agentDriverUpdateCount.value = componentUpdates.driverUpdateCount.value; if (updatedComponents.length) toast(t("updates.componentsAutoUpdated", { components: updatedComponents.join(t("updates.componentListSeparator")) })); if (result.skippedDrivers > 0) toast(t("updates.componentsAutoUpdateSkipped"), 6000); if (result.failed.length) toast(t("updates.componentsAutoUpdateFailed", { count: result.failed.length }), 6000); + + if ( + shouldCloseUpdateCenterAfterComponentUpdate({ + failedCount: result.failed.length, + skippedDriverCount: result.skippedDrivers, + hasAppUpdate: hasUpdateAvailable.value, + remainingComponentUpdateCount: componentUpdates.totalUpdateCount.value, + }) + ) { + showUpdateDialog.value = false; + } } async function rememberComponentUpdatesForRestartedApp(plan: PendingComponentUpdatePlan = { kind: "auto" }) { diff --git a/apps/desktop/src/composables/__tests__/useComponentUpdates.spec.ts b/apps/desktop/src/composables/__tests__/useComponentUpdates.spec.ts index 1e60743f79..a0ab921827 100644 --- a/apps/desktop/src/composables/__tests__/useComponentUpdates.spec.ts +++ b/apps/desktop/src/composables/__tests__/useComponentUpdates.spec.ts @@ -87,6 +87,16 @@ describe("useComponentUpdates", () => { expect(updates.totalUpdateCount.value).toBe(4); }); + it("clears the driver count after a successful update and refresh", async () => { + mocks.listInstalledAgents.mockResolvedValueOnce([{ db_type: "mysql", update_available: true }]).mockResolvedValueOnce([{ db_type: "mysql", update_available: false }]); + const updates = useComponentUpdates({ isDesktop: true }); + + await updates.installCategories(["drivers"]); + + expect(mocks.upgradeAllAgents).toHaveBeenCalledOnce(); + expect(updates.driverUpdateCount.value).toBe(0); + }); + it("installs every enabled component category after an app update", async () => { const updates = useComponentUpdates({ isDesktop: true }); diff --git a/apps/desktop/src/lib/updates/__tests__/componentUpdateOrchestration.spec.ts b/apps/desktop/src/lib/updates/__tests__/componentUpdateOrchestration.spec.ts index cd18478aac..781364c2c1 100644 --- a/apps/desktop/src/lib/updates/__tests__/componentUpdateOrchestration.spec.ts +++ b/apps/desktop/src/lib/updates/__tests__/componentUpdateOrchestration.spec.ts @@ -1,7 +1,7 @@ // @vitest-environment happy-dom import { beforeEach, describe, expect, it, vi } from "vitest"; -import { clearPendingComponentUpdatesAfterAppUpdate, markPendingComponentUpdatesAfterAppUpdate, resolveUpdateAllAction, runPendingComponentUpdatePlan, takePendingComponentUpdatesAfterAppRestart } from "@/lib/updates/componentUpdateOrchestration"; +import { clearPendingComponentUpdatesAfterAppUpdate, markPendingComponentUpdatesAfterAppUpdate, resolveUpdateAllAction, runPendingComponentUpdatePlan, shouldCloseUpdateCenterAfterComponentUpdate, takePendingComponentUpdatesAfterAppRestart } from "@/lib/updates/componentUpdateOrchestration"; describe("component update orchestration", () => { beforeEach(() => { @@ -88,6 +88,29 @@ describe("component update orchestration", () => { expect(resolveUpdateAllAction({ hasAppUpdate: false, appUpdateCanInstall: true, appUpdatePrepared: false, hasComponentUpdates: false })).toBe("none"); }); + it("closes the update center after a successful component-only update with nothing left to install", () => { + expect( + shouldCloseUpdateCenterAfterComponentUpdate({ + failedCount: 0, + skippedDriverCount: 0, + hasAppUpdate: false, + remainingComponentUpdateCount: 0, + }), + ).toBe(true); + }); + + it("keeps the update center open when an update failed or a driver update was skipped", () => { + const base = { hasAppUpdate: false, remainingComponentUpdateCount: 0 }; + expect(shouldCloseUpdateCenterAfterComponentUpdate({ ...base, failedCount: 1, skippedDriverCount: 0 })).toBe(false); + expect(shouldCloseUpdateCenterAfterComponentUpdate({ ...base, failedCount: 0, skippedDriverCount: 1 })).toBe(false); + }); + + it("keeps the update center open while the app or another component still needs an update", () => { + const base = { failedCount: 0, skippedDriverCount: 0 }; + expect(shouldCloseUpdateCenterAfterComponentUpdate({ ...base, hasAppUpdate: true, remainingComponentUpdateCount: 0 })).toBe(false); + expect(shouldCloseUpdateCenterAfterComponentUpdate({ ...base, hasAppUpdate: false, remainingComponentUpdateCount: 1 })).toBe(false); + }); + it("can explicitly clear pending state", () => { markPendingComponentUpdatesAfterAppUpdate("0.6.16", "0.6.17"); clearPendingComponentUpdatesAfterAppUpdate(); diff --git a/apps/desktop/src/lib/updates/componentUpdateOrchestration.ts b/apps/desktop/src/lib/updates/componentUpdateOrchestration.ts index 6685facc0b..a63e5dc218 100644 --- a/apps/desktop/src/lib/updates/componentUpdateOrchestration.ts +++ b/apps/desktop/src/lib/updates/componentUpdateOrchestration.ts @@ -96,6 +96,10 @@ export function resolveUpdateAllAction(options: { hasAppUpdate: boolean; appUpda return options.appUpdatePrepared ? "defer-components" : "download-app"; } +export function shouldCloseUpdateCenterAfterComponentUpdate(options: { failedCount: number; skippedDriverCount: number; hasAppUpdate: boolean; remainingComponentUpdateCount: number }): boolean { + return options.failedCount === 0 && options.skippedDriverCount === 0 && !options.hasAppUpdate && options.remainingComponentUpdateCount === 0; +} + export function runPendingComponentUpdatePlan(pending: PendingComponentUpdates, updates: { installCategories: (categories: ComponentUpdateCategory[]) => Promise; autoUpdateEnabledComponents: () => Promise }): Promise { return pending.plan.kind === "manual" ? updates.installCategories(pending.plan.categories) : updates.autoUpdateEnabledComponents(); }