diff --git a/packages/cli/src/ui/commands/upgradeCommand.test.ts b/packages/cli/src/ui/commands/upgradeCommand.test.ts index 224123612e..d511f69c3a 100644 --- a/packages/cli/src/ui/commands/upgradeCommand.test.ts +++ b/packages/cli/src/ui/commands/upgradeCommand.test.ts @@ -11,6 +11,7 @@ import { createMockCommandContext } from '../../test-utils/mockCommandContext.js import { AuthType, openBrowserSecurely, + shouldLaunchBrowser, UPGRADE_URL_PAGE, } from '@google/gemini-cli-core'; @@ -20,6 +21,7 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => { return { ...actual, openBrowserSecurely: vi.fn(), + shouldLaunchBrowser: vi.fn().mockReturnValue(true), UPGRADE_URL_PAGE: 'https://goo.gle/set-up-gemini-code-assist', }; }); @@ -96,4 +98,21 @@ describe('upgradeCommand', () => { content: 'Failed to open upgrade page: Failed to open', }); }); + + it('should return URL message when shouldLaunchBrowser returns false', async () => { + vi.mocked(shouldLaunchBrowser).mockReturnValue(false); + + 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: `Please open this URL in a browser: ${UPGRADE_URL_PAGE}`, + }); + expect(openBrowserSecurely).not.toHaveBeenCalled(); + }); }); diff --git a/packages/cli/src/ui/commands/upgradeCommand.ts b/packages/cli/src/ui/commands/upgradeCommand.ts index 532ff3b481..e863d8ee73 100644 --- a/packages/cli/src/ui/commands/upgradeCommand.ts +++ b/packages/cli/src/ui/commands/upgradeCommand.ts @@ -7,6 +7,7 @@ import { AuthType, openBrowserSecurely, + shouldLaunchBrowser, UPGRADE_URL_PAGE, } from '@google/gemini-cli-core'; import type { SlashCommand } from './types.js'; @@ -35,6 +36,14 @@ export const upgradeCommand: SlashCommand = { }; } + if (!shouldLaunchBrowser()) { + return { + type: 'message', + messageType: 'info', + content: `Please open this URL in a browser: ${UPGRADE_URL_PAGE}`, + }; + } + try { await openBrowserSecurely(UPGRADE_URL_PAGE); } catch (error) { diff --git a/packages/cli/src/ui/components/__snapshots__/ConfigInitDisplay.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/ConfigInitDisplay.test.tsx.snap index 8d03baaa49..1b14fadf55 100644 --- a/packages/cli/src/ui/components/__snapshots__/ConfigInitDisplay.test.tsx.snap +++ b/packages/cli/src/ui/components/__snapshots__/ConfigInitDisplay.test.tsx.snap @@ -18,6 +18,12 @@ Spinner Connecting to MCP servers... (0/5) - Waiting for: s1, s2, s3, +2 more " `; +exports[`ConfigInitDisplay > truncates list of waiting servers if too many 2`] = ` +" +Spinner Connecting to MCP servers... (0/5) - Waiting for: s1, s2, s3, +2 more +" +`; + exports[`ConfigInitDisplay > updates message on McpClientUpdate event 1`] = ` " Spinner Connecting to MCP servers... (1/2) - Waiting for: server2 diff --git a/packages/cli/src/ui/hooks/creditsFlowHandler.test.ts b/packages/cli/src/ui/hooks/creditsFlowHandler.test.ts index bd3a3aa719..37a6294010 100644 --- a/packages/cli/src/ui/hooks/creditsFlowHandler.test.ts +++ b/packages/cli/src/ui/hooks/creditsFlowHandler.test.ts @@ -15,6 +15,7 @@ import { shouldAutoUseCredits, shouldShowOverageMenu, shouldShowEmptyWalletMenu, + shouldLaunchBrowser, logBillingEvent, G1_CREDIT_TYPE, UserTierId, @@ -32,6 +33,7 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => { shouldShowEmptyWalletMenu: vi.fn(), logBillingEvent: vi.fn(), openBrowserSecurely: vi.fn(), + shouldLaunchBrowser: vi.fn().mockReturnValue(true), }; }); @@ -237,4 +239,49 @@ describe('handleCreditsFlow', () => { expect(isDialogPending.current).toBe(false); expect(mockSetEmptyWalletRequest).toHaveBeenCalledWith(null); }); + + describe('headless mode (shouldLaunchBrowser=false)', () => { + beforeEach(() => { + vi.mocked(shouldLaunchBrowser).mockReturnValue(false); + }); + + it('should show manage URL in history when manage selected in headless mode', async () => { + vi.mocked(shouldShowOverageMenu).mockReturnValue(true); + + const flowPromise = handleCreditsFlow(makeArgs()); + const request = mockSetOverageMenuRequest.mock.calls[0][0]; + request.resolve('manage'); + const result = await flowPromise; + + expect(result).toBe('stop'); + expect(mockHistoryManager.addItem).toHaveBeenCalledWith( + expect.objectContaining({ + type: MessageType.INFO, + text: expect.stringContaining('Please open this URL in a browser:'), + }), + expect.any(Number), + ); + }); + + it('should show credits URL in history when get_credits selected in headless mode', async () => { + vi.mocked(shouldShowEmptyWalletMenu).mockReturnValue(true); + + const flowPromise = handleCreditsFlow(makeArgs()); + const request = mockSetEmptyWalletRequest.mock.calls[0][0]; + + // Trigger onGetCredits callback and wait for it + await request.onGetCredits(); + + expect(mockHistoryManager.addItem).toHaveBeenCalledWith( + expect.objectContaining({ + type: MessageType.INFO, + text: expect.stringContaining('Please open this URL in a browser:'), + }), + expect.any(Number), + ); + + request.resolve('get_credits'); + await flowPromise; + }); + }); }); diff --git a/packages/cli/src/ui/hooks/creditsFlowHandler.ts b/packages/cli/src/ui/hooks/creditsFlowHandler.ts index 91f0997873..b743e1866c 100644 --- a/packages/cli/src/ui/hooks/creditsFlowHandler.ts +++ b/packages/cli/src/ui/hooks/creditsFlowHandler.ts @@ -14,6 +14,7 @@ import { shouldShowOverageMenu, shouldShowEmptyWalletMenu, openBrowserSecurely, + shouldLaunchBrowser, logBillingEvent, OverageMenuShownEvent, OverageOptionSelectedEvent, @@ -159,10 +160,23 @@ async function handleOverageMenu( case 'use_fallback': return 'retry_always'; - case 'manage': + case 'manage': { logCreditPurchaseClick(config, 'manage', usageLimitReachedModel); - await openG1Url('activity', G1_UTM_CAMPAIGNS.MANAGE_ACTIVITY); + const manageUrl = await openG1Url( + 'activity', + G1_UTM_CAMPAIGNS.MANAGE_ACTIVITY, + ); + if (manageUrl) { + args.historyManager.addItem( + { + type: MessageType.INFO, + text: `Please open this URL in a browser: ${manageUrl}`, + }, + Date.now(), + ); + } return 'stop'; + } case 'stop': default: @@ -205,13 +219,25 @@ async function handleEmptyWalletMenu( failedModel: usageLimitReachedModel, fallbackModel, resetTime, - onGetCredits: () => { + onGetCredits: async () => { logCreditPurchaseClick( config, 'empty_wallet_menu', usageLimitReachedModel, ); - void openG1Url('credits', G1_UTM_CAMPAIGNS.EMPTY_WALLET_ADD_CREDITS); + const creditsUrl = await openG1Url( + 'credits', + G1_UTM_CAMPAIGNS.EMPTY_WALLET_ADD_CREDITS, + ); + if (creditsUrl) { + args.historyManager.addItem( + { + type: MessageType.INFO, + text: `Please open this URL in a browser: ${creditsUrl}`, + }, + Date.now(), + ); + } }, resolve, }); @@ -272,11 +298,16 @@ function logCreditPurchaseClick( async function openG1Url( path: 'activity' | 'credits', campaign: string, -): Promise { +): Promise { try { const userEmail = new UserAccountManager().getCachedGoogleAccount() ?? ''; - await openBrowserSecurely(buildG1Url(path, userEmail, campaign)); + const url = buildG1Url(path, userEmail, campaign); + if (!shouldLaunchBrowser()) { + return url; + } + await openBrowserSecurely(url); } catch { // Ignore browser open errors } + return undefined; } diff --git a/packages/core/src/fallback/handler.test.ts b/packages/core/src/fallback/handler.test.ts index fbb925130c..c5b9acfeb6 100644 --- a/packages/core/src/fallback/handler.test.ts +++ b/packages/core/src/fallback/handler.test.ts @@ -44,6 +44,7 @@ vi.mock('../telemetry/index.js', () => ({ })); vi.mock('../utils/secure-browser-launcher.js', () => ({ openBrowserSecurely: vi.fn(), + shouldLaunchBrowser: vi.fn().mockReturnValue(true), })); // Mock debugLogger to prevent console pollution and allow spying diff --git a/packages/core/src/fallback/handler.ts b/packages/core/src/fallback/handler.ts index 1946e3a635..6d5d0416aa 100644 --- a/packages/core/src/fallback/handler.ts +++ b/packages/core/src/fallback/handler.ts @@ -6,7 +6,10 @@ import type { Config } from '../config/config.js'; import { AuthType } from '../core/contentGenerator.js'; -import { openBrowserSecurely } from '../utils/secure-browser-launcher.js'; +import { + openBrowserSecurely, + shouldLaunchBrowser, +} from '../utils/secure-browser-launcher.js'; import { debugLogger } from '../utils/debugLogger.js'; import { getErrorMessage } from '../utils/errors.js'; import type { FallbackIntent, FallbackRecommendation } from './types.js'; @@ -112,6 +115,12 @@ export async function handleFallback( } async function handleUpgrade() { + if (!shouldLaunchBrowser()) { + debugLogger.log( + `Cannot open browser in this environment. Please visit: ${UPGRADE_URL_PAGE}`, + ); + return; + } try { await openBrowserSecurely(UPGRADE_URL_PAGE); } catch (error) {