mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-13 05:12:55 -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.
|
// If the SessionItem layout changes, update this accordingly.
|
||||||
const FIXED_SESSION_COLUMNS_WIDTH = 30;
|
const FIXED_SESSION_COLUMNS_WIDTH = 30;
|
||||||
|
|
||||||
const Kbd = ({ name, shortcut }: { name: string; shortcut: string }) => (
|
import {
|
||||||
<>
|
SearchModeDisplay,
|
||||||
{name}: <Text bold>{shortcut}</Text>
|
NavigationHelpDisplay,
|
||||||
</>
|
NoResultsDisplay,
|
||||||
);
|
} from './SessionBrowser/SessionBrowserNav.js';
|
||||||
|
import { SessionListHeader } from './SessionBrowser/SessionListHeader.js';
|
||||||
import { SessionBrowserLoading } from './SessionBrowser/SessionBrowserLoading.js';
|
import { SessionBrowserLoading } from './SessionBrowser/SessionBrowserLoading.js';
|
||||||
import { SessionBrowserError } from './SessionBrowser/SessionBrowserError.js';
|
import { SessionBrowserError } from './SessionBrowser/SessionBrowserError.js';
|
||||||
import { SessionBrowserEmpty } from './SessionBrowser/SessionBrowserEmpty.js';
|
import { SessionBrowserEmpty } from './SessionBrowser/SessionBrowserEmpty.js';
|
||||||
|
|
||||||
import { sortSessions, filterSessions } from './SessionBrowser/utils.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.
|
* Table header component with column labels and scroll indicators.
|
||||||
*/
|
*/
|
||||||
@@ -219,21 +158,6 @@ const SessionTableHeader = ({
|
|||||||
</Box>
|
</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.
|
* Match snippet display component for search results.
|
||||||
*/
|
*/
|
||||||
@@ -398,7 +322,7 @@ const SessionList = ({
|
|||||||
<Box flexDirection="column">
|
<Box flexDirection="column">
|
||||||
{/* Table Header */}
|
{/* Table Header */}
|
||||||
<Box flexDirection="column">
|
<Box flexDirection="column">
|
||||||
{!state.isSearchMode && <NavigationHelp />}
|
{!state.isSearchMode && <NavigationHelpDisplay />}
|
||||||
<SessionTableHeader state={state} />
|
<SessionTableHeader state={state} />
|
||||||
</Box>
|
</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>
|
||||||
|
);
|
||||||
+29
@@ -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