diff --git a/src/components/CustomSelect/use-multi-select-state.ts b/src/components/CustomSelect/use-multi-select-state.ts index bf2bd8b3..f8d5a042 100644 --- a/src/components/CustomSelect/use-multi-select-state.ts +++ b/src/components/CustomSelect/use-multi-select-state.ts @@ -1,5 +1,4 @@ import { useCallback, useState } from 'react' -import { isDeepStrictEqual } from 'util' import { useRegisterOverlay } from '../../context/overlayContext.js' import type { InputEvent } from '../../ink/events/input-event.js' // eslint-disable-next-line custom-rules/prefer-use-keybindings -- raw space/arrow multiselect input @@ -9,6 +8,7 @@ import { normalizeFullWidthSpace, } from '../../utils/stringUtils.js' import type { OptionWithDescription } from './select.js' +import { optionsNavigateEqual } from './use-select-navigation.js' import { useSelectNavigation } from './use-select-navigation.js' export type UseMultiSelectStateProps = { @@ -174,7 +174,7 @@ export function useMultiSelectState({ // and the deleted ui/useMultiSelectState.ts — without this, MCPServerDesktopImportDialog // keeps colliding servers checked after getAllMcpConfigs() resolves. const [lastOptions, setLastOptions] = useState(options) - if (options !== lastOptions && !isDeepStrictEqual(options, lastOptions)) { + if (options !== lastOptions && !optionsNavigateEqual(options, lastOptions)) { setSelectedValues(defaultValue) setLastOptions(options) } diff --git a/src/components/CustomSelect/use-select-navigation.ts b/src/components/CustomSelect/use-select-navigation.ts index 544bbfa7..441895a0 100644 --- a/src/components/CustomSelect/use-select-navigation.ts +++ b/src/components/CustomSelect/use-select-navigation.ts @@ -6,10 +6,34 @@ import { useRef, useState, } from 'react' -import { isDeepStrictEqual } from 'util' import OptionMap from './option-map.js' import type { OptionWithDescription } from './select.js' +/** + * Compare two option arrays for structural equality on properties that + * affect navigation behavior. ReactNode `label` and function `onChange` + * are intentionally excluded — they are identity-unstable (new reference + * each render) but don't change navigation semantics. + */ +export function optionsNavigateEqual( + a: OptionWithDescription[], + b: OptionWithDescription[], +): boolean { + if (a.length !== b.length) return false + for (let i = 0; i < a.length; i++) { + const ao = a[i]! + const bo = b[i]! + if ( + ao.value !== bo.value || + ao.disabled !== bo.disabled || + ao.type !== bo.type + ) { + return false + } + } + return true +} + type State = { /** * Map where key is option's value and value is option's index. @@ -524,7 +548,7 @@ export function useSelectNavigation({ const [lastOptions, setLastOptions] = useState(options) - if (options !== lastOptions && !isDeepStrictEqual(options, lastOptions)) { + if (options !== lastOptions && !optionsNavigateEqual(options, lastOptions)) { dispatch({ type: 'reset', state: createDefaultState({