feat(ui): display user tier in about command (#17400)

This commit is contained in:
Sehoon Shon
2026-01-23 16:03:53 -05:00
committed by GitHub
parent 2c0cc7b9a5
commit 5c649d8db1
18 changed files with 108 additions and 10 deletions
+1 -1
View File
@@ -10,7 +10,7 @@ import path from 'node:path';
import fs from 'node:fs/promises'; import fs from 'node:fs/promises';
describe('generalist_agent', () => { describe('generalist_agent', () => {
evalTest('ALWAYS_PASSES', { evalTest('USUALLY_PASSES', {
name: 'should be able to use generalist agent by explicitly asking the main agent to invoke it', name: 'should be able to use generalist agent by explicitly asking the main agent to invoke it',
params: { params: {
settings: { settings: {
@@ -39,6 +39,7 @@ describe('aboutCommand', () => {
config: { config: {
getModel: vi.fn(), getModel: vi.fn(),
getIdeMode: vi.fn().mockReturnValue(true), getIdeMode: vi.fn().mockReturnValue(true),
getUserTierName: vi.fn().mockReturnValue(undefined),
}, },
settings: { settings: {
merged: { merged: {
@@ -97,6 +98,7 @@ describe('aboutCommand', () => {
gcpProject: 'test-gcp-project', gcpProject: 'test-gcp-project',
ideClient: 'test-ide', ideClient: 'test-ide',
userEmail: 'test-email@example.com', userEmail: 'test-email@example.com',
tier: undefined,
}); });
}); });
@@ -156,4 +158,21 @@ describe('aboutCommand', () => {
}), }),
); );
}); });
it('should display the tier when getUserTierName returns a value', async () => {
vi.mocked(mockContext.services.config!.getUserTierName).mockReturnValue(
'Enterprise Tier',
);
if (!aboutCommand.action) {
throw new Error('The about command must have an action.');
}
await aboutCommand.action(mockContext, '');
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
expect.objectContaining({
tier: 'Enterprise Tier',
}),
);
});
}); });
@@ -44,6 +44,8 @@ export const aboutCommand: SlashCommand = {
}); });
const userEmail = cachedAccount ?? undefined; const userEmail = cachedAccount ?? undefined;
const tier = context.services.config?.getUserTierName();
const aboutItem: Omit<HistoryItemAbout, 'id'> = { const aboutItem: Omit<HistoryItemAbout, 'id'> = {
type: MessageType.ABOUT, type: MessageType.ABOUT,
cliVersion, cliVersion,
@@ -54,6 +56,7 @@ export const aboutCommand: SlashCommand = {
gcpProject, gcpProject,
ideClient, ideClient,
userEmail, userEmail,
tier,
}; };
context.ui.addItem(aboutItem); context.ui.addItem(aboutItem);
@@ -33,13 +33,13 @@ describe('AboutBox', () => {
expect(output).toContain('gemini-pro'); expect(output).toContain('gemini-pro');
expect(output).toContain('default'); expect(output).toContain('default');
expect(output).toContain('macOS'); expect(output).toContain('macOS');
expect(output).toContain('OAuth'); expect(output).toContain('Logged in with Google');
}); });
it.each([ it.each([
['userEmail', 'test@example.com', 'User Email'],
['gcpProject', 'my-project', 'GCP Project'], ['gcpProject', 'my-project', 'GCP Project'],
['ideClient', 'vscode', 'IDE Client'], ['ideClient', 'vscode', 'IDE Client'],
['tier', 'Enterprise', 'Tier'],
])('renders optional prop %s', (prop, value, label) => { ])('renders optional prop %s', (prop, value, label) => {
const props = { ...defaultProps, [prop]: value }; const props = { ...defaultProps, [prop]: value };
const { lastFrame } = render(<AboutBox {...props} />); const { lastFrame } = render(<AboutBox {...props} />);
@@ -48,6 +48,13 @@ describe('AboutBox', () => {
expect(output).toContain(value); expect(output).toContain(value);
}); });
it('renders Auth Method with email when userEmail is provided', () => {
const props = { ...defaultProps, userEmail: 'test@example.com' };
const { lastFrame } = render(<AboutBox {...props} />);
const output = lastFrame();
expect(output).toContain('Logged in with Google (test@example.com)');
});
it('renders Auth Method correctly when not oauth', () => { it('renders Auth Method correctly when not oauth', () => {
const props = { ...defaultProps, selectedAuthType: 'api-key' }; const props = { ...defaultProps, selectedAuthType: 'api-key' };
const { lastFrame } = render(<AboutBox {...props} />); const { lastFrame } = render(<AboutBox {...props} />);
+10 -4
View File
@@ -18,6 +18,7 @@ interface AboutBoxProps {
gcpProject: string; gcpProject: string;
ideClient: string; ideClient: string;
userEmail?: string; userEmail?: string;
tier?: string;
} }
export const AboutBox: React.FC<AboutBoxProps> = ({ export const AboutBox: React.FC<AboutBoxProps> = ({
@@ -29,6 +30,7 @@ export const AboutBox: React.FC<AboutBoxProps> = ({
gcpProject, gcpProject,
ideClient, ideClient,
userEmail, userEmail,
tier,
}) => ( }) => (
<Box <Box
borderStyle="round" borderStyle="round"
@@ -103,19 +105,23 @@ export const AboutBox: React.FC<AboutBoxProps> = ({
</Box> </Box>
<Box> <Box>
<Text color={theme.text.primary}> <Text color={theme.text.primary}>
{selectedAuthType.startsWith('oauth') ? 'OAuth' : selectedAuthType} {selectedAuthType.startsWith('oauth')
? userEmail
? `Logged in with Google (${userEmail})`
: 'Logged in with Google'
: selectedAuthType}
</Text> </Text>
</Box> </Box>
</Box> </Box>
{userEmail && ( {tier && (
<Box flexDirection="row"> <Box flexDirection="row">
<Box width="35%"> <Box width="35%">
<Text bold color={theme.text.link}> <Text bold color={theme.text.link}>
User Email Tier
</Text> </Text>
</Box> </Box>
<Box> <Box>
<Text color={theme.text.primary}>{userEmail}</Text> <Text color={theme.text.primary}>{tier}</Text>
</Box> </Box>
</Box> </Box>
)} )}
@@ -112,6 +112,7 @@ export const HistoryItemDisplay: React.FC<HistoryItemDisplayProps> = ({
gcpProject={itemForDisplay.gcpProject} gcpProject={itemForDisplay.gcpProject}
ideClient={itemForDisplay.ideClient} ideClient={itemForDisplay.ideClient}
userEmail={itemForDisplay.userEmail} userEmail={itemForDisplay.userEmail}
tier={itemForDisplay.tier}
/> />
)} )}
{itemForDisplay.type === 'help' && commands && ( {itemForDisplay.type === 'help' && commands && (
+1
View File
@@ -145,6 +145,7 @@ export type HistoryItemAbout = HistoryItemBase & {
gcpProject: string; gcpProject: string;
ideClient: string; ideClient: string;
userEmail?: string; userEmail?: string;
tier?: string;
}; };
export type HistoryItemHelp = HistoryItemBase & { export type HistoryItemHelp = HistoryItemBase & {
@@ -64,6 +64,7 @@ describe('codeAssist', () => {
httpOptions, httpOptions,
'session-123', 'session-123',
'free-tier', 'free-tier',
undefined,
); );
expect(generator).toBeInstanceOf(MockedCodeAssistServer); expect(generator).toBeInstanceOf(MockedCodeAssistServer);
}); });
@@ -89,6 +90,7 @@ describe('codeAssist', () => {
httpOptions, httpOptions,
undefined, // No session ID undefined, // No session ID
'free-tier', 'free-tier',
undefined,
); );
expect(generator).toBeInstanceOf(MockedCodeAssistServer); expect(generator).toBeInstanceOf(MockedCodeAssistServer);
}); });
@@ -31,6 +31,7 @@ export async function createCodeAssistContentGenerator(
httpOptions, httpOptions,
sessionId, sessionId,
userData.userTier, userData.userTier,
userData.userTierName,
); );
} }
+1
View File
@@ -69,6 +69,7 @@ export class CodeAssistServer implements ContentGenerator {
readonly httpOptions: HttpOptions = {}, readonly httpOptions: HttpOptions = {},
readonly sessionId?: string, readonly sessionId?: string,
readonly userTier?: UserTierId, readonly userTier?: UserTierId,
readonly userTierName?: string,
) {} ) {}
async generateContentStream( async generateContentStream(
@@ -67,6 +67,7 @@ describe('setupUser for existing user', () => {
{}, {},
'', '',
undefined, undefined,
undefined,
); );
}); });
@@ -83,10 +84,12 @@ describe('setupUser for existing user', () => {
{}, {},
'', '',
undefined, undefined,
undefined,
); );
expect(projectId).toEqual({ expect(projectId).toEqual({
projectId: 'server-project', projectId: 'server-project',
userTier: 'standard-tier', userTier: 'standard-tier',
userTierName: 'paid',
}); });
}); });
@@ -148,6 +151,7 @@ describe('setupUser for new user', () => {
{}, {},
'', '',
undefined, undefined,
undefined,
); );
expect(mockLoad).toHaveBeenCalled(); expect(mockLoad).toHaveBeenCalled();
expect(mockOnboardUser).toHaveBeenCalledWith({ expect(mockOnboardUser).toHaveBeenCalledWith({
@@ -163,6 +167,7 @@ describe('setupUser for new user', () => {
expect(userData).toEqual({ expect(userData).toEqual({
projectId: 'server-project', projectId: 'server-project',
userTier: 'standard-tier', userTier: 'standard-tier',
userTierName: 'paid',
}); });
}); });
@@ -178,6 +183,7 @@ describe('setupUser for new user', () => {
{}, {},
'', '',
undefined, undefined,
undefined,
); );
expect(mockLoad).toHaveBeenCalled(); expect(mockLoad).toHaveBeenCalled();
expect(mockOnboardUser).toHaveBeenCalledWith({ expect(mockOnboardUser).toHaveBeenCalledWith({
@@ -192,6 +198,7 @@ describe('setupUser for new user', () => {
expect(userData).toEqual({ expect(userData).toEqual({
projectId: 'server-project', projectId: 'server-project',
userTier: 'free-tier', userTier: 'free-tier',
userTierName: 'free',
}); });
}); });
@@ -210,6 +217,7 @@ describe('setupUser for new user', () => {
expect(userData).toEqual({ expect(userData).toEqual({
projectId: 'test-project', projectId: 'test-project',
userTier: 'standard-tier', userTier: 'standard-tier',
userTierName: 'paid',
}); });
}); });
@@ -268,6 +276,7 @@ describe('setupUser for new user', () => {
expect(userData).toEqual({ expect(userData).toEqual({
projectId: 'server-project', projectId: 'server-project',
userTier: 'standard-tier', userTier: 'standard-tier',
userTierName: 'paid',
}); });
}); });
@@ -294,6 +303,7 @@ describe('setupUser for new user', () => {
expect(userData).toEqual({ expect(userData).toEqual({
projectId: 'server-project', projectId: 'server-project',
userTier: 'standard-tier', userTier: 'standard-tier',
userTierName: 'paid',
}); });
}); });
}); });
+13 -1
View File
@@ -25,6 +25,7 @@ export class ProjectIdRequiredError extends Error {
export interface UserData { export interface UserData {
projectId: string; projectId: string;
userTier: UserTierId; userTier: UserTierId;
userTierName?: string;
} }
/** /**
@@ -37,7 +38,14 @@ export async function setupUser(client: AuthClient): Promise<UserData> {
process.env['GOOGLE_CLOUD_PROJECT'] || process.env['GOOGLE_CLOUD_PROJECT'] ||
process.env['GOOGLE_CLOUD_PROJECT_ID'] || process.env['GOOGLE_CLOUD_PROJECT_ID'] ||
undefined; undefined;
const caServer = new CodeAssistServer(client, projectId, {}, '', undefined); const caServer = new CodeAssistServer(
client,
projectId,
{},
'',
undefined,
undefined,
);
const coreClientMetadata: ClientMetadata = { const coreClientMetadata: ClientMetadata = {
ideType: 'IDE_UNSPECIFIED', ideType: 'IDE_UNSPECIFIED',
platform: 'PLATFORM_UNSPECIFIED', platform: 'PLATFORM_UNSPECIFIED',
@@ -58,6 +66,7 @@ export async function setupUser(client: AuthClient): Promise<UserData> {
return { return {
projectId, projectId,
userTier: loadRes.currentTier.id, userTier: loadRes.currentTier.id,
userTierName: loadRes.currentTier.name,
}; };
} }
throw new ProjectIdRequiredError(); throw new ProjectIdRequiredError();
@@ -65,6 +74,7 @@ export async function setupUser(client: AuthClient): Promise<UserData> {
return { return {
projectId: loadRes.cloudaicompanionProject, projectId: loadRes.cloudaicompanionProject,
userTier: loadRes.currentTier.id, userTier: loadRes.currentTier.id,
userTierName: loadRes.currentTier.name,
}; };
} }
@@ -103,6 +113,7 @@ export async function setupUser(client: AuthClient): Promise<UserData> {
return { return {
projectId, projectId,
userTier: tier.id, userTier: tier.id,
userTierName: tier.name,
}; };
} }
throw new ProjectIdRequiredError(); throw new ProjectIdRequiredError();
@@ -111,6 +122,7 @@ export async function setupUser(client: AuthClient): Promise<UserData> {
return { return {
projectId: lroRes.response.cloudaicompanionProject.id, projectId: lroRes.response.cloudaicompanionProject.id,
userTier: tier.id, userTier: tier.id,
userTierName: tier.name,
}; };
} }
+4
View File
@@ -963,6 +963,10 @@ export class Config {
return this.contentGenerator?.userTier; return this.contentGenerator?.userTier;
} }
getUserTierName(): string | undefined {
return this.contentGenerator?.userTierName;
}
/** /**
* Provides access to the BaseLlmClient for stateless LLM operations. * Provides access to the BaseLlmClient for stateless LLM operations.
*/ */
@@ -44,6 +44,8 @@ export interface ContentGenerator {
embedContent(request: EmbedContentParameters): Promise<EmbedContentResponse>; embedContent(request: EmbedContentParameters): Promise<EmbedContentResponse>;
userTier?: UserTierId; userTier?: UserTierId;
userTierName?: string;
} }
export enum AuthType { export enum AuthType {
@@ -42,6 +42,7 @@ export type FakeResponse =
export class FakeContentGenerator implements ContentGenerator { export class FakeContentGenerator implements ContentGenerator {
private callCounter = 0; private callCounter = 0;
userTier?: UserTierId; userTier?: UserTierId;
userTierName?: string;
constructor(private readonly responses: FakeResponse[]) {} constructor(private readonly responses: FakeResponse[]) {}
@@ -31,6 +31,7 @@ import type { ContentGenerator } from './contentGenerator.js';
import { LoggingContentGenerator } from './loggingContentGenerator.js'; import { LoggingContentGenerator } from './loggingContentGenerator.js';
import type { Config } from '../config/config.js'; import type { Config } from '../config/config.js';
import { ApiRequestEvent } from '../telemetry/types.js'; import { ApiRequestEvent } from '../telemetry/types.js';
import { UserTierId } from '../code_assist/types.js';
describe('LoggingContentGenerator', () => { describe('LoggingContentGenerator', () => {
let wrapped: ContentGenerator; let wrapped: ContentGenerator;
@@ -302,4 +303,16 @@ describe('LoggingContentGenerator', () => {
expect(result).toBe(response); expect(result).toBe(response);
}); });
}); });
describe('delegation', () => {
it('should delegate userTier to wrapped', () => {
wrapped.userTier = UserTierId.STANDARD;
expect(loggingContentGenerator.userTier).toBe(UserTierId.STANDARD);
});
it('should delegate userTierName to wrapped', () => {
wrapped.userTierName = 'Standard Tier';
expect(loggingContentGenerator.userTierName).toBe('Standard Tier');
});
});
}); });
@@ -23,6 +23,7 @@ import {
ApiErrorEvent, ApiErrorEvent,
} from '../telemetry/types.js'; } from '../telemetry/types.js';
import type { Config } from '../config/config.js'; import type { Config } from '../config/config.js';
import type { UserTierId } from '../code_assist/types.js';
import { import {
logApiError, logApiError,
logApiRequest, logApiRequest,
@@ -51,6 +52,14 @@ export class LoggingContentGenerator implements ContentGenerator {
return this.wrapped; return this.wrapped;
} }
get userTier(): UserTierId | undefined {
return this.wrapped.userTier;
}
get userTierName(): string | undefined {
return this.wrapped.userTierName;
}
private logApiRequest( private logApiRequest(
contents: Content[], contents: Content[],
model: string, model: string,
@@ -25,13 +25,19 @@ import { safeJsonStringify } from '../utils/safeJsonStringify.js';
// //
// Note that only the "interesting" bits of the responses are actually kept. // Note that only the "interesting" bits of the responses are actually kept.
export class RecordingContentGenerator implements ContentGenerator { export class RecordingContentGenerator implements ContentGenerator {
userTier?: UserTierId;
constructor( constructor(
private readonly realGenerator: ContentGenerator, private readonly realGenerator: ContentGenerator,
private readonly filePath: string, private readonly filePath: string,
) {} ) {}
get userTier(): UserTierId | undefined {
return this.realGenerator.userTier;
}
get userTierName(): string | undefined {
return this.realGenerator.userTierName;
}
async generateContent( async generateContent(
request: GenerateContentParameters, request: GenerateContentParameters,
userPromptId: string, userPromptId: string,