diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx index 601eb82bc48..7a6d293dab1 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx @@ -85,6 +85,7 @@ export function Autocomplete(props: { index: 0, selected: 0, visible: false as AutocompleteRef["visible"], + mouseHasMoved: false, }) const [positionTick, setPositionTick] = createSignal(0) @@ -588,9 +589,17 @@ export function Autocomplete(props: { setStore({ visible: mode, index: props.input().cursorOffset, + mouseHasMoved: false, }) } + // Reset mouse movement tracking when filter changes to prevent accidental selection + // when items change position under a stationary cursor + createEffect(() => { + filter() + setStore("mouseHasMoved", false) + }) + function hide() { const text = props.input().plainText if (store.visible === "/" && !text.endsWith(" ") && text.startsWith("/")) { @@ -723,41 +732,54 @@ export function Autocomplete(props: { {...SplitBorder} borderColor={theme.border} > - (scroll = r)} - backgroundColor={theme.backgroundMenu} - height={height()} - scrollbarOptions={{ visible: false }} - > - - No matching items - - } + setStore("mouseHasMoved", true)}> + (scroll = r)} + backgroundColor={theme.backgroundMenu} + height={height()} + scrollbarOptions={{ visible: false }} > - {(option, index) => ( - moveTo(index)} - onMouseUp={() => select()} - > - - {option().display} - - - - {option().description} + + No matching items + + } + > + {(option, index) => ( + { + // Once mouse moves, enable hover selection and immediately select this item + if (!store.mouseHasMoved) { + setStore("mouseHasMoved", true) + moveTo(index) + } + }} + onMouseOver={() => { + // Only select on hover if mouse has moved (prevents accidental selection) + if (!store.mouseHasMoved) return + moveTo(index) + }} + onMouseUp={() => select()} + > + + {option().display} - - - )} - - + + + {option().description} + + + + )} + + + ) } diff --git a/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx b/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx index d3239ebac63..a3378030884 100644 --- a/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx +++ b/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx @@ -52,6 +52,7 @@ export function DialogSelect(props: DialogSelectProps) { const [store, setStore] = createStore({ selected: 0, filter: "", + mouseHasMoved: false, }) createEffect( @@ -109,6 +110,10 @@ export function DialogSelect(props: DialogSelectProps) { createEffect( on([() => store.filter, () => props.current], ([filter, current]) => { + // Reset mouse movement tracking when filter changes to prevent accidental selection + // when items change position under a stationary cursor + setStore("mouseHasMoved", false) + setTimeout(() => { if (filter.length > 0) { moveTo(0, true) @@ -248,61 +253,75 @@ export function DialogSelect(props: DialogSelectProps) { } > - (scroll = r)} - maxHeight={height()} - > - - {([category, options], index) => ( - <> - - 0 ? 1 : 0} paddingLeft={3}> - - {category} - - - - - {(option) => { - const active = createMemo(() => isDeepEqual(option.value, selected()?.value)) - const current = createMemo(() => isDeepEqual(option.value, props.current)) - return ( - { - option.onSelect?.(dialog) - props.onSelect?.(option) - }} - onMouseOver={() => { - const index = flat().findIndex((x) => isDeepEqual(x.value, option.value)) - if (index === -1) return - moveTo(index) - }} - backgroundColor={active() ? (option.bg ?? theme.primary) : RGBA.fromInts(0, 0, 0, 0)} - paddingLeft={current() || option.gutter ? 1 : 3} - paddingRight={3} - gap={1} - > - - ) - }} - - - )} - - + setStore("mouseHasMoved", true)}> + (scroll = r)} + maxHeight={height()} + > + + {([category, options], index) => ( + <> + + 0 ? 1 : 0} paddingLeft={3}> + + {category} + + + + + {(option) => { + const active = createMemo(() => isDeepEqual(option.value, selected()?.value)) + const current = createMemo(() => isDeepEqual(option.value, props.current)) + return ( + { + // Once mouse moves, enable hover selection and immediately select this item + if (!store.mouseHasMoved) { + setStore("mouseHasMoved", true) + const index = flat().findIndex((x) => isDeepEqual(x.value, option.value)) + if (index !== -1) moveTo(index) + } + }} + onMouseUp={() => { + option.onSelect?.(dialog) + props.onSelect?.(option) + }} + onMouseOver={() => { + // Only select on hover if mouse has moved (prevents accidental selection) + if (!store.mouseHasMoved) return + + const index = flat().findIndex((x) => isDeepEqual(x.value, option.value)) + if (index === -1) return + + moveTo(index) + }} + backgroundColor={active() ? (option.bg ?? theme.primary) : RGBA.fromInts(0, 0, 0, 0)} + paddingLeft={current() || option.gutter ? 1 : 3} + paddingRight={3} + gap={1} + > + + ) + }} + + + )} + + + }>