diff --git a/webview-ui/src/components/chat/ContextMenu.tsx b/webview-ui/src/components/chat/ContextMenu.tsx index cf4b10a981da..8e0ad1427c3a 100644 --- a/webview-ui/src/components/chat/ContextMenu.tsx +++ b/webview-ui/src/components/chat/ContextMenu.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useMemo, useRef, useState } from "react" +import React, { useCallback, useEffect, useMemo, useRef, useState } from "react" import { getIconForFilePath, getIconUrlByName, getIconForDirectoryPath } from "vscode-material-icons" import { Settings } from "lucide-react" @@ -46,6 +46,9 @@ const ContextMenu: React.FC = ({ }) => { const [materialIconsBaseUri, setMaterialIconsBaseUri] = useState("") const menuRef = useRef(null) + const [hoveredIndex, setHoveredIndex] = useState(null) + const [hasMouseMoved, setHasMouseMoved] = useState(false) + const lastMousePosRef = useRef<{ x: number; y: number } | null>(null) const filteredOptions = useMemo(() => { return getContextMenuOptions(searchQuery, selectedType, queryItems, dynamicSearchResults, modes, commands) @@ -73,6 +76,33 @@ const ContextMenu: React.FC = ({ setMaterialIconsBaseUri(w.MATERIAL_ICONS_BASE_URI) }, []) + // Track mouse movement to distinguish between actual hover and incidental cursor position + const handleMouseMove = useCallback((e: React.MouseEvent) => { + const currentPos = { x: e.clientX, y: e.clientY } + + // If this is the first mouse position, just store it + if (!lastMousePosRef.current) { + lastMousePosRef.current = currentPos + return + } + + // Check if the mouse has moved more than a threshold (e.g., 5 pixels) + const deltaX = Math.abs(currentPos.x - lastMousePosRef.current.x) + const deltaY = Math.abs(currentPos.y - lastMousePosRef.current.y) + + if (deltaX > 5 || deltaY > 5) { + setHasMouseMoved(true) + lastMousePosRef.current = currentPos + } + }, []) + + // Reset mouse tracking when menu opens/closes + useEffect(() => { + setHasMouseMoved(false) + setHoveredIndex(null) + lastMousePosRef.current = null + }, [searchQuery]) + const renderOptionContent = (option: ContextMenuQueryItem) => { switch (option.type) { case ContextMenuOptionType.SectionHeader: @@ -340,14 +370,18 @@ const ContextMenu: React.FC = ({ filteredOptions.map((option, index) => (
isOptionSelectable(option) && onSelect(option.type, option.value)} + onClick={() => { + if (isOptionSelectable(option)) { + setSelectedIndex(index) + onSelect(option.type, option.value) + } + }} style={{ padding: option.type === ContextMenuOptionType.SectionHeader ? "16px 8px 4px 8px" : "4px 8px", cursor: isOptionSelectable(option) ? "pointer" : "default", - color: "var(--vscode-dropdown-foreground)", display: "flex", alignItems: "center", justifyContent: "space-between", @@ -358,14 +392,33 @@ const ContextMenu: React.FC = ({ marginBottom: "2px", } : {}), + // Show different styles for selection vs hover ...(index === selectedIndex && isOptionSelectable(option) ? { backgroundColor: "var(--vscode-list-activeSelectionBackground)", color: "var(--vscode-list-activeSelectionForeground)", } - : {}), + : index === hoveredIndex && isOptionSelectable(option) && hasMouseMoved + ? { + backgroundColor: "var(--vscode-list-hoverBackground)", + color: "var(--vscode-dropdown-foreground)", + } + : { + color: "var(--vscode-dropdown-foreground)", + }), + }} + onMouseEnter={() => { + // Only update hover state if mouse has actually moved + if (isOptionSelectable(option) && hasMouseMoved) { + setHoveredIndex(index) + } }} - onMouseEnter={() => isOptionSelectable(option) && setSelectedIndex(index)}> + onMouseMove={handleMouseMove} + onMouseLeave={() => { + if (index === hoveredIndex) { + setHoveredIndex(null) + } + }}>