diff --git a/packages/cli/src/ui/components/AppHeader.test.tsx b/packages/cli/src/ui/components/AppHeader.test.tsx index 74388c816a..ca7488846f 100644 --- a/packages/cli/src/ui/components/AppHeader.test.tsx +++ b/packages/cli/src/ui/components/AppHeader.test.tsx @@ -200,4 +200,83 @@ describe('', () => { expect(lastFrame()).not.toContain('First line\\nSecond line'); unmount(); }); + + it('should render Tips when tipsShown is false', () => { + persistentStateMock.get.mockImplementation((key) => { + if (key === 'tipsShown') return false; + return {}; + }); + + const mockConfig = makeFakeConfig(); + const uiState = { + history: [], + bannerData: { + defaultText: 'First line\\nSecond line', + warningText: '', + }, + bannerVisible: true, + }; + const { lastFrame, unmount } = renderWithProviders( + , + { config: mockConfig, uiState }, + ); + + expect(lastFrame()).toContain('Tips'); + + expect(persistentStateMock.set).toHaveBeenCalledWith('tipsShown', true); + unmount(); + }); + + it('should NOT render Tips when tipsShown is true', () => { + persistentStateMock.get.mockImplementation((key) => { + if (key === 'tipsShown') return true; + return {}; + }); + + const mockConfig = makeFakeConfig(); + const { lastFrame, unmount } = renderWithProviders( + , + { config: mockConfig }, + ); + + expect(lastFrame()).not.toContain('Tips'); + unmount(); + }); + + it('should show tips on the first run and hide them on the second run (persistence flow)', () => { + const fakeStore: Record = {}; + + persistentStateMock.get.mockImplementation((key) => fakeStore[key]); + persistentStateMock.set.mockImplementation((key, val) => { + fakeStore[key] = val; + }); + + const mockConfig = makeFakeConfig(); + const uiState = { + history: [], + bannerData: { + defaultText: 'First line\\nSecond line', + warningText: '', + }, + bannerVisible: true, + }; + const session1 = renderWithProviders(, { + config: mockConfig, + uiState, + }); + + expect(session1.lastFrame()).toContain('Tips'); + + expect(fakeStore['tipsShown']).toBe(true); + + session1.unmount(); + + const session2 = renderWithProviders(, { + config: mockConfig, + }); + + expect(session2.lastFrame()).not.toContain('Tips'); + + session2.unmount(); + }); }); diff --git a/packages/cli/src/ui/components/AppHeader.tsx b/packages/cli/src/ui/components/AppHeader.tsx index a70a7b20d8..5ad1b1ed8b 100644 --- a/packages/cli/src/ui/components/AppHeader.tsx +++ b/packages/cli/src/ui/components/AppHeader.tsx @@ -12,6 +12,7 @@ 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'; interface AppHeaderProps { version: string; @@ -23,6 +24,7 @@ export const AppHeader = ({ version }: AppHeaderProps) => { const { nightly, mainAreaWidth, bannerData, bannerVisible } = useUIState(); const { bannerText } = useBanner(bannerData, config); + const tipsShown = useTips(); return ( @@ -38,9 +40,8 @@ export const AppHeader = ({ version }: AppHeaderProps) => { )} )} - {!(settings.merged.ui.hideTips || config.getScreenReader()) && ( - - )} + {!(settings.merged.ui.hideTips || config.getScreenReader()) && + !tipsShown && } ); }; diff --git a/packages/cli/src/ui/components/__snapshots__/AppHeader.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/AppHeader.test.tsx.snap index 6da8b523f2..dcd5a4ca2a 100644 --- a/packages/cli/src/ui/components/__snapshots__/AppHeader.test.tsx.snap +++ b/packages/cli/src/ui/components/__snapshots__/AppHeader.test.tsx.snap @@ -10,12 +10,7 @@ exports[` > should not render the banner when no flags are set 1`] ███░ ░░███ ░░███ ███░ ░░█████████ ░░░ ░░░░░░░░░ - -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." +" `; exports[` > should not render the banner when previewFeatures is enabled 1`] = ` @@ -28,12 +23,7 @@ exports[` > should not render the banner when previewFeatures is en ███░ ░░███ ░░███ ███░ ░░█████████ ░░░ ░░░░░░░░░ - -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." +" `; exports[` > should not render the default banner if shown count is 5 or more 1`] = ` @@ -46,12 +36,7 @@ exports[` > should not render the default banner if shown count is ███░ ░░███ ░░███ ███░ ░░█████████ ░░░ ░░░░░░░░░ - -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." +" `; exports[` > should render the banner when previewFeatures is disabled 1`] = ` @@ -67,12 +52,7 @@ exports[` > should render the banner when previewFeatures is disabl ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 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." +╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯" `; exports[` > should render the banner with default text 1`] = ` @@ -88,12 +68,7 @@ exports[` > should render the banner with default text 1`] = ` ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 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." +╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯" `; exports[` > should render the banner with warning text 1`] = ` @@ -109,10 +84,5 @@ exports[` > should render the banner with warning text 1`] = ` ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 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." +╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯" `; diff --git a/packages/cli/src/ui/hooks/useTips.test.ts b/packages/cli/src/ui/hooks/useTips.test.ts new file mode 100644 index 0000000000..0270f73864 --- /dev/null +++ b/packages/cli/src/ui/hooks/useTips.test.ts @@ -0,0 +1,42 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { renderHookWithProviders } from '../../test-utils/render.js'; +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { useTips } from './useTips.js'; +import { persistentState } from '../../utils/persistentState.js'; + +vi.mock('../../utils/persistentState.js', () => ({ + persistentState: { + get: vi.fn(), + set: vi.fn(), + }, +})); + +describe('useTips()', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('should return false and call set(true) if state is undefined', () => { + vi.mocked(persistentState.get).mockReturnValue(undefined); + + const { result } = renderHookWithProviders(() => useTips()); + + expect(result.current).toBe(false); + + expect(persistentState.set).toHaveBeenCalledWith('tipsShown', true); + }); + + it('should return true if state is already true', () => { + vi.mocked(persistentState.get).mockReturnValue(true); + + const { result } = renderHookWithProviders(() => useTips()); + + expect(result.current).toBe(true); + expect(persistentState.set).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/cli/src/ui/hooks/useTips.ts b/packages/cli/src/ui/hooks/useTips.ts new file mode 100644 index 0000000000..d28df9ddb8 --- /dev/null +++ b/packages/cli/src/ui/hooks/useTips.ts @@ -0,0 +1,20 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { useEffect, useState } from 'react'; +import { persistentState } from '../../utils/persistentState.js'; + +export function useTips() { + const [tipsShown] = useState(() => !!persistentState.get('tipsShown')); + + useEffect(() => { + if (!tipsShown) { + persistentState.set('tipsShown', true); + } + }, [tipsShown]); + + return tipsShown; +} diff --git a/packages/cli/src/utils/persistentState.ts b/packages/cli/src/utils/persistentState.ts index f5fc4d4b29..f565157f69 100644 --- a/packages/cli/src/utils/persistentState.ts +++ b/packages/cli/src/utils/persistentState.ts @@ -12,6 +12,7 @@ const STATE_FILENAME = 'state.json'; interface PersistentStateData { defaultBannerShownCount?: Record; + tipsShown?: boolean; // Add other persistent state keys here as needed }