From 0ec48a51c12d3d3da83ef541268abda9700575fe Mon Sep 17 00:00:00 2001 From: none23 Date: Wed, 21 Oct 2026 12:58:10 +0400 Subject: [PATCH 1/2] fix(web): wait for connection before updating providers --- ...erUpdateLaunchNotification.environments.ts | 2 +- ...iderUpdateLaunchNotification.logic.test.ts | 22 +++++++++++++++++++ .../ProviderUpdateLaunchNotification.logic.ts | 9 +++++--- .../ProviderUpdatePrimaryNotification.tsx | 19 ++++++++++++---- 4 files changed, 44 insertions(+), 8 deletions(-) diff --git a/apps/web/src/components/ProviderUpdateLaunchNotification.environments.ts b/apps/web/src/components/ProviderUpdateLaunchNotification.environments.ts index 4fa45a39d212c..13a5de8344222 100644 --- a/apps/web/src/components/ProviderUpdateLaunchNotification.environments.ts +++ b/apps/web/src/components/ProviderUpdateLaunchNotification.environments.ts @@ -64,7 +64,7 @@ export function useLocalEnvironmentUpdateGroups(): { }) : environment.label, isPrimary, - connectionState: isPrimary ? "ready" : normalizeConnectionState(environment.connection.phase), + connectionState: normalizeConnectionState(environment.connection.phase), providers: providersByEnvironment.get(environment.environmentId) ?? [], }); } diff --git a/apps/web/src/components/ProviderUpdateLaunchNotification.logic.test.ts b/apps/web/src/components/ProviderUpdateLaunchNotification.logic.test.ts index 66b8ff5318ab4..dd4a08c486590 100644 --- a/apps/web/src/components/ProviderUpdateLaunchNotification.logic.test.ts +++ b/apps/web/src/components/ProviderUpdateLaunchNotification.logic.test.ts @@ -20,6 +20,7 @@ import { hasOneClickUpdateProviderCandidate, isProviderUpdateCandidate, isProviderSettingsUpdateCandidate, + localEnvironmentUpdateNotificationKey, providerUpdateNotificationKey, resolveEnvironmentUpdateRowStatus, type LocalEnvironmentProvidersInput, @@ -445,6 +446,27 @@ describe("provider update launch notification logic", () => { ?.isSettling, ).toBe(true); }); + + it("waits for a connection before offering cached provider updates", () => { + const cachedProvider = provider({ driver: driver("codex") }); + const primary = environment({ + environmentId: "env-cached", + isPrimary: true, + connectionState: "connecting", + providers: [cachedProvider], + }); + const waiting = buildLocalEnvironmentUpdateGroups([primary]); + expect(waiting.groups[0]?.candidates).toEqual([]); + expect(localEnvironmentUpdateNotificationKey(waiting.groups)).toBeNull(); + + const connected = buildLocalEnvironmentUpdateGroups([ + { ...primary, connectionState: "ready" }, + ]); + expect(connected.groups[0]?.candidates).toEqual([cachedProvider]); + expect(localEnvironmentUpdateNotificationKey(connected.groups)).toBe( + "env-cached=codex:1.1.0", + ); + }); }); describe("resolveEnvironmentUpdateRowStatus", () => { diff --git a/apps/web/src/components/ProviderUpdateLaunchNotification.logic.ts b/apps/web/src/components/ProviderUpdateLaunchNotification.logic.ts index 8c65765111746..bf8a573af11b9 100644 --- a/apps/web/src/components/ProviderUpdateLaunchNotification.logic.ts +++ b/apps/web/src/components/ProviderUpdateLaunchNotification.logic.ts @@ -749,9 +749,12 @@ export function buildLocalEnvironmentUpdateGroups( label: environment.label, isPrimary: environment.isPrimary, isSettling: environment.connectionState === "connecting", - candidates: collectProviderUpdateCandidates(environment.providers).filter((candidate) => - canOneClickUpdateProviderCandidate(candidate, environment.providers), - ), + candidates: + environment.connectionState === "ready" + ? collectProviderUpdateCandidates(environment.providers).filter((candidate) => + canOneClickUpdateProviderCandidate(candidate, environment.providers), + ) + : [], providers: environment.providers, })); const isAnySettling = environments.some( diff --git a/apps/web/src/components/ProviderUpdatePrimaryNotification.tsx b/apps/web/src/components/ProviderUpdatePrimaryNotification.tsx index 74040e171c5d5..8621fc9f685c3 100644 --- a/apps/web/src/components/ProviderUpdatePrimaryNotification.tsx +++ b/apps/web/src/components/ProviderUpdatePrimaryNotification.tsx @@ -37,6 +37,7 @@ type ActiveProviderUpdateToast = readonly key: string; readonly toastId: ProviderUpdateToastId; readonly canManageProviders: boolean; + readonly canUpdateProviders: boolean; } | { readonly kind: "update"; @@ -109,6 +110,8 @@ export function ProviderUpdatePrimaryNotification() { primaryEnvironment?.environmentId ?? null, AuthProvidersManageScope, ); + const canUpdateProviders = + canManageProviders && primaryEnvironment?.connection.phase === "connected"; const updateProvider = useAtomCommand(serverEnvironment.updateProvider, { reportFailure: false, }); @@ -179,7 +182,8 @@ export function ProviderUpdatePrimaryNotification() { const updateExistingPrompt = activeToast?.kind === "prompt" && activeToast.key === notificationKey && - activeToast.canManageProviders !== canManageProviders; + (activeToast.canManageProviders !== canManageProviders || + activeToast.canUpdateProviders !== canUpdateProviders); if ( !notificationKey || dismissedNotificationKeys.has(notificationKey) || @@ -204,7 +208,7 @@ export function ProviderUpdatePrimaryNotification() { if ( updateStarted || oneClickProviders.length === 0 || - !primaryEnvironment || + primaryEnvironment?.connection.phase !== "connected" || !readEnvironmentScope(primaryEnvironment.environmentId, AuthProvidersManageScope) ) { return; @@ -291,7 +295,7 @@ export function ProviderUpdatePrimaryNotification() { oneClickProviders.length > 0 ? { children: "Update", - disabled: !canManageProviders, + disabled: !canUpdateProviders, onClick: runUpdates, } : { @@ -323,9 +327,16 @@ export function ProviderUpdatePrimaryNotification() { } else { toastId = toastManager.add(toastOptions); } - activeToastRef.current = { kind: "prompt", key: notificationKey, toastId, canManageProviders }; + activeToastRef.current = { + kind: "prompt", + key: notificationKey, + toastId, + canManageProviders, + canUpdateProviders, + }; }, [ canManageProviders, + canUpdateProviders, updateProvider, dismissNotificationKey, dismissedNotificationKeys, From 86d46a1f13ca030d649df01cbdecbc541a551085 Mon Sep 17 00:00:00 2001 From: none23 Date: Wed, 21 Oct 2026 13:39:44 +0400 Subject: [PATCH 2/2] fix(web): preserve provider update notices during reconnects --- .../ProviderUpdateEnvironmentRows.tsx | 7 +++--- ...iderUpdateLaunchNotification.logic.test.ts | 25 ++++++++++--------- .../ProviderUpdateLaunchNotification.logic.ts | 11 ++++---- 3 files changed, 22 insertions(+), 21 deletions(-) diff --git a/apps/web/src/components/ProviderUpdateEnvironmentRows.tsx b/apps/web/src/components/ProviderUpdateEnvironmentRows.tsx index eecea9a146531..8b3232f79f784 100644 --- a/apps/web/src/components/ProviderUpdateEnvironmentRows.tsx +++ b/apps/web/src/components/ProviderUpdateEnvironmentRows.tsx @@ -111,6 +111,7 @@ function EnvironmentUpdateRow({ readonly onUpdate: () => void; }) { const canManageProviders = useEnvironmentScope(group.environmentId, AuthProvidersManageScope); + const canUpdateProviders = canManageProviders && group.isReady; let trailing: ReactNode; switch (status.kind) { case "loading": @@ -122,14 +123,14 @@ function EnvironmentUpdateRow({ case "failed": case "unchanged": trailing = ( - ); break; default: trailing = ( - ); @@ -203,7 +204,7 @@ export function ProviderUpdateEnvironmentRows({ const handleUpdate = async (environmentId: EnvironmentId) => { if (!readEnvironmentScope(environmentId, AuthProvidersManageScope)) return; const group = groupByEnvironment.get(environmentId); - if (!group || group.candidates.length === 0) { + if (!group?.isReady || group.candidates.length === 0) { return; } if (inFlightEnvironmentsRef.current.has(environmentId)) { diff --git a/apps/web/src/components/ProviderUpdateLaunchNotification.logic.test.ts b/apps/web/src/components/ProviderUpdateLaunchNotification.logic.test.ts index dd4a08c486590..13bd6cc85e8ce 100644 --- a/apps/web/src/components/ProviderUpdateLaunchNotification.logic.test.ts +++ b/apps/web/src/components/ProviderUpdateLaunchNotification.logic.test.ts @@ -447,7 +447,7 @@ describe("provider update launch notification logic", () => { ).toBe(true); }); - it("waits for a connection before offering cached provider updates", () => { + it("keeps the notification key stable while connection readiness changes", () => { const cachedProvider = provider({ driver: driver("codex") }); const primary = environment({ environmentId: "env-cached", @@ -455,17 +455,17 @@ describe("provider update launch notification logic", () => { connectionState: "connecting", providers: [cachedProvider], }); - const waiting = buildLocalEnvironmentUpdateGroups([primary]); - expect(waiting.groups[0]?.candidates).toEqual([]); - expect(localEnvironmentUpdateNotificationKey(waiting.groups)).toBeNull(); - - const connected = buildLocalEnvironmentUpdateGroups([ - { ...primary, connectionState: "ready" }, - ]); - expect(connected.groups[0]?.candidates).toEqual([cachedProvider]); - expect(localEnvironmentUpdateNotificationKey(connected.groups)).toBe( - "env-cached=codex:1.1.0", - ); + for (const connectionState of [ + "connecting", + "ready", + "disconnected", + "error", + "ready", + ] as const) { + const { groups } = buildLocalEnvironmentUpdateGroups([{ ...primary, connectionState }]); + expect(groups[0]?.isReady).toBe(connectionState === "ready"); + expect(localEnvironmentUpdateNotificationKey(groups)).toBe("env-cached=codex:1.1.0"); + } }); }); @@ -475,6 +475,7 @@ describe("provider update launch notification logic", () => { label: "WSL", isPrimary: false, isSettling: false, + isReady: true, candidates: [updateCandidate({ driver: driver("codex"), latestVersion: "1.1.0" })], providers: [], }; diff --git a/apps/web/src/components/ProviderUpdateLaunchNotification.logic.ts b/apps/web/src/components/ProviderUpdateLaunchNotification.logic.ts index bf8a573af11b9..8206e2f9e0df3 100644 --- a/apps/web/src/components/ProviderUpdateLaunchNotification.logic.ts +++ b/apps/web/src/components/ProviderUpdateLaunchNotification.logic.ts @@ -737,6 +737,7 @@ export interface LocalEnvironmentUpdateGroup { readonly label: string; readonly isPrimary: boolean; readonly isSettling: boolean; + readonly isReady: boolean; readonly candidates: ProviderUpdateCandidate[]; readonly providers: ReadonlyArray; } @@ -749,12 +750,10 @@ export function buildLocalEnvironmentUpdateGroups( label: environment.label, isPrimary: environment.isPrimary, isSettling: environment.connectionState === "connecting", - candidates: - environment.connectionState === "ready" - ? collectProviderUpdateCandidates(environment.providers).filter((candidate) => - canOneClickUpdateProviderCandidate(candidate, environment.providers), - ) - : [], + isReady: environment.connectionState === "ready", + candidates: collectProviderUpdateCandidates(environment.providers).filter((candidate) => + canOneClickUpdateProviderCandidate(candidate, environment.providers), + ), providers: environment.providers, })); const isAnySettling = environments.some(