Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 73 additions & 4 deletions webview-ui/src/components/chat/ChatTextArea.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,10 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
commands,
} = useExtensionState()

// Detect if we're on Mac for modifier key detection
const isMac = typeof navigator !== "undefined" && navigator.platform.toUpperCase().indexOf("MAC") >= 0
const [isModifierPressed, setIsModifierPressed] = useState(false)

// Find the ID and display text for the currently selected API configuration.
const { currentConfigId, displayName } = useMemo(() => {
const currentConfig = listApiConfigMeta?.find((config) => config.name === currentApiConfigName)
Expand Down Expand Up @@ -221,6 +225,54 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
setInputValue,
})

// Handle modifier key detection for mention clicks
const handleGlobalKeyDown = useCallback(
(e: KeyboardEvent) => {
if ((isMac && e.metaKey) || (!isMac && e.ctrlKey)) {
setIsModifierPressed(true)
}
},
[isMac],
)

const handleGlobalKeyUp = useCallback(
(e: KeyboardEvent) => {
if ((isMac && !e.metaKey) || (!isMac && !e.ctrlKey)) {
setIsModifierPressed(false)
}
},
[isMac],
)

// Add global event listeners for modifier key detection
useEffect(() => {
document.addEventListener("keydown", handleGlobalKeyDown)
document.addEventListener("keyup", handleGlobalKeyUp)

return () => {
document.removeEventListener("keydown", handleGlobalKeyDown)
document.removeEventListener("keyup", handleGlobalKeyUp)
}
}, [handleGlobalKeyDown, handleGlobalKeyUp])

// Handle clicks on mentions in the highlight layer
const handleMentionClick = useCallback(
(e: MouseEvent) => {
if (!isModifierPressed) return

const target = e.target as HTMLElement
if (target.tagName === "MARK" && target.classList.contains("mention-context-textarea-highlight")) {
const mentionText = target.textContent
if (mentionText) {
// Remove @ symbol if present and send to VSCode
const cleanText = mentionText.startsWith("@") ? mentionText.slice(1) : mentionText
vscode.postMessage({ type: "openMention", text: cleanText })
}
}
},
[isModifierPressed],
)

// Fetch git commits when Git is selected or when typing a hash.
useEffect(() => {
if (selectedType === ContextMenuOptionType.Git || /^[a-f0-9]+$/i.test(searchQuery)) {
Expand Down Expand Up @@ -717,11 +769,17 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
return commands?.some((cmd) => cmd.name === commandName) || false
}

// Determine the class to use based on modifier key state
const mentionClass = `mention-context-textarea-highlight${isModifierPressed ? " clickable" : ""}`

// Process the text to highlight mentions and valid commands
let processedText = text
.replace(/\n$/, "\n\n")
.replace(/[<>&]/g, (c) => ({ "<": "&lt;", ">": "&gt;", "&": "&amp;" })[c] || c)
.replace(mentionRegexGlobal, '<mark class="mention-context-textarea-highlight">$&</mark>')
.replace(
mentionRegexGlobal,
`<mark class="${mentionClass}" title="${isModifierPressed ? `${isMac ? "Cmd" : "Ctrl"} + Click to open` : `Hold ${isMac ? "Cmd" : "Ctrl"} + Click to open`}">$&</mark>`,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider extracting the tooltip texts ('Cmd + Click to open' and 'Hold Cmd + Click to open') into translation keys using the translation function (t). Hardcoding these user‐facing texts may affect localization consistency.

This comment was generated because it violated a code review rule: irule_C0ez7Rji6ANcGkkX.

)

// Custom replacement for commands - only highlight valid ones
processedText = processedText.replace(commandRegexGlobal, (match, commandName) => {
Expand All @@ -733,10 +791,10 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(

if (startsWithSpace) {
// Keep the space but only highlight the command part
return ` <mark class="mention-context-textarea-highlight">${commandPart}</mark>`
return ` <mark class="${mentionClass}" title="${isModifierPressed ? `${isMac ? "Cmd" : "Ctrl"} + Click to open` : `Hold ${isMac ? "Cmd" : "Ctrl"} + Click to open`}">${commandPart}</mark>`
} else {
// Highlight the entire command (starts at beginning of line)
return `<mark class="mention-context-textarea-highlight">${commandPart}</mark>`
return `<mark class="${mentionClass}" title="${isModifierPressed ? `${isMac ? "Cmd" : "Ctrl"} + Click to open` : `Hold ${isMac ? "Cmd" : "Ctrl"} + Click to open`}">${commandPart}</mark>`
}
}
return match // Return unhighlighted if command is not valid
Expand All @@ -746,12 +804,23 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(

highlightLayerRef.current.scrollTop = textAreaRef.current.scrollTop
highlightLayerRef.current.scrollLeft = textAreaRef.current.scrollLeft
}, [commands])
}, [commands, isModifierPressed, isMac])

useLayoutEffect(() => {
updateHighlights()
}, [inputValue, updateHighlights])

// Add click event listener to highlight layer for mention clicks
useEffect(() => {
const highlightLayer = highlightLayerRef.current
if (highlightLayer) {
highlightLayer.addEventListener("click", handleMentionClick)
return () => {
highlightLayer.removeEventListener("click", handleMentionClick)
}
}
}, [handleMentionClick])

const updateCursorPosition = useCallback(() => {
if (textAreaRef.current) {
setCursorPosition(textAreaRef.current.selectionStart)
Expand Down
58 changes: 58 additions & 0 deletions webview-ui/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -389,12 +389,70 @@ vscode-dropdown::part(listbox) {
.mention-context-highlight {
background-color: color-mix(in srgb, var(--vscode-badge-foreground) 30%, transparent);
border-radius: 3px;
transition: all 0.2s ease-in-out;
}

.mention-context-highlight:hover {
background-color: color-mix(in srgb, var(--vscode-badge-foreground) 45%, transparent);
transform: translateY(-1px);
box-shadow: 0 2px 4px color-mix(in srgb, var(--vscode-badge-foreground) 20%, transparent);
}

.mention-context-highlight.clickable {
cursor: pointer;
}

.mention-context-highlight.clickable:hover {
background-color: color-mix(in srgb, var(--vscode-textLink-foreground) 40%, transparent);
border: 1px solid color-mix(in srgb, var(--vscode-textLink-foreground) 60%, transparent);
}

.mention-context-highlight-with-shadow {
background-color: color-mix(in srgb, var(--vscode-badge-foreground) 30%, transparent);
border-radius: 3px;
box-shadow: 0 0 0 0.5px color-mix(in srgb, var(--vscode-badge-foreground) 30%, transparent);
transition: all 0.2s ease-in-out;
}

.mention-context-highlight-with-shadow:hover {
background-color: color-mix(in srgb, var(--vscode-badge-foreground) 45%, transparent);
transform: translateY(-1px);
box-shadow: 0 2px 4px color-mix(in srgb, var(--vscode-badge-foreground) 20%, transparent);
}

.mention-context-highlight-with-shadow.clickable {
cursor: pointer;
}

.mention-context-highlight-with-shadow.clickable:hover {
background-color: color-mix(in srgb, var(--vscode-textLink-foreground) 40%, transparent);
border: 1px solid color-mix(in srgb, var(--vscode-textLink-foreground) 60%, transparent);
box-shadow: 0 2px 6px color-mix(in srgb, var(--vscode-textLink-foreground) 30%, transparent);
}

.mention-context-textarea-highlight {
background-color: color-mix(in srgb, var(--vscode-badge-foreground) 30%, transparent);
border-radius: 3px;
transition: all 0.2s ease-in-out;
cursor: default;
pointer-events: none;
}

.mention-context-textarea-highlight:hover {
background-color: color-mix(in srgb, var(--vscode-badge-foreground) 45%, transparent);
transform: translateY(-1px);
box-shadow: 0 2px 4px color-mix(in srgb, var(--vscode-badge-foreground) 20%, transparent);
}

.mention-context-textarea-highlight.clickable {
cursor: pointer;
pointer-events: auto;
}

.mention-context-textarea-highlight.clickable:hover {
background-color: color-mix(in srgb, var(--vscode-textLink-foreground) 40%, transparent);
border: 1px solid color-mix(in srgb, var(--vscode-textLink-foreground) 60%, transparent);
box-shadow: 0 2px 4px color-mix(in srgb, var(--vscode-textLink-foreground) 30%, transparent);
}

/**
Expand Down
Loading