From 173246723ebb0a7694e8cefa045e27098019e50b Mon Sep 17 00:00:00 2001 From: Nan Li Date: Wed, 23 Jul 2025 14:15:35 +0800 Subject: [PATCH] fix: resolve RadioButtonSelect array bounds crash and auth dialog navigation (#46) - Add bounds checking in RadioButtonSelect to prevent accessing undefined array elements - Add useEffect to ensure activeIndex stays within valid bounds when items array changes - Add validation guards around navigation handlers (up/down arrow keys) - Fix AuthDialog initialAuthIndex calculation to prevent negative values from findIndex - Ensure Enter key works properly on authentication screen Fixes TypeError: Cannot read properties of undefined (reading 'value') that occurred when activeIndex was out of bounds due to dynamic array changes or invalid initialization. Signed-off-by: loheagn Co-authored-by: linan.loheagn3 --- packages/cli/src/ui/components/AuthDialog.tsx | 4 +-- .../components/shared/RadioButtonSelect.tsx | 36 ++++++++++++++----- 2 files changed, 30 insertions(+), 10 deletions(-) diff --git a/packages/cli/src/ui/components/AuthDialog.tsx b/packages/cli/src/ui/components/AuthDialog.tsx index 1a7c73a3..4984a1c9 100644 --- a/packages/cli/src/ui/components/AuthDialog.tsx +++ b/packages/cli/src/ui/components/AuthDialog.tsx @@ -47,7 +47,7 @@ export function AuthDialog({ const [showOpenAIKeyPrompt, setShowOpenAIKeyPrompt] = useState(false); const items = [{ label: 'OpenAI', value: AuthType.USE_OPENAI }]; - const initialAuthIndex = items.findIndex((item) => { + const initialAuthIndex = Math.max(0, items.findIndex((item) => { if (settings.merged.selectedAuthType) { return item.value === settings.merged.selectedAuthType; } @@ -64,7 +64,7 @@ export function AuthDialog({ } return item.value === AuthType.LOGIN_WITH_GOOGLE; - }); + })); const handleAuthSelect = (authMethod: AuthType) => { const error = validateAuthMethod(authMethod); diff --git a/packages/cli/src/ui/components/shared/RadioButtonSelect.tsx b/packages/cli/src/ui/components/shared/RadioButtonSelect.tsx index 499c136a..53bf29fa 100644 --- a/packages/cli/src/ui/components/shared/RadioButtonSelect.tsx +++ b/packages/cli/src/ui/components/shared/RadioButtonSelect.tsx @@ -59,6 +59,15 @@ export function RadioButtonSelect({ const [activeIndex, setActiveIndex] = useState(initialIndex); const [scrollOffset, setScrollOffset] = useState(0); + // Ensure activeIndex is always within bounds when items change + useEffect(() => { + if (items.length === 0) { + setActiveIndex(0); + } else if (activeIndex >= items.length) { + setActiveIndex(Math.max(0, items.length - 1)); + } + }, [items.length, activeIndex]); + useEffect(() => { const newScrollOffset = Math.max( 0, @@ -74,17 +83,28 @@ export function RadioButtonSelect({ useInput( (input, key) => { if (input === 'k' || key.upArrow) { - const newIndex = activeIndex > 0 ? activeIndex - 1 : items.length - 1; - setActiveIndex(newIndex); - onHighlight?.(items[newIndex]!.value); + if (items.length > 0) { + const newIndex = activeIndex > 0 ? activeIndex - 1 : items.length - 1; + setActiveIndex(newIndex); + if (items[newIndex]) { + onHighlight?.(items[newIndex].value); + } + } } if (input === 'j' || key.downArrow) { - const newIndex = activeIndex < items.length - 1 ? activeIndex + 1 : 0; - setActiveIndex(newIndex); - onHighlight?.(items[newIndex]!.value); + if (items.length > 0) { + const newIndex = activeIndex < items.length - 1 ? activeIndex + 1 : 0; + setActiveIndex(newIndex); + if (items[newIndex]) { + onHighlight?.(items[newIndex].value); + } + } } if (key.return) { - onSelect(items[activeIndex]!.value); + // Add bounds check before accessing items[activeIndex] + if (activeIndex >= 0 && activeIndex < items.length && items[activeIndex]) { + onSelect(items[activeIndex].value); + } } // Enable selection directly from number keys. @@ -98,7 +118,7 @@ export function RadioButtonSelect({ } } }, - { isActive: isFocused && items.length > 0 }, + { isActive: isFocused && items.length > 0 && activeIndex >= 0 && activeIndex < items.length }, ); const visibleItems = items.slice(scrollOffset, scrollOffset + maxItemsToShow);