diff --git a/apps/web/src/components/ProviderUpdateLaunchNotification.logic.test.ts b/apps/web/src/components/ProviderUpdateLaunchNotification.logic.test.ts index 223960f8314d..2ee06a6b6620 100644 --- a/apps/web/src/components/ProviderUpdateLaunchNotification.logic.test.ts +++ b/apps/web/src/components/ProviderUpdateLaunchNotification.logic.test.ts @@ -31,6 +31,7 @@ import { parseWslDistroFromInstanceId, providerUpdateNotificationKey, resolveEnvironmentUpdateRowStatus, + shouldShowPrimaryProviderUpdateToast, type LocalEnvironmentProvidersInput, type LocalEnvironmentUpdateGroup, type LocalProviderUpdateOutcome, @@ -325,6 +326,21 @@ describe("provider update launch notification logic", () => { type: "loading", title: "Updating provider", }); + expect(shouldShowPrimaryProviderUpdateToast(view)).toBe(false); + }); + + it("keeps the initial prompt and terminal outcomes visible as toasts", () => { + expect( + shouldShowPrimaryProviderUpdateToast( + getProviderUpdateInitialToastView({ + updateProviders: [updateCandidate({ driver: driver("codex") })], + oneClickProviders: [updateCandidate({ driver: driver("codex") })], + }), + ), + ).toBe(true); + expect( + shouldShowPrimaryProviderUpdateToast(getProviderUpdateRejectedToastView(1, "boom")), + ).toBe(true); }); it("uses server failure state for failed progress", () => { diff --git a/apps/web/src/components/ProviderUpdateLaunchNotification.logic.ts b/apps/web/src/components/ProviderUpdateLaunchNotification.logic.ts index 55999d2a31d8..8d8abf73e312 100644 --- a/apps/web/src/components/ProviderUpdateLaunchNotification.logic.ts +++ b/apps/web/src/components/ProviderUpdateLaunchNotification.logic.ts @@ -231,6 +231,10 @@ export function getProviderUpdateInitialToastView(input: { }; } +export function shouldShowPrimaryProviderUpdateToast(view: ProviderUpdateToastView): boolean { + return view.phase !== "running"; +} + export function getProviderUpdateRunningToastView(providerCount: number): ProviderUpdateToastView { return { phase: "running", diff --git a/apps/web/src/components/ProviderUpdatePrimaryNotification.tsx b/apps/web/src/components/ProviderUpdatePrimaryNotification.tsx index 00112ccec198..639f07c38c13 100644 --- a/apps/web/src/components/ProviderUpdatePrimaryNotification.tsx +++ b/apps/web/src/components/ProviderUpdatePrimaryNotification.tsx @@ -16,8 +16,8 @@ import { getProviderUpdateInitialToastView, getProviderUpdateProgressToastView, getProviderUpdateRejectedToastView, - getProviderUpdateRunningToastView, providerUpdateNotificationKey, + shouldShowPrimaryProviderUpdateToast, type ProviderUpdateToastView, } from "./ProviderUpdateLaunchNotification.logic"; import { hiddenToastActionProps, stackedThreadToast, toastManager } from "./ui/toast"; @@ -31,7 +31,6 @@ type ActiveProviderUpdateToast = | { readonly kind: "update"; readonly key: string; - readonly toastId: ProviderUpdateToastId; readonly providerInstanceIds: ReadonlySet; readonly providerCount: number; }; @@ -57,20 +56,16 @@ function ProviderUpdateToastIcon({ provider }: { provider: ProviderDriverKind }) ); } -function updateProviderUpdateToast(input: { - readonly toastId: ProviderUpdateToastId; +function addProviderUpdateToast(input: { readonly view: ProviderUpdateToastView; - readonly openSettings: () => void; + readonly openSettings: (toastId: ProviderUpdateToastId) => void; }) { if (input.view.type === "loading" || input.view.type === "success") { - toastManager.update(input.toastId, { + return toastManager.add({ type: input.view.type, title: input.view.title, description: input.view.description, timeout: 0, - // Base UI merges toast updates and omits `undefined` keys, so `undefined` - // would leave the prompt's Update button in place. Replace it with a - // defined empty action so the CTA cannot linger while the update runs. actionProps: hiddenToastActionProps, data: { hideCopyButton: true, @@ -79,11 +74,10 @@ function updateProviderUpdateToast(input: { : {}), }, }); - return; } - toastManager.update( - input.toastId, + let toastId!: ProviderUpdateToastId; + toastId = toastManager.add( stackedThreadToast({ type: input.view.type, title: input.view.title, @@ -91,7 +85,7 @@ function updateProviderUpdateToast(input: { timeout: 0, actionProps: { children: "Settings", - onClick: input.openSettings, + onClick: () => input.openSettings(toastId), }, actionVariant: "outline", data: { @@ -99,10 +93,7 @@ function updateProviderUpdateToast(input: { }, }), ); -} - -function isTerminalProviderUpdateToastView(view: ProviderUpdateToastView) { - return view.phase === "failed" || view.phase === "unchanged" || view.phase === "succeeded"; + return toastId; } /** @@ -126,10 +117,10 @@ export function ProviderUpdatePrimaryNotification() { useEffect(() => { return () => { const activeToast = activeToastRef.current; - if (activeToast) { + if (activeToast?.kind === "prompt") { toastManager.close(activeToast.toastId); - activeToastRef.current = null; } + activeToastRef.current = null; }; }, []); @@ -149,10 +140,14 @@ export function ProviderUpdatePrimaryNotification() { const activeToast = activeToastRef.current; if (toastId !== undefined) { toastManager.close(toastId); - } else if (activeToast) { + } else if (activeToast?.kind === "prompt") { toastManager.close(activeToast.toastId); } - if (activeToast && (toastId === undefined || activeToast.toastId === toastId)) { + if ( + activeToast && + (toastId === undefined || + (activeToast.kind === "prompt" && activeToast.toastId === toastId)) + ) { activeToastRef.current = null; } void navigate({ to: "/settings/providers" }); @@ -173,15 +168,12 @@ export function ProviderUpdatePrimaryNotification() { providers: activeProviders, providerCount: activeToast.providerCount, }); - updateProviderUpdateToast({ - toastId: activeToast.toastId, - view, - openSettings: () => openProviderSettings(activeToast.toastId), - }); - - if (isTerminalProviderUpdateToastView(view)) { - activeToastRef.current = null; + if (!shouldShowPrimaryProviderUpdateToast(view)) { + return; } + + addProviderUpdateToast({ view, openSettings: openProviderSettings }); + activeToastRef.current = null; }, [providers, openProviderSettings]); useEffect(() => { @@ -219,19 +211,15 @@ export function ProviderUpdatePrimaryNotification() { const providerCount = oneClickProviders.length; const providerInstanceIds = new Set(oneClickProviders.map((provider) => provider.instanceId)); - activeToastRef.current = { + const activeUpdate: ActiveProviderUpdateToast = { kind: "update", key: notificationKey, - toastId, providerInstanceIds, providerCount, }; + activeToastRef.current = activeUpdate; - updateProviderUpdateToast({ - toastId, - view: getProviderUpdateRunningToastView(providerCount), - openSettings, - }); + toastManager.close(toastId); void (async () => { const results = []; @@ -248,16 +236,15 @@ export function ProviderUpdatePrimaryNotification() { } const activeUpdateToast = activeToastRef.current; - if (activeUpdateToast?.kind !== "update" || activeUpdateToast.toastId !== toastId) { + if (activeUpdateToast !== activeUpdate) { return; } const failedMessage = firstFailedProviderUpdateMessage(results); if (failedMessage) { - updateProviderUpdateToast({ - toastId, + addProviderUpdateToast({ view: getProviderUpdateRejectedToastView(providerCount, failedMessage), - openSettings, + openSettings: openProviderSettings, }); activeToastRef.current = null; return; @@ -271,13 +258,8 @@ export function ProviderUpdatePrimaryNotification() { providers: updatedProviderSnapshots, providerCount, }); - updateProviderUpdateToast({ - toastId, - view, - openSettings, - }); - - if (isTerminalProviderUpdateToastView(view)) { + if (shouldShowPrimaryProviderUpdateToast(view)) { + addProviderUpdateToast({ view, openSettings: openProviderSettings }); activeToastRef.current = null; } })();