Skip to content

Commit 00a3738

Browse files
authored
Better command highlighting (RooCodeInc#6336)
1 parent e117208 commit 00a3738

File tree

2 files changed

+168
-3
lines changed

2 files changed

+168
-3
lines changed

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

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -714,15 +714,41 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
714714

715715
const text = textAreaRef.current.value
716716

717-
highlightLayerRef.current.innerHTML = text
717+
// Helper function to check if a command is valid
718+
const isValidCommand = (commandName: string): boolean => {
719+
return commands?.some((cmd) => cmd.name === commandName) || false
720+
}
721+
722+
// Process the text to highlight mentions and valid commands
723+
let processedText = text
718724
.replace(/\n$/, "\n\n")
719725
.replace(/[<>&]/g, (c) => ({ "<": "&lt;", ">": "&gt;", "&": "&amp;" })[c] || c)
720726
.replace(mentionRegexGlobal, '<mark class="mention-context-textarea-highlight">$&</mark>')
721-
.replace(commandRegexGlobal, '<mark class="mention-context-textarea-highlight">$&</mark>')
727+
728+
// Custom replacement for commands - only highlight valid ones
729+
processedText = processedText.replace(commandRegexGlobal, (match, commandName) => {
730+
// Only highlight if the command exists in the valid commands list
731+
if (isValidCommand(commandName)) {
732+
// Check if the match starts with a space
733+
const startsWithSpace = match.startsWith(" ")
734+
const commandPart = `/${commandName}`
735+
736+
if (startsWithSpace) {
737+
// Keep the space but only highlight the command part
738+
return ` <mark class="mention-context-textarea-highlight">${commandPart}</mark>`
739+
} else {
740+
// Highlight the entire command (starts at beginning of line)
741+
return `<mark class="mention-context-textarea-highlight">${commandPart}</mark>`
742+
}
743+
}
744+
return match // Return unhighlighted if command is not valid
745+
})
746+
747+
highlightLayerRef.current.innerHTML = processedText
722748

723749
highlightLayerRef.current.scrollTop = textAreaRef.current.scrollTop
724750
highlightLayerRef.current.scrollLeft = textAreaRef.current.scrollLeft
725-
}, [])
751+
}, [commands])
726752

727753
useLayoutEffect(() => {
728754
updateHighlights()
@@ -973,6 +999,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
973999
)}>
9741000
<div
9751001
ref={highlightLayerRef}
1002+
data-testid="highlight-layer"
9761003
className={cn(
9771004
"absolute",
9781005
"inset-0",

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

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -904,6 +904,144 @@ describe("ChatTextArea", () => {
904904
})
905905
})
906906

907+
describe("slash command highlighting", () => {
908+
const mockCommands = [
909+
{ name: "setup", source: "project", description: "Setup the project" },
910+
{ name: "deploy", source: "global", description: "Deploy the application" },
911+
{ name: "test-command", source: "project", description: "Test command with dash" },
912+
]
913+
914+
beforeEach(() => {
915+
;(useExtensionState as ReturnType<typeof vi.fn>).mockReturnValue({
916+
filePaths: [],
917+
openedTabs: [],
918+
taskHistory: [],
919+
cwd: "/test/workspace",
920+
commands: mockCommands,
921+
})
922+
})
923+
924+
it("should highlight valid slash commands", () => {
925+
const { getByTestId } = render(<ChatTextArea {...defaultProps} inputValue="/setup the project" />)
926+
927+
const highlightLayer = getByTestId("highlight-layer")
928+
expect(highlightLayer).toBeInTheDocument()
929+
930+
// The highlighting is applied via innerHTML, so we need to check the content
931+
// The valid command "/setup" should be highlighted
932+
expect(highlightLayer.innerHTML).toContain('<mark class="mention-context-textarea-highlight">/setup</mark>')
933+
})
934+
935+
it("should not highlight invalid slash commands", () => {
936+
const { getByTestId } = render(<ChatTextArea {...defaultProps} inputValue="/invalid command" />)
937+
938+
const highlightLayer = getByTestId("highlight-layer")
939+
expect(highlightLayer).toBeInTheDocument()
940+
941+
// The invalid command "/invalid" should not be highlighted
942+
expect(highlightLayer.innerHTML).not.toContain(
943+
'<mark class="mention-context-textarea-highlight">/invalid</mark>',
944+
)
945+
// But it should still contain the text without highlighting
946+
expect(highlightLayer.innerHTML).toContain("/invalid")
947+
})
948+
949+
it("should highlight only the command portion, not arguments", () => {
950+
const { getByTestId } = render(<ChatTextArea {...defaultProps} inputValue="/deploy to production" />)
951+
952+
const highlightLayer = getByTestId("highlight-layer")
953+
expect(highlightLayer).toBeInTheDocument()
954+
955+
// Only "/deploy" should be highlighted, not "to production"
956+
expect(highlightLayer.innerHTML).toContain(
957+
'<mark class="mention-context-textarea-highlight">/deploy</mark>',
958+
)
959+
expect(highlightLayer.innerHTML).not.toContain(
960+
'<mark class="mention-context-textarea-highlight">/deploy to production</mark>',
961+
)
962+
})
963+
964+
it("should handle commands with dashes and underscores", () => {
965+
const { getByTestId } = render(<ChatTextArea {...defaultProps} inputValue="/test-command with args" />)
966+
967+
const highlightLayer = getByTestId("highlight-layer")
968+
expect(highlightLayer).toBeInTheDocument()
969+
970+
// The command with dash should be highlighted
971+
expect(highlightLayer.innerHTML).toContain(
972+
'<mark class="mention-context-textarea-highlight">/test-command</mark>',
973+
)
974+
})
975+
976+
it("should be case-sensitive when matching commands", () => {
977+
const { getByTestId } = render(<ChatTextArea {...defaultProps} inputValue="/Setup the project" />)
978+
979+
const highlightLayer = getByTestId("highlight-layer")
980+
expect(highlightLayer).toBeInTheDocument()
981+
982+
// "/Setup" (capital S) should not be highlighted since the command is "setup" (lowercase)
983+
expect(highlightLayer.innerHTML).not.toContain(
984+
'<mark class="mention-context-textarea-highlight">/Setup</mark>',
985+
)
986+
expect(highlightLayer.innerHTML).toContain("/Setup")
987+
})
988+
989+
it("should highlight multiple valid commands in the same text", () => {
990+
const { getByTestId } = render(<ChatTextArea {...defaultProps} inputValue="/setup first then /deploy" />)
991+
992+
const highlightLayer = getByTestId("highlight-layer")
993+
expect(highlightLayer).toBeInTheDocument()
994+
995+
// Both valid commands should be highlighted
996+
expect(highlightLayer.innerHTML).toContain('<mark class="mention-context-textarea-highlight">/setup</mark>')
997+
expect(highlightLayer.innerHTML).toContain(
998+
'<mark class="mention-context-textarea-highlight">/deploy</mark>',
999+
)
1000+
})
1001+
1002+
it("should handle mixed valid and invalid commands", () => {
1003+
const { getByTestId } = render(
1004+
<ChatTextArea {...defaultProps} inputValue="/setup first then /invalid then /deploy" />,
1005+
)
1006+
1007+
const highlightLayer = getByTestId("highlight-layer")
1008+
expect(highlightLayer).toBeInTheDocument()
1009+
1010+
// Valid commands should be highlighted
1011+
expect(highlightLayer.innerHTML).toContain('<mark class="mention-context-textarea-highlight">/setup</mark>')
1012+
expect(highlightLayer.innerHTML).toContain(
1013+
'<mark class="mention-context-textarea-highlight">/deploy</mark>',
1014+
)
1015+
1016+
// Invalid command should not be highlighted
1017+
expect(highlightLayer.innerHTML).not.toContain(
1018+
'<mark class="mention-context-textarea-highlight">/invalid</mark>',
1019+
)
1020+
expect(highlightLayer.innerHTML).toContain("/invalid")
1021+
})
1022+
1023+
it("should work when no commands are available", () => {
1024+
;(useExtensionState as ReturnType<typeof vi.fn>).mockReturnValue({
1025+
filePaths: [],
1026+
openedTabs: [],
1027+
taskHistory: [],
1028+
cwd: "/test/workspace",
1029+
commands: undefined,
1030+
})
1031+
1032+
const { getByTestId } = render(<ChatTextArea {...defaultProps} inputValue="/setup the project" />)
1033+
1034+
const highlightLayer = getByTestId("highlight-layer")
1035+
expect(highlightLayer).toBeInTheDocument()
1036+
1037+
// No commands should be highlighted when commands array is undefined
1038+
expect(highlightLayer.innerHTML).not.toContain(
1039+
'<mark class="mention-context-textarea-highlight">/setup</mark>',
1040+
)
1041+
expect(highlightLayer.innerHTML).toContain("/setup")
1042+
})
1043+
})
1044+
9071045
describe("selectApiConfig", () => {
9081046
// Helper function to get the API config dropdown
9091047
const getApiConfigDropdown = () => {

0 commit comments

Comments
 (0)