From 4fcc3b76476459580a04f3cbb3164f1426066327 Mon Sep 17 00:00:00 2001 From: Abhi Date: Fri, 13 Mar 2026 18:08:51 -0400 Subject: [PATCH] refactor(ui): extract SessionBrowser search and navigation components --- .../cli/src/ui/components/SessionBrowser.tsx | 86 +------------------ .../SessionBrowser/NavigationHelp.tsx | 41 +++++++++ .../SessionBrowser/NoResultsDisplay.tsx | 25 ++++++ .../SessionBrowser/SearchModeDisplay.tsx | 25 ++++++ .../SessionBrowserSearchNav.test.tsx | 68 +++++++++++++++ .../SessionBrowser/SessionListHeader.tsx | 29 +++++++ .../SessionBrowserSearchNav.test.tsx.snap | 29 +++++++ 7 files changed, 221 insertions(+), 82 deletions(-) create mode 100644 packages/cli/src/ui/components/SessionBrowser/NavigationHelp.tsx create mode 100644 packages/cli/src/ui/components/SessionBrowser/NoResultsDisplay.tsx create mode 100644 packages/cli/src/ui/components/SessionBrowser/SearchModeDisplay.tsx create mode 100644 packages/cli/src/ui/components/SessionBrowser/SessionBrowserSearchNav.test.tsx create mode 100644 packages/cli/src/ui/components/SessionBrowser/SessionListHeader.tsx create mode 100644 packages/cli/src/ui/components/SessionBrowser/__snapshots__/SessionBrowserSearchNav.test.tsx.snap diff --git a/packages/cli/src/ui/components/SessionBrowser.tsx b/packages/cli/src/ui/components/SessionBrowser.tsx index 0fc80a1d4e..afe716cf1c 100644 --- a/packages/cli/src/ui/components/SessionBrowser.tsx +++ b/packages/cli/src/ui/components/SessionBrowser.tsx @@ -110,78 +110,15 @@ const SESSIONS_PER_PAGE = 20; // If the SessionItem layout changes, update this accordingly. const FIXED_SESSION_COLUMNS_WIDTH = 30; -const Kbd = ({ name, shortcut }: { name: string; shortcut: string }) => ( - <> - {name}: {shortcut} - -); - +import { SearchModeDisplay } from './SessionBrowser/SearchModeDisplay.js'; +import { NavigationHelp } from './SessionBrowser/NavigationHelp.js'; +import { SessionListHeader } from './SessionBrowser/SessionListHeader.js'; +import { NoResultsDisplay } from './SessionBrowser/NoResultsDisplay.js'; import { SessionBrowserLoading } from './SessionBrowser/SessionBrowserLoading.js'; import { SessionBrowserError } from './SessionBrowser/SessionBrowserError.js'; import { SessionBrowserEmpty } from './SessionBrowser/SessionBrowserEmpty.js'; - import { sortSessions, filterSessions } from './SessionBrowser/utils.js'; -/** - * Search input display component. - */ -const SearchModeDisplay = ({ - state, -}: { - state: SessionBrowserState; -}): React.JSX.Element => ( - - Search: - {state.searchQuery} - (Esc to cancel) - -); - -/** - * Header component showing session count and sort information. - */ -const SessionListHeader = ({ - state, -}: { - state: SessionBrowserState; -}): React.JSX.Element => ( - - - Chat Sessions ({state.totalSessions} total - {state.searchQuery ? `, filtered` : ''}) - - - sorted by {state.sortOrder} {state.sortReverse ? 'asc' : 'desc'} - - -); - -/** - * Navigation help component showing keyboard shortcuts. - */ -const NavigationHelp = (): React.JSX.Element => ( - - - - {' '} - - {' '} - - {' '} - - {' '} - - - - - {' '} - - {' '} - - - -); - /** * Table header component with column labels and scroll indicators. */ @@ -219,21 +156,6 @@ const SessionTableHeader = ({ ); -/** - * No results display component for empty search results. - */ -const NoResultsDisplay = ({ - state, -}: { - state: SessionBrowserState; -}): React.JSX.Element => ( - - - No sessions found matching '{state.searchQuery}'. - - -); - /** * Match snippet display component for search results. */ diff --git a/packages/cli/src/ui/components/SessionBrowser/NavigationHelp.tsx b/packages/cli/src/ui/components/SessionBrowser/NavigationHelp.tsx new file mode 100644 index 0000000000..cb49b45adf --- /dev/null +++ b/packages/cli/src/ui/components/SessionBrowser/NavigationHelp.tsx @@ -0,0 +1,41 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import type React from 'react'; +import { Box, Text } from 'ink'; +import { Colors } from '../../colors.js'; + +const Kbd = ({ name, shortcut }: { name: string; shortcut: string }) => ( + <> + {name}: {shortcut} + +); + +/** + * Navigation help component showing keyboard shortcuts. + */ +export const NavigationHelp = (): React.JSX.Element => ( + + + + {' '} + + {' '} + + {' '} + + {' '} + + + + + {' '} + + {' '} + + + +); diff --git a/packages/cli/src/ui/components/SessionBrowser/NoResultsDisplay.tsx b/packages/cli/src/ui/components/SessionBrowser/NoResultsDisplay.tsx new file mode 100644 index 0000000000..d37fa3d0e6 --- /dev/null +++ b/packages/cli/src/ui/components/SessionBrowser/NoResultsDisplay.tsx @@ -0,0 +1,25 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import type React from 'react'; +import { Box, Text } from 'ink'; +import { Colors } from '../../colors.js'; +import type { SessionBrowserState } from '../SessionBrowser.js'; + +/** + * No results display component for empty search results. + */ +export const NoResultsDisplay = ({ + state, +}: { + state: SessionBrowserState; +}): React.JSX.Element => ( + + + No sessions found matching '{state.searchQuery}'. + + +); diff --git a/packages/cli/src/ui/components/SessionBrowser/SearchModeDisplay.tsx b/packages/cli/src/ui/components/SessionBrowser/SearchModeDisplay.tsx new file mode 100644 index 0000000000..8c1dac92f1 --- /dev/null +++ b/packages/cli/src/ui/components/SessionBrowser/SearchModeDisplay.tsx @@ -0,0 +1,25 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import type React from 'react'; +import { Box, Text } from 'ink'; +import { Colors } from '../../colors.js'; +import type { SessionBrowserState } from '../SessionBrowser.js'; + +/** + * Search input display component. + */ +export const SearchModeDisplay = ({ + state, +}: { + state: SessionBrowserState; +}): React.JSX.Element => ( + + Search: + {state.searchQuery} + (Esc to cancel) + +); diff --git a/packages/cli/src/ui/components/SessionBrowser/SessionBrowserSearchNav.test.tsx b/packages/cli/src/ui/components/SessionBrowser/SessionBrowserSearchNav.test.tsx new file mode 100644 index 0000000000..251f86c4a9 --- /dev/null +++ b/packages/cli/src/ui/components/SessionBrowser/SessionBrowserSearchNav.test.tsx @@ -0,0 +1,68 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +// import type React from 'react'; +import { render } from '../../../test-utils/render.js'; +import { describe, it, expect } from 'vitest'; +import { SearchModeDisplay } from './SearchModeDisplay.js'; +import { NavigationHelp } from './NavigationHelp.js'; +import { SessionListHeader } from './SessionListHeader.js'; +import { NoResultsDisplay } from './NoResultsDisplay.js'; +import type { SessionBrowserState } from '../SessionBrowser.js'; + +describe('SessionBrowser Search and Navigation Components', () => { + it('SearchModeDisplay renders correctly with query', async () => { + const mockState = { searchQuery: 'test query' } as SessionBrowserState; + const { lastFrame, waitUntilReady } = render( + , + ); + await waitUntilReady(); + expect(lastFrame()).toMatchSnapshot(); + }); + + it('NavigationHelp renders correctly', async () => { + const { lastFrame, waitUntilReady } = render(); + await waitUntilReady(); + expect(lastFrame()).toMatchSnapshot(); + }); + + it('SessionListHeader renders correctly', async () => { + const mockState = { + totalSessions: 10, + searchQuery: '', + sortOrder: 'date', + sortReverse: false, + } as SessionBrowserState; + const { lastFrame, waitUntilReady } = render( + , + ); + await waitUntilReady(); + expect(lastFrame()).toMatchSnapshot(); + }); + + it('SessionListHeader renders correctly with filter', async () => { + const mockState = { + totalSessions: 5, + searchQuery: 'test', + sortOrder: 'name', + sortReverse: true, + } as SessionBrowserState; + const { lastFrame, waitUntilReady } = render( + , + ); + await waitUntilReady(); + expect(lastFrame()).toMatchSnapshot(); + }); + + it('NoResultsDisplay renders correctly', async () => { + const mockState = { searchQuery: 'no match' } as SessionBrowserState; + const { lastFrame, waitUntilReady } = render( + , + ); + await waitUntilReady(); + expect(lastFrame()).toMatchSnapshot(); + }); +}); diff --git a/packages/cli/src/ui/components/SessionBrowser/SessionListHeader.tsx b/packages/cli/src/ui/components/SessionBrowser/SessionListHeader.tsx new file mode 100644 index 0000000000..2b7fb79d40 --- /dev/null +++ b/packages/cli/src/ui/components/SessionBrowser/SessionListHeader.tsx @@ -0,0 +1,29 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import type React from 'react'; +import { Box, Text } from 'ink'; +import { Colors } from '../../colors.js'; +import type { SessionBrowserState } from '../SessionBrowser.js'; + +/** + * Header component showing session count and sort information. + */ +export const SessionListHeader = ({ + state, +}: { + state: SessionBrowserState; +}): React.JSX.Element => ( + + + Chat Sessions ({state.totalSessions} total + {state.searchQuery ? `, filtered` : ''}) + + + sorted by {state.sortOrder} {state.sortReverse ? 'asc' : 'desc'} + + +); diff --git a/packages/cli/src/ui/components/SessionBrowser/__snapshots__/SessionBrowserSearchNav.test.tsx.snap b/packages/cli/src/ui/components/SessionBrowser/__snapshots__/SessionBrowserSearchNav.test.tsx.snap new file mode 100644 index 0000000000..c5ed5e5454 --- /dev/null +++ b/packages/cli/src/ui/components/SessionBrowser/__snapshots__/SessionBrowserSearchNav.test.tsx.snap @@ -0,0 +1,29 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`SessionBrowser Search and Navigation Components > NavigationHelp renders correctly 1`] = ` +"Navigate: ↑/↓ Resume: Enter Search: / Delete: x Quit: q +Sort: s Reverse: r First/Last: g/G +" +`; + +exports[`SessionBrowser Search and Navigation Components > NoResultsDisplay renders correctly 1`] = ` +" +No sessions found matching 'no match'. +" +`; + +exports[`SessionBrowser Search and Navigation Components > SearchModeDisplay renders correctly with query 1`] = ` +" +Search: test query (Esc to cancel) +" +`; + +exports[`SessionBrowser Search and Navigation Components > SessionListHeader renders correctly 1`] = ` +"Chat Sessions (10 total) sorted by date desc +" +`; + +exports[`SessionBrowser Search and Navigation Components > SessionListHeader renders correctly with filter 1`] = ` +"Chat Sessions (5 total, filtered) sorted by name asc +" +`;