mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-02-01 22:48:03 +00:00
feat(cli): Display user identity (auth, email, tier) on startup (#17591)
Co-authored-by: Keith Guerin <keithguerin@gmail.com> Co-authored-by: Yuna Seol <yunaseol@google.com>
This commit is contained in:
@@ -21,6 +21,7 @@ import { act, useContext, type ReactElement } from 'react';
|
|||||||
import { AppContainer } from './AppContainer.js';
|
import { AppContainer } from './AppContainer.js';
|
||||||
import { SettingsContext } from './contexts/SettingsContext.js';
|
import { SettingsContext } from './contexts/SettingsContext.js';
|
||||||
import { type TrackedToolCall } from './hooks/useReactToolScheduler.js';
|
import { type TrackedToolCall } from './hooks/useReactToolScheduler.js';
|
||||||
|
import { MessageType } from './types.js';
|
||||||
import {
|
import {
|
||||||
type Config,
|
type Config,
|
||||||
makeFakeConfig,
|
makeFakeConfig,
|
||||||
@@ -28,6 +29,8 @@ import {
|
|||||||
type UserFeedbackPayload,
|
type UserFeedbackPayload,
|
||||||
type ResumedSessionData,
|
type ResumedSessionData,
|
||||||
AuthType,
|
AuthType,
|
||||||
|
UserAccountManager,
|
||||||
|
type ContentGeneratorConfig,
|
||||||
type AgentDefinition,
|
type AgentDefinition,
|
||||||
MessageBusType,
|
MessageBusType,
|
||||||
QuestionType,
|
QuestionType,
|
||||||
@@ -50,6 +53,11 @@ const mockIdeClient = vi.hoisted(() => ({
|
|||||||
getInstance: vi.fn().mockReturnValue(new Promise(() => {})),
|
getInstance: vi.fn().mockReturnValue(new Promise(() => {})),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// Mock UserAccountManager
|
||||||
|
const mockUserAccountManager = vi.hoisted(() => ({
|
||||||
|
getCachedGoogleAccount: vi.fn().mockReturnValue(null),
|
||||||
|
}));
|
||||||
|
|
||||||
// Mock stdout
|
// Mock stdout
|
||||||
const mocks = vi.hoisted(() => ({
|
const mocks = vi.hoisted(() => ({
|
||||||
mockStdout: { write: vi.fn() },
|
mockStdout: { write: vi.fn() },
|
||||||
@@ -79,6 +87,9 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
|||||||
})),
|
})),
|
||||||
enableMouseEvents: vi.fn(),
|
enableMouseEvents: vi.fn(),
|
||||||
disableMouseEvents: vi.fn(),
|
disableMouseEvents: vi.fn(),
|
||||||
|
UserAccountManager: vi
|
||||||
|
.fn()
|
||||||
|
.mockImplementation(() => mockUserAccountManager),
|
||||||
FileDiscoveryService: vi.fn().mockImplementation(() => ({
|
FileDiscoveryService: vi.fn().mockImplementation(() => ({
|
||||||
initialize: vi.fn(),
|
initialize: vi.fn(),
|
||||||
})),
|
})),
|
||||||
@@ -405,6 +416,7 @@ describe('AppContainer State Management', () => {
|
|||||||
...defaultMergedSettings.ui,
|
...defaultMergedSettings.ui,
|
||||||
showStatusInTitle: false,
|
showStatusInTitle: false,
|
||||||
hideWindowTitle: false,
|
hideWindowTitle: false,
|
||||||
|
showUserIdentity: true,
|
||||||
},
|
},
|
||||||
useAlternateBuffer: false,
|
useAlternateBuffer: false,
|
||||||
},
|
},
|
||||||
@@ -476,6 +488,162 @@ describe('AppContainer State Management', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Authentication Check', () => {
|
||||||
|
it('displays correct message for LOGIN_WITH_GOOGLE auth type', async () => {
|
||||||
|
// Explicitly mock implementation to ensure we control the instance
|
||||||
|
(UserAccountManager as unknown as Mock).mockImplementation(
|
||||||
|
() => mockUserAccountManager,
|
||||||
|
);
|
||||||
|
|
||||||
|
mockUserAccountManager.getCachedGoogleAccount.mockReturnValue(
|
||||||
|
'test@example.com',
|
||||||
|
);
|
||||||
|
const mockAddItem = vi.fn();
|
||||||
|
mockedUseHistory.mockReturnValue({
|
||||||
|
history: [],
|
||||||
|
addItem: mockAddItem,
|
||||||
|
updateItem: vi.fn(),
|
||||||
|
clearItems: vi.fn(),
|
||||||
|
loadHistory: vi.fn(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Explicitly enable showUserIdentity
|
||||||
|
mockSettings.merged.ui = {
|
||||||
|
...mockSettings.merged.ui,
|
||||||
|
showUserIdentity: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Need to ensure config.getContentGeneratorConfig() returns appropriate authType
|
||||||
|
const authConfig = makeFakeConfig();
|
||||||
|
// Mock getTargetDir as well since makeFakeConfig might not set it up fully for the component
|
||||||
|
vi.spyOn(authConfig, 'getTargetDir').mockReturnValue('/test/workspace');
|
||||||
|
vi.spyOn(authConfig, 'initialize').mockResolvedValue(undefined);
|
||||||
|
vi.spyOn(authConfig, 'getExtensionLoader').mockReturnValue(
|
||||||
|
mockExtensionManager,
|
||||||
|
);
|
||||||
|
|
||||||
|
vi.spyOn(authConfig, 'getContentGeneratorConfig').mockReturnValue({
|
||||||
|
authType: AuthType.LOGIN_WITH_GOOGLE,
|
||||||
|
} as unknown as ContentGeneratorConfig);
|
||||||
|
vi.spyOn(authConfig, 'getUserTierName').mockReturnValue('Standard Tier');
|
||||||
|
|
||||||
|
let unmount: () => void;
|
||||||
|
await act(async () => {
|
||||||
|
const result = renderAppContainer({ config: authConfig });
|
||||||
|
unmount = result.unmount;
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(UserAccountManager).toHaveBeenCalled();
|
||||||
|
expect(
|
||||||
|
mockUserAccountManager.getCachedGoogleAccount,
|
||||||
|
).toHaveBeenCalled();
|
||||||
|
expect(mockAddItem).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
text: 'Logged in with Google: test@example.com (Plan: Standard Tier)',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await act(async () => {
|
||||||
|
unmount!();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('displays correct message for USE_GEMINI auth type', async () => {
|
||||||
|
// Explicitly mock implementation to ensure we control the instance
|
||||||
|
(UserAccountManager as unknown as Mock).mockImplementation(
|
||||||
|
() => mockUserAccountManager,
|
||||||
|
);
|
||||||
|
|
||||||
|
mockUserAccountManager.getCachedGoogleAccount.mockReturnValue(null);
|
||||||
|
const mockAddItem = vi.fn();
|
||||||
|
mockedUseHistory.mockReturnValue({
|
||||||
|
history: [],
|
||||||
|
addItem: mockAddItem,
|
||||||
|
updateItem: vi.fn(),
|
||||||
|
clearItems: vi.fn(),
|
||||||
|
loadHistory: vi.fn(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const authConfig = makeFakeConfig();
|
||||||
|
vi.spyOn(authConfig, 'getTargetDir').mockReturnValue('/test/workspace');
|
||||||
|
vi.spyOn(authConfig, 'initialize').mockResolvedValue(undefined);
|
||||||
|
vi.spyOn(authConfig, 'getExtensionLoader').mockReturnValue(
|
||||||
|
mockExtensionManager,
|
||||||
|
);
|
||||||
|
|
||||||
|
vi.spyOn(authConfig, 'getContentGeneratorConfig').mockReturnValue({
|
||||||
|
authType: AuthType.USE_GEMINI,
|
||||||
|
} as unknown as ContentGeneratorConfig);
|
||||||
|
vi.spyOn(authConfig, 'getUserTierName').mockReturnValue('Standard Tier');
|
||||||
|
|
||||||
|
let unmount: () => void;
|
||||||
|
await act(async () => {
|
||||||
|
const result = renderAppContainer({ config: authConfig });
|
||||||
|
unmount = result.unmount;
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockAddItem).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
text: expect.stringContaining('Authenticated with gemini-api-key'),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await act(async () => {
|
||||||
|
unmount!();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not display authentication message if showUserIdentity is false', async () => {
|
||||||
|
mockUserAccountManager.getCachedGoogleAccount.mockReturnValue(
|
||||||
|
'test@example.com',
|
||||||
|
);
|
||||||
|
const mockAddItem = vi.fn();
|
||||||
|
mockedUseHistory.mockReturnValue({
|
||||||
|
history: [],
|
||||||
|
addItem: mockAddItem,
|
||||||
|
updateItem: vi.fn(),
|
||||||
|
clearItems: vi.fn(),
|
||||||
|
loadHistory: vi.fn(),
|
||||||
|
});
|
||||||
|
|
||||||
|
mockSettings.merged.ui = {
|
||||||
|
...mockSettings.merged.ui,
|
||||||
|
showUserIdentity: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const authConfig = makeFakeConfig();
|
||||||
|
vi.spyOn(authConfig, 'getTargetDir').mockReturnValue('/test/workspace');
|
||||||
|
vi.spyOn(authConfig, 'initialize').mockResolvedValue(undefined);
|
||||||
|
vi.spyOn(authConfig, 'getExtensionLoader').mockReturnValue(
|
||||||
|
mockExtensionManager,
|
||||||
|
);
|
||||||
|
|
||||||
|
vi.spyOn(authConfig, 'getContentGeneratorConfig').mockReturnValue({
|
||||||
|
authType: AuthType.LOGIN_WITH_GOOGLE,
|
||||||
|
} as unknown as ContentGeneratorConfig);
|
||||||
|
|
||||||
|
let unmount: () => void;
|
||||||
|
await act(async () => {
|
||||||
|
const result = renderAppContainer({ config: authConfig });
|
||||||
|
unmount = result.unmount;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Give it some time to potentially call addItem
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||||
|
|
||||||
|
expect(mockAddItem).not.toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
type: MessageType.INFO,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
unmount!();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('Context Providers', () => {
|
describe('Context Providers', () => {
|
||||||
it('provides AppContext with correct values', async () => {
|
it('provides AppContext with correct values', async () => {
|
||||||
let unmount: () => void;
|
let unmount: () => void;
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ import {
|
|||||||
getErrorMessage,
|
getErrorMessage,
|
||||||
getAllGeminiMdFilenames,
|
getAllGeminiMdFilenames,
|
||||||
AuthType,
|
AuthType,
|
||||||
|
UserAccountManager,
|
||||||
clearCachedCredentialFile,
|
clearCachedCredentialFile,
|
||||||
type ResumedSessionData,
|
type ResumedSessionData,
|
||||||
recordExitFail,
|
recordExitFail,
|
||||||
@@ -186,11 +187,58 @@ const SHELL_HEIGHT_PADDING = 10;
|
|||||||
|
|
||||||
export const AppContainer = (props: AppContainerProps) => {
|
export const AppContainer = (props: AppContainerProps) => {
|
||||||
const { config, initializationResult, resumedSessionData } = props;
|
const { config, initializationResult, resumedSessionData } = props;
|
||||||
|
const settings = useSettings();
|
||||||
|
|
||||||
const historyManager = useHistory({
|
const historyManager = useHistory({
|
||||||
chatRecordingService: config.getGeminiClient()?.getChatRecordingService(),
|
chatRecordingService: config.getGeminiClient()?.getChatRecordingService(),
|
||||||
});
|
});
|
||||||
|
const { addItem } = historyManager;
|
||||||
|
|
||||||
|
const authCheckPerformed = useRef(false);
|
||||||
|
useEffect(() => {
|
||||||
|
if (authCheckPerformed.current) return;
|
||||||
|
authCheckPerformed.current = true;
|
||||||
|
|
||||||
|
if (resumedSessionData || settings.merged.ui.showUserIdentity === false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const authType = config.getContentGeneratorConfig()?.authType;
|
||||||
|
|
||||||
|
// Run this asynchronously to avoid blocking the event loop.
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const userAccountManager = new UserAccountManager();
|
||||||
|
const email = userAccountManager.getCachedGoogleAccount();
|
||||||
|
const tierName = config.getUserTierName();
|
||||||
|
|
||||||
|
if (authType) {
|
||||||
|
let message =
|
||||||
|
authType === AuthType.LOGIN_WITH_GOOGLE
|
||||||
|
? email
|
||||||
|
? `Logged in with Google: ${email}`
|
||||||
|
: 'Logged in with Google'
|
||||||
|
: `Authenticated with ${authType}`;
|
||||||
|
if (tierName) {
|
||||||
|
message += ` (Plan: ${tierName})`;
|
||||||
|
}
|
||||||
|
addItem({
|
||||||
|
type: MessageType.INFO,
|
||||||
|
text: message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (_e) {
|
||||||
|
// Ignore errors during initial auth check
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}, [
|
||||||
|
config,
|
||||||
|
resumedSessionData,
|
||||||
|
settings.merged.ui.showUserIdentity,
|
||||||
|
addItem,
|
||||||
|
]);
|
||||||
|
|
||||||
useMemoryMonitor(historyManager);
|
useMemoryMonitor(historyManager);
|
||||||
const settings = useSettings();
|
|
||||||
const isAlternateBuffer = useAlternateBuffer();
|
const isAlternateBuffer = useAlternateBuffer();
|
||||||
const [corgiMode, setCorgiMode] = useState(false);
|
const [corgiMode, setCorgiMode] = useState(false);
|
||||||
const [debugMessage, setDebugMessage] = useState<string>('');
|
const [debugMessage, setDebugMessage] = useState<string>('');
|
||||||
|
|||||||
@@ -219,4 +219,40 @@ describe('useHistoryManager', () => {
|
|||||||
expect(result.current.history[0].id).toBeGreaterThanOrEqual(before + 1);
|
expect(result.current.history[0].id).toBeGreaterThanOrEqual(before + 1);
|
||||||
expect(result.current.history[0].id).toBeLessThanOrEqual(after + 1);
|
expect(result.current.history[0].id).toBeLessThanOrEqual(after + 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('initialItems with auth information', () => {
|
||||||
|
it('should initialize with auth information', () => {
|
||||||
|
const email = 'user@example.com';
|
||||||
|
const tier = 'Pro';
|
||||||
|
const authMessage = `Authenticated as: ${email} (Plan: ${tier})`;
|
||||||
|
const initialItems: HistoryItem[] = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
type: 'info',
|
||||||
|
text: authMessage,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const { result } = renderHook(() => useHistory({ initialItems }));
|
||||||
|
expect(result.current.history).toHaveLength(1);
|
||||||
|
expect(result.current.history[0].text).toBe(authMessage);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add items with auth information via addItem', () => {
|
||||||
|
const { result } = renderHook(() => useHistory());
|
||||||
|
const email = 'user@example.com';
|
||||||
|
const tier = 'Pro';
|
||||||
|
const authMessage = `Authenticated as: ${email} (Plan: ${tier})`;
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.addItem({
|
||||||
|
type: 'info',
|
||||||
|
text: authMessage,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.history).toHaveLength(1);
|
||||||
|
expect(result.current.history[0].text).toBe(authMessage);
|
||||||
|
expect(result.current.history[0].type).toBe('info');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -36,10 +36,12 @@ export interface UseHistoryManagerReturn {
|
|||||||
*/
|
*/
|
||||||
export function useHistory({
|
export function useHistory({
|
||||||
chatRecordingService,
|
chatRecordingService,
|
||||||
|
initialItems = [],
|
||||||
}: {
|
}: {
|
||||||
chatRecordingService?: ChatRecordingService | null;
|
chatRecordingService?: ChatRecordingService | null;
|
||||||
|
initialItems?: HistoryItem[];
|
||||||
} = {}): UseHistoryManagerReturn {
|
} = {}): UseHistoryManagerReturn {
|
||||||
const [history, setHistory] = useState<HistoryItem[]>([]);
|
const [history, setHistory] = useState<HistoryItem[]>(initialItems);
|
||||||
const messageIdCounterRef = useRef(0);
|
const messageIdCounterRef = useRef(0);
|
||||||
|
|
||||||
// Generates a unique message ID based on a timestamp and a counter.
|
// Generates a unique message ID based on a timestamp and a counter.
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import {
|
|||||||
import type { ContentGeneratorConfig } from '../core/contentGenerator.js';
|
import type { ContentGeneratorConfig } from '../core/contentGenerator.js';
|
||||||
import {
|
import {
|
||||||
AuthType,
|
AuthType,
|
||||||
|
createContentGenerator,
|
||||||
createContentGeneratorConfig,
|
createContentGeneratorConfig,
|
||||||
} from '../core/contentGenerator.js';
|
} from '../core/contentGenerator.js';
|
||||||
import { GeminiClient } from '../core/client.js';
|
import { GeminiClient } from '../core/client.js';
|
||||||
@@ -197,6 +198,7 @@ import { getCodeAssistServer } from '../code_assist/codeAssist.js';
|
|||||||
import { getExperiments } from '../code_assist/experiments/experiments.js';
|
import { getExperiments } from '../code_assist/experiments/experiments.js';
|
||||||
import type { CodeAssistServer } from '../code_assist/server.js';
|
import type { CodeAssistServer } from '../code_assist/server.js';
|
||||||
import { ContextManager } from '../services/contextManager.js';
|
import { ContextManager } from '../services/contextManager.js';
|
||||||
|
import { UserTierId } from 'src/code_assist/types.js';
|
||||||
|
|
||||||
vi.mock('../core/baseLlmClient.js');
|
vi.mock('../core/baseLlmClient.js');
|
||||||
vi.mock('../core/tokenLimits.js', () => ({
|
vi.mock('../core/tokenLimits.js', () => ({
|
||||||
@@ -2061,6 +2063,35 @@ describe('Config Quota & Preview Model Access', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('getUserTier and getUserTierName', () => {
|
||||||
|
it('should return undefined if contentGenerator is not initialized', () => {
|
||||||
|
const config = new Config(baseParams);
|
||||||
|
expect(config.getUserTier()).toBeUndefined();
|
||||||
|
expect(config.getUserTierName()).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return values from contentGenerator after refreshAuth', async () => {
|
||||||
|
const config = new Config(baseParams);
|
||||||
|
const mockTier = UserTierId.STANDARD;
|
||||||
|
const mockTierName = 'Standard Tier';
|
||||||
|
|
||||||
|
vi.mocked(createContentGeneratorConfig).mockResolvedValue({
|
||||||
|
authType: AuthType.USE_GEMINI,
|
||||||
|
} as ContentGeneratorConfig);
|
||||||
|
|
||||||
|
vi.mocked(createContentGenerator).mockResolvedValue({
|
||||||
|
userTier: mockTier,
|
||||||
|
userTierName: mockTierName,
|
||||||
|
} as unknown as CodeAssistServer);
|
||||||
|
|
||||||
|
await config.refreshAuth(AuthType.USE_GEMINI);
|
||||||
|
|
||||||
|
expect(config.getUserTier()).toBe(mockTier);
|
||||||
|
// TODO(#1275): User tier name is disabled until re-enabled.
|
||||||
|
expect(config.getUserTierName()).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('setPreviewFeatures', () => {
|
describe('setPreviewFeatures', () => {
|
||||||
it('should reset model to default auto if disabling preview features while using a preview model', () => {
|
it('should reset model to default auto if disabling preview features while using a preview model', () => {
|
||||||
config.setPreviewFeatures(true);
|
config.setPreviewFeatures(true);
|
||||||
|
|||||||
Reference in New Issue
Block a user