Skip to content

Commit b3b7b9d

Browse files
authored
ENG-516 Slash commands (RooCodeInc#3044)
* scroll * menu * changeset * nit
1 parent b0df763 commit b3b7b9d

File tree

3 files changed

+69
-7
lines changed

3 files changed

+69
-7
lines changed

.changeset/clean-birds-count.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"claude-dev": minor
3+
---
4+
5+
menu fix for slash commands

webview-ui/src/components/chat/ChatTextArea.tsx

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,11 @@ import {
1717
} from "@/utils/context-mentions"
1818
import {
1919
SlashCommand,
20+
slashCommandDeleteRegex,
2021
shouldShowSlashCommandsMenu,
2122
getMatchingSlashCommands,
2223
insertSlashCommand,
24+
removeSlashCommand,
2325
validateSlashCommand,
2426
} from "@/utils/slash-commands"
2527
import { useMetaKeyDetection, useShortcut } from "@/utils/hooks"
@@ -253,6 +255,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
253255
const [selectedMenuIndex, setSelectedMenuIndex] = useState(-1)
254256
const [selectedType, setSelectedType] = useState<ContextMenuOptionType | null>(null)
255257
const [justDeletedSpaceAfterMention, setJustDeletedSpaceAfterMention] = useState(false)
258+
const [justDeletedSpaceAfterSlashCommand, setJustDeletedSpaceAfterSlashCommand] = useState(false)
256259
const [intendedCursorPosition, setIntendedCursorPosition] = useState<number | null>(null)
257260
const contextMenuContainerRef = useRef<HTMLDivElement>(null)
258261
const [showModelSelector, setShowModelSelector] = useState(false)
@@ -525,31 +528,59 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
525528
charBeforeCursor === " " || charBeforeCursor === "\n" || charBeforeCursor === "\r\n"
526529
const charAfterIsWhitespace =
527530
charAfterCursor === " " || charAfterCursor === "\n" || charAfterCursor === "\r\n"
528-
// checks if char before cursor is whitespace after a mention
531+
532+
// Check if we're right after a space that follows a mention or slash command
529533
if (
530534
charBeforeIsWhitespace &&
531-
inputValue.slice(0, cursorPosition - 1).match(new RegExp(mentionRegex.source + "$")) // "$" is added to ensure the match occurs at the end of the string
535+
inputValue.slice(0, cursorPosition - 1).match(new RegExp(mentionRegex.source + "$"))
532536
) {
537+
// File mention handling
533538
const newCursorPosition = cursorPosition - 1
534-
// if mention is followed by another word, then instead of deleting the space separating them we just move the cursor to the end of the mention
535539
if (!charAfterIsWhitespace) {
536540
event.preventDefault()
537541
textAreaRef.current?.setSelectionRange(newCursorPosition, newCursorPosition)
538542
setCursorPosition(newCursorPosition)
539543
}
540544
setCursorPosition(newCursorPosition)
541545
setJustDeletedSpaceAfterMention(true)
542-
} else if (justDeletedSpaceAfterMention) {
546+
setJustDeletedSpaceAfterSlashCommand(false)
547+
} else if (charBeforeIsWhitespace && inputValue.slice(0, cursorPosition - 1).match(slashCommandDeleteRegex)) {
548+
// New slash command handling
549+
const newCursorPosition = cursorPosition - 1
550+
if (!charAfterIsWhitespace) {
551+
event.preventDefault()
552+
textAreaRef.current?.setSelectionRange(newCursorPosition, newCursorPosition)
553+
setCursorPosition(newCursorPosition)
554+
}
555+
setCursorPosition(newCursorPosition)
556+
setJustDeletedSpaceAfterSlashCommand(true)
557+
setJustDeletedSpaceAfterMention(false)
558+
}
559+
// Handle the second backspace press for mentions or slash commands
560+
else if (justDeletedSpaceAfterMention) {
543561
const { newText, newPosition } = removeMention(inputValue, cursorPosition)
544562
if (newText !== inputValue) {
545563
event.preventDefault()
546564
setInputValue(newText)
547-
setIntendedCursorPosition(newPosition) // Store the new cursor position in state
565+
setIntendedCursorPosition(newPosition)
548566
}
549567
setJustDeletedSpaceAfterMention(false)
550568
setShowContextMenu(false)
551-
} else {
569+
} else if (justDeletedSpaceAfterSlashCommand) {
570+
// New slash command deletion
571+
const { newText, newPosition } = removeSlashCommand(inputValue, cursorPosition)
572+
if (newText !== inputValue) {
573+
event.preventDefault()
574+
setInputValue(newText)
575+
setIntendedCursorPosition(newPosition)
576+
}
577+
setJustDeletedSpaceAfterSlashCommand(false)
578+
setShowSlashCommandsMenu(false)
579+
}
580+
// Default case - reset flags if none of the above apply
581+
else {
552582
setJustDeletedSpaceAfterMention(false)
583+
setJustDeletedSpaceAfterSlashCommand(false)
553584
}
554585
}
555586
},
@@ -566,6 +597,10 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
566597
justDeletedSpaceAfterMention,
567598
queryItems,
568599
fileSearchResults,
600+
showSlashCommandsMenu,
601+
selectedSlashCommandsIndex,
602+
slashCommandsQuery,
603+
handleSlashCommandsSelect,
569604
],
570605
)
571606

@@ -792,7 +827,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
792827
if (isValidCommand) {
793828
const fullCommand = processedText.substring(slashIndex, endIndex) // includes slash
794829

795-
const highlighted = `<mark class="slash-command-match-textarea-highlight">${fullCommand}</mark>`
830+
const highlighted = `<mark class="mention-context-textarea-highlight">${fullCommand}</mark>`
796831
processedText = processedText.substring(0, slashIndex) + highlighted + processedText.substring(endIndex)
797832
}
798833
}

webview-ui/src/utils/slash-commands.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,28 @@ export const SUPPORTED_SLASH_COMMANDS: SlashCommand[] = [
1313
// Regex for detecting slash commands in text
1414
export const slashCommandRegex = /\/([a-zA-Z0-9_-]+)(\s|$)/
1515
export const slashCommandRegexGlobal = new RegExp(slashCommandRegex.source, "g")
16+
export const slashCommandDeleteRegex = /^\s*\/([a-zA-Z0-9_-]+)$/
17+
18+
/**
19+
* Removes a slash command at the cursor position
20+
*/
21+
export function removeSlashCommand(text: string, position: number): { newText: string; newPosition: number } {
22+
const beforeCursor = text.slice(0, position)
23+
const afterCursor = text.slice(position)
24+
25+
// Check if we're at the end of a slash command
26+
const matchEnd = beforeCursor.match(slashCommandDeleteRegex)
27+
28+
if (matchEnd) {
29+
// If we're at the end of a slash command, remove it
30+
const newText = text.slice(0, position - matchEnd[0].length) + afterCursor.replace(" ", "") // removes the first space after the command
31+
const newPosition = position - matchEnd[0].length
32+
return { newText, newPosition }
33+
}
34+
35+
// If we're not at the end of a slash command, just return the original text and position
36+
return { newText: text, newPosition: position }
37+
}
1638

1739
/**
1840
* Determines whether the slash command menu should be displayed based on text input

0 commit comments

Comments
 (0)