diff --git a/packages/cli/src/ui/components/shared/BaseSelectionList.tsx b/packages/cli/src/ui/components/shared/BaseSelectionList.tsx
index 1090d4010d..32a353d283 100644
--- a/packages/cli/src/ui/components/shared/BaseSelectionList.tsx
+++ b/packages/cli/src/ui/components/shared/BaseSelectionList.tsx
@@ -5,7 +5,7 @@
*/
import type React from 'react';
-import { useEffect, useState } from 'react';
+import { useState } from 'react';
import { Text, Box } from 'ink';
import { theme } from '../../semantic-colors.js';
import {
@@ -82,35 +82,39 @@ export function BaseSelectionList<
priority,
});
- const [scrollOffset, setScrollOffset] = useState(0);
+ const [scrollOffsetState, setScrollOffsetState] = useState(0);
- // Handle scrolling for long lists
- useEffect(() => {
- const newScrollOffset = Math.max(
+ // Compute effective scroll offset synchronously during render to avoid flicker
+ let scrollOffset = scrollOffsetState;
+
+ if (activeIndex < scrollOffset) {
+ scrollOffset = activeIndex;
+ } else if (activeIndex >= scrollOffset + maxItemsToShow) {
+ scrollOffset = Math.max(
0,
Math.min(activeIndex - maxItemsToShow + 1, items.length - maxItemsToShow),
);
- if (activeIndex < scrollOffset) {
- setScrollOffset(activeIndex);
- } else if (activeIndex >= scrollOffset + maxItemsToShow) {
- setScrollOffset(newScrollOffset);
- }
- }, [activeIndex, items.length, scrollOffset, maxItemsToShow]);
+ }
+
+ const maxScroll = Math.max(0, items.length - maxItemsToShow);
+ if (scrollOffset > maxScroll) {
+ scrollOffset = maxScroll;
+ }
+
+ // Update state to match derived value if it changed
+ if (scrollOffsetState !== scrollOffset) {
+ setScrollOffsetState(scrollOffset);
+ }
const visibleItems = items.slice(scrollOffset, scrollOffset + maxItemsToShow);
const numberColumnWidth = String(items.length).length;
+ const canScrollUp = scrollOffset > 0;
+ const canScrollDown = scrollOffset + maxItemsToShow < items.length;
+ const hasScrollArrows = showScrollArrows && items.length > maxItemsToShow;
+
return (
- {/* Use conditional coloring instead of conditional rendering */}
- {showScrollArrows && items.length > maxItemsToShow && (
- 0 ? theme.text.primary : theme.text.secondary}
- >
- ▲
-
- )}
-
{visibleItems.map((item, index) => {
const itemIndex = scrollOffset + index;
const isSelected = activeIndex === itemIndex;
@@ -139,19 +143,35 @@ export function BaseSelectionList<
numberColumnWidth,
)}.`;
+ // Determine the indicator character for the radio column
+ let indicator = ' ';
+ let indicatorColor = theme.text.primary;
+
+ if (isSelected) {
+ indicator = selectedIndicator;
+ indicatorColor = theme.ui.focus;
+ } else if (hasScrollArrows && index === 0 && canScrollUp) {
+ indicator = '▲';
+ indicatorColor = theme.text.secondary;
+ } else if (
+ hasScrollArrows &&
+ index === visibleItems.length - 1 &&
+ canScrollDown
+ ) {
+ indicator = '▼';
+ indicatorColor = theme.text.secondary;
+ }
+
return (
- {/* Radio button indicator */}
+ {/* Radio button indicator (also shows scroll arrows inline) */}
-
- {isSelected ? selectedIndicator : ' '}
+
+ {indicator}
@@ -178,18 +198,6 @@ export function BaseSelectionList<
);
})}
-
- {showScrollArrows && items.length > maxItemsToShow && (
-
- ▼
-
- )}
);
}
diff --git a/packages/cli/src/ui/components/shared/SearchableList.tsx b/packages/cli/src/ui/components/shared/SearchableList.tsx
index 5b6c5259e7..c6b9ee1226 100644
--- a/packages/cli/src/ui/components/shared/SearchableList.tsx
+++ b/packages/cli/src/ui/components/shared/SearchableList.tsx
@@ -172,11 +172,12 @@ export function SearchableList({
item: T,
isActive: boolean,
labelWidth: number,
+ arrowChar?: string,
) => (
- {isActive ? '●' : ''}
+ {isActive ? '●' : arrowChar || ' '}
@@ -226,26 +227,44 @@ export function SearchableList({
) : (
<>
- {filteredItems.length > maxItemsToShow && (
-
- ▲
-
- )}
{visibleItems.map((item, index) => {
const isSelected = activeIndex === scrollOffset + index;
+ const hasScrollArrows = filteredItems.length > maxItemsToShow;
+ const canScrollUp = scrollOffset > 0;
+ const canScrollDown =
+ scrollOffset + maxItemsToShow < filteredItems.length;
+
+ // Determine inline scroll arrow for this item position
+ let arrowChar = '';
+ if (
+ hasScrollArrows &&
+ index === 0 &&
+ canScrollUp &&
+ !isSelected
+ ) {
+ arrowChar = '▲';
+ } else if (
+ hasScrollArrows &&
+ index === visibleItems.length - 1 &&
+ canScrollDown &&
+ !isSelected
+ ) {
+ arrowChar = '▼';
+ }
+
return (
{renderItem
? renderItem(item, isSelected, maxLabelWidth)
- : defaultRenderItem(item, isSelected, maxLabelWidth)}
+ : defaultRenderItem(
+ item,
+ isSelected,
+ maxLabelWidth,
+ arrowChar,
+ )}
);
})}
- {filteredItems.length > maxItemsToShow && (
-
- ▼
-
- )}
>
)}
diff --git a/packages/cli/src/ui/components/shared/__snapshots__/BaseSelectionList.test.tsx.snap b/packages/cli/src/ui/components/shared/__snapshots__/BaseSelectionList.test.tsx.snap
index 040b6babec..09a6162d3d 100644
--- a/packages/cli/src/ui/components/shared/__snapshots__/BaseSelectionList.test.tsx.snap
+++ b/packages/cli/src/ui/components/shared/__snapshots__/BaseSelectionList.test.tsx.snap
@@ -8,28 +8,22 @@ exports[`BaseSelectionList > Scroll Arrows (showScrollArrows) > should not show
`;
exports[`BaseSelectionList > Scroll Arrows (showScrollArrows) > should show arrows and correct items when scrolled to the end 1`] = `
-"▲
- 8. Item 8
+"▲ 8. Item 8
9. Item 9
● 10. Item 10
-▼
"
`;
exports[`BaseSelectionList > Scroll Arrows (showScrollArrows) > should show arrows and correct items when scrolled to the middle 1`] = `
-"▲
- 4. Item 4
+"▲ 4. Item 4
5. Item 5
● 6. Item 6
-▼
"
`;
exports[`BaseSelectionList > Scroll Arrows (showScrollArrows) > should show arrows with correct colors when enabled (at the top) 1`] = `
-"▲
-● 1. Item 1
+"● 1. Item 1
2. Item 2
- 3. Item 3
-▼
+▼ 3. Item 3
"
`;
diff --git a/packages/cli/src/ui/components/shared/__snapshots__/SearchableList.test.tsx.snap b/packages/cli/src/ui/components/shared/__snapshots__/SearchableList.test.tsx.snap
index 9f256d4cb6..35f21daee3 100644
--- a/packages/cli/src/ui/components/shared/__snapshots__/SearchableList.test.tsx.snap
+++ b/packages/cli/src/ui/components/shared/__snapshots__/SearchableList.test.tsx.snap
@@ -7,10 +7,10 @@ exports[`SearchableList > should match snapshot 1`] = `
│ Search... │
╰────────────────────────────────────────────────────────────────────────────────────────────────╯
- ● Item One
+ ● Item One
Description for item one
- Item Two
+ Item Two
Description for item two
Item Three
@@ -25,10 +25,10 @@ exports[`SearchableList > should reset selection to top when items change if res
│ Search... │
╰────────────────────────────────────────────────────────────────────────────────────────────────╯
- Item One
+ Item One
Description for item one
- ● Item Two
+ ● Item Two
Description for item two
Item Three
@@ -43,7 +43,7 @@ exports[`SearchableList > should reset selection to top when items change if res
│ One │
╰────────────────────────────────────────────────────────────────────────────────────────────────╯
- ● Item One
+ ● Item One
Description for item one
"
`;
@@ -55,10 +55,10 @@ exports[`SearchableList > should reset selection to top when items change if res
│ Search... │
╰────────────────────────────────────────────────────────────────────────────────────────────────╯
- ● Item One
+ ● Item One
Description for item one
- Item Two
+ Item Two
Description for item two
Item Three