diff --git a/src/cli/tui/hooks/__tests__/useListNavigation.test.tsx b/src/cli/tui/hooks/__tests__/useListNavigation.test.tsx
index 8650ba62f..a8624e0e1 100644
--- a/src/cli/tui/hooks/__tests__/useListNavigation.test.tsx
+++ b/src/cli/tui/hooks/__tests__/useListNavigation.test.tsx
@@ -83,6 +83,8 @@ function ListNav({
isDisabled,
getHotkeys,
onHotkeySelect,
+ initialSelectedIndex,
+ resetKey,
}: {
items: string[];
onSelect?: (item: string, index: number) => void;
@@ -90,6 +92,8 @@ function ListNav({
isDisabled?: (item: string) => boolean;
getHotkeys?: (item: string) => string[] | undefined;
onHotkeySelect?: (item: string, index: number) => void;
+ initialSelectedIndex?: number;
+ resetKey?: string | number;
}) {
const { selectedIndex } = useListNavigation({
items,
@@ -98,6 +102,8 @@ function ListNav({
isDisabled,
getHotkeys,
onHotkeySelect,
+ initialSelectedIndex,
+ resetKey,
});
return idx:{selectedIndex};
}
@@ -233,4 +239,43 @@ describe('useListNavigation hook', () => {
expect(lastFrame()).toContain('idx:0');
});
+
+ describe('initialSelectedIndex', () => {
+ it('honors initialSelectedIndex on mount', () => {
+ const { lastFrame } = render();
+ expect(lastFrame()).toContain('idx:2');
+ });
+
+ it('still defaults to 0 when initialSelectedIndex is unset', () => {
+ const { lastFrame } = render();
+ expect(lastFrame()).toContain('idx:0');
+ });
+
+ it('ignores an out-of-range initialSelectedIndex and falls back to first enabled', () => {
+ const { lastFrame } = render();
+ expect(lastFrame()).toContain('idx:0');
+ });
+
+ it('falls back to first enabled when initialSelectedIndex points to a disabled item', () => {
+ const isDisabled = (item: string) => item === 'alpha';
+ const { lastFrame } = render();
+ expect(lastFrame()).toContain('idx:1');
+ });
+
+ it('re-applies initialSelectedIndex when resetKey changes', async () => {
+ const { lastFrame, stdin, rerender } = render(
+
+ );
+ await new Promise(resolve => setTimeout(resolve, 50));
+ expect(lastFrame()).toContain('idx:2');
+
+ stdin.write(UP_ARROW); // move off the seeded index
+ await new Promise(resolve => setTimeout(resolve, 50));
+ expect(lastFrame()).toContain('idx:1');
+
+ rerender();
+ await new Promise(resolve => setTimeout(resolve, 50));
+ expect(lastFrame()).toContain('idx:2');
+ });
+ });
});
diff --git a/src/cli/tui/hooks/useListNavigation.ts b/src/cli/tui/hooks/useListNavigation.ts
index 6e39bbdee..b4555b8cc 100644
--- a/src/cli/tui/hooks/useListNavigation.ts
+++ b/src/cli/tui/hooks/useListNavigation.ts
@@ -20,6 +20,8 @@ interface UseListNavigationOptions {
isDisabled?: (item: T) => boolean;
/** Optional key to reset selection when changed */
resetKey?: string | number;
+ /** Optional index to start the cursor on (default: first enabled). Honored on mount and on resetKey change. */
+ initialSelectedIndex?: number;
}
interface UseListNavigationResult {
@@ -85,20 +87,32 @@ export function useListNavigation({
onHotkeySelect,
isDisabled,
resetKey,
+ initialSelectedIndex,
}: UseListNavigationOptions): UseListNavigationResult {
- // Initialize with first enabled index (parent should ensure data is loaded before mounting)
- const [selectedIndex, setSelectedIndex] = useState(() => {
+ // Resolve the starting index: honor initialSelectedIndex when it points to a valid,
+ // non-disabled item; otherwise fall back to the first enabled index (default: 0).
+ const resolveInitialIndex = (): number => {
+ if (
+ initialSelectedIndex !== undefined &&
+ initialSelectedIndex >= 0 &&
+ initialSelectedIndex < items.length &&
+ !isDisabled?.(items[initialSelectedIndex] as T)
+ ) {
+ return initialSelectedIndex;
+ }
if (!isDisabled) return 0;
const idx = items.findIndex(item => !isDisabled(item));
return idx >= 0 ? idx : 0;
- });
+ };
+
+ // Initialize with the resolved index (parent should ensure data is loaded before mounting)
+ const [selectedIndex, setSelectedIndex] = useState(resolveInitialIndex);
// Reset selection when resetKey changes (using state sync pattern to avoid setState in effect)
const [prevResetKey, setPrevResetKey] = useState(resetKey);
if (resetKey !== undefined && resetKey !== prevResetKey) {
setPrevResetKey(resetKey);
- const idx = isDisabled ? items.findIndex(item => !isDisabled(item)) : 0;
- setSelectedIndex(idx >= 0 ? idx : 0);
+ setSelectedIndex(resolveInitialIndex());
}
// Find next non-disabled index in given direction (delegates to standalone function)
diff --git a/src/cli/tui/screens/agent/AddAgentScreen.tsx b/src/cli/tui/screens/agent/AddAgentScreen.tsx
index ad52b60f1..2d66217de 100644
--- a/src/cli/tui/screens/agent/AddAgentScreen.tsx
+++ b/src/cli/tui/screens/agent/AddAgentScreen.tsx
@@ -997,7 +997,7 @@ export function AddAgentScreen({ existingAgentNames, onComplete, onExit }: AddAg
)[wizard.step];
+ const initialSelectedIndex = items.findIndex(item => item.id === selectedValueForStep);
+
const { selectedIndex } = useListNavigation({
items,
onSelect: handleSelect,
@@ -168,6 +173,7 @@ export function GenerateWizardUI({
isActive: isActive && isSelectStep && !isAdvancedStep,
isDisabled: item => item.disabled ?? false,
resetKey: wizard.step,
+ initialSelectedIndex: initialSelectedIndex >= 0 ? initialSelectedIndex : undefined,
});
const advancedNav = useMultiSelectNavigation({
@@ -390,7 +396,7 @@ export function GenerateWizardUI({
{isIdleTimeoutStep && (
{
if (!value) return true;
@@ -413,7 +419,7 @@ export function GenerateWizardUI({
{isMaxLifetimeStep && (
{
if (!value) return true;