fix(cli): do not override GOOGLE_CLOUD_PROJECT in Cloud Shell when using Vertex AI (#24455)

Co-authored-by: David Pierce <davidapierce@google.com>
This commit is contained in:
Jack Wotherspoon
2026-04-30 13:52:18 -04:00
committed by GitHub
parent 0f1077076e
commit c94edcd862
14 changed files with 257 additions and 49 deletions
+16 -15
View File
@@ -173,7 +173,6 @@ import {
QUEUE_ERROR_DISPLAY_DURATION_MS,
EXPAND_HINT_DURATION_MS,
} from './constants.js';
import { LoginWithGoogleRestartDialog } from './auth/LoginWithGoogleRestartDialog.js';
import { NewAgentsChoice } from './components/NewAgentsNotification.js';
import { isSlashCommand } from './utils/commandUtils.js';
import { parseSlashCommand } from '../utils/commands.js';
@@ -756,7 +755,7 @@ export const AppContainer = (props: AppContainerProps) => {
useEffect(() => {
if (authState === AuthState.Authenticated && authContext.requiresRestart) {
setAuthState(AuthState.AwaitingGoogleLoginRestart);
setAuthState(AuthState.AwaitingLoginRestart);
setAuthContext({});
}
}, [authState, authContext, setAuthState]);
@@ -2185,8 +2184,13 @@ Logging in with Google... Restarting Gemini CLI to continue.
const nightly = props.version.includes('nightly');
const isAwaitingLoginRestart = authState === AuthState.AwaitingLoginRestart;
const loginRestartMessage =
settings.merged.security.auth.selectedType === AuthType.USE_VERTEX_AI
? 'Authenticating to Vertex AI in Cloud Shell requires a restart to apply project settings.'
: undefined;
const dialogsVisible =
shouldShowIdePrompt ||
shouldShowIdePrompt ||
isFolderTrustDialogOpen ||
isPolicyUpdateDialogOpen ||
@@ -2214,6 +2218,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
!!emptyWalletRequest ||
isSessionBrowserOpen ||
authState === AuthState.AwaitingApiKeyInput ||
isAwaitingLoginRestart ||
!!newAgents;
const hasPendingToolConfirmation = useMemo(
@@ -2447,6 +2452,8 @@ Logging in with Google... Restarting Gemini CLI to continue.
accountSuspensionInfo,
isAuthDialogOpen,
isAwaitingApiKeyInput: authState === AuthState.AwaitingApiKeyInput,
isAwaitingLoginRestart,
loginRestartMessage,
apiKeyDefaultValue,
editorError,
isEditorDialogOpen,
@@ -2652,6 +2659,8 @@ Logging in with Google... Restarting Gemini CLI to continue.
customDialog,
apiKeyDefaultValue,
authState,
isAwaitingLoginRestart,
loginRestartMessage,
transientMessage,
bannerData,
bannerVisible,
@@ -2726,6 +2735,10 @@ Logging in with Google... Restarting Gemini CLI to continue.
setActiveBackgroundTaskPid,
setIsBackgroundTaskListOpen,
setAuthContext,
dismissLoginRestart: () => {
setAuthContext({});
setAuthState(AuthState.Updating);
},
onHintInput: () => {},
onHintBackspace: () => {},
onHintClear: () => {},
@@ -2832,18 +2845,6 @@ Logging in with Google... Restarting Gemini CLI to continue.
],
);
if (authState === AuthState.AwaitingGoogleLoginRestart) {
return (
<LoginWithGoogleRestartDialog
onDismiss={() => {
setAuthContext({});
setAuthState(AuthState.Updating);
}}
config={config}
/>
);
}
return (
<UIStateContext.Provider value={uiState}>
<QuotaContext.Provider value={quotaState}>
@@ -243,6 +243,32 @@ describe('AuthDialog', () => {
unmount();
});
it('sets auth context with requiresRestart: true for USE_VERTEX_AI in Cloud Shell', async () => {
vi.stubEnv('CLOUD_SHELL', 'true');
mockedValidateAuthMethod.mockReturnValue(null);
const { unmount } = await renderWithProviders(<AuthDialog {...props} />);
const { onSelect: handleAuthSelect } =
mockedRadioButtonSelect.mock.calls[0][0];
await handleAuthSelect(AuthType.USE_VERTEX_AI);
expect(props.setAuthContext).toHaveBeenCalledWith({
requiresRestart: true,
});
unmount();
});
it('sets auth context with empty object for USE_VERTEX_AI outside Cloud Shell', async () => {
vi.stubEnv('CLOUD_SHELL', '');
mockedValidateAuthMethod.mockReturnValue(null);
const { unmount } = await renderWithProviders(<AuthDialog {...props} />);
const { onSelect: handleAuthSelect } =
mockedRadioButtonSelect.mock.calls[0][0];
await handleAuthSelect(AuthType.USE_VERTEX_AI);
expect(props.setAuthContext).toHaveBeenCalledWith({});
unmount();
});
it('sets auth context with empty object for other auth types', async () => {
mockedValidateAuthMethod.mockReturnValue(null);
const { unmount } = await renderWithProviders(<AuthDialog {...props} />);
+6 -1
View File
@@ -119,7 +119,12 @@ export function AuthDialog({
return;
}
if (authType) {
if (authType === AuthType.LOGIN_WITH_GOOGLE) {
const needsRestart =
authType === AuthType.LOGIN_WITH_GOOGLE ||
(authType === AuthType.USE_VERTEX_AI &&
process.env['CLOUD_SHELL'] === 'true');
if (needsRestart) {
setAuthContext({ requiresRestart: true });
} else {
setAuthContext({});
@@ -1,12 +1,12 @@
/**
* @license
* Copyright 2025 Google LLC
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { render } from '../../test-utils/render.js';
import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest';
import { LoginWithGoogleRestartDialog } from './LoginWithGoogleRestartDialog.js';
import { LoginRestartDialog } from './LoginRestartDialog.js';
import { useKeypress } from '../hooks/useKeypress.js';
import { runExitCleanup } from '../../utils/cleanup.js';
import {
@@ -27,7 +27,7 @@ vi.mock('../../utils/cleanup.js', () => ({
const mockedUseKeypress = useKeypress as Mock;
const mockedRunExitCleanup = runExitCleanup as Mock;
describe('LoginWithGoogleRestartDialog', () => {
describe('LoginRestartDialog', () => {
const onDismiss = vi.fn();
const exitSpy = vi
.spyOn(process, 'exit')
@@ -44,11 +44,20 @@ describe('LoginWithGoogleRestartDialog', () => {
_resetRelaunchStateForTesting();
});
it('renders correctly', async () => {
it('renders correctly with default message', async () => {
const { lastFrame, unmount } = await render(
<LoginWithGoogleRestartDialog
<LoginRestartDialog onDismiss={onDismiss} config={mockConfig} />,
);
expect(lastFrame()).toMatchSnapshot();
unmount();
});
it('renders correctly with custom message', async () => {
const { lastFrame, unmount } = await render(
<LoginRestartDialog
onDismiss={onDismiss}
config={mockConfig}
message="Authenticating to Vertex AI in Cloud Shell requires a restart to apply project settings."
/>,
);
expect(lastFrame()).toMatchSnapshot();
@@ -57,10 +66,7 @@ describe('LoginWithGoogleRestartDialog', () => {
it('calls onDismiss when escape is pressed', async () => {
const { unmount } = await render(
<LoginWithGoogleRestartDialog
onDismiss={onDismiss}
config={mockConfig}
/>,
<LoginRestartDialog onDismiss={onDismiss} config={mockConfig} />,
);
const keypressHandler = mockedUseKeypress.mock.calls[0][0];
@@ -82,10 +88,7 @@ describe('LoginWithGoogleRestartDialog', () => {
vi.useFakeTimers();
const { unmount } = await render(
<LoginWithGoogleRestartDialog
onDismiss={onDismiss}
config={mockConfig}
/>,
<LoginRestartDialog onDismiss={onDismiss} config={mockConfig} />,
);
const keypressHandler = mockedUseKeypress.mock.calls[0][0];
@@ -1,6 +1,6 @@
/**
* @license
* Copyright 2025 Google LLC
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
@@ -10,15 +10,17 @@ import { theme } from '../semantic-colors.js';
import { useKeypress } from '../hooks/useKeypress.js';
import { relaunchApp } from '../../utils/processUtils.js';
interface LoginWithGoogleRestartDialogProps {
interface LoginRestartDialogProps {
onDismiss: () => void;
config: Config;
message?: string;
}
export const LoginWithGoogleRestartDialog = ({
export const LoginRestartDialog = ({
onDismiss,
config,
}: LoginWithGoogleRestartDialogProps) => {
message,
}: LoginRestartDialogProps) => {
useKeypress(
(key) => {
if (key.name === 'escape') {
@@ -44,14 +46,20 @@ export const LoginWithGoogleRestartDialog = ({
{ isActive: true },
);
const message =
const displayMessage =
message ??
"You've successfully signed in with Google. Gemini CLI needs to be restarted.";
return (
<Box borderStyle="round" borderColor={theme.status.warning} paddingX={1}>
<Box
borderStyle="round"
borderColor={theme.status.warning}
paddingX={1}
flexDirection="column"
>
<Text color={theme.status.warning}>{displayMessage}</Text>
<Text color={theme.status.warning}>
{message} Press R to restart, or Esc to choose a different
authentication method.
Press R to restart, or Esc to choose a different authentication method.
</Text>
</Box>
);
@@ -0,0 +1,17 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`LoginRestartDialog > renders correctly with custom message 1`] = `
"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
│ Authenticating to Vertex AI in Cloud Shell requires a restart to apply project settings. │
│ Press R to restart, or Esc to choose a different authentication method. │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
"
`;
exports[`LoginRestartDialog > renders correctly with default message 1`] = `
"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
│ You've successfully signed in with Google. Gemini CLI needs to be restarted. │
│ Press R to restart, or Esc to choose a different authentication method. │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
"
`;
@@ -1,9 +0,0 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`LoginWithGoogleRestartDialog > renders correctly 1`] = `
"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
│ You've successfully signed in with Google. Gemini CLI needs to be restarted. Press R to restart, │
│ or Esc to choose a different authentication method. │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
"
`;
@@ -39,6 +39,7 @@ import { IdeTrustChangeDialog } from './IdeTrustChangeDialog.js';
import { NewAgentsNotification } from './NewAgentsNotification.js';
import { AgentConfigDialog } from './AgentConfigDialog.js';
import { PolicyUpdateDialog } from './PolicyUpdateDialog.js';
import { LoginRestartDialog } from '../auth/LoginRestartDialog.js';
interface DialogManagerProps {
addItem: UseHistoryManagerReturn['addItem'];
@@ -306,6 +307,17 @@ export const DialogManager = ({
);
}
if (uiState.isAwaitingLoginRestart) {
return (
<Box flexDirection="column">
<LoginRestartDialog
onDismiss={uiActions.dismissLoginRestart}
config={config}
message={uiState.loginRestartMessage}
/>
</Box>
);
}
if (uiState.isAuthDialogOpen) {
return (
<Box flexDirection="column">
@@ -87,6 +87,7 @@ export interface UIActions {
setActiveBackgroundTaskPid: (pid: number) => void;
setIsBackgroundTaskListOpen: (isOpen: boolean) => void;
setAuthContext: (context: { requiresRestart?: boolean }) => void;
dismissLoginRestart: () => void;
onHintInput: (char: string) => void;
onHintBackspace: () => void;
onHintClear: () => void;
@@ -101,6 +101,8 @@ export interface UIState {
accountSuspensionInfo: AccountSuspensionInfo | null;
isAuthDialogOpen: boolean;
isAwaitingApiKeyInput: boolean;
isAwaitingLoginRestart: boolean;
loginRestartMessage?: string;
apiKeyDefaultValue?: string;
editorError: string | null;
isEditorDialogOpen: boolean;
+2 -2
View File
@@ -42,8 +42,8 @@ export enum AuthState {
AwaitingApiKeyInput = 'awaiting_api_key_input',
// Successfully authenticated
Authenticated = 'authenticated',
// Waiting for the user to restart after a Google login
AwaitingGoogleLoginRestart = 'awaiting_google_login_restart',
// Waiting for the user to restart after a login
AwaitingLoginRestart = 'awaiting_login_restart',
}
// Only defining the state enum needed by the UI