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

fix(ui): move sync file I/O out of render loop in AppHeader

test(ui): update AppHeader snapshots

test(ui): fix Tips test and update all snapshots for compact header
This commit is contained in:
Keith Guerin
2026-02-09 23:05:54 -08:00
parent a1367e9cdd
commit f307ff00ac
7 changed files with 321 additions and 274 deletions

View File

@@ -2,20 +2,20 @@
exports[`App > Snapshots > renders default layout correctly 1`] = `
"
███ █████████
░░░███ ███░░░░░███
░░░███ ███ ░░░
░░░███░███
███░ ░███ █████
███░ ░░███ ░░███
███░ ░░█████████
░░░ ░░░░░░░░░
▝▜▄ Gemini CLI v1.2.3
▝▜▄
▗▟▀ Authenticated with undefined /auth
▝▀ Gemini Code Assist for individuals /upgrade
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. /help for more information
2. Ask coding questions, edit code or run commands
3. Be specific for the best results
@@ -47,34 +47,29 @@ exports[`App > Snapshots > renders screen reader layout correctly 1`] = `
"Notifications
Footer
███ █████████
░░░███ ███░░░░░███
░░░███ ███ ░░░
░░░███░███
███░ ░███ █████
███░ ░░███ ░░███
███░ ░░█████████
░░░ ░░░░░░░░░
▝▜▄ Gemini CLI v1.2.3
▝▜▄
▗▟▀ Authenticated with undefined /auth
▝▀ Gemini Code Assist for individuals /upgrade
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.
Composer
"
1. /help for more information
2. Ask coding questions, edit code or run commands
3. Be specific for the best results
Composer"
`;
exports[`App > Snapshots > renders with dialogs visible 1`] = `
"
███ █████████
░░░███ ███░░░░░███
░░░███ ███ ░░░
░░░███░███
███░ ░███ █████
███░ ░░███ ░░███
███░ ░░█████████
░░░ ░░░░░░░░░
▝▜▄ Gemini CLI v1.2.3
▝▜▄
▗▟▀ Authenticated with undefined /auth
▝▀ Gemini Code Assist for individuals /upgrade
@@ -110,34 +105,34 @@ DialogManager
exports[`App > should render ToolConfirmationQueue along with Composer when tool is confirming and experiment is on 1`] = `
"
███ █████████
░░░███ ███░░░░░███
░░░███ ███ ░░░
░░░███░███
███░ ░███ █████
███░ ░░███ ░░███
███░ ░░█████████
░░░ ░░░░░░░░░
▝▜▄ Gemini CLI v1.2.3
▝▜▄
▗▟▀ Authenticated with undefined /auth
▝▀ Gemini Code Assist for individuals /upgrade
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. /help for more information
2. Ask coding questions, edit code or run commands
3. Be specific for the best results
HistoryItemDisplay
╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
│ Action Required │
│ │
│ ? ls list directory │
│ │
│ ls │
│ Allow execution of: 'ls'? │
│ │
│ ● 1. Allow once │
│ 2. Allow for this session │
│ 3. No, suggest changes (esc) │
│ │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
│ Action Required
│ ? ls list directory
│ ls
│ Allow execution of: 'ls'?
│ ● 1. Allow once
│ 2. Allow for this session
│ 3. No, suggest changes (esc)
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

View File

@@ -4,55 +4,140 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { Box } from 'ink';
import { Header } from './Header.js';
import { Box, Text } from 'ink';
import { useEffect, useMemo, useState } from 'react';
import { UserAccountManager, AuthType } from '@google/gemini-cli-core';
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';
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 authType = config.getContentGeneratorConfig()?.authType;
const [email, setEmail] = useState<string | undefined>();
useEffect(() => {
if (authType) {
const userAccountManager = new UserAccountManager();
// Even though the current implementation of getCachedGoogleAccount is sync,
// it performs file I/O. Moving it to useEffect ensures it doesn't block the render cycle.
setEmail(userAccountManager.getCachedGoogleAccount() ?? undefined);
}
}, [authType]);
const tierName = useMemo(() => config.getUserTierName(), [config]);
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} />
{/* Line 3: User Email /auth */}
<Box>
<Text color={theme.text.primary}>
{authType === AuthType.LOGIN_WITH_GOOGLE ? (
<Text>{email ?? 'Logged in with Google'}</Text>
) : (
`Authenticated with ${authType}`
)}
</Text>
<Text color={theme.text.secondary}> /auth</Text>
</Box>
{/* Line 4: Tier Name /upgrade */}
<Box>
<Text color={theme.text.primary}>
{tierName ?? 'Gemini Code Assist for individuals'}
</Text>
<Text color={theme.text.secondary}> /upgrade</Text>
</Box>
</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

@@ -6,27 +6,23 @@
import { render } from '../../test-utils/render.js';
import { Tips } from './Tips.js';
import { describe, it, expect, vi } from 'vitest';
import { describe, it, expect } from 'vitest';
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;
it('renders correct tips', async () => {
const config = {} 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();
const output = lastFrame();
expect(output).toContain('1. /help for more information');
expect(output).toContain(
'2. Ask coding questions, edit code or run commands',
);
expect(output).toContain('3. Be specific for the best results');
unmount();
});
});

View File

@@ -13,33 +13,15 @@ interface TipsProps {
config: Config;
}
export const Tips: React.FC<TipsProps> = ({ config }) => {
const geminiMdFileCount = config.getGeminiMdFileCount();
return (
<Box flexDirection="column">
<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.
</Text>
)}
<Text color={theme.text.primary}>
{geminiMdFileCount === 0 ? '4.' : '3.'}{' '}
<Text bold color={theme.text.accent}>
/help
</Text>{' '}
for more information.
</Text>
</Box>
);
};
export const Tips: React.FC<TipsProps> = () => (
<Box flexDirection="column" marginTop={1}>
<Text color={theme.text.primary}>Tips for getting started:</Text>
<Text color={theme.text.primary}>
1. <Text color={theme.text.secondary}>/help</Text> for more information
</Text>
<Text color={theme.text.primary}>
2. Ask coding questions, edit code or run commands
</Text>
<Text color={theme.text.primary}>3. Be specific for the best results</Text>
</Box>
);

View File

@@ -2,20 +2,16 @@
exports[`AlternateBufferQuittingDisplay > renders with a tool awaiting confirmation > with_confirming_tool 1`] = `
"
███ █████████
░░░███ ███░░░░░███
░░░███ ███ ░░░
░░░███░███
███░ ░███ █████
███░ ░░███ ░░███
███░ ░░█████████
░░░ ░░░░░░░░░
▝▜▄ Gemini CLI v0.10.0
▝▜▄
▗▟▀ Authenticated with undefined /auth
▝▀ Gemini Code Assist for individuals /upgrade
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. /help for more information
2. Ask coding questions, edit code or run commands
3. Be specific for the best results
Action Required (was prompted):
@@ -25,20 +21,16 @@ Action Required (was prompted):
exports[`AlternateBufferQuittingDisplay > renders with active and pending tool messages > with_history_and_pending 1`] = `
"
███ █████████
░░░███ ███░░░░░███
░░░███ ███ ░░░
░░░███░███
███░ ░███ █████
███░ ░░███ ░░███
███░ ░░█████████
░░░ ░░░░░░░░░
▝▜▄ Gemini CLI v0.10.0
▝▜▄
▗▟▀ Authenticated with undefined /auth
▝▀ Gemini Code Assist for individuals /upgrade
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. /help for more information
2. Ask coding questions, edit code or run commands
3. Be specific for the best results
╭──────────────────────────────────────────────────────────────────────────╮
│ ✓ tool1 Description for tool 1 │
│ │
@@ -46,45 +38,35 @@ Tips for getting started:
╭──────────────────────────────────────────────────────────────────────────╮
│ ✓ tool2 Description for tool 2 │
│ │
╰──────────────────────────────────────────────────────────────────────────╯
"
╰──────────────────────────────────────────────────────────────────────────╯"
`;
exports[`AlternateBufferQuittingDisplay > renders with empty history and no pending items > empty 1`] = `
"
███ █████████
░░░███ ███░░░░░███
░░░███ ███ ░░░
░░░███░███
███░ ░███ █████
███░ ░░███ ░░███
███░ ░░█████████
░░░ ░░░░░░░░░
▝▜▄ Gemini CLI v0.10.0
▝▜▄
▗▟▀ Authenticated with undefined /auth
▝▀ Gemini Code Assist for individuals /upgrade
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. /help for more information
2. Ask coding questions, edit code or run commands
3. 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
▝▜▄
▗▟▀ Authenticated with undefined /auth
▝▀ Gemini Code Assist for individuals /upgrade
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. /help for more information
2. Ask coding questions, edit code or run commands
3. Be specific for the best results
╭──────────────────────────────────────────────────────────────────────────╮
│ ✓ tool1 Description for tool 1 │
│ │
@@ -92,47 +74,37 @@ Tips for getting started:
╭──────────────────────────────────────────────────────────────────────────╮
│ ✓ tool2 Description for tool 2 │
│ │
╰──────────────────────────────────────────────────────────────────────────╯
"
╰──────────────────────────────────────────────────────────────────────────╯"
`;
exports[`AlternateBufferQuittingDisplay > renders with pending items but no history > with_pending_no_history 1`] = `
"
███ █████████
░░░███ ███░░░░░███
░░░███ ███ ░░░
░░░███░███
███░ ░███ █████
███░ ░░███ ░░███
███░ ░░█████████
░░░ ░░░░░░░░░
▝▜▄ Gemini CLI v0.10.0
▝▜▄
▗▟▀ Authenticated with undefined /auth
▝▀ Gemini Code Assist for individuals /upgrade
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. /help for more information
2. Ask coding questions, edit code or run commands
3. Be specific for the best results"
`;
exports[`AlternateBufferQuittingDisplay > renders with user and gemini messages > with_user_gemini_messages 1`] = `
"
███ █████████
░░░███ ███░░░░░███
░░░███ ███ ░░░
░░░███░███
███░ ░███ █████
███░ ░░███ ░░███
███░ ░░█████████
░░░ ░░░░░░░░░
▝▜▄ Gemini CLI v0.10.0
▝▜▄
▗▟▀ Authenticated with undefined /auth
▝▀ Gemini Code Assist for individuals /upgrade
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. /help for more information
2. Ask coding questions, edit code or run commands
3. Be specific for the best results
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
> Hello Gemini
> Hello Gemini
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
✦ Hello User!
"

View File

@@ -2,82 +2,62 @@
exports[`<AppHeader /> > should not render the banner when no flags are set 1`] = `
"
███ █████████
░░░███ ███░░░░░███
░░░███ ███ ░░░
░░░███░███
███░ ░███ █████
███░ ░░███ ░░███
███░ ░░█████████
░░░ ░░░░░░░░░
▝▜▄ Gemini CLI v1.0.0
▝▜▄
▗▟▀ Authenticated with undefined /auth
▝▀ Gemini Code Assist for individuals /upgrade
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. /help for more information
2. Ask coding questions, edit code or run commands
3. 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
▝▜▄
▗▟▀ Authenticated with undefined /auth
▝▀ Gemini Code Assist for individuals /upgrade
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. /help for more information
2. Ask coding questions, edit code or run commands
3. Be specific for the best results"
`;
exports[`<AppHeader /> > should render the banner with default text 1`] = `
"
███ █████████
░░░███ ███░░░░░███
░░░███ ███ ░░░
░░░███░███
███░ ░███ █████
███░ ░░███ ░░███
███░ ░░█████████
░░░ ░░░░░░░░░
▝▜▄ Gemini CLI v1.0.0
▝▜▄
▗▟▀ Authenticated with undefined /auth
▝▀ Gemini Code Assist for individuals /upgrade
╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ This is the default banner │
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
│ 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. /help for more information
2. Ask coding questions, edit code or run commands
3. Be specific for the best results"
`;
exports[`<AppHeader /> > should render the banner with warning text 1`] = `
"
███ █████████
░░░███ ███░░░░░███
░░░███ ███ ░░░
░░░███░███
███░ ░███ █████
███░ ░░███ ░░███
███░ ░░█████████
░░░ ░░░░░░░░░
▝▜▄ Gemini CLI v1.0.0
▝▜▄
▗▟▀ Authenticated with undefined /auth
▝▀ Gemini Code Assist for individuals /upgrade
╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ There are capacity issues │
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
│ 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. /help for more information
2. Ask coding questions, edit code or run commands
3. Be specific for the best results"
`;

View File

@@ -8,20 +8,57 @@ import type React from 'react';
import { Box } from 'ink';
import { theme } from '../../semantic-colors.js';
export type LinePosition = 'top' | 'center' | 'bottom';
interface HorizontalLineProps {
color?: string;
width?: number | string;
position?: LinePosition;
}
const overlineStyle = {
top: '‾',
bottom: '',
left: '',
right: '',
topLeft: '',
topRight: '',
bottomLeft: '',
bottomRight: '',
};
const underlineStyle = {
top: '_',
bottom: '',
left: '',
right: '',
topLeft: '',
topRight: '',
bottomLeft: '',
bottomRight: '',
};
export const HorizontalLine: React.FC<HorizontalLineProps> = ({
color = theme.border.default,
}) => (
<Box
width="100%"
borderStyle="single"
borderTop
borderBottom={false}
borderLeft={false}
borderRight={false}
borderColor={color}
/>
);
width = '100%',
position = 'center',
}) => {
const borderStyle =
position === 'top'
? overlineStyle
: position === 'bottom'
? underlineStyle
: 'single';
return (
<Box
width={width}
borderStyle={borderStyle}
borderTop
borderBottom={false}
borderLeft={false}
borderRight={false}
borderColor={color}
/>
);
};