authorgravatar for naveediqbal949@gmail.comNaveed Iqbal <naveediqbal949@gmail.com> 2026-08-23 05:16:07+05:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-08-22 20:16:07-04:00
log4e169df1dd12e0ed960dcb310e9460897759a55e
treed220cf1b421119de4659461f9d4130f7daac31ec
parent5a7a7cf2925c88388a023f0d4eb6b9096884e817
signature Signed by PGP key B5690EEEBB952194

fix(web): remove duplicate provider update progress (#7761)


3 files changed, 49 insertions(+), 47 deletions(-)

apps/web/src/components/ProviderUpdateLaunchNotification.logic.test.ts+16
......@@ -31,6 +31,7 @@ import {
3131 parseWslDistroFromInstanceId,
3232 providerUpdateNotificationKey,
3333 resolveEnvironmentUpdateRowStatus,
34 shouldShowPrimaryProviderUpdateToast,
3435 type LocalEnvironmentProvidersInput,
3536 type LocalEnvironmentUpdateGroup,
3637 type LocalProviderUpdateOutcome,
......@@ -325,6 +326,21 @@ describe("provider update launch notification logic", () => {
325326 type: "loading",
326327 title: "Updating provider",
327328 });
329 expect(shouldShowPrimaryProviderUpdateToast(view)).toBe(false);
330 });
331
332 it("keeps the initial prompt and terminal outcomes visible as toasts", () => {
333 expect(
334 shouldShowPrimaryProviderUpdateToast(
335 getProviderUpdateInitialToastView({
336 updateProviders: [updateCandidate({ driver: driver("codex") })],
337 oneClickProviders: [updateCandidate({ driver: driver("codex") })],
338 }),
339 ),
340 ).toBe(true);
341 expect(
342 shouldShowPrimaryProviderUpdateToast(getProviderUpdateRejectedToastView(1, "boom")),
343 ).toBe(true);
328344 });
329345
330346 it("uses server failure state for failed progress", () => {
apps/web/src/components/ProviderUpdateLaunchNotification.logic.ts+4
......@@ -231,6 +231,10 @@ export function getProviderUpdateInitialToastView(input: {
231231 };
232232}
233233
234export function shouldShowPrimaryProviderUpdateToast(view: ProviderUpdateToastView): boolean {
235 return view.phase !== "running";
236}
237
234238export function getProviderUpdateRunningToastView(providerCount: number): ProviderUpdateToastView {
235239 return {
236240 phase: "running",
apps/web/src/components/ProviderUpdatePrimaryNotification.tsx+29-47
......@@ -16,8 +16,8 @@ import {
1616 getProviderUpdateInitialToastView,
1717 getProviderUpdateProgressToastView,
1818 getProviderUpdateRejectedToastView,
19 getProviderUpdateRunningToastView,
2019 providerUpdateNotificationKey,
20 shouldShowPrimaryProviderUpdateToast,
2121 type ProviderUpdateToastView,
2222} from "./ProviderUpdateLaunchNotification.logic";
2323import { hiddenToastActionProps, stackedThreadToast, toastManager } from "./ui/toast";
......@@ -31,7 +31,6 @@ type ActiveProviderUpdateToast =
3131 | {
3232 readonly kind: "update";
3333 readonly key: string;
34 readonly toastId: ProviderUpdateToastId;
3534 readonly providerInstanceIds: ReadonlySet<ProviderInstanceId>;
3635 readonly providerCount: number;
3736 };
......@@ -57,20 +56,16 @@ function ProviderUpdateToastIcon({ provider }: { provider: ProviderDriverKind })
5756 );
5857}
5958
60function updateProviderUpdateToast(input: {
61 readonly toastId: ProviderUpdateToastId;
59function addProviderUpdateToast(input: {
6260 readonly view: ProviderUpdateToastView;
63 readonly openSettings: () => void;
61 readonly openSettings: (toastId: ProviderUpdateToastId) => void;
6462}) {
6563 if (input.view.type === "loading" || input.view.type === "success") {
66 toastManager.update(input.toastId, {
64 return toastManager.add({
6765 type: input.view.type,
6866 title: input.view.title,
6967 description: input.view.description,
7068 timeout: 0,
71 // Base UI merges toast updates and omits `undefined` keys, so `undefined`
72 // would leave the prompt's Update button in place. Replace it with a
73 // defined empty action so the CTA cannot linger while the update runs.
7469 actionProps: hiddenToastActionProps,
7570 data: {
7671 hideCopyButton: true,
......@@ -79,11 +74,10 @@ function updateProviderUpdateToast(input: {
7974 : {}),
8075 },
8176 });
82 return;
8377 }
8478
85 toastManager.update(
86 input.toastId,
79 let toastId!: ProviderUpdateToastId;
80 toastId = toastManager.add(
8781 stackedThreadToast({
8882 type: input.view.type,
8983 title: input.view.title,
......@@ -91,7 +85,7 @@ function updateProviderUpdateToast(input: {
9185 timeout: 0,
9286 actionProps: {
9387 children: "Settings",
94 onClick: input.openSettings,
88 onClick: () => input.openSettings(toastId),
9589 },
9690 actionVariant: "outline",
9791 data: {
......@@ -99,10 +93,7 @@ function updateProviderUpdateToast(input: {
9993 },
10094 }),
10195 );
102}
103
104function isTerminalProviderUpdateToastView(view: ProviderUpdateToastView) {
105 return view.phase === "failed" || view.phase === "unchanged" || view.phase === "succeeded";
96 return toastId;
10697}
10798
10899/**
......@@ -126,10 +117,10 @@ export function ProviderUpdatePrimaryNotification() {
126117 useEffect(() => {
127118 return () => {
128119 const activeToast = activeToastRef.current;
129 if (activeToast) {
120 if (activeToast?.kind === "prompt") {
130121 toastManager.close(activeToast.toastId);
131 activeToastRef.current = null;
132122 }
123 activeToastRef.current = null;
133124 };
134125 }, []);
135126
......@@ -149,10 +140,14 @@ export function ProviderUpdatePrimaryNotification() {
149140 const activeToast = activeToastRef.current;
150141 if (toastId !== undefined) {
151142 toastManager.close(toastId);
152 } else if (activeToast) {
143 } else if (activeToast?.kind === "prompt") {
153144 toastManager.close(activeToast.toastId);
154145 }
155 if (activeToast && (toastId === undefined || activeToast.toastId === toastId)) {
146 if (
147 activeToast &&
148 (toastId === undefined ||
149 (activeToast.kind === "prompt" && activeToast.toastId === toastId))
150 ) {
156151 activeToastRef.current = null;
157152 }
158153 void navigate({ to: "/settings/providers" });
......@@ -173,15 +168,12 @@ export function ProviderUpdatePrimaryNotification() {
173168 providers: activeProviders,
174169 providerCount: activeToast.providerCount,
175170 });
176 updateProviderUpdateToast({
177 toastId: activeToast.toastId,
178 view,
179 openSettings: () => openProviderSettings(activeToast.toastId),
180 });
181
182 if (isTerminalProviderUpdateToastView(view)) {
183 activeToastRef.current = null;
171 if (!shouldShowPrimaryProviderUpdateToast(view)) {
172 return;
184173 }
174
175 addProviderUpdateToast({ view, openSettings: openProviderSettings });
176 activeToastRef.current = null;
185177 }, [providers, openProviderSettings]);
186178
187179 useEffect(() => {
......@@ -219,19 +211,15 @@ export function ProviderUpdatePrimaryNotification() {
219211
220212 const providerCount = oneClickProviders.length;
221213 const providerInstanceIds = new Set(oneClickProviders.map((provider) => provider.instanceId));
222 activeToastRef.current = {
214 const activeUpdate: ActiveProviderUpdateToast = {
223215 kind: "update",
224216 key: notificationKey,
225 toastId,
226217 providerInstanceIds,
227218 providerCount,
228219 };
220 activeToastRef.current = activeUpdate;
229221
230 updateProviderUpdateToast({
231 toastId,
232 view: getProviderUpdateRunningToastView(providerCount),
233 openSettings,
234 });
222 toastManager.close(toastId);
235223
236224 void (async () => {
237225 const results = [];
......@@ -248,16 +236,15 @@ export function ProviderUpdatePrimaryNotification() {
248236 }
249237
250238 const activeUpdateToast = activeToastRef.current;
251 if (activeUpdateToast?.kind !== "update" || activeUpdateToast.toastId !== toastId) {
239 if (activeUpdateToast !== activeUpdate) {
252240 return;
253241 }
254242
255243 const failedMessage = firstFailedProviderUpdateMessage(results);
256244 if (failedMessage) {
257 updateProviderUpdateToast({
258 toastId,
245 addProviderUpdateToast({
259246 view: getProviderUpdateRejectedToastView(providerCount, failedMessage),
260 openSettings,
247 openSettings: openProviderSettings,
261248 });
262249 activeToastRef.current = null;
263250 return;
......@@ -271,13 +258,8 @@ export function ProviderUpdatePrimaryNotification() {
271258 providers: updatedProviderSnapshots,
272259 providerCount,
273260 });
274 updateProviderUpdateToast({
275 toastId,
276 view,
277 openSettings,
278 });
279
280 if (isTerminalProviderUpdateToastView(view)) {
261 if (shouldShowPrimaryProviderUpdateToast(view)) {
262 addProviderUpdateToast({ view, openSettings: openProviderSettings });
281263 activeToastRef.current = null;
282264 }
283265 })();