mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-10 22:21:22 -07:00
fix(ui): handle headless execution in credits and upgrade dialogs (#21850)
This commit is contained in:
@@ -11,6 +11,7 @@ import { createMockCommandContext } from '../../test-utils/mockCommandContext.js
|
|||||||
import {
|
import {
|
||||||
AuthType,
|
AuthType,
|
||||||
openBrowserSecurely,
|
openBrowserSecurely,
|
||||||
|
shouldLaunchBrowser,
|
||||||
UPGRADE_URL_PAGE,
|
UPGRADE_URL_PAGE,
|
||||||
} from '@google/gemini-cli-core';
|
} from '@google/gemini-cli-core';
|
||||||
|
|
||||||
@@ -20,6 +21,7 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
|||||||
return {
|
return {
|
||||||
...actual,
|
...actual,
|
||||||
openBrowserSecurely: vi.fn(),
|
openBrowserSecurely: vi.fn(),
|
||||||
|
shouldLaunchBrowser: vi.fn().mockReturnValue(true),
|
||||||
UPGRADE_URL_PAGE: 'https://goo.gle/set-up-gemini-code-assist',
|
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',
|
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();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
import {
|
import {
|
||||||
AuthType,
|
AuthType,
|
||||||
openBrowserSecurely,
|
openBrowserSecurely,
|
||||||
|
shouldLaunchBrowser,
|
||||||
UPGRADE_URL_PAGE,
|
UPGRADE_URL_PAGE,
|
||||||
} from '@google/gemini-cli-core';
|
} from '@google/gemini-cli-core';
|
||||||
import type { SlashCommand } from './types.js';
|
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 {
|
try {
|
||||||
await openBrowserSecurely(UPGRADE_URL_PAGE);
|
await openBrowserSecurely(UPGRADE_URL_PAGE);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -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`] = `
|
exports[`ConfigInitDisplay > updates message on McpClientUpdate event 1`] = `
|
||||||
"
|
"
|
||||||
Spinner Connecting to MCP servers... (1/2) - Waiting for: server2
|
Spinner Connecting to MCP servers... (1/2) - Waiting for: server2
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
shouldAutoUseCredits,
|
shouldAutoUseCredits,
|
||||||
shouldShowOverageMenu,
|
shouldShowOverageMenu,
|
||||||
shouldShowEmptyWalletMenu,
|
shouldShowEmptyWalletMenu,
|
||||||
|
shouldLaunchBrowser,
|
||||||
logBillingEvent,
|
logBillingEvent,
|
||||||
G1_CREDIT_TYPE,
|
G1_CREDIT_TYPE,
|
||||||
UserTierId,
|
UserTierId,
|
||||||
@@ -32,6 +33,7 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
|||||||
shouldShowEmptyWalletMenu: vi.fn(),
|
shouldShowEmptyWalletMenu: vi.fn(),
|
||||||
logBillingEvent: vi.fn(),
|
logBillingEvent: vi.fn(),
|
||||||
openBrowserSecurely: vi.fn(),
|
openBrowserSecurely: vi.fn(),
|
||||||
|
shouldLaunchBrowser: vi.fn().mockReturnValue(true),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -237,4 +239,49 @@ describe('handleCreditsFlow', () => {
|
|||||||
expect(isDialogPending.current).toBe(false);
|
expect(isDialogPending.current).toBe(false);
|
||||||
expect(mockSetEmptyWalletRequest).toHaveBeenCalledWith(null);
|
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;
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
shouldShowOverageMenu,
|
shouldShowOverageMenu,
|
||||||
shouldShowEmptyWalletMenu,
|
shouldShowEmptyWalletMenu,
|
||||||
openBrowserSecurely,
|
openBrowserSecurely,
|
||||||
|
shouldLaunchBrowser,
|
||||||
logBillingEvent,
|
logBillingEvent,
|
||||||
OverageMenuShownEvent,
|
OverageMenuShownEvent,
|
||||||
OverageOptionSelectedEvent,
|
OverageOptionSelectedEvent,
|
||||||
@@ -159,10 +160,23 @@ async function handleOverageMenu(
|
|||||||
case 'use_fallback':
|
case 'use_fallback':
|
||||||
return 'retry_always';
|
return 'retry_always';
|
||||||
|
|
||||||
case 'manage':
|
case 'manage': {
|
||||||
logCreditPurchaseClick(config, 'manage', usageLimitReachedModel);
|
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';
|
return 'stop';
|
||||||
|
}
|
||||||
|
|
||||||
case 'stop':
|
case 'stop':
|
||||||
default:
|
default:
|
||||||
@@ -205,13 +219,25 @@ async function handleEmptyWalletMenu(
|
|||||||
failedModel: usageLimitReachedModel,
|
failedModel: usageLimitReachedModel,
|
||||||
fallbackModel,
|
fallbackModel,
|
||||||
resetTime,
|
resetTime,
|
||||||
onGetCredits: () => {
|
onGetCredits: async () => {
|
||||||
logCreditPurchaseClick(
|
logCreditPurchaseClick(
|
||||||
config,
|
config,
|
||||||
'empty_wallet_menu',
|
'empty_wallet_menu',
|
||||||
usageLimitReachedModel,
|
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,
|
resolve,
|
||||||
});
|
});
|
||||||
@@ -272,11 +298,16 @@ function logCreditPurchaseClick(
|
|||||||
async function openG1Url(
|
async function openG1Url(
|
||||||
path: 'activity' | 'credits',
|
path: 'activity' | 'credits',
|
||||||
campaign: string,
|
campaign: string,
|
||||||
): Promise<void> {
|
): Promise<string | undefined> {
|
||||||
try {
|
try {
|
||||||
const userEmail = new UserAccountManager().getCachedGoogleAccount() ?? '';
|
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 {
|
} catch {
|
||||||
// Ignore browser open errors
|
// Ignore browser open errors
|
||||||
}
|
}
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ vi.mock('../telemetry/index.js', () => ({
|
|||||||
}));
|
}));
|
||||||
vi.mock('../utils/secure-browser-launcher.js', () => ({
|
vi.mock('../utils/secure-browser-launcher.js', () => ({
|
||||||
openBrowserSecurely: vi.fn(),
|
openBrowserSecurely: vi.fn(),
|
||||||
|
shouldLaunchBrowser: vi.fn().mockReturnValue(true),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Mock debugLogger to prevent console pollution and allow spying
|
// Mock debugLogger to prevent console pollution and allow spying
|
||||||
|
|||||||
@@ -6,7 +6,10 @@
|
|||||||
|
|
||||||
import type { Config } from '../config/config.js';
|
import type { Config } from '../config/config.js';
|
||||||
import { AuthType } from '../core/contentGenerator.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 { debugLogger } from '../utils/debugLogger.js';
|
||||||
import { getErrorMessage } from '../utils/errors.js';
|
import { getErrorMessage } from '../utils/errors.js';
|
||||||
import type { FallbackIntent, FallbackRecommendation } from './types.js';
|
import type { FallbackIntent, FallbackRecommendation } from './types.js';
|
||||||
@@ -112,6 +115,12 @@ export async function handleFallback(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleUpgrade() {
|
async function handleUpgrade() {
|
||||||
|
if (!shouldLaunchBrowser()) {
|
||||||
|
debugLogger.log(
|
||||||
|
`Cannot open browser in this environment. Please visit: ${UPGRADE_URL_PAGE}`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
await openBrowserSecurely(UPGRADE_URL_PAGE);
|
await openBrowserSecurely(UPGRADE_URL_PAGE);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
Reference in New Issue
Block a user