diff --git a/packages/cli/src/ui/components/AppHeader.tsx b/packages/cli/src/ui/components/AppHeader.tsx index b9601e772a..0b15f917a6 100644 --- a/packages/cli/src/ui/components/AppHeader.tsx +++ b/packages/cli/src/ui/components/AppHeader.tsx @@ -17,16 +17,30 @@ import { theme } from '../semantic-colors.js'; import { ThemedGradient } from './ThemedGradient.js'; import { CliSpinner } from './CliSpinner.js'; +import { isAppleTerminal } from '@google/gemini-cli-core'; + interface AppHeaderProps { version: string; showDetails?: boolean; } -const ICON = `▝▜▄ +const DEFAULT_ICON = `▝▜▄ ▝▜▄ ▗▟▀ ▝▀ `; +/** + * The default Apple Terminal.app adds significant line-height padding between + * rows. This breaks Unicode block-drawing characters that rely on vertical + * adjacency (like half-blocks). This version is perfectly symmetric vertically, + * which makes the padding gaps look like an intentional "scanline" design + * rather than a broken image. + */ +const MAC_TERMINAL_ICON = `▝▜▄ + ▝▜▄ + ▗▟▀ +▗▟▀ `; + export const AppHeader = ({ version, showDetails = true }: AppHeaderProps) => { const settings = useSettings(); const config = useConfig(); @@ -39,6 +53,8 @@ export const AppHeader = ({ version, showDetails = true }: AppHeaderProps) => { settings.merged.ui.hideBanner || config.getScreenReader() ); + const ICON = isAppleTerminal() ? MAC_TERMINAL_ICON : DEFAULT_ICON; + if (!showDetails) { return ( diff --git a/packages/cli/src/ui/components/AppHeaderIcon.test.tsx b/packages/cli/src/ui/components/AppHeaderIcon.test.tsx new file mode 100644 index 0000000000..c16febea66 --- /dev/null +++ b/packages/cli/src/ui/components/AppHeaderIcon.test.tsx @@ -0,0 +1,49 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { renderWithProviders } from '../../test-utils/render.js'; +import { AppHeader } from './AppHeader.js'; + +// We mock the entire module to control the isAppleTerminal export +vi.mock('@google/gemini-cli-core', async (importOriginal) => { + const actual = + await importOriginal(); + return { + ...actual, + isAppleTerminal: vi.fn(), + }; +}); + +import { isAppleTerminal } from '@google/gemini-cli-core'; + +describe('AppHeader Icon Rendering', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + afterEach(() => { + vi.unstubAllEnvs(); + }); + + it('renders the default icon in standard terminals', async () => { + vi.mocked(isAppleTerminal).mockReturnValue(false); + + const result = renderWithProviders(); + await result.waitUntilReady(); + + await expect(result).toMatchSvgSnapshot(); + }); + + it('renders the symmetric icon in Apple Terminal', async () => { + vi.mocked(isAppleTerminal).mockReturnValue(true); + + const result = renderWithProviders(); + await result.waitUntilReady(); + + await expect(result).toMatchSvgSnapshot(); + }); +}); diff --git a/packages/cli/src/ui/components/UserIdentity.test.tsx b/packages/cli/src/ui/components/UserIdentity.test.tsx index 5391944d26..aa7f4d3da2 100644 --- a/packages/cli/src/ui/components/UserIdentity.test.tsx +++ b/packages/cli/src/ui/components/UserIdentity.test.tsx @@ -51,6 +51,24 @@ describe('', () => { unmount(); }); + it('should render the user email on the very first frame (regression test)', () => { + 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 { lastFrameRaw, unmount } = renderWithProviders( + , + ); + + // Assert immediately on the first available frame before any async ticks happen + const output = lastFrameRaw(); + expect(output).toContain('test@example.com'); + unmount(); + }); + it('should render login message if email is missing', async () => { // Modify the mock for this specific test vi.mocked(UserAccountManager).mockImplementationOnce( diff --git a/packages/cli/src/ui/components/UserIdentity.tsx b/packages/cli/src/ui/components/UserIdentity.tsx index 98c62ec68f..7b07a4f91c 100644 --- a/packages/cli/src/ui/components/UserIdentity.tsx +++ b/packages/cli/src/ui/components/UserIdentity.tsx @@ -5,7 +5,7 @@ */ import type React from 'react'; -import { useMemo, useEffect, useState } from 'react'; +import { useMemo } from 'react'; import { Box, Text } from 'ink'; import { theme } from '../semantic-colors.js'; import { @@ -20,13 +20,12 @@ interface UserIdentityProps { export const UserIdentity: React.FC = ({ config }) => { const authType = config.getContentGeneratorConfig()?.authType; - const [email, setEmail] = useState(); - - useEffect(() => { + const email = useMemo(() => { if (authType) { const userAccountManager = new UserAccountManager(); - setEmail(userAccountManager.getCachedGoogleAccount() ?? undefined); + return userAccountManager.getCachedGoogleAccount() ?? undefined; } + return undefined; }, [authType]); const tierName = useMemo( diff --git a/packages/cli/src/ui/components/__snapshots__/AppHeaderIcon-AppHeader-Icon-Rendering-renders-the-default-icon-in-standard-terminals.snap.svg b/packages/cli/src/ui/components/__snapshots__/AppHeaderIcon-AppHeader-Icon-Rendering-renders-the-default-icon-in-standard-terminals.snap.svg new file mode 100644 index 0000000000..4e9d0e67a5 --- /dev/null +++ b/packages/cli/src/ui/components/__snapshots__/AppHeaderIcon-AppHeader-Icon-Rendering-renders-the-default-icon-in-standard-terminals.snap.svg @@ -0,0 +1,30 @@ + + + + + + + + Gemini CLI + v1.0.0 + + + + + + + + + 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 + + \ No newline at end of file diff --git a/packages/cli/src/ui/components/__snapshots__/AppHeaderIcon-AppHeader-Icon-Rendering-renders-the-symmetric-icon-in-Apple-Terminal.snap.svg b/packages/cli/src/ui/components/__snapshots__/AppHeaderIcon-AppHeader-Icon-Rendering-renders-the-symmetric-icon-in-Apple-Terminal.snap.svg new file mode 100644 index 0000000000..fa8373acc7 --- /dev/null +++ b/packages/cli/src/ui/components/__snapshots__/AppHeaderIcon-AppHeader-Icon-Rendering-renders-the-symmetric-icon-in-Apple-Terminal.snap.svg @@ -0,0 +1,31 @@ + + + + + + + + Gemini CLI + v1.0.0 + + + + + + + + + + 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 + + \ No newline at end of file diff --git a/packages/cli/src/ui/components/__snapshots__/AppHeaderIcon.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/AppHeaderIcon.test.tsx.snap new file mode 100644 index 0000000000..2bb5276ee8 --- /dev/null +++ b/packages/cli/src/ui/components/__snapshots__/AppHeaderIcon.test.tsx.snap @@ -0,0 +1,31 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`AppHeader Icon Rendering > renders the default icon in standard terminals 1`] = ` +" + ▝▜▄ Gemini CLI v1.0.0 + ▝▜▄ + ▗▟▀ + ▝▀ + + +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[`AppHeader Icon Rendering > renders the symmetric icon in Apple Terminal 1`] = ` +" + ▝▜▄ Gemini CLI v1.0.0 + ▝▜▄ + ▗▟▀ + ▗▟▀ + + +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" +`;