Skip to content

Commit 47e4f6b

Browse files
authored
fix(ui): handle headless execution in credits and upgrade dialogs (#21850)
1 parent 94ab449 commit 47e4f6b

7 files changed

Lines changed: 129 additions & 7 deletions

File tree

packages/cli/src/ui/commands/upgradeCommand.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { createMockCommandContext } from '../../test-utils/mockCommandContext.js
1111
import {
1212
AuthType,
1313
openBrowserSecurely,
14+
shouldLaunchBrowser,
1415
UPGRADE_URL_PAGE,
1516
} from '@google/gemini-cli-core';
1617

@@ -20,6 +21,7 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
2021
return {
2122
...actual,
2223
openBrowserSecurely: vi.fn(),
24+
shouldLaunchBrowser: vi.fn().mockReturnValue(true),
2325
UPGRADE_URL_PAGE: 'https://goo.gle/set-up-gemini-code-assist',
2426
};
2527
});
@@ -96,4 +98,21 @@ describe('upgradeCommand', () => {
9698
content: 'Failed to open upgrade page: Failed to open',
9799
});
98100
});
101+
102+
it('should return URL message when shouldLaunchBrowser returns false', async () => {
103+
vi.mocked(shouldLaunchBrowser).mockReturnValue(false);
104+
105+
if (!upgradeCommand.action) {
106+
throw new Error('The upgrade command must have an action.');
107+
}
108+
109+
const result = await upgradeCommand.action(mockContext, '');
110+
111+
expect(result).toEqual({
112+
type: 'message',
113+
messageType: 'info',
114+
content: `Please open this URL in a browser: ${UPGRADE_URL_PAGE}`,
115+
});
116+
expect(openBrowserSecurely).not.toHaveBeenCalled();
117+
});
99118
});

packages/cli/src/ui/commands/upgradeCommand.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import {
88
AuthType,
99
openBrowserSecurely,
10+
shouldLaunchBrowser,
1011
UPGRADE_URL_PAGE,
1112
} from '@google/gemini-cli-core';
1213
import type { SlashCommand } from './types.js';
@@ -35,6 +36,14 @@ export const upgradeCommand: SlashCommand = {
3536
};
3637
}
3738

39+
if (!shouldLaunchBrowser()) {
40+
return {
41+
type: 'message',
42+
messageType: 'info',
43+
content: `Please open this URL in a browser: ${UPGRADE_URL_PAGE}`,
44+
};
45+
}
46+
3847
try {
3948
await openBrowserSecurely(UPGRADE_URL_PAGE);
4049
} catch (error) {

packages/cli/src/ui/components/__snapshots__/ConfigInitDisplay.test.tsx.snap

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ Spinner Connecting to MCP servers... (0/5) - Waiting for: s1, s2, s3, +2 more
1818
"
1919
`;
2020

21+
exports[`ConfigInitDisplay > truncates list of waiting servers if too many 2`] = `
22+
"
23+
Spinner Connecting to MCP servers... (0/5) - Waiting for: s1, s2, s3, +2 more
24+
"
25+
`;
26+
2127
exports[`ConfigInitDisplay > updates message on McpClientUpdate event 1`] = `
2228
"
2329
Spinner Connecting to MCP servers... (1/2) - Waiting for: server2

packages/cli/src/ui/hooks/creditsFlowHandler.test.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
shouldAutoUseCredits,
1616
shouldShowOverageMenu,
1717
shouldShowEmptyWalletMenu,
18+
shouldLaunchBrowser,
1819
logBillingEvent,
1920
G1_CREDIT_TYPE,
2021
UserTierId,
@@ -32,6 +33,7 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
3233
shouldShowEmptyWalletMenu: vi.fn(),
3334
logBillingEvent: vi.fn(),
3435
openBrowserSecurely: vi.fn(),
36+
shouldLaunchBrowser: vi.fn().mockReturnValue(true),
3537
};
3638
});
3739

@@ -237,4 +239,49 @@ describe('handleCreditsFlow', () => {
237239
expect(isDialogPending.current).toBe(false);
238240
expect(mockSetEmptyWalletRequest).toHaveBeenCalledWith(null);
239241
});
242+
243+
describe('headless mode (shouldLaunchBrowser=false)', () => {
244+
beforeEach(() => {
245+
vi.mocked(shouldLaunchBrowser).mockReturnValue(false);
246+
});
247+
248+
it('should show manage URL in history when manage selected in headless mode', async () => {
249+
vi.mocked(shouldShowOverageMenu).mockReturnValue(true);
250+
251+
const flowPromise = handleCreditsFlow(makeArgs());
252+
const request = mockSetOverageMenuRequest.mock.calls[0][0];
253+
request.resolve('manage');
254+
const result = await flowPromise;
255+
256+
expect(result).toBe('stop');
257+
expect(mockHistoryManager.addItem).toHaveBeenCalledWith(
258+
expect.objectContaining({
259+
type: MessageType.INFO,
260+
text: expect.stringContaining('Please open this URL in a browser:'),
261+
}),
262+
expect.any(Number),
263+
);
264+
});
265+
266+
it('should show credits URL in history when get_credits selected in headless mode', async () => {
267+
vi.mocked(shouldShowEmptyWalletMenu).mockReturnValue(true);
268+
269+
const flowPromise = handleCreditsFlow(makeArgs());
270+
const request = mockSetEmptyWalletRequest.mock.calls[0][0];
271+
272+
// Trigger onGetCredits callback and wait for it
273+
await request.onGetCredits();
274+
275+
expect(mockHistoryManager.addItem).toHaveBeenCalledWith(
276+
expect.objectContaining({
277+
type: MessageType.INFO,
278+
text: expect.stringContaining('Please open this URL in a browser:'),
279+
}),
280+
expect.any(Number),
281+
);
282+
283+
request.resolve('get_credits');
284+
await flowPromise;
285+
});
286+
});
240287
});

packages/cli/src/ui/hooks/creditsFlowHandler.ts

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
shouldShowOverageMenu,
1515
shouldShowEmptyWalletMenu,
1616
openBrowserSecurely,
17+
shouldLaunchBrowser,
1718
logBillingEvent,
1819
OverageMenuShownEvent,
1920
OverageOptionSelectedEvent,
@@ -159,10 +160,23 @@ async function handleOverageMenu(
159160
case 'use_fallback':
160161
return 'retry_always';
161162

162-
case 'manage':
163+
case 'manage': {
163164
logCreditPurchaseClick(config, 'manage', usageLimitReachedModel);
164-
await openG1Url('activity', G1_UTM_CAMPAIGNS.MANAGE_ACTIVITY);
165+
const manageUrl = await openG1Url(
166+
'activity',
167+
G1_UTM_CAMPAIGNS.MANAGE_ACTIVITY,
168+
);
169+
if (manageUrl) {
170+
args.historyManager.addItem(
171+
{
172+
type: MessageType.INFO,
173+
text: `Please open this URL in a browser: ${manageUrl}`,
174+
},
175+
Date.now(),
176+
);
177+
}
165178
return 'stop';
179+
}
166180

167181
case 'stop':
168182
default:
@@ -205,13 +219,25 @@ async function handleEmptyWalletMenu(
205219
failedModel: usageLimitReachedModel,
206220
fallbackModel,
207221
resetTime,
208-
onGetCredits: () => {
222+
onGetCredits: async () => {
209223
logCreditPurchaseClick(
210224
config,
211225
'empty_wallet_menu',
212226
usageLimitReachedModel,
213227
);
214-
void openG1Url('credits', G1_UTM_CAMPAIGNS.EMPTY_WALLET_ADD_CREDITS);
228+
const creditsUrl = await openG1Url(
229+
'credits',
230+
G1_UTM_CAMPAIGNS.EMPTY_WALLET_ADD_CREDITS,
231+
);
232+
if (creditsUrl) {
233+
args.historyManager.addItem(
234+
{
235+
type: MessageType.INFO,
236+
text: `Please open this URL in a browser: ${creditsUrl}`,
237+
},
238+
Date.now(),
239+
);
240+
}
215241
},
216242
resolve,
217243
});
@@ -272,11 +298,16 @@ function logCreditPurchaseClick(
272298
async function openG1Url(
273299
path: 'activity' | 'credits',
274300
campaign: string,
275-
): Promise<void> {
301+
): Promise<string | undefined> {
276302
try {
277303
const userEmail = new UserAccountManager().getCachedGoogleAccount() ?? '';
278-
await openBrowserSecurely(buildG1Url(path, userEmail, campaign));
304+
const url = buildG1Url(path, userEmail, campaign);
305+
if (!shouldLaunchBrowser()) {
306+
return url;
307+
}
308+
await openBrowserSecurely(url);
279309
} catch {
280310
// Ignore browser open errors
281311
}
312+
return undefined;
282313
}

packages/core/src/fallback/handler.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ vi.mock('../telemetry/index.js', () => ({
4444
}));
4545
vi.mock('../utils/secure-browser-launcher.js', () => ({
4646
openBrowserSecurely: vi.fn(),
47+
shouldLaunchBrowser: vi.fn().mockReturnValue(true),
4748
}));
4849

4950
// Mock debugLogger to prevent console pollution and allow spying

packages/core/src/fallback/handler.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@
66

77
import type { Config } from '../config/config.js';
88
import { AuthType } from '../core/contentGenerator.js';
9-
import { openBrowserSecurely } from '../utils/secure-browser-launcher.js';
9+
import {
10+
openBrowserSecurely,
11+
shouldLaunchBrowser,
12+
} from '../utils/secure-browser-launcher.js';
1013
import { debugLogger } from '../utils/debugLogger.js';
1114
import { getErrorMessage } from '../utils/errors.js';
1215
import type { FallbackIntent, FallbackRecommendation } from './types.js';
@@ -112,6 +115,12 @@ export async function handleFallback(
112115
}
113116

114117
async function handleUpgrade() {
118+
if (!shouldLaunchBrowser()) {
119+
debugLogger.log(
120+
`Cannot open browser in this environment. Please visit: ${UPGRADE_URL_PAGE}`,
121+
);
122+
return;
123+
}
115124
try {
116125
await openBrowserSecurely(UPGRADE_URL_PAGE);
117126
} catch (error) {

0 commit comments

Comments
 (0)