From 18e8dd768aa65fcd9bb227de0a3ab1ac8bdb61d8 Mon Sep 17 00:00:00 2001 From: Sehoon Shon Date: Thu, 12 Mar 2026 09:46:58 -0400 Subject: [PATCH] feat(ui): Do not show Ultra users /upgrade hint (#22154) (#22156) --- .../src/ui/commands/upgradeCommand.test.ts | 20 +++++++++++ .../cli/src/ui/commands/upgradeCommand.ts | 10 ++++++ .../cli/src/ui/components/DialogManager.tsx | 1 + .../src/ui/components/ProQuotaDialog.test.tsx | 34 +++++++++++++++++++ .../cli/src/ui/components/ProQuotaDialog.tsx | 7 +++- .../src/ui/components/UserIdentity.test.tsx | 19 +++++++++++ .../cli/src/ui/components/UserIdentity.tsx | 5 ++- packages/cli/src/utils/tierUtils.test.ts | 28 +++++++++++++++ packages/cli/src/utils/tierUtils.ts | 15 ++++++++ 9 files changed, 137 insertions(+), 2 deletions(-) create mode 100644 packages/cli/src/utils/tierUtils.test.ts create mode 100644 packages/cli/src/utils/tierUtils.ts diff --git a/packages/cli/src/ui/commands/upgradeCommand.test.ts b/packages/cli/src/ui/commands/upgradeCommand.test.ts index d511f69c3a..9c54eb0191 100644 --- a/packages/cli/src/ui/commands/upgradeCommand.test.ts +++ b/packages/cli/src/ui/commands/upgradeCommand.test.ts @@ -37,6 +37,7 @@ describe('upgradeCommand', () => { getContentGeneratorConfig: vi.fn().mockReturnValue({ authType: AuthType.LOGIN_WITH_GOOGLE, }), + getUserTierName: vi.fn().mockReturnValue(undefined), }, }, } as unknown as CommandContext); @@ -115,4 +116,23 @@ describe('upgradeCommand', () => { }); expect(openBrowserSecurely).not.toHaveBeenCalled(); }); + + it('should return info message for ultra tiers', async () => { + vi.mocked(mockContext.services.config!.getUserTierName).mockReturnValue( + 'Advanced Ultra', + ); + + if (!upgradeCommand.action) { + throw new Error('The upgrade command must have an action.'); + } + + const result = await upgradeCommand.action(mockContext, ''); + + expect(result).toEqual({ + type: 'message', + messageType: 'info', + content: 'You are already on the highest tier: Advanced Ultra.', + }); + expect(openBrowserSecurely).not.toHaveBeenCalled(); + }); }); diff --git a/packages/cli/src/ui/commands/upgradeCommand.ts b/packages/cli/src/ui/commands/upgradeCommand.ts index 4904509df1..9bbea156ce 100644 --- a/packages/cli/src/ui/commands/upgradeCommand.ts +++ b/packages/cli/src/ui/commands/upgradeCommand.ts @@ -10,6 +10,7 @@ import { shouldLaunchBrowser, UPGRADE_URL_PAGE, } from '@google/gemini-cli-core'; +import { isUltraTier } from '../../utils/tierUtils.js'; import { CommandKind, type SlashCommand } from './types.js'; /** @@ -35,6 +36,15 @@ export const upgradeCommand: SlashCommand = { }; } + const tierName = context.services.config?.getUserTierName(); + if (isUltraTier(tierName)) { + return { + type: 'message', + messageType: 'info', + content: `You are already on the highest tier: ${tierName}.`, + }; + } + if (!shouldLaunchBrowser()) { return { type: 'message', diff --git a/packages/cli/src/ui/components/DialogManager.tsx b/packages/cli/src/ui/components/DialogManager.tsx index de62401e1e..e7e23c834d 100644 --- a/packages/cli/src/ui/components/DialogManager.tsx +++ b/packages/cli/src/ui/components/DialogManager.tsx @@ -87,6 +87,7 @@ export const DialogManager = ({ !!uiState.quota.proQuotaRequest.isModelNotFoundError } authType={uiState.quota.proQuotaRequest.authType} + tierName={config?.getUserTierName()} onChoice={uiActions.handleProQuotaChoice} /> ); diff --git a/packages/cli/src/ui/components/ProQuotaDialog.test.tsx b/packages/cli/src/ui/components/ProQuotaDialog.test.tsx index d97d53314e..2b69770582 100644 --- a/packages/cli/src/ui/components/ProQuotaDialog.test.tsx +++ b/packages/cli/src/ui/components/ProQuotaDialog.test.tsx @@ -202,6 +202,40 @@ describe('ProQuotaDialog', () => { ); unmount(); }); + + it('should NOT render upgrade option for LOGIN_WITH_GOOGLE if tier is Ultra', () => { + const { unmount } = render( + , + ); + + expect(RadioButtonSelect).toHaveBeenCalledWith( + expect.objectContaining({ + items: [ + { + label: 'Switch to gemini-2.5-flash', + value: 'retry_always', + key: 'retry_always', + }, + { + label: 'Stop', + value: 'retry_later', + key: 'retry_later', + }, + ], + }), + undefined, + ); + unmount(); + }); }); describe('when it is a capacity error', () => { diff --git a/packages/cli/src/ui/components/ProQuotaDialog.tsx b/packages/cli/src/ui/components/ProQuotaDialog.tsx index 82a679db8c..e9e869edb0 100644 --- a/packages/cli/src/ui/components/ProQuotaDialog.tsx +++ b/packages/cli/src/ui/components/ProQuotaDialog.tsx @@ -9,6 +9,7 @@ import { Box, Text } from 'ink'; import { RadioButtonSelect } from './shared/RadioButtonSelect.js'; import { theme } from '../semantic-colors.js'; import { AuthType } from '@google/gemini-cli-core'; +import { isUltraTier } from '../../utils/tierUtils.js'; interface ProQuotaDialogProps { failedModel: string; @@ -17,6 +18,7 @@ interface ProQuotaDialogProps { isTerminalQuotaError: boolean; isModelNotFoundError?: boolean; authType?: AuthType; + tierName?: string; onChoice: ( choice: 'retry_later' | 'retry_once' | 'retry_always' | 'upgrade', ) => void; @@ -29,6 +31,7 @@ export function ProQuotaDialog({ isTerminalQuotaError, isModelNotFoundError, authType, + tierName, onChoice, }: ProQuotaDialogProps): React.JSX.Element { let items; @@ -47,6 +50,8 @@ export function ProQuotaDialog({ }, ]; } else if (isModelNotFoundError || isTerminalQuotaError) { + const isUltra = isUltraTier(tierName); + // free users and out of quota users on G1 pro and Cloud Console gets an option to upgrade items = [ { @@ -54,7 +59,7 @@ export function ProQuotaDialog({ value: 'retry_always' as const, key: 'retry_always', }, - ...(authType === AuthType.LOGIN_WITH_GOOGLE + ...(authType === AuthType.LOGIN_WITH_GOOGLE && !isUltra ? [ { label: 'Upgrade for higher limits', diff --git a/packages/cli/src/ui/components/UserIdentity.test.tsx b/packages/cli/src/ui/components/UserIdentity.test.tsx index 2aade5675b..8caa21b808 100644 --- a/packages/cli/src/ui/components/UserIdentity.test.tsx +++ b/packages/cli/src/ui/components/UserIdentity.test.tsx @@ -182,4 +182,23 @@ describe('', () => { expect(output).toContain('/upgrade'); unmount(); }); + + it('should not render /upgrade indicator for ultra tiers', async () => { + const mockConfig = makeFakeConfig(); + vi.spyOn(mockConfig, 'getContentGeneratorConfig').mockReturnValue({ + authType: AuthType.LOGIN_WITH_GOOGLE, + model: 'gemini-pro', + } as unknown as ContentGeneratorConfig); + vi.spyOn(mockConfig, 'getUserTierName').mockReturnValue('Advanced Ultra'); + + const { lastFrame, waitUntilReady, unmount } = renderWithProviders( + , + ); + await waitUntilReady(); + + const output = lastFrame(); + expect(output).toContain('Plan: Advanced Ultra'); + expect(output).not.toContain('/upgrade'); + unmount(); + }); }); diff --git a/packages/cli/src/ui/components/UserIdentity.tsx b/packages/cli/src/ui/components/UserIdentity.tsx index fa2f5c5afa..5ce4452aa4 100644 --- a/packages/cli/src/ui/components/UserIdentity.tsx +++ b/packages/cli/src/ui/components/UserIdentity.tsx @@ -13,6 +13,7 @@ import { UserAccountManager, AuthType, } from '@google/gemini-cli-core'; +import { isUltraTier } from '../../utils/tierUtils.js'; interface UserIdentityProps { config: Config; @@ -33,6 +34,8 @@ export const UserIdentity: React.FC = ({ config }) => { [config, authType], ); + const isUltra = useMemo(() => isUltraTier(tierName), [tierName]); + if (!authType) { return null; } @@ -60,7 +63,7 @@ export const UserIdentity: React.FC = ({ config }) => { Plan: {tierName} - /upgrade + {!isUltra && /upgrade} )} diff --git a/packages/cli/src/utils/tierUtils.test.ts b/packages/cli/src/utils/tierUtils.test.ts new file mode 100644 index 0000000000..05cdaa22bd --- /dev/null +++ b/packages/cli/src/utils/tierUtils.test.ts @@ -0,0 +1,28 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, expect, it } from 'vitest'; +import { isUltraTier } from './tierUtils.js'; + +describe('tierUtils', () => { + describe('isUltraTier', () => { + it('should return true if tier name contains "ultra" (case-insensitive)', () => { + expect(isUltraTier('Advanced Ultra')).toBe(true); + expect(isUltraTier('gemini ultra')).toBe(true); + expect(isUltraTier('ULTRA')).toBe(true); + }); + + it('should return false if tier name does not contain "ultra"', () => { + expect(isUltraTier('Free')).toBe(false); + expect(isUltraTier('Pro')).toBe(false); + expect(isUltraTier('Standard')).toBe(false); + }); + + it('should return false if tier name is undefined', () => { + expect(isUltraTier(undefined)).toBe(false); + }); + }); +}); diff --git a/packages/cli/src/utils/tierUtils.ts b/packages/cli/src/utils/tierUtils.ts new file mode 100644 index 0000000000..7722a9a411 --- /dev/null +++ b/packages/cli/src/utils/tierUtils.ts @@ -0,0 +1,15 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Checks if the given tier name corresponds to an "Ultra" tier. + * + * @param tierName The name of the user's tier. + * @returns True if the tier is an "Ultra" tier, false otherwise. + */ +export function isUltraTier(tierName?: string): boolean { + return !!tierName?.toLowerCase().includes('ultra'); +}