mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-19 02:20:42 -07:00
refactor(ui): extract SessionBrowser search and navigation components (#22377)
This commit is contained in:
@@ -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}: <Text bold>{shortcut}</Text>
|
||||
</>
|
||||
);
|
||||
|
||||
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 => (
|
||||
<Box marginTop={1}>
|
||||
<Text color={Colors.Gray}>Search: </Text>
|
||||
<Text color={Colors.AccentPurple}>{state.searchQuery}</Text>
|
||||
<Text color={Colors.Gray}> (Esc to cancel)</Text>
|
||||
</Box>
|
||||
);
|
||||
|
||||
/**
|
||||
* Header component showing session count and sort information.
|
||||
*/
|
||||
const SessionListHeader = ({
|
||||
state,
|
||||
}: {
|
||||
state: SessionBrowserState;
|
||||
}): React.JSX.Element => (
|
||||
<Box flexDirection="row" justifyContent="space-between">
|
||||
<Text color={Colors.AccentPurple}>
|
||||
Chat Sessions ({state.totalSessions} total
|
||||
{state.searchQuery ? `, filtered` : ''})
|
||||
</Text>
|
||||
<Text color={Colors.Gray}>
|
||||
sorted by {state.sortOrder} {state.sortReverse ? 'asc' : 'desc'}
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
|
||||
/**
|
||||
* Navigation help component showing keyboard shortcuts.
|
||||
*/
|
||||
const NavigationHelp = (): React.JSX.Element => (
|
||||
<Box flexDirection="column">
|
||||
<Text color={Colors.Gray}>
|
||||
<Kbd name="Navigate" shortcut="↑/↓" />
|
||||
{' '}
|
||||
<Kbd name="Resume" shortcut="Enter" />
|
||||
{' '}
|
||||
<Kbd name="Search" shortcut="/" />
|
||||
{' '}
|
||||
<Kbd name="Delete" shortcut="x" />
|
||||
{' '}
|
||||
<Kbd name="Quit" shortcut="q" />
|
||||
</Text>
|
||||
<Text color={Colors.Gray}>
|
||||
<Kbd name="Sort" shortcut="s" />
|
||||
{' '}
|
||||
<Kbd name="Reverse" shortcut="r" />
|
||||
{' '}
|
||||
<Kbd name="First/Last" shortcut="g/G" />
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
|
||||
/**
|
||||
* Table header component with column labels and scroll indicators.
|
||||
*/
|
||||
@@ -219,21 +158,6 @@ const SessionTableHeader = ({
|
||||
</Box>
|
||||
);
|
||||
|
||||
/**
|
||||
* No results display component for empty search results.
|
||||
*/
|
||||
const NoResultsDisplay = ({
|
||||
state,
|
||||
}: {
|
||||
state: SessionBrowserState;
|
||||
}): React.JSX.Element => (
|
||||
<Box marginTop={1}>
|
||||
<Text color={Colors.Gray} dimColor>
|
||||
No sessions found matching '{state.searchQuery}'.
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
|
||||
/**
|
||||
* Match snippet display component for search results.
|
||||
*/
|
||||
@@ -398,7 +322,7 @@ const SessionList = ({
|
||||
<Box flexDirection="column">
|
||||
{/* Table Header */}
|
||||
<Box flexDirection="column">
|
||||
{!state.isSearchMode && <NavigationHelp />}
|
||||
{!state.isSearchMode && <NavigationHelpDisplay />}
|
||||
<SessionTableHeader state={state} />
|
||||
</Box>
|
||||
|
||||
|
||||
@@ -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}: <Text bold>{shortcut}</Text>
|
||||
</>
|
||||
);
|
||||
|
||||
/**
|
||||
* Navigation help component showing keyboard shortcuts.
|
||||
*/
|
||||
export const NavigationHelpDisplay = (): React.JSX.Element => (
|
||||
<Box flexDirection="column">
|
||||
<Text color={Colors.Gray}>
|
||||
<Kbd name="Navigate" shortcut="↑/↓" />
|
||||
{' '}
|
||||
<Kbd name="Resume" shortcut="Enter" />
|
||||
{' '}
|
||||
<Kbd name="Search" shortcut="/" />
|
||||
{' '}
|
||||
<Kbd name="Delete" shortcut="x" />
|
||||
{' '}
|
||||
<Kbd name="Quit" shortcut="q" />
|
||||
</Text>
|
||||
<Text color={Colors.Gray}>
|
||||
<Kbd name="Sort" shortcut="s" />
|
||||
{' '}
|
||||
<Kbd name="Reverse" shortcut="r" />
|
||||
{' '}
|
||||
<Kbd name="First/Last" shortcut="g/G" />
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
|
||||
/**
|
||||
* Search input display component.
|
||||
*/
|
||||
export const SearchModeDisplay = ({
|
||||
state,
|
||||
}: {
|
||||
state: SessionBrowserState;
|
||||
}): React.JSX.Element => (
|
||||
<Box marginTop={1}>
|
||||
<Text color={Colors.Gray}>Search: </Text>
|
||||
<Text color={Colors.AccentPurple}>{state.searchQuery}</Text>
|
||||
<Text color={Colors.Gray}> (Esc to cancel)</Text>
|
||||
</Box>
|
||||
);
|
||||
|
||||
/**
|
||||
* No results display component for empty search results.
|
||||
*/
|
||||
export const NoResultsDisplay = ({
|
||||
state,
|
||||
}: {
|
||||
state: SessionBrowserState;
|
||||
}): React.JSX.Element => (
|
||||
<Box marginTop={1}>
|
||||
<Text color={Colors.Gray} dimColor>
|
||||
No sessions found matching '{state.searchQuery}'.
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
@@ -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(
|
||||
<SearchModeDisplay state={mockState} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('NavigationHelp renders correctly', async () => {
|
||||
const { lastFrame, waitUntilReady } = render(<NavigationHelpDisplay />);
|
||||
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(
|
||||
<SessionListHeader state={mockState} />,
|
||||
);
|
||||
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(
|
||||
<SessionListHeader state={mockState} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('NoResultsDisplay renders correctly', async () => {
|
||||
const mockState = { searchQuery: 'no match' } as SessionBrowserState;
|
||||
const { lastFrame, waitUntilReady } = render(
|
||||
<NoResultsDisplay state={mockState} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -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 => (
|
||||
<Box flexDirection="row" justifyContent="space-between">
|
||||
<Text color={Colors.AccentPurple}>
|
||||
Chat Sessions ({state.totalSessions} total
|
||||
{state.searchQuery ? `, filtered` : ''})
|
||||
</Text>
|
||||
<Text color={Colors.Gray}>
|
||||
sorted by {state.sortOrder} {state.sortReverse ? 'asc' : 'desc'}
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
@@ -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
|
||||
"
|
||||
`;
|
||||
Reference in New Issue
Block a user