mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-13 05:12:55 -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({
|
const historyManager = useHistory({
|
||||||
chatRecordingService: config.getGeminiClient()?.getChatRecordingService(),
|
chatRecordingService: config.getGeminiClient()?.getChatRecordingService(),
|
||||||
});
|
});
|
||||||
|
<<<<<<< HEAD
|
||||||
|
=======
|
||||||
|
|
||||||
|
>>>>>>> d63c34b6e (feat(ui): move user identity display to header (#18216))
|
||||||
useMemoryMonitor(historyManager);
|
useMemoryMonitor(historyManager);
|
||||||
const settings = useSettings();
|
const settings = useSettings();
|
||||||
const isAlternateBuffer = useAlternateBuffer();
|
const isAlternateBuffer = useAlternateBuffer();
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
import { Box } from 'ink';
|
import { Box } from 'ink';
|
||||||
import { Header } from './Header.js';
|
import { Header } from './Header.js';
|
||||||
import { Tips } from './Tips.js';
|
import { Tips } from './Tips.js';
|
||||||
|
import { UserIdentity } from './UserIdentity.js';
|
||||||
import { useSettings } from '../contexts/SettingsContext.js';
|
import { useSettings } from '../contexts/SettingsContext.js';
|
||||||
import { useConfig } from '../contexts/ConfigContext.js';
|
import { useConfig } from '../contexts/ConfigContext.js';
|
||||||
import { useUIState } from '../contexts/UIStateContext.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()) &&
|
{!(settings.merged.ui.hideTips || config.getScreenReader()) &&
|
||||||
showTips && <Tips config={config} />}
|
showTips && <Tips config={config} />}
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -132,8 +132,8 @@ export async function setupUser(
|
|||||||
if (projectId) {
|
if (projectId) {
|
||||||
return {
|
return {
|
||||||
projectId,
|
projectId,
|
||||||
userTier: loadRes.currentTier.id,
|
userTier: loadRes.paidTier?.id ?? loadRes.currentTier.id,
|
||||||
userTierName: loadRes.currentTier.name,
|
userTierName: loadRes.paidTier?.name ?? loadRes.currentTier.name,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,8 +142,8 @@ export async function setupUser(
|
|||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
projectId: loadRes.cloudaicompanionProject,
|
projectId: loadRes.cloudaicompanionProject,
|
||||||
userTier: loadRes.currentTier.id,
|
userTier: loadRes.paidTier?.id ?? loadRes.currentTier.id,
|
||||||
userTierName: loadRes.currentTier.name,
|
userTierName: loadRes.paidTier?.name ?? loadRes.currentTier.name,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ export interface LoadCodeAssistResponse {
|
|||||||
allowedTiers?: GeminiUserTier[] | null;
|
allowedTiers?: GeminiUserTier[] | null;
|
||||||
ineligibleTiers?: IneligibleTier[] | null;
|
ineligibleTiers?: IneligibleTier[] | null;
|
||||||
cloudaicompanionProject?: string | null;
|
cloudaicompanionProject?: string | null;
|
||||||
|
paidTier?: GeminiUserTier | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -109,13 +110,17 @@ export enum IneligibleTierReasonCode {
|
|||||||
/**
|
/**
|
||||||
* UserTierId represents IDs returned from the Cloud Code Private API representing a user's tier
|
* UserTierId represents IDs returned from the Cloud Code Private API representing a user's tier
|
||||||
*
|
*
|
||||||
* //depot/google3/cloud/developer_experience/cloudcode/pa/service/usertier.go;l=16
|
* http://google3/cloud/developer_experience/codeassist/shared/usertier/tiers.go
|
||||||
|
* This is a subset of all available tiers. Since the source list is frequently updated,
|
||||||
|
* only add a tierId here if specific client-side handling is required.
|
||||||
*/
|
*/
|
||||||
export enum UserTierId {
|
export const UserTierId = {
|
||||||
FREE = 'free-tier',
|
FREE: 'free-tier',
|
||||||
LEGACY = 'legacy-tier',
|
LEGACY: 'legacy-tier',
|
||||||
STANDARD = 'standard-tier',
|
STANDARD: 'standard-tier',
|
||||||
}
|
} as const;
|
||||||
|
|
||||||
|
export type UserTierId = (typeof UserTierId)[keyof typeof UserTierId] | string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PrivacyNotice reflects the structure received from the CodeAssist in regards to a tier
|
* PrivacyNotice reflects the structure received from the CodeAssist in regards to a tier
|
||||||
|
|||||||
Reference in New Issue
Block a user