From 4ecb4bb24b8f986818c42698b2a84974188e0b3a Mon Sep 17 00:00:00 2001 From: Abhi <43648792+abhipatel12@users.noreply.github.com> Date: Wed, 18 Mar 2026 00:44:01 -0400 Subject: [PATCH] refactor(ui): extract SessionBrowser search and navigation components (#22377) --- .../cli/src/ui/components/SessionBrowser.tsx | 90 ++----------------- .../SessionBrowser/SessionBrowserNav.tsx | 72 +++++++++++++++ .../SessionBrowserSearchNav.test.tsx | 69 ++++++++++++++ .../SessionBrowser/SessionListHeader.tsx | 29 ++++++ .../SessionBrowserSearchNav.test.tsx.snap | 29 ++++++ 5 files changed, 206 insertions(+), 83 deletions(-) create mode 100644 packages/cli/src/ui/components/SessionBrowser/SessionBrowserNav.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..ac9b2c2b00 100644 --- a/packages/cli/src/ui/components/SessionBrowser.tsx +++ b/packages/cli/src/ui/components/SessionBrowser.tsx @@ -110,78 +110,17 @@ 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, + NavigationHelpDisplay, + NoResultsDisplay, +} from './SessionBrowser/SessionBrowserNav.js'; +import { SessionListHeader } from './SessionBrowser/SessionListHeader.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 +158,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. */ @@ -398,7 +322,7 @@ const SessionList = ({ {/* Table Header */} - {!state.isSearchMode && } + {!state.isSearchMode && } diff --git a/packages/cli/src/ui/components/SessionBrowser/SessionBrowserNav.tsx b/packages/cli/src/ui/components/SessionBrowser/SessionBrowserNav.tsx new file mode 100644 index 0000000000..99d0363ed5 --- /dev/null +++ b/packages/cli/src/ui/components/SessionBrowser/SessionBrowserNav.tsx @@ -0,0 +1,72 @@ +/** + * @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'; + +const Kbd = ({ name, shortcut }: { name: string; shortcut: string }) => ( + <> + {name}: {shortcut} + +); + +/** + * Navigation help component showing keyboard shortcuts. + */ +export const NavigationHelpDisplay = (): React.JSX.Element => ( + + + + {' '} + + {' '} + + {' '} + + {' '} + + + + + {' '} + + {' '} + + + +); + +/** + * Search input display component. + */ +export const SearchModeDisplay = ({ + state, +}: { + state: SessionBrowserState; +}): React.JSX.Element => ( + + Search: + {state.searchQuery} + (Esc to cancel) + +); + +/** + * 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/SessionBrowserSearchNav.test.tsx b/packages/cli/src/ui/components/SessionBrowser/SessionBrowserSearchNav.test.tsx new file mode 100644 index 0000000000..af7f1a6906 --- /dev/null +++ b/packages/cli/src/ui/components/SessionBrowser/SessionBrowserSearchNav.test.tsx @@ -0,0 +1,69 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { render } from '../../../test-utils/render.js'; +import { describe, it, expect } from 'vitest'; +import { + SearchModeDisplay, + NavigationHelpDisplay, + NoResultsDisplay, +} from './SessionBrowserNav.js'; +import { SessionListHeader } from './SessionListHeader.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 +" +`;