Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 26 additions & 3 deletions packages/prompts.chat/src/cli/components/PromptList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@ interface PromptListProps {

type ViewMode = 'list' | 'categories';

/**
* Renders the interactive CLI prompt browser with searchable prompt and category views,
* keyboard navigation, and pagination support.
*
* @param props - Component props including selection handlers, search state, pagination,
* and category filters used to drive prompt list interactions.
* @returns The PromptList Ink UI tree for list and category browsing modes.
*/
export function PromptList({
onSelect,
onQuit,
Expand Down Expand Up @@ -52,6 +60,9 @@ export function PromptList({
const headerLines = 3;
const footerLines = 2;
const listHeight = Math.max(terminalHeight - headerLines - footerLines, 5);
// Category view: header + "All Categories" + footer + margins = 4 reserved
const categoryReservedLines = 4 + (isSearchingCategories ? 1 : 0);
const categoryListHeight = Math.max(terminalHeight - categoryReservedLines, 5);
const perPage = listHeight;

useEffect(() => {
Expand Down Expand Up @@ -214,6 +225,15 @@ export function PromptList({
? categories.filter(c => c.name.toLowerCase().includes(categorySearchQuery.toLowerCase()))
: categories;

// Category viewport scrolling - calculate which items to show
const isCategorySearchActive = Boolean(categorySearchQuery);
const categoryTotalItems = categorySearchQuery ? filteredCategories.length : filteredCategories.length + 1;
const categoryScrollOffset = Math.max(0, Math.min(categoryIndex - categoryListHeight + 2, Math.max(0, categoryTotalItems - categoryListHeight)));
const sliceStart = isCategorySearchActive ? categoryScrollOffset : Math.max(0, categoryScrollOffset - 1);
const sliceEnd = isCategorySearchActive
? categoryScrollOffset + categoryListHeight
: (categoryScrollOffset === 0 ? categoryListHeight - 1 : categoryScrollOffset - 1 + categoryListHeight);

const maxTitleLength = terminalWidth - 30;

if (error) {
Expand Down Expand Up @@ -288,7 +308,7 @@ export function PromptList({
</Box>
) : (
<>
{!categorySearchQuery && (
{!categorySearchQuery && categoryScrollOffset === 0 && (
<Box>
<Text color={categoryIndex === 0 ? 'cyan' : undefined}>
{categoryIndex === 0 ? '❯ ' : ' '}
Expand All @@ -298,8 +318,11 @@ export function PromptList({
</Text>
</Box>
)}
{filteredCategories.slice(0, listHeight - 1).map((cat, index) => {
const adjustedIndex = categorySearchQuery ? index : index + 1;
{filteredCategories.slice(sliceStart, sliceEnd).map((cat, index) => {
const actualIndex = (isCategorySearchActive ? sliceStart : categoryScrollOffset) + index;
// When All Categories is visible (scrollOffset=0), categories are at positions 1+ so add 1
// When scrolled out, positions match actualIndex directly
const adjustedIndex = isCategorySearchActive || categoryScrollOffset > 0 ? actualIndex : actualIndex + 1;
return (
<Box key={cat.id}>
<Text color={adjustedIndex === categoryIndex ? 'cyan' : undefined}>
Expand Down