From 5ce784741b72900fdbad7e5ea09b27a0782a2072 Mon Sep 17 00:00:00 2001 From: Aidan Daly Date: Thu, 25 Jun 2026 06:13:26 +0000 Subject: [PATCH] fix(tui): restore prior wizard input on back-navigation (#1590) Seed text inputs and single-select cursors from retained wizard config so navigating back to a step shows the previously typed/selected value instead of regenerated defaults or the first option. - useListNavigation: add initialSelectedIndex, honored on mount and on resetKey change, defaults to 0/first-enabled when unset or invalid - AddAgentScreen: name step initialValue = name || generateUniqueName(...) - GenerateWizardUI: seed select cursors and idle-timeout/max-lifetime text inputs from wizard.config Refs aws/agentcore-cli#1590 --- .../__tests__/useListNavigation.test.tsx | 45 +++++++++++++++++++ src/cli/tui/hooks/useListNavigation.ts | 24 +++++++--- src/cli/tui/screens/agent/AddAgentScreen.tsx | 2 +- .../tui/screens/generate/GenerateWizardUI.tsx | 10 ++++- 4 files changed, 73 insertions(+), 8 deletions(-) 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;