Refactor Authentication Components and Hooks (#7750)

This commit is contained in:
Tommaso Sciortino
2025-09-05 15:35:41 -07:00
committed by GitHub
parent d8dbe6271f
commit 7239c5cd9a
11 changed files with 462 additions and 756 deletions
+5 -63
View File
@@ -15,7 +15,6 @@ import type {
ToolRegistry, ToolRegistry,
SandboxConfig, SandboxConfig,
GeminiClient, GeminiClient,
AuthType,
} from '@google/gemini-cli-core'; } from '@google/gemini-cli-core';
import { import {
ApprovalMode, ApprovalMode,
@@ -34,7 +33,6 @@ import type { UpdateObject } from './utils/updateCheck.js';
import { checkForUpdates } from './utils/updateCheck.js'; import { checkForUpdates } from './utils/updateCheck.js';
import { EventEmitter } from 'node:events'; import { EventEmitter } from 'node:events';
import { updateEventEmitter } from '../utils/updateEventEmitter.js'; import { updateEventEmitter } from '../utils/updateEventEmitter.js';
import * as auth from '../config/auth.js';
import * as useTerminalSize from './hooks/useTerminalSize.js'; import * as useTerminalSize from './hooks/useTerminalSize.js';
// Define a more complete mock server config based on actual Config // Define a more complete mock server config based on actual Config
@@ -224,14 +222,12 @@ vi.mock('./hooks/useGeminiStream', () => ({
})), })),
})); }));
vi.mock('./hooks/useAuthCommand', () => ({ vi.mock('./auth/useAuth.js', () => ({
useAuthCommand: vi.fn(() => ({ useAuthCommand: vi.fn(() => ({
isAuthDialogOpen: false, authState: 'authenticated',
openAuthDialog: vi.fn(), setAuthState: vi.fn(),
handleAuthSelect: vi.fn(), authError: null,
handleAuthHighlight: vi.fn(), onAuthError: vi.fn(),
isAuthenticating: false,
cancelAuthentication: vi.fn(),
})), })),
})); }));
@@ -1189,60 +1185,6 @@ describe('App UI', () => {
}); });
}); });
describe('auth validation', () => {
it('should call validateAuthMethod when useExternalAuth is false', async () => {
const validateAuthMethodSpy = vi.spyOn(auth, 'validateAuthMethod');
mockSettings = createMockSettings({
workspace: {
security: {
auth: {
selectedType: 'USE_GEMINI' as AuthType,
useExternal: false,
},
},
ui: { theme: 'Default' },
},
});
const { unmount } = renderWithProviders(
<App
config={mockConfig as unknown as ServerConfig}
settings={mockSettings}
version={mockVersion}
/>,
);
currentUnmount = unmount;
expect(validateAuthMethodSpy).toHaveBeenCalledWith('USE_GEMINI');
});
it('should NOT call validateAuthMethod when useExternalAuth is true', async () => {
const validateAuthMethodSpy = vi.spyOn(auth, 'validateAuthMethod');
mockSettings = createMockSettings({
workspace: {
security: {
auth: {
selectedType: 'USE_GEMINI' as AuthType,
useExternal: true,
},
},
ui: { theme: 'Default' },
},
});
const { unmount } = renderWithProviders(
<App
config={mockConfig as unknown as ServerConfig}
settings={mockSettings}
version={mockVersion}
/>,
);
currentUnmount = unmount;
expect(validateAuthMethodSpy).not.toHaveBeenCalled();
});
});
describe('when in a narrow terminal', () => { describe('when in a narrow terminal', () => {
it('should render with a column layout', () => { it('should render with a column layout', () => {
vi.spyOn(useTerminalSize, 'useTerminalSize').mockReturnValue({ vi.spyOn(useTerminalSize, 'useTerminalSize').mockReturnValue({
+23 -64
View File
@@ -15,6 +15,7 @@ import {
useStdout, useStdout,
} from 'ink'; } from 'ink';
import { import {
AuthState,
StreamingState, StreamingState,
type HistoryItem, type HistoryItem,
MessageType, MessageType,
@@ -25,7 +26,7 @@ import { useTerminalSize } from './hooks/useTerminalSize.js';
import { useGeminiStream } from './hooks/useGeminiStream.js'; import { useGeminiStream } from './hooks/useGeminiStream.js';
import { useLoadingIndicator } from './hooks/useLoadingIndicator.js'; import { useLoadingIndicator } from './hooks/useLoadingIndicator.js';
import { useThemeCommand } from './hooks/useThemeCommand.js'; import { useThemeCommand } from './hooks/useThemeCommand.js';
import { useAuthCommand } from './hooks/useAuthCommand.js'; import { useAuthCommand } from './auth/useAuth.js';
import { useFolderTrust } from './hooks/useFolderTrust.js'; import { useFolderTrust } from './hooks/useFolderTrust.js';
import { useIdeTrustListener } from './hooks/useIdeTrustListener.js'; import { useIdeTrustListener } from './hooks/useIdeTrustListener.js';
import { useEditorSettings } from './hooks/useEditorSettings.js'; import { useEditorSettings } from './hooks/useEditorSettings.js';
@@ -40,8 +41,6 @@ import { ShellModeIndicator } from './components/ShellModeIndicator.js';
import { InputPrompt } from './components/InputPrompt.js'; import { InputPrompt } from './components/InputPrompt.js';
import { Footer } from './components/Footer.js'; import { Footer } from './components/Footer.js';
import { ThemeDialog } from './components/ThemeDialog.js'; import { ThemeDialog } from './components/ThemeDialog.js';
import { AuthDialog } from './components/AuthDialog.js';
import { AuthInProgress } from './components/AuthInProgress.js';
import { EditorSettingsDialog } from './components/EditorSettingsDialog.js'; import { EditorSettingsDialog } from './components/EditorSettingsDialog.js';
import { FolderTrustDialog } from './components/FolderTrustDialog.js'; import { FolderTrustDialog } from './components/FolderTrustDialog.js';
import { ShellConfirmationDialog } from './components/ShellConfirmationDialog.js'; import { ShellConfirmationDialog } from './components/ShellConfirmationDialog.js';
@@ -82,7 +81,6 @@ import {
} from '@google/gemini-cli-core'; } from '@google/gemini-cli-core';
import type { IdeIntegrationNudgeResult } from './IdeIntegrationNudge.js'; import type { IdeIntegrationNudgeResult } from './IdeIntegrationNudge.js';
import { IdeIntegrationNudge } from './IdeIntegrationNudge.js'; import { IdeIntegrationNudge } from './IdeIntegrationNudge.js';
import { validateAuthMethod } from '../config/auth.js';
import { useLogger } from './hooks/useLogger.js'; import { useLogger } from './hooks/useLogger.js';
import { StreamingContext } from './contexts/StreamingContext.js'; import { StreamingContext } from './contexts/StreamingContext.js';
import { import {
@@ -116,6 +114,8 @@ import { isNarrowWidth } from './utils/isNarrowWidth.js';
import { useWorkspaceMigration } from './hooks/useWorkspaceMigration.js'; import { useWorkspaceMigration } from './hooks/useWorkspaceMigration.js';
import { WorkspaceMigrationDialog } from './components/WorkspaceMigrationDialog.js'; import { WorkspaceMigrationDialog } from './components/WorkspaceMigrationDialog.js';
import { isWorkspaceTrusted } from '../config/trustedFolders.js'; import { isWorkspaceTrusted } from '../config/trustedFolders.js';
import { AuthInProgress } from './auth/AuthInProgress.js';
import { AuthDialog } from './auth/AuthDialog.js';
const CTRL_EXIT_PROMPT_DURATION_MS = 1000; const CTRL_EXIT_PROMPT_DURATION_MS = 1000;
// Maximum number of queued messages to display in UI to prevent performance issues // Maximum number of queued messages to display in UI to prevent performance issues
@@ -217,7 +217,7 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
const [geminiMdFileCount, setGeminiMdFileCount] = useState<number>(0); const [geminiMdFileCount, setGeminiMdFileCount] = useState<number>(0);
const [debugMessage, setDebugMessage] = useState<string>(''); const [debugMessage, setDebugMessage] = useState<string>('');
const [themeError, setThemeError] = useState<string | null>(null); const [themeError, setThemeError] = useState<string | null>(null);
const [authError, setAuthError] = useState<string | null>(null);
const [editorError, setEditorError] = useState<string | null>(null); const [editorError, setEditorError] = useState<string | null>(null);
const [footerHeight, setFooterHeight] = useState<number>(0); const [footerHeight, setFooterHeight] = useState<number>(0);
const [corgiMode, setCorgiMode] = useState(false); const [corgiMode, setCorgiMode] = useState(false);
@@ -338,52 +338,18 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
{ isActive: showIdeRestartPrompt }, { isActive: showIdeRestartPrompt },
); );
const { const { authState, setAuthState, authError, onAuthError } = useAuthCommand(
isAuthDialogOpen, settings,
openAuthDialog, config,
handleAuthSelect,
isAuthenticating,
cancelAuthentication,
} = useAuthCommand(settings, setAuthError, config);
useEffect(() => {
if (
settings.merged.security?.auth?.enforcedType &&
settings.merged.security?.auth.selectedType &&
settings.merged.security?.auth.enforcedType !==
settings.merged.security?.auth.selectedType
) {
setAuthError(
`Authentication is enforced to be ${settings.merged.security?.auth.enforcedType}, but you are currently using ${settings.merged.security?.auth.selectedType}.`,
); );
openAuthDialog();
} else if (
settings.merged.security?.auth?.selectedType &&
!settings.merged.security?.auth?.useExternal
) {
const error = validateAuthMethod(
settings.merged.security.auth.selectedType,
);
if (error) {
setAuthError(error);
openAuthDialog();
}
}
}, [
settings.merged.security?.auth?.selectedType,
settings.merged.security?.auth?.enforcedType,
settings.merged.security?.auth?.useExternal,
openAuthDialog,
setAuthError,
]);
// Sync user tier from config when authentication changes // Sync user tier from config when authentication changes
useEffect(() => { useEffect(() => {
// Only sync when not currently authenticating // Only sync when not currently authenticating
if (!isAuthenticating) { if (authState === AuthState.Authenticated) {
setUserTier(config.getGeminiClient()?.getUserTier()); setUserTier(config.getGeminiClient()?.getUserTier());
} }
}, [config, isAuthenticating]); }, [config, authState]);
const { const {
isEditorDialogOpen, isEditorDialogOpen,
@@ -622,11 +588,6 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
return editorType as EditorType; return editorType as EditorType;
}, [settings, openEditorDialog]); }, [settings, openEditorDialog]);
const onAuthError = useCallback(() => {
setAuthError('reauth required');
openAuthDialog();
}, [openAuthDialog, setAuthError]);
// Core hooks and processors // Core hooks and processors
const { const {
vimEnabled: vimModeEnabled, vimEnabled: vimModeEnabled,
@@ -650,7 +611,7 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
refreshStatic, refreshStatic,
setDebugMessage, setDebugMessage,
openThemeDialog, openThemeDialog,
openAuthDialog, setAuthState,
openEditorDialog, openEditorDialog,
toggleCorgiMode, toggleCorgiMode,
setQuittingMessages, setQuittingMessages,
@@ -843,7 +804,7 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
handleSlashCommand('/ide status'); handleSlashCommand('/ide status');
} else if (keyMatchers[Command.QUIT](key)) { } else if (keyMatchers[Command.QUIT](key)) {
// When authenticating, let AuthInProgress component handle Ctrl+C. // When authenticating, let AuthInProgress component handle Ctrl+C.
if (isAuthenticating) { if (authState === AuthState.Unauthenticated) {
return; return;
} }
if (!ctrlCPressedOnce) { if (!ctrlCPressedOnce) {
@@ -879,7 +840,7 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
setCtrlDPressedOnce, setCtrlDPressedOnce,
ctrlDTimerRef, ctrlDTimerRef,
handleSlashCommand, handleSlashCommand,
isAuthenticating, authState,
cancelOngoingRequest, cancelOngoingRequest,
settings.merged.general?.debugKeystrokeLogging, settings.merged.general?.debugKeystrokeLogging,
], ],
@@ -981,8 +942,7 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
if ( if (
initialPrompt && initialPrompt &&
!initialPromptSubmitted.current && !initialPromptSubmitted.current &&
!isAuthenticating && authState === AuthState.Authenticated &&
!isAuthDialogOpen &&
!isThemeDialogOpen && !isThemeDialogOpen &&
!isEditorDialogOpen && !isEditorDialogOpen &&
!showPrivacyNotice && !showPrivacyNotice &&
@@ -994,8 +954,7 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
}, [ }, [
initialPrompt, initialPrompt,
submitQuery, submitQuery,
isAuthenticating, authState,
isAuthDialogOpen,
isThemeDialogOpen, isThemeDialogOpen,
isEditorDialogOpen, isEditorDialogOpen,
showPrivacyNotice, showPrivacyNotice,
@@ -1136,7 +1095,7 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
if (choice === 'auth') { if (choice === 'auth') {
cancelOngoingRequest?.(); cancelOngoingRequest?.();
openAuthDialog(); setAuthState(AuthState.Updating);
} else { } else {
addItem( addItem(
{ {
@@ -1209,13 +1168,11 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
onRestartRequest={() => process.exit(0)} onRestartRequest={() => process.exit(0)}
/> />
</Box> </Box>
) : isAuthenticating ? ( ) : authState === AuthState.Unauthenticated ? (
<> <>
<AuthInProgress <AuthInProgress
onTimeout={() => { onTimeout={() => {
setAuthError('Authentication timed out. Please try again.'); onAuthError('Authentication timed out. Please try again.');
cancelAuthentication();
openAuthDialog();
}} }}
/> />
{showErrorDetails && ( {showErrorDetails && (
@@ -1233,12 +1190,14 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
</OverflowProvider> </OverflowProvider>
)} )}
</> </>
) : isAuthDialogOpen ? ( ) : authState === AuthState.Updating ? (
<Box flexDirection="column"> <Box flexDirection="column">
<AuthDialog <AuthDialog
onSelect={handleAuthSelect} config={config}
settings={settings} settings={settings}
initialErrorMessage={authError} authError={authError}
onAuthError={onAuthError}
setAuthState={setAuthState}
/> />
</Box> </Box>
) : isEditorDialogOpen ? ( ) : isEditorDialogOpen ? (
@@ -0,0 +1,258 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { renderWithProviders } from '../../test-utils/render.js';
import {
describe,
it,
expect,
vi,
beforeEach,
afterEach,
type Mock,
} from 'vitest';
import { AuthDialog } from './AuthDialog.js';
import { AuthType, type Config } from '@google/gemini-cli-core';
import type { LoadedSettings } from '../../config/settings.js';
import { SettingScope } from '../../config/settings.js';
import { AuthState } from '../types.js';
import { RadioButtonSelect } from '../components/shared/RadioButtonSelect.js';
import { useKeypress } from '../hooks/useKeypress.js';
import { validateAuthMethodWithSettings } from './useAuth.js';
import { runExitCleanup } from '../../utils/cleanup.js';
import { clearCachedCredentialFile } from '@google/gemini-cli-core';
import { Text } from 'ink';
// Mocks
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
const actual =
await importOriginal<typeof import('@google/gemini-cli-core')>();
return {
...actual,
clearCachedCredentialFile: vi.fn(),
};
});
vi.mock('../../utils/cleanup.js', () => ({
runExitCleanup: vi.fn(),
}));
vi.mock('./useAuth.js', () => ({
validateAuthMethodWithSettings: vi.fn(),
}));
vi.mock('../hooks/useKeypress.js', () => ({
useKeypress: vi.fn(),
}));
vi.mock('../components/shared/RadioButtonSelect.js', () => ({
RadioButtonSelect: vi.fn(({ items, initialIndex }) => (
<>
{items.map((item: { value: string; label: string }, index: number) => (
<Text key={item.value}>
{index === initialIndex ? '(selected)' : '(not selected)'}{' '}
{item.label}
</Text>
))}
</>
)),
}));
const mockedUseKeypress = useKeypress as Mock;
const mockedRadioButtonSelect = RadioButtonSelect as Mock;
const mockedValidateAuthMethod = validateAuthMethodWithSettings as Mock;
const mockedRunExitCleanup = runExitCleanup as Mock;
const mockedClearCachedCredentialFile = clearCachedCredentialFile as Mock;
describe('AuthDialog', () => {
let props: {
config: Config;
settings: LoadedSettings;
setAuthState: (state: AuthState) => void;
authError: string | null;
onAuthError: (error: string) => void;
};
const originalEnv = { ...process.env };
beforeEach(() => {
vi.resetAllMocks();
process.env = {};
props = {
config: {
isBrowserLaunchSuppressed: vi.fn().mockReturnValue(false),
} as unknown as Config,
settings: {
merged: {
security: {
auth: {},
},
},
setValue: vi.fn(),
} as unknown as LoadedSettings,
setAuthState: vi.fn(),
authError: null,
onAuthError: vi.fn(),
};
});
afterEach(() => {
process.env = originalEnv;
});
it('shows Cloud Shell option when in Cloud Shell environment', () => {
process.env['CLOUD_SHELL'] = 'true';
renderWithProviders(<AuthDialog {...props} />);
const items = mockedRadioButtonSelect.mock.calls[0][0].items;
expect(items).toContainEqual({
label: 'Use Cloud Shell user credentials',
value: AuthType.CLOUD_SHELL,
});
});
it('filters auth types when enforcedType is set', () => {
props.settings.merged.security!.auth!.enforcedType = AuthType.USE_GEMINI;
renderWithProviders(<AuthDialog {...props} />);
const items = mockedRadioButtonSelect.mock.calls[0][0].items;
expect(items).toHaveLength(1);
expect(items[0].value).toBe(AuthType.USE_GEMINI);
});
it('sets initial index to 0 when enforcedType is set', () => {
props.settings.merged.security!.auth!.enforcedType = AuthType.USE_GEMINI;
renderWithProviders(<AuthDialog {...props} />);
const { initialIndex } = mockedRadioButtonSelect.mock.calls[0][0];
expect(initialIndex).toBe(0);
});
it('selects initial auth type from settings', () => {
props.settings.merged.security!.auth!.selectedType = AuthType.USE_VERTEX_AI;
renderWithProviders(<AuthDialog {...props} />);
const { items, initialIndex } = mockedRadioButtonSelect.mock.calls[0][0];
expect(items[initialIndex].value).toBe(AuthType.USE_VERTEX_AI);
});
it('selects initial auth type from GEMINI_DEFAULT_AUTH_TYPE env var', () => {
process.env['GEMINI_DEFAULT_AUTH_TYPE'] = AuthType.USE_GEMINI;
renderWithProviders(<AuthDialog {...props} />);
const { items, initialIndex } = mockedRadioButtonSelect.mock.calls[0][0];
expect(items[initialIndex].value).toBe(AuthType.USE_GEMINI);
});
it('selects initial auth type from GEMINI_API_KEY env var', () => {
process.env['GEMINI_API_KEY'] = 'test-key';
renderWithProviders(<AuthDialog {...props} />);
const { items, initialIndex } = mockedRadioButtonSelect.mock.calls[0][0];
expect(items[initialIndex].value).toBe(AuthType.USE_GEMINI);
});
it('defaults to Login with Google', () => {
renderWithProviders(<AuthDialog {...props} />);
const { items, initialIndex } = mockedRadioButtonSelect.mock.calls[0][0];
expect(items[initialIndex].value).toBe(AuthType.LOGIN_WITH_GOOGLE);
});
describe('handleAuthSelect', () => {
it('calls onAuthError if validation fails', () => {
mockedValidateAuthMethod.mockReturnValue('Invalid method');
renderWithProviders(<AuthDialog {...props} />);
const { onSelect: handleAuthSelect } =
mockedRadioButtonSelect.mock.calls[0][0];
handleAuthSelect(AuthType.USE_GEMINI);
expect(mockedValidateAuthMethod).toHaveBeenCalledWith(
AuthType.USE_GEMINI,
props.settings,
);
expect(props.onAuthError).toHaveBeenCalledWith('Invalid method');
expect(props.settings.setValue).not.toHaveBeenCalled();
});
it('calls onSelect if validation passes', async () => {
mockedValidateAuthMethod.mockReturnValue(null);
renderWithProviders(<AuthDialog {...props} />);
const { onSelect: handleAuthSelect } =
mockedRadioButtonSelect.mock.calls[0][0];
await handleAuthSelect(AuthType.USE_GEMINI);
expect(mockedValidateAuthMethod).toHaveBeenCalledWith(
AuthType.USE_GEMINI,
props.settings,
);
expect(props.onAuthError).not.toHaveBeenCalled();
expect(mockedClearCachedCredentialFile).toHaveBeenCalled();
expect(props.settings.setValue).toHaveBeenCalledWith(
SettingScope.User,
'security.auth.selectedType',
AuthType.USE_GEMINI,
);
expect(props.setAuthState).toHaveBeenCalledWith(
AuthState.Unauthenticated,
);
});
it('exits process for Login with Google when browser is suppressed', async () => {
const exitSpy = vi
.spyOn(process, 'exit')
.mockImplementation(() => undefined as never);
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
vi.mocked(props.config.isBrowserLaunchSuppressed).mockReturnValue(true);
mockedValidateAuthMethod.mockReturnValue(null);
renderWithProviders(<AuthDialog {...props} />);
const { onSelect: handleAuthSelect } =
mockedRadioButtonSelect.mock.calls[0][0];
await handleAuthSelect(AuthType.LOGIN_WITH_GOOGLE);
expect(mockedRunExitCleanup).toHaveBeenCalled();
expect(logSpy).toHaveBeenCalledWith(
expect.stringContaining('Please restart Gemini CLI'),
);
expect(exitSpy).toHaveBeenCalledWith(0);
exitSpy.mockRestore();
logSpy.mockRestore();
});
});
it('displays authError when provided', () => {
props.authError = 'Something went wrong';
const { lastFrame } = renderWithProviders(<AuthDialog {...props} />);
expect(lastFrame()).toContain('Something went wrong');
});
describe('useKeypress', () => {
it('does nothing on escape if authError is present', () => {
props.authError = 'Some error';
renderWithProviders(<AuthDialog {...props} />);
const keypressHandler = mockedUseKeypress.mock.calls[0][0];
keypressHandler({ name: 'escape' });
expect(props.onAuthError).not.toHaveBeenCalled();
expect(props.setAuthState).not.toHaveBeenCalled();
});
it('calls onAuthError on escape if no auth method is set', () => {
props.settings.merged.security!.auth!.selectedType = undefined;
renderWithProviders(<AuthDialog {...props} />);
const keypressHandler = mockedUseKeypress.mock.calls[0][0];
keypressHandler({ name: 'escape' });
expect(props.onAuthError).toHaveBeenCalledWith(
'You must select an auth method to proceed. Press Ctrl+C twice to exit.',
);
});
it('calls onSelect(undefined) on escape if auth method is set', () => {
props.settings.merged.security!.auth!.selectedType = AuthType.USE_GEMINI;
renderWithProviders(<AuthDialog {...props} />);
const keypressHandler = mockedUseKeypress.mock.calls[0][0];
keypressHandler({ name: 'escape' });
expect(props.setAuthState).toHaveBeenCalledWith(
AuthState.Unauthenticated,
);
expect(props.settings.setValue).not.toHaveBeenCalled();
});
});
});
@@ -5,63 +5,37 @@
*/ */
import type React from 'react'; import type React from 'react';
import { useState } from 'react'; import { useCallback } from 'react';
import { Box, Text } from 'ink'; import { Box, Text } from 'ink';
import { Colors } from '../colors.js'; import { Colors } from '../colors.js';
import { RadioButtonSelect } from './shared/RadioButtonSelect.js'; import { RadioButtonSelect } from '../components/shared/RadioButtonSelect.js';
import type { LoadedSettings } from '../../config/settings.js'; import type { LoadedSettings } from '../../config/settings.js';
import { SettingScope } from '../../config/settings.js'; import { SettingScope } from '../../config/settings.js';
import { AuthType } from '@google/gemini-cli-core'; import {
import { validateAuthMethod } from '../../config/auth.js'; AuthType,
clearCachedCredentialFile,
type Config,
} from '@google/gemini-cli-core';
import { useKeypress } from '../hooks/useKeypress.js'; import { useKeypress } from '../hooks/useKeypress.js';
import { AuthState } from '../types.js';
import { runExitCleanup } from '../../utils/cleanup.js';
import { validateAuthMethodWithSettings } from './useAuth.js';
interface AuthDialogProps { interface AuthDialogProps {
onSelect: (authMethod: AuthType | undefined, scope: SettingScope) => void; config: Config;
settings: LoadedSettings; settings: LoadedSettings;
initialErrorMessage?: string | null; setAuthState: (state: AuthState) => void;
} authError: string | null;
onAuthError: (error: string) => void;
function parseDefaultAuthType(
defaultAuthType: string | undefined,
): AuthType | null {
if (
defaultAuthType &&
Object.values(AuthType).includes(defaultAuthType as AuthType)
) {
return defaultAuthType as AuthType;
}
return null;
} }
export function AuthDialog({ export function AuthDialog({
onSelect, config,
settings, settings,
initialErrorMessage, setAuthState,
authError,
onAuthError,
}: AuthDialogProps): React.JSX.Element { }: AuthDialogProps): React.JSX.Element {
const [errorMessage, setErrorMessage] = useState<string | null>(() => {
if (initialErrorMessage) {
return initialErrorMessage;
}
const defaultAuthType = parseDefaultAuthType(
process.env['GEMINI_DEFAULT_AUTH_TYPE'],
);
if (process.env['GEMINI_DEFAULT_AUTH_TYPE'] && defaultAuthType === null) {
return (
`Invalid value for GEMINI_DEFAULT_AUTH_TYPE: "${process.env['GEMINI_DEFAULT_AUTH_TYPE']}". ` +
`Valid values are: ${Object.values(AuthType).join(', ')}.`
);
}
if (
process.env['GEMINI_API_KEY'] &&
(!defaultAuthType || defaultAuthType === AuthType.USE_GEMINI)
) {
return 'Existing API key detected (GEMINI_API_KEY). Select "Gemini API Key" option to use it.';
}
return null;
});
let items = [ let items = [
{ {
label: 'Login with Google', label: 'Login with Google',
@@ -88,14 +62,20 @@ export function AuthDialog({
); );
} }
let defaultAuthType = null;
const defaultAuthTypeEnv = process.env['GEMINI_DEFAULT_AUTH_TYPE'];
if (
defaultAuthTypeEnv &&
Object.values(AuthType).includes(defaultAuthTypeEnv as AuthType)
) {
defaultAuthType = defaultAuthTypeEnv as AuthType;
}
let initialAuthIndex = items.findIndex((item) => { let initialAuthIndex = items.findIndex((item) => {
if (settings.merged.security?.auth?.selectedType) { if (settings.merged.security?.auth?.selectedType) {
return item.value === settings.merged.security.auth.selectedType; return item.value === settings.merged.security.auth.selectedType;
} }
const defaultAuthType = parseDefaultAuthType(
process.env['GEMINI_DEFAULT_AUTH_TYPE'],
);
if (defaultAuthType) { if (defaultAuthType) {
return item.value === defaultAuthType; return item.value === defaultAuthType;
} }
@@ -110,12 +90,37 @@ export function AuthDialog({
initialAuthIndex = 0; initialAuthIndex = 0;
} }
const onSelect = useCallback(
async (authType: AuthType | undefined, scope: SettingScope) => {
if (authType) {
await clearCachedCredentialFile();
settings.setValue(scope, 'security.auth.selectedType', authType);
if (
authType === AuthType.LOGIN_WITH_GOOGLE &&
config.isBrowserLaunchSuppressed()
) {
runExitCleanup();
console.log(
`
----------------------------------------------------------------
Logging in with Google... Please restart Gemini CLI to continue.
----------------------------------------------------------------
`,
);
process.exit(0);
}
}
setAuthState(AuthState.Unauthenticated);
},
[settings, config, setAuthState],
);
const handleAuthSelect = (authMethod: AuthType) => { const handleAuthSelect = (authMethod: AuthType) => {
const error = validateAuthMethod(authMethod); const error = validateAuthMethodWithSettings(authMethod, settings);
if (error) { if (error) {
setErrorMessage(error); onAuthError(error);
} else { } else {
setErrorMessage(null);
onSelect(authMethod, SettingScope.User); onSelect(authMethod, SettingScope.User);
} }
}; };
@@ -125,12 +130,12 @@ export function AuthDialog({
if (key.name === 'escape') { if (key.name === 'escape') {
// Prevent exit if there is an error message. // Prevent exit if there is an error message.
// This means they user is not authenticated yet. // This means they user is not authenticated yet.
if (errorMessage) { if (authError) {
return; return;
} }
if (settings.merged.security?.auth?.selectedType === undefined) { if (settings.merged.security?.auth?.selectedType === undefined) {
// Prevent exiting if no auth method is set // Prevent exiting if no auth method is set
setErrorMessage( onAuthError(
'You must select an auth method to proceed. Press Ctrl+C twice to exit.', 'You must select an auth method to proceed. Press Ctrl+C twice to exit.',
); );
return; return;
@@ -160,9 +165,9 @@ export function AuthDialog({
onSelect={handleAuthSelect} onSelect={handleAuthSelect}
/> />
</Box> </Box>
{errorMessage && ( {authError && (
<Box marginTop={1}> <Box marginTop={1}>
<Text color={Colors.AccentRed}>{errorMessage}</Text> <Text color={Colors.AccentRed}>{authError}</Text>
</Box> </Box>
)} )}
<Box marginTop={1}> <Box marginTop={1}>
+96
View File
@@ -0,0 +1,96 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { useState, useEffect, useCallback } from 'react';
import type { LoadedSettings } from '../../config/settings.js';
import { AuthType, type Config } from '@google/gemini-cli-core';
import { getErrorMessage } from '@google/gemini-cli-core';
import { AuthState } from '../types.js';
import { validateAuthMethod } from '../../config/auth.js';
export function validateAuthMethodWithSettings(
authType: AuthType,
settings: LoadedSettings,
): string | null {
const enforcedType = settings.merged.security?.auth?.enforcedType;
if (enforcedType && enforcedType !== authType) {
return `Authentication is enforced to be ${enforcedType}, but you are currently using ${authType}.`;
}
if (settings.merged.security?.auth?.useExternal) {
return null;
}
return validateAuthMethod(authType);
}
export const useAuthCommand = (settings: LoadedSettings, config: Config) => {
const [authState, setAuthState] = useState<AuthState>(
AuthState.Unauthenticated,
);
const [authError, setAuthError] = useState<string | null>(null);
const onAuthError = useCallback(
(error: string) => {
setAuthError(error);
setAuthState(AuthState.Updating);
},
[setAuthError, setAuthState],
);
useEffect(() => {
(async () => {
if (authState !== AuthState.Unauthenticated) {
return;
}
const authType = settings.merged.security?.auth?.selectedType;
if (!authType) {
if (process.env['GEMINI_API_KEY']) {
onAuthError(
'Existing API key detected (GEMINI_API_KEY). Select "Gemini API Key" option to use it.',
);
} else {
onAuthError('No authentication method selected.');
}
return;
}
const error = validateAuthMethodWithSettings(authType, settings);
if (error) {
onAuthError(error);
return;
}
const defaultAuthType = process.env['GEMINI_DEFAULT_AUTH_TYPE'];
if (
defaultAuthType &&
!Object.values(AuthType).includes(defaultAuthType as AuthType)
) {
onAuthError(
`Invalid value for GEMINI_DEFAULT_AUTH_TYPE: "${defaultAuthType}". ` +
`Valid values are: ${Object.values(AuthType).join(', ')}.`,
);
return;
}
try {
await config.refreshAuth(authType);
console.log(`Authenticated via "${authType}".`);
setAuthError(null);
setAuthState(AuthState.Authenticated);
} catch (e) {
onAuthError(`Failed to login. Message: ${getErrorMessage(e)}`);
}
})();
}, [settings, config, authState, setAuthState, setAuthError, onAuthError]);
return {
authState,
setAuthState,
authError,
onAuthError,
};
};
@@ -1,473 +0,0 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { AuthDialog } from './AuthDialog.js';
import { LoadedSettings, SettingScope } from '../../config/settings.js';
import { AuthType } from '@google/gemini-cli-core';
import { renderWithProviders } from '../../test-utils/render.js';
describe('AuthDialog', () => {
const wait = (ms = 50) => new Promise((resolve) => setTimeout(resolve, ms));
let originalEnv: NodeJS.ProcessEnv;
beforeEach(() => {
originalEnv = { ...process.env };
process.env['GEMINI_API_KEY'] = '';
process.env['GEMINI_DEFAULT_AUTH_TYPE'] = '';
vi.clearAllMocks();
});
afterEach(() => {
process.env = originalEnv;
});
it('should show an error if the initial auth type is invalid', () => {
process.env['GEMINI_API_KEY'] = '';
const settings: LoadedSettings = new LoadedSettings(
{
settings: { ui: { customThemes: {} }, mcpServers: {} },
path: '',
},
{
settings: {},
path: '',
},
{
settings: {
security: {
auth: {
selectedType: AuthType.USE_GEMINI,
},
},
},
path: '',
},
{
settings: { ui: { customThemes: {} }, mcpServers: {} },
path: '',
},
true,
new Set(),
);
const { lastFrame } = renderWithProviders(
<AuthDialog
onSelect={() => {}}
settings={settings}
initialErrorMessage="GEMINI_API_KEY environment variable not found"
/>,
);
expect(lastFrame()).toContain(
'GEMINI_API_KEY environment variable not found',
);
});
describe('GEMINI_API_KEY environment variable', () => {
it('should detect GEMINI_API_KEY environment variable', () => {
process.env['GEMINI_API_KEY'] = 'foobar';
const settings: LoadedSettings = new LoadedSettings(
{
settings: {
security: { auth: { selectedType: undefined } },
ui: { customThemes: {} },
mcpServers: {},
},
path: '',
},
{
settings: {},
path: '',
},
{
settings: { ui: { customThemes: {} }, mcpServers: {} },
path: '',
},
{
settings: { ui: { customThemes: {} }, mcpServers: {} },
path: '',
},
true,
new Set(),
);
const { lastFrame } = renderWithProviders(
<AuthDialog onSelect={() => {}} settings={settings} />,
);
expect(lastFrame()).toContain(
'Existing API key detected (GEMINI_API_KEY)',
);
});
it('should not show the GEMINI_API_KEY message if GEMINI_DEFAULT_AUTH_TYPE is set to something else', () => {
process.env['GEMINI_API_KEY'] = 'foobar';
process.env['GEMINI_DEFAULT_AUTH_TYPE'] = AuthType.LOGIN_WITH_GOOGLE;
const settings: LoadedSettings = new LoadedSettings(
{
settings: {
security: { auth: { selectedType: undefined } },
ui: { customThemes: {} },
mcpServers: {},
},
path: '',
},
{
settings: {},
path: '',
},
{
settings: { ui: { customThemes: {} }, mcpServers: {} },
path: '',
},
{
settings: { ui: { customThemes: {} }, mcpServers: {} },
path: '',
},
true,
new Set(),
);
const { lastFrame } = renderWithProviders(
<AuthDialog onSelect={() => {}} settings={settings} />,
);
expect(lastFrame()).not.toContain(
'Existing API key detected (GEMINI_API_KEY)',
);
});
it('should show the GEMINI_API_KEY message if GEMINI_DEFAULT_AUTH_TYPE is set to use api key', () => {
process.env['GEMINI_API_KEY'] = 'foobar';
process.env['GEMINI_DEFAULT_AUTH_TYPE'] = AuthType.USE_GEMINI;
const settings: LoadedSettings = new LoadedSettings(
{
settings: {
security: { auth: { selectedType: undefined } },
ui: { customThemes: {} },
mcpServers: {},
},
path: '',
},
{
settings: {},
path: '',
},
{
settings: { ui: { customThemes: {} }, mcpServers: {} },
path: '',
},
{
settings: { ui: { customThemes: {} }, mcpServers: {} },
path: '',
},
true,
new Set(),
);
const { lastFrame } = renderWithProviders(
<AuthDialog onSelect={() => {}} settings={settings} />,
);
expect(lastFrame()).toContain(
'Existing API key detected (GEMINI_API_KEY)',
);
});
});
describe('GEMINI_DEFAULT_AUTH_TYPE environment variable', () => {
it('should select the auth type specified by GEMINI_DEFAULT_AUTH_TYPE', () => {
process.env['GEMINI_DEFAULT_AUTH_TYPE'] = AuthType.LOGIN_WITH_GOOGLE;
const settings: LoadedSettings = new LoadedSettings(
{
settings: {
security: { auth: { selectedType: undefined } },
ui: { customThemes: {} },
mcpServers: {},
},
path: '',
},
{
settings: {},
path: '',
},
{
settings: { ui: { customThemes: {} }, mcpServers: {} },
path: '',
},
{
settings: { ui: { customThemes: {} }, mcpServers: {} },
path: '',
},
true,
new Set(),
);
const { lastFrame } = renderWithProviders(
<AuthDialog onSelect={() => {}} settings={settings} />,
);
// This is a bit brittle, but it's the best way to check which item is selected.
expect(lastFrame()).toContain('● 1. Login with Google');
});
it('should fall back to default if GEMINI_DEFAULT_AUTH_TYPE is not set', () => {
const settings: LoadedSettings = new LoadedSettings(
{
settings: {
security: { auth: { selectedType: undefined } },
ui: { customThemes: {} },
mcpServers: {},
},
path: '',
},
{
settings: {},
path: '',
},
{
settings: { ui: { customThemes: {} }, mcpServers: {} },
path: '',
},
{
settings: { ui: { customThemes: {} }, mcpServers: {} },
path: '',
},
true,
new Set(),
);
const { lastFrame } = renderWithProviders(
<AuthDialog onSelect={() => {}} settings={settings} />,
);
// Default is LOGIN_WITH_GOOGLE
expect(lastFrame()).toContain('● 1. Login with Google');
});
it('should show an error and fall back to default if GEMINI_DEFAULT_AUTH_TYPE is invalid', () => {
process.env['GEMINI_DEFAULT_AUTH_TYPE'] = 'invalid-auth-type';
const settings: LoadedSettings = new LoadedSettings(
{
settings: {
security: { auth: { selectedType: undefined } },
ui: { customThemes: {} },
mcpServers: {},
},
path: '',
},
{
settings: {},
path: '',
},
{
settings: { ui: { customThemes: {} }, mcpServers: {} },
path: '',
},
{
settings: { ui: { customThemes: {} }, mcpServers: {} },
path: '',
},
true,
new Set(),
);
const { lastFrame } = renderWithProviders(
<AuthDialog onSelect={() => {}} settings={settings} />,
);
expect(lastFrame()).toContain(
'Invalid value for GEMINI_DEFAULT_AUTH_TYPE: "invalid-auth-type"',
);
// Default is LOGIN_WITH_GOOGLE
expect(lastFrame()).toContain('● 1. Login with Google');
});
});
it('should prevent exiting when no auth method is selected and show error message', async () => {
const onSelect = vi.fn();
const settings: LoadedSettings = new LoadedSettings(
{
settings: { ui: { customThemes: {} }, mcpServers: {} },
path: '',
},
{
settings: {},
path: '',
},
{
settings: {
security: { auth: { selectedType: undefined } },
ui: { customThemes: {} },
mcpServers: {},
},
path: '',
},
{
settings: { ui: { customThemes: {} }, mcpServers: {} },
path: '',
},
true,
new Set(),
);
const { lastFrame, stdin, unmount } = renderWithProviders(
<AuthDialog onSelect={onSelect} settings={settings} />,
);
await wait();
// Simulate pressing escape key
stdin.write('\u001b'); // ESC key
await wait();
// Should show error message instead of calling onSelect
expect(lastFrame()).toContain(
'You must select an auth method to proceed. Press Ctrl+C twice to exit.',
);
expect(onSelect).not.toHaveBeenCalled();
unmount();
});
it('should not exit if there is already an error message', async () => {
const onSelect = vi.fn();
const settings: LoadedSettings = new LoadedSettings(
{
settings: { ui: { customThemes: {} }, mcpServers: {} },
path: '',
},
{
settings: {},
path: '',
},
{
settings: {
security: { auth: { selectedType: undefined } },
ui: { customThemes: {} },
mcpServers: {},
},
path: '',
},
{
settings: { ui: { customThemes: {} }, mcpServers: {} },
path: '',
},
true,
new Set(),
);
const { lastFrame, stdin, unmount } = renderWithProviders(
<AuthDialog
onSelect={onSelect}
settings={settings}
initialErrorMessage="Initial error"
/>,
);
await wait();
expect(lastFrame()).toContain('Initial error');
// Simulate pressing escape key
stdin.write('\u001b'); // ESC key
await wait();
// Should not call onSelect
expect(onSelect).not.toHaveBeenCalled();
unmount();
});
it('should allow exiting when auth method is already selected', async () => {
const onSelect = vi.fn();
const settings: LoadedSettings = new LoadedSettings(
{
settings: { ui: { customThemes: {} }, mcpServers: {} },
path: '',
},
{
settings: {},
path: '',
},
{
settings: {
security: { auth: { selectedType: AuthType.LOGIN_WITH_GOOGLE } },
ui: { customThemes: {} },
mcpServers: {},
},
path: '',
},
{
settings: { ui: { customThemes: {} }, mcpServers: {} },
path: '',
},
true,
new Set(),
);
const { stdin, unmount } = renderWithProviders(
<AuthDialog onSelect={onSelect} settings={settings} />,
);
await wait();
// Simulate pressing escape key
stdin.write('\u001b'); // ESC key
await wait();
// Should call onSelect with undefined to exit
expect(onSelect).toHaveBeenCalledWith(undefined, SettingScope.User);
unmount();
});
describe('enforcedAuthType', () => {
it('should display the enforced auth type message if enforcedAuthType is set', () => {
const settings: LoadedSettings = new LoadedSettings(
{
settings: {
security: {
auth: {
enforcedType: AuthType.USE_VERTEX_AI,
},
},
},
path: '',
},
{
settings: {
security: {
auth: {
selectedType: AuthType.USE_VERTEX_AI,
},
},
},
path: '',
},
{
settings: {},
path: '',
},
{
settings: {},
path: '',
},
true,
new Set(),
);
const { lastFrame } = renderWithProviders(
<AuthDialog onSelect={() => {}} settings={settings} />,
);
expect(lastFrame()).toContain('1. Vertex AI');
});
});
});
@@ -21,11 +21,12 @@ import {
} from '@google/gemini-cli-core'; } from '@google/gemini-cli-core';
import { useSessionStats } from '../contexts/SessionContext.js'; import { useSessionStats } from '../contexts/SessionContext.js';
import { runExitCleanup } from '../../utils/cleanup.js'; import { runExitCleanup } from '../../utils/cleanup.js';
import type { import {
Message, type Message,
HistoryItemWithoutId, type HistoryItemWithoutId,
HistoryItem, type HistoryItem,
SlashCommandProcessorResult, type SlashCommandProcessorResult,
AuthState,
} from '../types.js'; } from '../types.js';
import { MessageType } from '../types.js'; import { MessageType } from '../types.js';
import type { LoadedSettings } from '../../config/settings.js'; import type { LoadedSettings } from '../../config/settings.js';
@@ -47,7 +48,7 @@ export const useSlashCommandProcessor = (
refreshStatic: () => void, refreshStatic: () => void,
onDebugMessage: (message: string) => void, onDebugMessage: (message: string) => void,
openThemeDialog: () => void, openThemeDialog: () => void,
openAuthDialog: () => void, setAuthState: (state: AuthState) => void,
openEditorDialog: () => void, openEditorDialog: () => void,
toggleCorgiMode: () => void, toggleCorgiMode: () => void,
setQuittingMessages: (message: HistoryItem[]) => void, setQuittingMessages: (message: HistoryItem[]) => void,
@@ -375,7 +376,7 @@ export const useSlashCommandProcessor = (
case 'dialog': case 'dialog':
switch (result.dialog) { switch (result.dialog) {
case 'auth': case 'auth':
openAuthDialog(); setAuthState(AuthState.Updating);
return { type: 'handled' }; return { type: 'handled' };
case 'theme': case 'theme':
openThemeDialog(); openThemeDialog();
@@ -554,7 +555,7 @@ export const useSlashCommandProcessor = (
[ [
config, config,
addItem, addItem,
openAuthDialog, setAuthState,
commands, commands,
commandContext, commandContext,
addMessage, addMessage,
@@ -1,91 +0,0 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { useState, useCallback, useEffect } from 'react';
import type { LoadedSettings, SettingScope } from '../../config/settings.js';
import { AuthType, type Config } from '@google/gemini-cli-core';
import {
clearCachedCredentialFile,
getErrorMessage,
} from '@google/gemini-cli-core';
import { runExitCleanup } from '../../utils/cleanup.js';
export const useAuthCommand = (
settings: LoadedSettings,
setAuthError: (error: string | null) => void,
config: Config,
) => {
const [isAuthDialogOpen, setIsAuthDialogOpen] = useState(
settings.merged.security?.auth?.selectedType === undefined,
);
const openAuthDialog = useCallback(() => {
setIsAuthDialogOpen(true);
}, []);
const [isAuthenticating, setIsAuthenticating] = useState(false);
useEffect(() => {
const authFlow = async () => {
const authType = settings.merged.security?.auth?.selectedType;
if (isAuthDialogOpen || !authType) {
return;
}
try {
setIsAuthenticating(true);
await config.refreshAuth(authType);
console.log(`Authenticated via "${authType}".`);
} catch (e) {
setAuthError(`Failed to login. Message: ${getErrorMessage(e)}`);
openAuthDialog();
} finally {
setIsAuthenticating(false);
}
};
void authFlow();
}, [isAuthDialogOpen, settings, config, setAuthError, openAuthDialog]);
const handleAuthSelect = useCallback(
async (authType: AuthType | undefined, scope: SettingScope) => {
if (authType) {
await clearCachedCredentialFile();
settings.setValue(scope, 'security.auth.selectedType', authType);
if (
authType === AuthType.LOGIN_WITH_GOOGLE &&
config.isBrowserLaunchSuppressed()
) {
runExitCleanup();
console.log(
`
----------------------------------------------------------------
Logging in with Google... Please restart Gemini CLI to continue.
----------------------------------------------------------------
`,
);
process.exit(0);
}
}
setIsAuthDialogOpen(false);
setAuthError(null);
},
[settings, setAuthError, config],
);
const cancelAuthentication = useCallback(() => {
setIsAuthenticating(false);
}, []);
return {
isAuthDialogOpen,
openAuthDialog,
handleAuthSelect,
isAuthenticating,
cancelAuthentication,
};
};
+2 -2
View File
@@ -95,7 +95,7 @@ export const useGeminiStream = (
) => Promise<SlashCommandProcessorResult | false>, ) => Promise<SlashCommandProcessorResult | false>,
shellModeActive: boolean, shellModeActive: boolean,
getPreferredEditor: () => EditorType | undefined, getPreferredEditor: () => EditorType | undefined,
onAuthError: () => void, onAuthError: (error: string) => void,
performMemoryRefresh: () => Promise<void>, performMemoryRefresh: () => Promise<void>,
modelSwitchedFromQuotaError: boolean, modelSwitchedFromQuotaError: boolean,
setModelSwitchedFromQuotaError: React.Dispatch<React.SetStateAction<boolean>>, setModelSwitchedFromQuotaError: React.Dispatch<React.SetStateAction<boolean>>,
@@ -751,7 +751,7 @@ export const useGeminiStream = (
} }
} catch (error: unknown) { } catch (error: unknown) {
if (error instanceof UnauthorizedError) { if (error instanceof UnauthorizedError) {
onAuthError(); onAuthError('Session expired or is unauthorized.');
} else if (!isNodeError(error) || error.name !== 'AbortError') { } else if (!isNodeError(error) || error.name !== 'AbortError') {
addItem( addItem(
{ {
+9
View File
@@ -11,6 +11,15 @@ import type {
} from '@google/gemini-cli-core'; } from '@google/gemini-cli-core';
import type { PartListUnion } from '@google/genai'; import type { PartListUnion } from '@google/genai';
export enum AuthState {
// Attemtping to authenticate or re-authenticate
Unauthenticated = 'unauthenticated',
// Auth dialog is open for user to select auth method
Updating = 'updating',
// Successfully authenticated
Authenticated = 'authenticated',
}
// Only defining the state enum needed by the UI // Only defining the state enum needed by the UI
export enum StreamingState { export enum StreamingState {
Idle = 'idle', Idle = 'idle',