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.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..13bd6cc85e8ce 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("keeps the notification key stable while connection readiness changes", () => { + const cachedProvider = provider({ driver: driver("codex") }); + const primary = environment({ + environmentId: "env-cached", + isPrimary: true, + connectionState: "connecting", + providers: [cachedProvider], + }); + 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"); + } + }); }); describe("resolveEnvironmentUpdateRowStatus", () => { @@ -453,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 8c65765111746..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,6 +750,7 @@ export function buildLocalEnvironmentUpdateGroups( label: environment.label, isPrimary: environment.isPrimary, isSettling: environment.connectionState === "connecting", + isReady: environment.connectionState === "ready", candidates: collectProviderUpdateCandidates(environment.providers).filter((candidate) => canOneClickUpdateProviderCandidate(candidate, environment.providers), ), 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,