feat: redesign header to be compact with ASCII icon (#18713)

Co-authored-by: Jacob Richman <jacob314@gmail.com>
This commit is contained in:
Keith Guerin
2026-03-02 13:12:17 -08:00
committed by GitHub
parent b7a8f0d1f9
commit 31ca57ec94
15 changed files with 382 additions and 631 deletions

View File

@@ -213,6 +213,12 @@ describe('<AppHeader />', () => {
it('should NOT render Tips when tipsShown is 10 or more', async () => {
const mockConfig = makeFakeConfig();
const uiState = {
bannerData: {
defaultText: '',
warningText: '',
},
};
persistentStateMock.setData({ tipsShown: 10 });
@@ -220,6 +226,7 @@ describe('<AppHeader />', () => {
<AppHeader version="1.0.0" />,
{
config: mockConfig,
uiState,
},
);
await waitUntilReady();

View File

@@ -1,58 +1,113 @@
/**
* @license
* Copyright 2025 Google LLC
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { Box } from 'ink';
import { Header } from './Header.js';
import { Tips } from './Tips.js';
import { Box, Text } from 'ink';
import { UserIdentity } from './UserIdentity.js';
import { Tips } from './Tips.js';
import { useSettings } from '../contexts/SettingsContext.js';
import { useConfig } from '../contexts/ConfigContext.js';
import { useUIState } from '../contexts/UIStateContext.js';
import { Banner } from './Banner.js';
import { useBanner } from '../hooks/useBanner.js';
import { useTips } from '../hooks/useTips.js';
import { theme } from '../semantic-colors.js';
import { ThemedGradient } from './ThemedGradient.js';
import { CliSpinner } from './CliSpinner.js';
interface AppHeaderProps {
version: string;
showDetails?: boolean;
}
const ICON = `▝▜▄
▝▜▄
▗▟▀
▝▀ `;
export const AppHeader = ({ version, showDetails = true }: AppHeaderProps) => {
const settings = useSettings();
const config = useConfig();
const { nightly, terminalWidth, bannerData, bannerVisible } = useUIState();
const { terminalWidth, bannerData, bannerVisible, updateInfo } = useUIState();
const { bannerText } = useBanner(bannerData);
const { showTips } = useTips();
const showHeader = !(
settings.merged.ui.hideBanner || config.getScreenReader()
);
if (!showDetails) {
return (
<Box flexDirection="column">
<Header version={version} nightly={false} />
{showHeader && (
<Box
flexDirection="row"
marginTop={1}
marginBottom={1}
paddingLeft={2}
>
<Box flexShrink={0}>
<ThemedGradient>{ICON}</ThemedGradient>
</Box>
<Box marginLeft={2} flexDirection="column">
<Box>
<Text bold color={theme.text.primary}>
Gemini CLI
</Text>
<Text color={theme.text.secondary}> v{version}</Text>
</Box>
</Box>
</Box>
)}
</Box>
);
}
return (
<Box flexDirection="column">
{!(settings.merged.ui.hideBanner || config.getScreenReader()) && (
<>
<Header version={version} nightly={nightly} />
{bannerVisible && bannerText && (
<Banner
width={terminalWidth}
bannerText={bannerText}
isWarning={bannerData.warningText !== ''}
/>
)}
</>
{showHeader && (
<Box flexDirection="row" marginTop={1} marginBottom={1} paddingLeft={2}>
<Box flexShrink={0}>
<ThemedGradient>{ICON}</ThemedGradient>
</Box>
<Box marginLeft={2} flexDirection="column">
{/* Line 1: Gemini CLI vVersion [Updating] */}
<Box>
<Text bold color={theme.text.primary}>
Gemini CLI
</Text>
<Text color={theme.text.secondary}> v{version}</Text>
{updateInfo && (
<Box marginLeft={2}>
<Text color={theme.text.secondary}>
<CliSpinner /> Updating
</Text>
</Box>
)}
</Box>
{/* Line 2: Blank */}
<Box height={1} />
{/* Lines 3 & 4: User Identity info (Email /auth and Plan /upgrade) */}
{settings.merged.ui.showUserIdentity !== false && (
<UserIdentity config={config} />
)}
</Box>
</Box>
)}
{settings.merged.ui.showUserIdentity !== false && (
<UserIdentity config={config} />
{bannerVisible && bannerText && (
<Banner
width={terminalWidth}
bannerText={bannerText}
isWarning={bannerData.warningText !== ''}
/>
)}
{!(settings.merged.ui.hideTips || config.getScreenReader()) &&
showTips && <Tips config={config} />}
</Box>

View File

@@ -1,6 +1,6 @@
/**
* @license
* Copyright 2025 Google LLC
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
@@ -11,22 +11,18 @@ import type { Config } from '@google/gemini-cli-core';
describe('Tips', () => {
it.each([
[0, '3. Create GEMINI.md files'],
[5, '3. /help for more information'],
])(
'renders correct tips when file count is %i',
async (count, expectedText) => {
const config = {
getGeminiMdFileCount: vi.fn().mockReturnValue(count),
} as unknown as Config;
{ fileCount: 0, description: 'renders all tips including GEMINI.md tip' },
{ fileCount: 5, description: 'renders fewer tips when GEMINI.md exists' },
])('$description', async ({ fileCount }) => {
const config = {
getGeminiMdFileCount: vi.fn().mockReturnValue(fileCount),
} as unknown as Config;
const { lastFrame, waitUntilReady, unmount } = render(
<Tips config={config} />,
);
await waitUntilReady();
const output = lastFrame();
expect(output).toContain(expectedText);
unmount();
},
);
const { lastFrame, waitUntilReady, unmount } = render(
<Tips config={config} />,
);
await waitUntilReady();
expect(lastFrame()).toMatchSnapshot();
unmount();
});
});

View File

@@ -1,6 +1,6 @@
/**
* @license
* Copyright 2025 Google LLC
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
@@ -15,30 +15,26 @@ interface TipsProps {
export const Tips: React.FC<TipsProps> = ({ config }) => {
const geminiMdFileCount = config.getGeminiMdFileCount();
return (
<Box flexDirection="column">
<Box flexDirection="column" marginTop={1}>
<Text color={theme.text.primary}>Tips for getting started:</Text>
<Text color={theme.text.primary}>
1. Ask questions, edit files, or run commands.
</Text>
<Text color={theme.text.primary}>
2. Be specific for the best results.
</Text>
{geminiMdFileCount === 0 && (
<Text color={theme.text.primary}>
3. Create{' '}
<Text bold color={theme.text.accent}>
GEMINI.md
</Text>{' '}
files to customize your interactions with Gemini.
1. Create <Text bold>GEMINI.md</Text> files to customize your
interactions
</Text>
)}
<Text color={theme.text.primary}>
{geminiMdFileCount === 0 ? '4.' : '3.'}{' '}
<Text bold color={theme.text.accent}>
/help
</Text>{' '}
for more information.
{geminiMdFileCount === 0 ? '2.' : '1.'}{' '}
<Text color={theme.text.secondary}>/help</Text> for more information
</Text>
<Text color={theme.text.primary}>
{geminiMdFileCount === 0 ? '3.' : '2.'} Ask coding questions, edit code
or run commands
</Text>
<Text color={theme.text.primary}>
{geminiMdFileCount === 0 ? '4.' : '3.'} Be specific for the best results
</Text>
</Box>
);

View File

@@ -45,12 +45,12 @@ describe('<UserIdentity />', () => {
await waitUntilReady();
const output = lastFrame();
expect(output).toContain('Logged in with Google: test@example.com');
expect(output).toContain('test@example.com');
expect(output).toContain('/auth');
unmount();
});
it('should render login message without colon if email is missing', async () => {
it('should render login message if email is missing', async () => {
// Modify the mock for this specific test
vi.mocked(UserAccountManager).mockImplementationOnce(
() =>
@@ -73,12 +73,11 @@ describe('<UserIdentity />', () => {
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', async () => {
it('should render plan name and upgrade indicator', async () => {
const mockConfig = makeFakeConfig();
vi.spyOn(mockConfig, 'getContentGeneratorConfig').mockReturnValue({
authType: AuthType.LOGIN_WITH_GOOGLE,
@@ -92,18 +91,10 @@ describe('<UserIdentity />', () => {
await waitUntilReady();
const output = lastFrame();
expect(output).toContain('Logged in with Google: test@example.com');
expect(output).toContain('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,
);
expect(output).toContain('Premium Plan');
expect(output).toContain('/upgrade');
unmount();
});

View File

@@ -1,11 +1,11 @@
/**
* @license
* Copyright 2025 Google LLC
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type React from 'react';
import { useMemo } from 'react';
import { useMemo, useEffect, useState } from 'react';
import { Box, Text } from 'ink';
import { theme } from '../semantic-colors.js';
import {
@@ -20,42 +20,45 @@ interface UserIdentityProps {
export const UserIdentity: React.FC<UserIdentityProps> = ({ config }) => {
const authType = config.getContentGeneratorConfig()?.authType;
const [email, setEmail] = useState<string | undefined>();
const { email, tierName } = useMemo(() => {
if (!authType) {
return { email: undefined, tierName: undefined };
useEffect(() => {
if (authType) {
const userAccountManager = new UserAccountManager();
setEmail(userAccountManager.getCachedGoogleAccount() ?? undefined);
}
const userAccountManager = new UserAccountManager();
return {
email: userAccountManager.getCachedGoogleAccount(),
tierName: config.getUserTierName(),
};
}, [config, authType]);
}, [authType]);
const tierName = useMemo(
() => (authType ? config.getUserTierName() : undefined),
[config, authType],
);
if (!authType) {
return null;
}
return (
<Box marginTop={1} flexDirection="column">
<Box flexDirection="column">
{/* User Email /auth */}
<Box>
<Text color={theme.text.primary}>
<Text color={theme.text.primary} wrap="truncate-end">
{authType === AuthType.LOGIN_WITH_GOOGLE ? (
<Text>
<Text bold>Logged in with Google{email ? ':' : ''}</Text>
{email ? ` ${email}` : ''}
</Text>
<Text>{email ?? 'Logged in with Google'}</Text>
) : (
`Authenticated with ${authType}`
)}
</Text>
<Text color={theme.text.secondary}> /auth</Text>
</Box>
{tierName && (
<Text color={theme.text.primary}>
<Text bold>Plan:</Text> {tierName}
{/* Tier Name /upgrade */}
<Box>
<Text color={theme.text.primary} wrap="truncate-end">
{tierName ?? 'Gemini Code Assist for individuals'}
</Text>
)}
<Text color={theme.text.secondary}> /upgrade</Text>
</Box>
</Box>
);
};

View File

@@ -2,20 +2,17 @@
exports[`AlternateBufferQuittingDisplay > renders with a tool awaiting confirmation > with_confirming_tool 1`] = `
"
███ █████████
░░░███ ███░░░░░███
░░░███ ███ ░░░
░░░███░███
███░ ░███ █████
███░ ░░███ ░░███
███░ ░░█████████
░░░ ░░░░░░░░░
▝▜▄ Gemini CLI v0.10.0
▝▜▄
▗▟▀
▝▀
Tips for getting started:
1. Ask questions, edit files, or run commands.
2. Be specific for the best results.
3. Create GEMINI.md files to customize your interactions with Gemini.
4. /help for more information.
1. Create GEMINI.md files to customize your interactions
2. /help for more information
3. Ask coding questions, edit code or run commands
4. Be specific for the best results
Action Required (was prompted):
@@ -25,20 +22,17 @@ Action Required (was prompted):
exports[`AlternateBufferQuittingDisplay > renders with active and pending tool messages > with_history_and_pending 1`] = `
"
███ █████████
░░░███ ███░░░░░███
░░░███ ███ ░░░
░░░███░███
███░ ░███ █████
███░ ░░███ ░░███
███░ ░░█████████
░░░ ░░░░░░░░░
▝▜▄ Gemini CLI v0.10.0
▝▜▄
▗▟▀
▝▀
Tips for getting started:
1. Ask questions, edit files, or run commands.
2. Be specific for the best results.
3. Create GEMINI.md files to customize your interactions with Gemini.
4. /help for more information.
1. Create GEMINI.md files to customize your interactions
2. /help for more information
3. Ask coding questions, edit code or run commands
4. Be specific for the best results
╭──────────────────────────────────────────────────────────────────────────╮
│ ✓ tool1 Description for tool 1 │
│ │
@@ -52,39 +46,33 @@ Tips for getting started:
exports[`AlternateBufferQuittingDisplay > renders with empty history and no pending items > empty 1`] = `
"
███ █████████
░░░███ ███░░░░░███
░░░███ ███ ░░░
░░░███░███
███░ ░███ █████
███░ ░░███ ░░███
███░ ░░█████████
░░░ ░░░░░░░░░
▝▜▄ Gemini CLI v0.10.0
▝▜▄
▗▟▀
▝▀
Tips for getting started:
1. Ask questions, edit files, or run commands.
2. Be specific for the best results.
3. Create GEMINI.md files to customize your interactions with Gemini.
4. /help for more information.
1. Create GEMINI.md files to customize your interactions
2. /help for more information
3. Ask coding questions, edit code or run commands
4. Be specific for the best results
"
`;
exports[`AlternateBufferQuittingDisplay > renders with history but no pending items > with_history_no_pending 1`] = `
"
███ █████████
░░░███ ███░░░░░███
░░░███ ███ ░░░
░░░███░███
███░ ░███ █████
███░ ░░███ ░░███
███░ ░░█████████
░░░ ░░░░░░░░░
▝▜▄ Gemini CLI v0.10.0
▝▜▄
▗▟▀
▝▀
Tips for getting started:
1. Ask questions, edit files, or run commands.
2. Be specific for the best results.
3. Create GEMINI.md files to customize your interactions with Gemini.
4. /help for more information.
1. Create GEMINI.md files to customize your interactions
2. /help for more information
3. Ask coding questions, edit code or run commands
4. Be specific for the best results
╭──────────────────────────────────────────────────────────────────────────╮
│ ✓ tool1 Description for tool 1 │
│ │
@@ -98,39 +86,33 @@ Tips for getting started:
exports[`AlternateBufferQuittingDisplay > renders with pending items but no history > with_pending_no_history 1`] = `
"
███ █████████
░░░███ ███░░░░░███
░░░███ ███ ░░░
░░░███░███
███░ ░███ █████
███░ ░░███ ░░███
███░ ░░█████████
░░░ ░░░░░░░░░
▝▜▄ Gemini CLI v0.10.0
▝▜▄
▗▟▀
▝▀
Tips for getting started:
1. Ask questions, edit files, or run commands.
2. Be specific for the best results.
3. Create GEMINI.md files to customize your interactions with Gemini.
4. /help for more information.
1. Create GEMINI.md files to customize your interactions
2. /help for more information
3. Ask coding questions, edit code or run commands
4. Be specific for the best results
"
`;
exports[`AlternateBufferQuittingDisplay > renders with user and gemini messages > with_user_gemini_messages 1`] = `
"
███ █████████
░░░███ ███░░░░░███
░░░███ ███ ░░░
░░░███░███
███░ ░███ █████
███░ ░░███ ░░███
███░ ░░█████████
░░░ ░░░░░░░░░
▝▜▄ Gemini CLI v0.10.0
▝▜▄
▗▟▀
▝▀
Tips for getting started:
1. Ask questions, edit files, or run commands.
2. Be specific for the best results.
3. Create GEMINI.md files to customize your interactions with Gemini.
4. /help for more information.
1. Create GEMINI.md files to customize your interactions
2. /help for more information
3. Ask coding questions, edit code or run commands
4. Be specific for the best results
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
> Hello Gemini
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄

View File

@@ -2,82 +2,70 @@
exports[`<AppHeader /> > should not render the banner when no flags are set 1`] = `
"
███ █████████
░░░███ ███░░░░░███
░░░███ ███ ░░░
░░░███░███
███░ ░███ █████
███░ ░░███ ░░███
███░ ░░█████████
░░░ ░░░░░░░░░
▝▜▄ Gemini CLI v1.0.0
▝▜▄
▗▟▀
▝▀
Tips for getting started:
1. Ask questions, edit files, or run commands.
2. Be specific for the best results.
3. Create GEMINI.md files to customize your interactions with Gemini.
4. /help for more information.
1. Create GEMINI.md files to customize your interactions
2. /help for more information
3. Ask coding questions, edit code or run commands
4. Be specific for the best results
"
`;
exports[`<AppHeader /> > should not render the default banner if shown count is 5 or more 1`] = `
"
███ █████████
░░░███ ███░░░░░███
░░░███ ███ ░░░
░░░███░███
███░ ░███ █████
███░ ░░███ ░░███
███░ ░░█████████
░░░ ░░░░░░░░░
▝▜▄ Gemini CLI v1.0.0
▝▜▄
▗▟▀
▝▀
Tips for getting started:
1. Ask questions, edit files, or run commands.
2. Be specific for the best results.
3. Create GEMINI.md files to customize your interactions with Gemini.
4. /help for more information.
1. Create GEMINI.md files to customize your interactions
2. /help for more information
3. Ask coding questions, edit code or run commands
4. Be specific for the best results
"
`;
exports[`<AppHeader /> > should render the banner with default text 1`] = `
"
███ █████████
░░░███ ███░░░░░███
░░░███ ███ ░░░
░░░███░███
███░ ░███ █████
███░ ░░███ ░░███
███░ ░░█████████
░░░ ░░░░░░░░░
▝▜▄ Gemini CLI v1.0.0
▝▜▄
▗▟▀
▝▀
╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
│ This is the default banner │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
Tips for getting started:
1. Ask questions, edit files, or run commands.
2. Be specific for the best results.
3. Create GEMINI.md files to customize your interactions with Gemini.
4. /help for more information.
1. Create GEMINI.md files to customize your interactions
2. /help for more information
3. Ask coding questions, edit code or run commands
4. Be specific for the best results
"
`;
exports[`<AppHeader /> > should render the banner with warning text 1`] = `
"
███ █████████
░░░███ ███░░░░░███
░░░███ ███ ░░░
░░░███░███
███░ ░███ █████
███░ ░░███ ░░███
███░ ░░█████████
░░░ ░░░░░░░░░
▝▜▄ Gemini CLI v1.0.0
▝▜▄
▗▟▀
▝▀
╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
│ There are capacity issues │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
Tips for getting started:
1. Ask questions, edit files, or run commands.
2. Be specific for the best results.
3. Create GEMINI.md files to customize your interactions with Gemini.
4. /help for more information.
1. Create GEMINI.md files to customize your interactions
2. /help for more information
3. Ask coding questions, edit code or run commands
4. Be specific for the best results
"
`;

View File

@@ -0,0 +1,20 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`Tips > 'renders all tips including GEMINI.md …' 1`] = `
"
Tips for getting started:
1. Create GEMINI.md files to customize your interactions
2. /help for more information
3. Ask coding questions, edit code or run commands
4. Be specific for the best results
"
`;
exports[`Tips > 'renders fewer tips when GEMINI.md exi…' 1`] = `
"
Tips for getting started:
1. /help for more information
2. Ask coding questions, edit code or run commands
3. Be specific for the best results
"
`;