diff --git a/packages/prompts.chat/src/cli/components/PromptList.tsx b/packages/prompts.chat/src/cli/components/PromptList.tsx index dfea156e599..82cc768dc61 100644 --- a/packages/prompts.chat/src/cli/components/PromptList.tsx +++ b/packages/prompts.chat/src/cli/components/PromptList.tsx @@ -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, @@ -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(() => { @@ -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) { @@ -288,7 +308,7 @@ export function PromptList({ ) : ( <> - {!categorySearchQuery && ( + {!categorySearchQuery && categoryScrollOffset === 0 && ( {categoryIndex === 0 ? '❯ ' : ' '} @@ -298,8 +318,11 @@ export function PromptList({ )} - {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 (