diff --git a/webview-ui/src/components/chat/ChatTextArea.tsx b/webview-ui/src/components/chat/ChatTextArea.tsx index fb105056b5..cc30ca103b 100644 --- a/webview-ui/src/components/chat/ChatTextArea.tsx +++ b/webview-ui/src/components/chat/ChatTextArea.tsx @@ -31,6 +31,7 @@ import { MAX_IMAGES_PER_MESSAGE } from "./ChatView" import ContextMenu from "./ContextMenu" import { IndexingStatusBadge } from "./IndexingStatusBadge" import { usePromptHistory } from "./hooks/usePromptHistory" +import "../../styles/chat-textarea.css" interface ChatTextAreaProps { inputValue: string @@ -205,6 +206,7 @@ export const ChatTextArea = forwardRef( const [isMouseDownOnMenu, setIsMouseDownOnMenu] = useState(false) const highlightLayerRef = useRef(null) const [selectedMenuIndex, setSelectedMenuIndex] = useState(-1) + const [isCtrlOrCmdPressed, setIsCtrlOrCmdPressed] = useState(false) const [selectedType, setSelectedType] = useState(null) const [justDeletedSpaceAfterMention, setJustDeletedSpaceAfterMention] = useState(false) const [intendedCursorPosition, setIntendedCursorPosition] = useState(null) @@ -712,6 +714,59 @@ export const ChatTextArea = forwardRef( setIsMouseDownOnMenu(true) }, []) + // Track ctrl/cmd key state + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.ctrlKey || e.metaKey) { + setIsCtrlOrCmdPressed(true) + } + } + + const handleKeyUp = (e: KeyboardEvent) => { + if (!e.ctrlKey && !e.metaKey) { + setIsCtrlOrCmdPressed(false) + } + } + + window.addEventListener("keydown", handleKeyDown) + window.addEventListener("keyup", handleKeyUp) + + return () => { + window.removeEventListener("keydown", handleKeyDown) + window.removeEventListener("keyup", handleKeyUp) + } + }, []) + + // Handle clicks on mentions in the highlight layer + const handleHighlightClick = useCallback((e: React.MouseEvent) => { + // Only handle clicks with ctrl/cmd key pressed + if (!e.ctrlKey && !e.metaKey) { + return + } + + // Get the clicked element + const target = e.target as HTMLElement + + // Check if we clicked on a mention highlight + if (target.classList.contains("mention-context-textarea-highlight")) { + const mentionText = target.textContent + if (mentionText) { + // Extract the mention text (remove @ prefix if present) + const cleanMention = mentionText.startsWith("@") ? mentionText.slice(1) : mentionText + + // Send message to open the mention + vscode.postMessage({ + type: "openMention", + text: cleanMention, + }) + + // Prevent default behavior + e.preventDefault() + e.stopPropagation() + } + } + }, []) + const updateHighlights = useCallback(() => { if (!textAreaRef.current || !highlightLayerRef.current) return @@ -982,6 +1037,7 @@ export const ChatTextArea = forwardRef(
( "z-10", "forced-color-adjust-none", "rounded", + isCtrlOrCmdPressed && "mention-highlight-clickable", )} style={{ color: "transparent", diff --git a/webview-ui/src/styles/chat-textarea.css b/webview-ui/src/styles/chat-textarea.css new file mode 100644 index 0000000000..c5a2965d5e --- /dev/null +++ b/webview-ui/src/styles/chat-textarea.css @@ -0,0 +1,28 @@ +/* Styles for clickable mentions in the chat textarea */ +.mention-highlight-clickable .mention-context-textarea-highlight { + cursor: pointer; + position: relative; +} + +.mention-highlight-clickable .mention-context-textarea-highlight:hover { + text-decoration: underline; + text-decoration-color: var(--vscode-textLink-foreground); + text-underline-offset: 2px; +} + +/* Visual feedback for clickable state */ +.mention-highlight-clickable .mention-context-textarea-highlight::after { + content: ""; + position: absolute; + inset: -2px; + border-radius: 3px; + pointer-events: none; + opacity: 0; + transition: opacity 0.15s ease; + background: var(--vscode-textLink-foreground); + mix-blend-mode: multiply; +} + +.mention-highlight-clickable .mention-context-textarea-highlight:hover::after { + opacity: 0.1; +}