Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
parseWslDistroFromInstanceId,
providerUpdateNotificationKey,
resolveEnvironmentUpdateRowStatus,
shouldShowPrimaryProviderUpdateToast,
type LocalEnvironmentProvidersInput,
type LocalEnvironmentUpdateGroup,
type LocalProviderUpdateOutcome,
Expand Down Expand Up @@ -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", () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
76 changes: 29 additions & 47 deletions apps/web/src/components/ProviderUpdatePrimaryNotification.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ import {
getProviderUpdateInitialToastView,
getProviderUpdateProgressToastView,
getProviderUpdateRejectedToastView,
getProviderUpdateRunningToastView,
providerUpdateNotificationKey,
shouldShowPrimaryProviderUpdateToast,
type ProviderUpdateToastView,
} from "./ProviderUpdateLaunchNotification.logic";
import { hiddenToastActionProps, stackedThreadToast, toastManager } from "./ui/toast";
Expand All @@ -31,7 +31,6 @@ type ActiveProviderUpdateToast =
| {
readonly kind: "update";
readonly key: string;
readonly toastId: ProviderUpdateToastId;
readonly providerInstanceIds: ReadonlySet<ProviderInstanceId>;
readonly providerCount: number;
};
Expand All @@ -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,
Expand All @@ -79,30 +74,26 @@ function updateProviderUpdateToast(input: {
: {}),
},
});
return;
}

toastManager.update(
input.toastId,
let toastId!: ProviderUpdateToastId;
toastId = toastManager.add(
stackedThreadToast({
type: input.view.type,
title: input.view.title,
description: input.view.description,
timeout: 0,
actionProps: {
children: "Settings",
onClick: input.openSettings,
onClick: () => input.openSettings(toastId),
},
actionVariant: "outline",
data: {
hideCopyButton: true,
},
}),
);
}

function isTerminalProviderUpdateToastView(view: ProviderUpdateToastView) {
return view.phase === "failed" || view.phase === "unchanged" || view.phase === "succeeded";
return toastId;
}

/**
Expand All @@ -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;
};
}, []);

Expand All @@ -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" });
Expand All @@ -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(() => {
Expand Down Expand Up @@ -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 = [];
Expand All @@ -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;
Expand All @@ -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;
}
})();
Expand Down
Loading