diff --git a/src/chat-prefix-suggestions.ts b/src/chat-prefix-suggestions.ts index 983af790..215230dc 100644 --- a/src/chat-prefix-suggestions.ts +++ b/src/chat-prefix-suggestions.ts @@ -28,3 +28,37 @@ export function isPrefixPopoverUsable( ): boolean { return showPopover && suggestions.length > 0; } + +/** + * Keep the keyboard-selected suggestion visible. + * + * The popover scrolls when it has more suggestions than fit, but arrow keys + * only move the selection, so it would walk out of view. `block: 'nearest'` + * scrolls the list by the smallest amount and leaves it alone when the row is + * already visible. + */ +export function scrollSelectedSuggestionIntoView( + popover: HTMLElement | null, + index: number +): void { + const item = popover?.children[index] as HTMLElement | undefined; + item?.scrollIntoView?.({ block: 'nearest' }); +} + +/** + * The suggestion the keyboard currently points at. + * + * The selected index is kept in state and outlives the list it was chosen + * from: typing narrows the suggestions, so an index picked on row 12 of 30 can + * point past the end of a list of 3. Read as-is it selects nothing, and Enter + * then tries to apply a suggestion that does not exist. Fall back to the first + * row instead, so the selection is always a real row while the list has any. + */ +export function activeSuggestionIndex( + selectedIndex: number, + suggestionCount: number +): number { + return selectedIndex >= 0 && selectedIndex < suggestionCount + ? selectedIndex + : 0; +} diff --git a/src/chat-sidebar.tsx b/src/chat-sidebar.tsx index c0f46e79..24152795 100644 --- a/src/chat-sidebar.tsx +++ b/src/chat-sidebar.tsx @@ -103,8 +103,10 @@ import { recordStoppedTurn } from './chat-stopped-turn'; import { upsertMessageById } from './chat-transcript'; import { IClaudeSessionInfo } from './api'; import { + activeSuggestionIndex, isPrefixPopoverUsable, - prefixesMatching + prefixesMatching, + scrollSelectedSuggestionIntoView } from './chat-prefix-suggestions'; import { NOTEBOOK_GENERATION_PROGRESS_EVENT, @@ -2910,6 +2912,18 @@ function SidebarComponent(props: any) { }; const popoverUsable = isPrefixPopoverUsable(showPopover, prefixSuggestions); + const activePrefixSuggestionIndex = activeSuggestionIndex( + selectedPrefixSuggestionIndex, + prefixSuggestions.length + ); + useEffect(() => { + if (popoverUsable) { + scrollSelectedSuggestionIntoView( + autocompleteRef.current, + activePrefixSuggestionIndex + ); + } + }, [popoverUsable, activePrefixSuggestionIndex]); const applyPrefixSuggestion = async (prefix: string) => { let mcpArguments = ''; @@ -3397,6 +3411,9 @@ function SidebarComponent(props: any) { const filterPrefixSuggestions = (prmpt: string) => { setPrefixSuggestions(prefixesMatching(originalPrefixes, prmpt)); + // The narrowed list is a different list, so start from its first row + // rather than keep an index chosen from the previous one. + setSelectedPrefixSuggestionIndex(0); }; const resetPrefixSuggestions = () => { @@ -3464,7 +3481,7 @@ function SidebarComponent(props: any) { event.stopPropagation(); event.preventDefault(); if (popoverUsable) { - applyPrefixSuggestion(prefixSuggestions[selectedPrefixSuggestionIndex]); + applyPrefixSuggestion(prefixSuggestions[activePrefixSuggestionIndex]); return; } @@ -3475,7 +3492,7 @@ function SidebarComponent(props: any) { if (popoverUsable) { event.stopPropagation(); event.preventDefault(); - applyPrefixSuggestion(prefixSuggestions[selectedPrefixSuggestionIndex]); + applyPrefixSuggestion(prefixSuggestions[activePrefixSuggestionIndex]); return; } } else if (event.key === 'Escape') { @@ -3491,7 +3508,7 @@ function SidebarComponent(props: any) { if (popoverUsable) { setSelectedPrefixSuggestionIndex( - (selectedPrefixSuggestionIndex - 1 + prefixSuggestions.length) % + (activePrefixSuggestionIndex - 1 + prefixSuggestions.length) % prefixSuggestions.length ); return; @@ -3522,7 +3539,7 @@ function SidebarComponent(props: any) { if (popoverUsable) { setSelectedPrefixSuggestionIndex( - (selectedPrefixSuggestionIndex + 1 + prefixSuggestions.length) % + (activePrefixSuggestionIndex + 1 + prefixSuggestions.length) % prefixSuggestions.length ); return; @@ -4619,7 +4636,7 @@ function SidebarComponent(props: any) { {prefixSuggestions.map((prefix, index) => (