Something went wrong. Try again.
source dump of claude code forked from oppi.li/claude-code
Something went wrong. Try again.
11 kB · 414 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415import { 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 inputimport { useInput } from '../../ink.js'import { normalizeFullWidthDigits, normalizeFullWidthSpace,} from '../../utils/stringUtils.js'import type { OptionWithDescription } from './select.js'import { useSelectNavigation } from './use-select-navigation.js'
export type UseMultiSelectStateProps<T> = { /** * When disabled, user input is ignored. * * @default false */ isDisabled?: boolean
/** * Number of items to display. * * @default 5 */ visibleOptionCount?: number
/** * Options. */ options: OptionWithDescription<T>[]
/** * Initially selected values. */ defaultValue?: T[]
/** * Callback when selection changes. */ onChange?: (values: T[]) => void
/** * Callback for canceling the select. */ onCancel: () => void
/** * Callback for focusing an option. */ onFocus?: (value: T) => void
/** * Value to focus */ focusValue?: T
/** * Text for the submit button. When provided, a submit button is shown and * Enter toggles selection (submit only fires when the button is focused). * When omitted, Enter submits directly and Space toggles selection. */ submitButtonText?: string
/** * Callback when user submits. Receives the currently selected values. */ onSubmit?: (values: T[]) => void
/** * Callback when user presses down from the last item (submit button). * If provided, navigation will not wrap to the first item. */ onDownFromLastItem?: () => void
/** * Callback when user presses up from the first item. * If provided, navigation will not wrap to the last item. */ onUpFromFirstItem?: () => void
/** * Focus the last option initially instead of the first. */ initialFocusLast?: boolean
/** * When true, numeric keys (1-9) do not toggle options by index. * Mirrors the rendering layer's hideIndexes: if index labels aren't shown, * pressing a number shouldn't silently toggle an invisible mapping. */ hideIndexes?: boolean}
export type MultiSelectState<T> = { /** * Value of the currently focused option. */ focusedValue: T | undefined
/** * Index of the first visible option. */ visibleFromIndex: number
/** * Index of the last visible option. */ visibleToIndex: number
/** * All options. */ options: OptionWithDescription<T>[]
/** * Visible options. */ visibleOptions: Array<OptionWithDescription<T> & { index: number }>
/** * Whether the focused option is an input type. */ isInInput: boolean
/** * Currently selected values. */ selectedValues: T[]
/** * Current input field values. */ inputValues: Map<T, string>
/** * Whether the submit button is focused. */ isSubmitFocused: boolean
/** * Update an input field value. */ updateInputValue: (value: T, inputValue: string) => void
/** * Callback for canceling the select. */ onCancel: () => void}
export function useMultiSelectState<T>({ isDisabled = false, visibleOptionCount = 5, options, defaultValue = [], onChange, onCancel, onFocus, focusValue, submitButtonText, onSubmit, onDownFromLastItem, onUpFromFirstItem, initialFocusLast, hideIndexes = false,}: UseMultiSelectStateProps<T>): MultiSelectState<T> { const [selectedValues, setSelectedValues] = useState<T[]>(defaultValue) const [isSubmitFocused, setIsSubmitFocused] = useState(false)
// Reset selectedValues when options change (e.g. async-loaded data changes // defaultValue after mount). Mirrors the reset pattern in use-select-navigation.ts // 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)) { setSelectedValues(defaultValue) setLastOptions(options) }
// State for input type options const [inputValues, setInputValues] = useState<Map<T, string>>(() => { const initialMap = new Map<T, string>() options.forEach(option => { if (option.type === 'input' && option.initialValue) { initialMap.set(option.value, option.initialValue) } }) return initialMap })
const updateSelectedValues = useCallback( (values: T[] | ((prev: T[]) => T[])) => { const newValues = typeof values === 'function' ? values(selectedValues) : values setSelectedValues(newValues) onChange?.(newValues) }, [selectedValues, onChange], )
const navigation = useSelectNavigation<T>({ visibleOptionCount, options, initialFocusValue: initialFocusLast ? options[options.length - 1]?.value : undefined, onFocus, focusValue, })
// Automatically register as an overlay. // This ensures CancelRequestHandler won't intercept Escape when the multi-select is active. useRegisterOverlay('multi-select')
const updateInputValue = useCallback( (value: T, inputValue: string) => { setInputValues(prev => { const next = new Map(prev) next.set(value, inputValue) return next })
// Find the option and call its onChange const option = options.find(opt => opt.value === value) if (option && option.type === 'input') { option.onChange(inputValue) }
// Update selected values to include/exclude based on input updateSelectedValues(prev => { if (inputValue) { if (!prev.includes(value)) { return [...prev, value] } return prev } else { return prev.filter(v => v !== value) } }) }, [options, updateSelectedValues], )
// Handle all keyboard input useInput( (input, key, event: InputEvent) => { const normalizedInput = normalizeFullWidthDigits(input) const focusedOption = options.find( opt => opt.value === navigation.focusedValue, ) const isInInput = focusedOption?.type === 'input'
// When in input field, only allow navigation keys if (isInInput) { const isAllowedKey = key.upArrow || key.downArrow || key.escape || key.tab || key.return || (key.ctrl && (input === 'n' || input === 'p' || key.return)) if (!isAllowedKey) return }
const lastOptionValue = options[options.length - 1]?.value
// Handle Tab to move forward if (key.tab && !key.shift) { if ( submitButtonText && onSubmit && navigation.focusedValue === lastOptionValue && !isSubmitFocused ) { setIsSubmitFocused(true) } else if (!isSubmitFocused) { navigation.focusNextOption() } return }
// Handle Shift+Tab to move backward if (key.tab && key.shift) { if (submitButtonText && onSubmit && isSubmitFocused) { setIsSubmitFocused(false) navigation.focusOption(lastOptionValue) } else { navigation.focusPreviousOption() } return }
// Handle arrow down / Ctrl+N / j if ( key.downArrow || (key.ctrl && input === 'n') || (!key.ctrl && !key.shift && input === 'j') ) { if (isSubmitFocused && onDownFromLastItem) { onDownFromLastItem() } else if ( submitButtonText && onSubmit && navigation.focusedValue === lastOptionValue && !isSubmitFocused ) { setIsSubmitFocused(true) } else if ( !submitButtonText && onDownFromLastItem && navigation.focusedValue === lastOptionValue ) { // No submit button — exit from the last option onDownFromLastItem() } else if (!isSubmitFocused) { navigation.focusNextOption() } return }
// Handle arrow up / Ctrl+P / k if ( key.upArrow || (key.ctrl && input === 'p') || (!key.ctrl && !key.shift && input === 'k') ) { if (submitButtonText && onSubmit && isSubmitFocused) { setIsSubmitFocused(false) navigation.focusOption(lastOptionValue) } else if ( onUpFromFirstItem && navigation.focusedValue === options[0]?.value ) { onUpFromFirstItem() } else { navigation.focusPreviousOption() } return }
// Handle page navigation if (key.pageDown) { navigation.focusNextPage() return }
if (key.pageUp) { navigation.focusPreviousPage() return }
// Handle Enter or Space for selection/submit if (key.return || normalizeFullWidthSpace(input) === ' ') { // Ctrl+Enter from input field submits if (key.ctrl && key.return && isInInput && onSubmit) { onSubmit(selectedValues) return }
// Enter on submit button submits if (isSubmitFocused && onSubmit) { onSubmit(selectedValues) return }
// No submit button: Enter submits directly, Space still toggles if (key.return && !submitButtonText && onSubmit) { onSubmit(selectedValues) return }
// Enter or Space toggles selection (including for input fields) if (navigation.focusedValue !== undefined) { const newValues = selectedValues.includes(navigation.focusedValue) ? selectedValues.filter(v => v !== navigation.focusedValue) : [...selectedValues, navigation.focusedValue] updateSelectedValues(newValues) } return }
// Handle numeric keys (1-9) for direct selection if (!hideIndexes && /^[0-9]+$/.test(normalizedInput)) { const index = parseInt(normalizedInput) - 1 if (index >= 0 && index < options.length) { const value = options[index]!.value const newValues = selectedValues.includes(value) ? selectedValues.filter(v => v !== value) : [...selectedValues, value] updateSelectedValues(newValues) } return }
// Handle Escape if (key.escape) { onCancel() event.stopImmediatePropagation() } }, { isActive: !isDisabled }, )
return { ...navigation, selectedValues, inputValues, isSubmitFocused, updateInputValue, onCancel, }}