mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-11 13:51:10 -07:00
feat(ui): move user identity display to header (#18216)
# Conflicts: # packages/cli/src/ui/AppContainer.tsx
This commit is contained in:
committed by
gemini-cli-robot
parent
ec10a76923
commit
f24eaeffc6
@@ -189,6 +189,10 @@ export const AppContainer = (props: AppContainerProps) => {
|
||||
const historyManager = useHistory({
|
||||
chatRecordingService: config.getGeminiClient()?.getChatRecordingService(),
|
||||
});
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
|
||||
>>>>>>> d63c34b6e (feat(ui): move user identity display to header (#18216))
|
||||
useMemoryMonitor(historyManager);
|
||||
const settings = useSettings();
|
||||
const isAlternateBuffer = useAlternateBuffer();
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
import { Box } from 'ink';
|
||||
import { Header } from './Header.js';
|
||||
import { Tips } from './Tips.js';
|
||||
import { UserIdentity } from './UserIdentity.js';
|
||||
import { useSettings } from '../contexts/SettingsContext.js';
|
||||
import { useConfig } from '../contexts/ConfigContext.js';
|
||||
import { useUIState } from '../contexts/UIStateContext.js';
|
||||
@@ -40,6 +41,9 @@ export const AppHeader = ({ version }: AppHeaderProps) => {
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{settings.merged.ui.showUserIdentity !== false && (
|
||||
<UserIdentity config={config} />
|
||||
)}
|
||||
{!(settings.merged.ui.hideTips || config.getScreenReader()) &&
|
||||
showTips && <Tips config={config} />}
|
||||
</Box>
|
||||
|
||||
139
packages/cli/src/ui/components/UserIdentity.test.tsx
Normal file
139
packages/cli/src/ui/components/UserIdentity.test.tsx
Normal file
@@ -0,0 +1,139 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { renderWithProviders } from '../../test-utils/render.js';
|
||||
import { UserIdentity } from './UserIdentity.js';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import {
|
||||
makeFakeConfig,
|
||||
AuthType,
|
||||
UserAccountManager,
|
||||
type ContentGeneratorConfig,
|
||||
} from '@google/gemini-cli-core';
|
||||
|
||||
// Mock UserAccountManager to control cached account
|
||||
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
const original =
|
||||
await importOriginal<typeof import('@google/gemini-cli-core')>();
|
||||
return {
|
||||
...original,
|
||||
UserAccountManager: vi.fn().mockImplementation(() => ({
|
||||
getCachedGoogleAccount: () => 'test@example.com',
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
describe('<UserIdentity />', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should render login message and auth indicator', () => {
|
||||
const mockConfig = makeFakeConfig();
|
||||
vi.spyOn(mockConfig, 'getContentGeneratorConfig').mockReturnValue({
|
||||
authType: AuthType.LOGIN_WITH_GOOGLE,
|
||||
model: 'gemini-pro',
|
||||
} as unknown as ContentGeneratorConfig);
|
||||
vi.spyOn(mockConfig, 'getUserTierName').mockReturnValue(undefined);
|
||||
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
<UserIdentity config={mockConfig} />,
|
||||
);
|
||||
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('Logged in with Google: test@example.com');
|
||||
expect(output).toContain('/auth');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should render login message without colon if email is missing', () => {
|
||||
// Modify the mock for this specific test
|
||||
vi.mocked(UserAccountManager).mockImplementationOnce(
|
||||
() =>
|
||||
({
|
||||
getCachedGoogleAccount: () => undefined,
|
||||
}) as unknown as UserAccountManager,
|
||||
);
|
||||
|
||||
const mockConfig = makeFakeConfig();
|
||||
vi.spyOn(mockConfig, 'getContentGeneratorConfig').mockReturnValue({
|
||||
authType: AuthType.LOGIN_WITH_GOOGLE,
|
||||
model: 'gemini-pro',
|
||||
} as unknown as ContentGeneratorConfig);
|
||||
vi.spyOn(mockConfig, 'getUserTierName').mockReturnValue(undefined);
|
||||
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
<UserIdentity config={mockConfig} />,
|
||||
);
|
||||
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('Logged in with Google');
|
||||
expect(output).not.toContain('Logged in with Google:');
|
||||
expect(output).toContain('/auth');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should render plan name on a separate line if provided', () => {
|
||||
const mockConfig = makeFakeConfig();
|
||||
vi.spyOn(mockConfig, 'getContentGeneratorConfig').mockReturnValue({
|
||||
authType: AuthType.LOGIN_WITH_GOOGLE,
|
||||
model: 'gemini-pro',
|
||||
} as unknown as ContentGeneratorConfig);
|
||||
vi.spyOn(mockConfig, 'getUserTierName').mockReturnValue('Premium Plan');
|
||||
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
<UserIdentity config={mockConfig} />,
|
||||
);
|
||||
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('Logged in with Google: test@example.com');
|
||||
expect(output).toContain('/auth');
|
||||
expect(output).toContain('Plan: Premium Plan');
|
||||
|
||||
// Check for two lines (or more if wrapped, but here it should be separate)
|
||||
const lines = output?.split('\n').filter((line) => line.trim().length > 0);
|
||||
expect(lines?.some((line) => line.includes('Logged in with Google'))).toBe(
|
||||
true,
|
||||
);
|
||||
expect(lines?.some((line) => line.includes('Plan: Premium Plan'))).toBe(
|
||||
true,
|
||||
);
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should not render if authType is missing', () => {
|
||||
const mockConfig = makeFakeConfig();
|
||||
vi.spyOn(mockConfig, 'getContentGeneratorConfig').mockReturnValue(
|
||||
{} as unknown as ContentGeneratorConfig,
|
||||
);
|
||||
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
<UserIdentity config={mockConfig} />,
|
||||
);
|
||||
|
||||
expect(lastFrame()).toBe('');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should render non-Google auth message', () => {
|
||||
const mockConfig = makeFakeConfig();
|
||||
vi.spyOn(mockConfig, 'getContentGeneratorConfig').mockReturnValue({
|
||||
authType: AuthType.USE_GEMINI,
|
||||
model: 'gemini-pro',
|
||||
} as unknown as ContentGeneratorConfig);
|
||||
vi.spyOn(mockConfig, 'getUserTierName').mockReturnValue(undefined);
|
||||
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
<UserIdentity config={mockConfig} />,
|
||||
);
|
||||
|
||||
const output = lastFrame();
|
||||
expect(output).toContain(`Authenticated with ${AuthType.USE_GEMINI}`);
|
||||
expect(output).toContain('/auth');
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
61
packages/cli/src/ui/components/UserIdentity.tsx
Normal file
61
packages/cli/src/ui/components/UserIdentity.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
import {
|
||||
type Config,
|
||||
UserAccountManager,
|
||||
AuthType,
|
||||
} from '@google/gemini-cli-core';
|
||||
|
||||
interface UserIdentityProps {
|
||||
config: Config;
|
||||
}
|
||||
|
||||
export const UserIdentity: React.FC<UserIdentityProps> = ({ config }) => {
|
||||
const authType = config.getContentGeneratorConfig()?.authType;
|
||||
|
||||
const { email, tierName } = useMemo(() => {
|
||||
if (!authType) {
|
||||
return { email: undefined, tierName: undefined };
|
||||
}
|
||||
const userAccountManager = new UserAccountManager();
|
||||
return {
|
||||
email: userAccountManager.getCachedGoogleAccount(),
|
||||
tierName: config.getUserTierName(),
|
||||
};
|
||||
}, [config, authType]);
|
||||
|
||||
if (!authType) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box marginY={1} flexDirection="column">
|
||||
<Box>
|
||||
<Text color={theme.text.primary}>
|
||||
{authType === AuthType.LOGIN_WITH_GOOGLE ? (
|
||||
<Text>
|
||||
<Text bold>Logged in with Google{email ? ':' : ''}</Text>
|
||||
{email ? ` ${email}` : ''}
|
||||
</Text>
|
||||
) : (
|
||||
`Authenticated with ${authType}`
|
||||
)}
|
||||
</Text>
|
||||
<Text color={theme.text.secondary}> /auth</Text>
|
||||
</Box>
|
||||
{tierName && (
|
||||
<Text color={theme.text.primary}>
|
||||
<Text bold>Plan:</Text> {tierName}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user