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 {...@@ -31,6 +31,7 @@ import {
31 parseWslDistroFromInstanceId,31 parseWslDistroFromInstanceId,
32 providerUpdateNotificationKey,32 providerUpdateNotificationKey,
33 resolveEnvironmentUpdateRowStatus,33 resolveEnvironmentUpdateRowStatus,
34 shouldShowPrimaryProviderUpdateToast,
34 type LocalEnvironmentProvidersInput,35 type LocalEnvironmentProvidersInput,
35 type LocalEnvironmentUpdateGroup,36 type LocalEnvironmentUpdateGroup,
36 type LocalProviderUpdateOutcome,37 type LocalProviderUpdateOutcome,
...@@ -325,6 +326,21 @@ describe("provider update launch notification logic", () => {...@@ -325,6 +326,21 @@ describe("provider update launch notification logic", () => {
325 type: "loading",326 type: "loading",
326 title: "Updating provider",327 title: "Updating provider",
327 });328 });
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);
328 });344 });
329345
330 it("uses server failure state for failed progress", () => {346 it("uses server failure state for failed progress", () => {
apps/web/src/components/ProviderUpdateLaunchNotification.logic.ts+4
...@@ -231,6 +231,10 @@ export function getProviderUpdateInitialToastView(input: {...@@ -231,6 +231,10 @@ export function getProviderUpdateInitialToastView(input: {
231 };231 };
232}232}
233233
234export function shouldShowPrimaryProviderUpdateToast(view: ProviderUpdateToastView): boolean {
235 return view.phase !== "running";
236}
237
234export function getProviderUpdateRunningToastView(providerCount: number): ProviderUpdateToastView {238export function getProviderUpdateRunningToastView(providerCount: number): ProviderUpdateToastView {
235 return {239 return {
236 phase: "running",240 phase: "running",
apps/web/src/components/ProviderUpdatePrimaryNotification.tsx+29-47
...@@ -16,8 +16,8 @@ import {...@@ -16,8 +16,8 @@ import {
16 getProviderUpdateInitialToastView,16 getProviderUpdateInitialToastView,
17 getProviderUpdateProgressToastView,17 getProviderUpdateProgressToastView,
18 getProviderUpdateRejectedToastView,18 getProviderUpdateRejectedToastView,
19 getProviderUpdateRunningToastView,
20 providerUpdateNotificationKey,19 providerUpdateNotificationKey,
20 shouldShowPrimaryProviderUpdateToast,
21 type ProviderUpdateToastView,21 type ProviderUpdateToastView,
22} from "./ProviderUpdateLaunchNotification.logic";22} from "./ProviderUpdateLaunchNotification.logic";
23import { hiddenToastActionProps, stackedThreadToast, toastManager } from "./ui/toast";23import { hiddenToastActionProps, stackedThreadToast, toastManager } from "./ui/toast";
...@@ -31,7 +31,6 @@ type ActiveProviderUpdateToast =...@@ -31,7 +31,6 @@ type ActiveProviderUpdateToast =
31 | {31 | {
32 readonly kind: "update";32 readonly kind: "update";
33 readonly key: string;33 readonly key: string;
34 readonly toastId: ProviderUpdateToastId;
35 readonly providerInstanceIds: ReadonlySet<ProviderInstanceId>;34 readonly providerInstanceIds: ReadonlySet<ProviderInstanceId>;
36 readonly providerCount: number;35 readonly providerCount: number;
37 };36 };
...@@ -57,20 +56,16 @@ function ProviderUpdateToastIcon({ provider }: { provider: ProviderDriverKind })...@@ -57,20 +56,16 @@ function ProviderUpdateToastIcon({ provider }: { provider: ProviderDriverKind })
57 );56 );
58}57}
5958
60function updateProviderUpdateToast(input: {59function addProviderUpdateToast(input: {
61 readonly toastId: ProviderUpdateToastId;
62 readonly view: ProviderUpdateToastView;60 readonly view: ProviderUpdateToastView;
63 readonly openSettings: () => void;61 readonly openSettings: (toastId: ProviderUpdateToastId) => void;
64}) {62}) {
65 if (input.view.type === "loading" || input.view.type === "success") {63 if (input.view.type === "loading" || input.view.type === "success") {
66 toastManager.update(input.toastId, {64 return toastManager.add({
67 type: input.view.type,65 type: input.view.type,
68 title: input.view.title,66 title: input.view.title,
69 description: input.view.description,67 description: input.view.description,
70 timeout: 0,68 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.
74 actionProps: hiddenToastActionProps,69 actionProps: hiddenToastActionProps,
75 data: {70 data: {
76 hideCopyButton: true,71 hideCopyButton: true,
...@@ -79,11 +74,10 @@ function updateProviderUpdateToast(input: {...@@ -79,11 +74,10 @@ function updateProviderUpdateToast(input: {
79 : {}),74 : {}),
80 },75 },
81 });76 });
82 return;
83 }77 }
8478
85 toastManager.update(79 let toastId!: ProviderUpdateToastId;
86 input.toastId,80 toastId = toastManager.add(
87 stackedThreadToast({81 stackedThreadToast({
88 type: input.view.type,82 type: input.view.type,
89 title: input.view.title,83 title: input.view.title,
...@@ -91,7 +85,7 @@ function updateProviderUpdateToast(input: {...@@ -91,7 +85,7 @@ function updateProviderUpdateToast(input: {
91 timeout: 0,85 timeout: 0,
92 actionProps: {86 actionProps: {
93 children: "Settings",87 children: "Settings",
94 onClick: input.openSettings,88 onClick: () => input.openSettings(toastId),
95 },89 },
96 actionVariant: "outline",90 actionVariant: "outline",
97 data: {91 data: {
...@@ -99,10 +93,7 @@ function updateProviderUpdateToast(input: {...@@ -99,10 +93,7 @@ function updateProviderUpdateToast(input: {
99 },93 },
100 }),94 }),
101 );95 );
102}96 return toastId;
103
104function isTerminalProviderUpdateToastView(view: ProviderUpdateToastView) {
105 return view.phase === "failed" || view.phase === "unchanged" || view.phase === "succeeded";
106}97}
10798
108/**99/**
...@@ -126,10 +117,10 @@ export function ProviderUpdatePrimaryNotification() {...@@ -126,10 +117,10 @@ export function ProviderUpdatePrimaryNotification() {
126 useEffect(() => {117 useEffect(() => {
127 return () => {118 return () => {
128 const activeToast = activeToastRef.current;119 const activeToast = activeToastRef.current;
129 if (activeToast) {120 if (activeToast?.kind === "prompt") {
130 toastManager.close(activeToast.toastId);121 toastManager.close(activeToast.toastId);
131 activeToastRef.current = null;
132 }122 }
123 activeToastRef.current = null;
133 };124 };
134 }, []);125 }, []);
135126
...@@ -149,10 +140,14 @@ export function ProviderUpdatePrimaryNotification() {...@@ -149,10 +140,14 @@ export function ProviderUpdatePrimaryNotification() {
149 const activeToast = activeToastRef.current;140 const activeToast = activeToastRef.current;
150 if (toastId !== undefined) {141 if (toastId !== undefined) {
151 toastManager.close(toastId);142 toastManager.close(toastId);
152 } else if (activeToast) {143 } else if (activeToast?.kind === "prompt") {
153 toastManager.close(activeToast.toastId);144 toastManager.close(activeToast.toastId);
154 }145 }
155 if (activeToast && (toastId === undefined || activeToast.toastId === toastId)) {146 if (
147 activeToast &&
148 (toastId === undefined ||
149 (activeToast.kind === "prompt" && activeToast.toastId === toastId))
150 ) {
156 activeToastRef.current = null;151 activeToastRef.current = null;
157 }152 }
158 void navigate({ to: "/settings/providers" });153 void navigate({ to: "/settings/providers" });
...@@ -173,15 +168,12 @@ export function ProviderUpdatePrimaryNotification() {...@@ -173,15 +168,12 @@ export function ProviderUpdatePrimaryNotification() {
173 providers: activeProviders,168 providers: activeProviders,
174 providerCount: activeToast.providerCount,169 providerCount: activeToast.providerCount,
175 });170 });
176 updateProviderUpdateToast({171 if (!shouldShowPrimaryProviderUpdateToast(view)) {
177 toastId: activeToast.toastId,172 return;
178 view,
179 openSettings: () => openProviderSettings(activeToast.toastId),
180 });
181
182 if (isTerminalProviderUpdateToastView(view)) {
183 activeToastRef.current = null;
184 }173 }
174
175 addProviderUpdateToast({ view, openSettings: openProviderSettings });
176 activeToastRef.current = null;
185 }, [providers, openProviderSettings]);177 }, [providers, openProviderSettings]);
186178
187 useEffect(() => {179 useEffect(() => {
...@@ -219,19 +211,15 @@ export function ProviderUpdatePrimaryNotification() {...@@ -219,19 +211,15 @@ export function ProviderUpdatePrimaryNotification() {
219211
220 const providerCount = oneClickProviders.length;212 const providerCount = oneClickProviders.length;
221 const providerInstanceIds = new Set(oneClickProviders.map((provider) => provider.instanceId));213 const providerInstanceIds = new Set(oneClickProviders.map((provider) => provider.instanceId));
222 activeToastRef.current = {214 const activeUpdate: ActiveProviderUpdateToast = {
223 kind: "update",215 kind: "update",
224 key: notificationKey,216 key: notificationKey,
225 toastId,
226 providerInstanceIds,217 providerInstanceIds,
227 providerCount,218 providerCount,
228 };219 };
220 activeToastRef.current = activeUpdate;
229221
230 updateProviderUpdateToast({222 toastManager.close(toastId);
231 toastId,
232 view: getProviderUpdateRunningToastView(providerCount),
233 openSettings,
234 });
235223
236 void (async () => {224 void (async () => {
237 const results = [];225 const results = [];
...@@ -248,16 +236,15 @@ export function ProviderUpdatePrimaryNotification() {...@@ -248,16 +236,15 @@ export function ProviderUpdatePrimaryNotification() {
248 }236 }
249237
250 const activeUpdateToast = activeToastRef.current;238 const activeUpdateToast = activeToastRef.current;
251 if (activeUpdateToast?.kind !== "update" || activeUpdateToast.toastId !== toastId) {239 if (activeUpdateToast !== activeUpdate) {
252 return;240 return;
253 }241 }
254242
255 const failedMessage = firstFailedProviderUpdateMessage(results);243 const failedMessage = firstFailedProviderUpdateMessage(results);
256 if (failedMessage) {244 if (failedMessage) {
257 updateProviderUpdateToast({245 addProviderUpdateToast({
258 toastId,
259 view: getProviderUpdateRejectedToastView(providerCount, failedMessage),246 view: getProviderUpdateRejectedToastView(providerCount, failedMessage),
260 openSettings,247 openSettings: openProviderSettings,
261 });248 });
262 activeToastRef.current = null;249 activeToastRef.current = null;
263 return;250 return;
...@@ -271,13 +258,8 @@ export function ProviderUpdatePrimaryNotification() {...@@ -271,13 +258,8 @@ export function ProviderUpdatePrimaryNotification() {
271 providers: updatedProviderSnapshots,258 providers: updatedProviderSnapshots,
272 providerCount,259 providerCount,
273 });260 });
274 updateProviderUpdateToast({261 if (shouldShowPrimaryProviderUpdateToast(view)) {
275 toastId,262 addProviderUpdateToast({ view, openSettings: openProviderSettings });
276 view,
277 openSettings,
278 });
279
280 if (isTerminalProviderUpdateToastView(view)) {
281 activeToastRef.current = null;263 activeToastRef.current = null;
282 }264 }
283 })();265 })();