Skip to content

Commit 511ebb7

Browse files
authored
Make sure the slash commands only fire if they're the first character (#2702)
1 parent 0374436 commit 511ebb7

File tree

4 files changed

+55
-14
lines changed

4 files changed

+55
-14
lines changed

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
305305
const direction = event.key === "ArrowUp" ? -1 : 1
306306
const options = getContextMenuOptions(
307307
searchQuery,
308+
inputValue,
308309
selectedType,
309310
queryItems,
310311
fileSearchResults,
@@ -341,6 +342,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
341342
event.preventDefault()
342343
const selectedOption = getContextMenuOptions(
343344
searchQuery,
345+
inputValue,
344346
selectedType,
345347
queryItems,
346348
fileSearchResults,
@@ -780,6 +782,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
780782
<ContextMenu
781783
onSelect={handleMentionSelect}
782784
searchQuery={searchQuery}
785+
inputValue={inputValue}
783786
onMouseDown={handleMenuMouseDown}
784787
selectedIndex={selectedMenuIndex}
785788
setSelectedIndex={setSelectedMenuIndex}

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { ModeConfig } from "../../../../src/shared/modes"
1111
interface ContextMenuProps {
1212
onSelect: (type: ContextMenuOptionType, value?: string) => void
1313
searchQuery: string
14+
inputValue: string
1415
onMouseDown: () => void
1516
selectedIndex: number
1617
setSelectedIndex: (index: number) => void
@@ -24,6 +25,7 @@ interface ContextMenuProps {
2425
const ContextMenu: React.FC<ContextMenuProps> = ({
2526
onSelect,
2627
searchQuery,
28+
inputValue,
2729
onMouseDown,
2830
selectedIndex,
2931
setSelectedIndex,
@@ -36,8 +38,8 @@ const ContextMenu: React.FC<ContextMenuProps> = ({
3638
const menuRef = useRef<HTMLDivElement>(null)
3739

3840
const filteredOptions = useMemo(() => {
39-
return getContextMenuOptions(searchQuery, selectedType, queryItems, dynamicSearchResults, modes)
40-
}, [searchQuery, selectedType, queryItems, dynamicSearchResults, modes])
41+
return getContextMenuOptions(searchQuery, inputValue, selectedType, queryItems, dynamicSearchResults, modes)
42+
}, [searchQuery, inputValue, selectedType, queryItems, dynamicSearchResults, modes])
4143

4244
useEffect(() => {
4345
if (menuRef.current) {

webview-ui/src/utils/__tests__/context-mentions.test.ts

Lines changed: 46 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ describe("getContextMenuOptions", () => {
9090
]
9191

9292
it("should return all option types for empty query", () => {
93-
const result = getContextMenuOptions("", null, [])
93+
const result = getContextMenuOptions("", "", null, [])
9494
expect(result).toHaveLength(6)
9595
expect(result.map((item) => item.type)).toEqual([
9696
ContextMenuOptionType.Problems,
@@ -103,7 +103,7 @@ describe("getContextMenuOptions", () => {
103103
})
104104

105105
it("should filter by selected type when query is empty", () => {
106-
const result = getContextMenuOptions("", ContextMenuOptionType.File, mockQueryItems)
106+
const result = getContextMenuOptions("", "", ContextMenuOptionType.File, mockQueryItems)
107107
expect(result).toHaveLength(2)
108108
expect(result.map((item) => item.type)).toContain(ContextMenuOptionType.File)
109109
expect(result.map((item) => item.type)).toContain(ContextMenuOptionType.OpenedFile)
@@ -112,19 +112,19 @@ describe("getContextMenuOptions", () => {
112112
})
113113

114114
it("should match git commands", () => {
115-
const result = getContextMenuOptions("git", null, mockQueryItems)
115+
const result = getContextMenuOptions("git", "git", null, mockQueryItems)
116116
expect(result[0].type).toBe(ContextMenuOptionType.Git)
117117
expect(result[0].label).toBe("Git Commits")
118118
})
119119

120120
it("should match git commit hashes", () => {
121-
const result = getContextMenuOptions("abc1234", null, mockQueryItems)
121+
const result = getContextMenuOptions("abc1234", "abc1234", null, mockQueryItems)
122122
expect(result[0].type).toBe(ContextMenuOptionType.Git)
123123
expect(result[0].value).toBe("abc1234")
124124
})
125125

126126
it("should return NoResults when no matches found", () => {
127-
const result = getContextMenuOptions("nonexistent", null, mockQueryItems)
127+
const result = getContextMenuOptions("nonexistent", "nonexistent", null, mockQueryItems)
128128
expect(result).toHaveLength(1)
129129
expect(result[0].type).toBe(ContextMenuOptionType.NoResults)
130130
})
@@ -145,7 +145,7 @@ describe("getContextMenuOptions", () => {
145145
},
146146
]
147147

148-
const result = getContextMenuOptions("test", null, testItems, mockDynamicSearchResults)
148+
const result = getContextMenuOptions("test", "test", null, testItems, mockDynamicSearchResults)
149149

150150
// Check if opened files and dynamic search results are included
151151
expect(result.some((item) => item.type === ContextMenuOptionType.OpenedFile)).toBe(true)
@@ -154,7 +154,7 @@ describe("getContextMenuOptions", () => {
154154

155155
it("should maintain correct result ordering according to implementation", () => {
156156
// Add multiple item types to test ordering
157-
const result = getContextMenuOptions("t", null, mockQueryItems, mockDynamicSearchResults)
157+
const result = getContextMenuOptions("t", "t", null, mockQueryItems, mockDynamicSearchResults)
158158

159159
// Find the different result types
160160
const fileResults = result.filter(
@@ -185,7 +185,7 @@ describe("getContextMenuOptions", () => {
185185
})
186186

187187
it("should include opened files when dynamic search results exist", () => {
188-
const result = getContextMenuOptions("open", null, mockQueryItems, mockDynamicSearchResults)
188+
const result = getContextMenuOptions("open", "open", null, mockQueryItems, mockDynamicSearchResults)
189189

190190
// Verify opened files are included
191191
expect(result.some((item) => item.type === ContextMenuOptionType.OpenedFile)).toBe(true)
@@ -194,7 +194,7 @@ describe("getContextMenuOptions", () => {
194194
})
195195

196196
it("should include git results when dynamic search results exist", () => {
197-
const result = getContextMenuOptions("commit", null, mockQueryItems, mockDynamicSearchResults)
197+
const result = getContextMenuOptions("commit", "commit", null, mockQueryItems, mockDynamicSearchResults)
198198

199199
// Verify git results are included
200200
expect(result.some((item) => item.type === ContextMenuOptionType.Git)).toBe(true)
@@ -215,7 +215,7 @@ describe("getContextMenuOptions", () => {
215215
},
216216
]
217217

218-
const result = getContextMenuOptions("test", null, mockQueryItems, duplicateSearchResults)
218+
const result = getContextMenuOptions("test", "test", null, mockQueryItems, duplicateSearchResults)
219219

220220
// Count occurrences of src/test.ts in results
221221
const duplicateCount = result.filter(
@@ -233,6 +233,7 @@ describe("getContextMenuOptions", () => {
233233
it("should return NoResults when all combined results are empty with dynamic search", () => {
234234
// Use a query that won't match anything
235235
const result = getContextMenuOptions(
236+
"nonexistentquery123456",
236237
"nonexistentquery123456",
237238
null,
238239
mockQueryItems,
@@ -281,7 +282,7 @@ describe("getContextMenuOptions", () => {
281282
]
282283

283284
// Get results for "test" query
284-
const result = getContextMenuOptions(testQuery, null, testItems, testSearchResults)
285+
const result = getContextMenuOptions(testQuery, testQuery, null, testItems, testSearchResults)
285286

286287
// Verify we have results
287288
expect(result.length).toBeGreaterThan(0)
@@ -310,6 +311,40 @@ describe("getContextMenuOptions", () => {
310311
expect(firstGitResultIndex).toBeGreaterThan(firstSearchResultIndex)
311312
}
312313
})
314+
315+
it("should process slash commands when both query and inputValue start with slash", () => {
316+
const mockModes = [
317+
{
318+
slug: "code",
319+
name: "Code",
320+
roleDefinition: "You are a coding assistant",
321+
groups: ["read" as const, "edit" as const],
322+
},
323+
{
324+
slug: "architect",
325+
name: "Architect",
326+
roleDefinition: "You are an architecture assistant",
327+
groups: ["read" as const],
328+
},
329+
]
330+
331+
const result = getContextMenuOptions("/co", "/co", null, [], [], mockModes)
332+
333+
// Verify mode results are returned
334+
expect(result[0].type).toBe(ContextMenuOptionType.Mode)
335+
expect(result[0].value).toBe("code")
336+
})
337+
338+
it("should not process slash commands when query starts with slash but inputValue doesn't", () => {
339+
// Use a completely non-matching query to ensure we get NoResults
340+
// and provide empty query items to avoid any matches
341+
const result = getContextMenuOptions("/nonexistentquery", "Hello /code", null, [], [])
342+
343+
// Should not process as a mode command
344+
expect(result[0].type).not.toBe(ContextMenuOptionType.Mode)
345+
// Should return NoResults since it won't match anything
346+
expect(result[0].type).toBe(ContextMenuOptionType.NoResults)
347+
})
313348
})
314349

315350
describe("shouldShowContextMenu", () => {

webview-ui/src/utils/context-mentions.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,13 +84,14 @@ export interface ContextMenuQueryItem {
8484

8585
export function getContextMenuOptions(
8686
query: string,
87+
inputValue: string,
8788
selectedType: ContextMenuOptionType | null = null,
8889
queryItems: ContextMenuQueryItem[],
8990
dynamicSearchResults: SearchResult[] = [],
9091
modes?: ModeConfig[],
9192
): ContextMenuQueryItem[] {
9293
// Handle slash commands for modes
93-
if (query.startsWith("/")) {
94+
if (query.startsWith("/") && inputValue.startsWith("/")) {
9495
const modeQuery = query.slice(1)
9596
if (!modes?.length) return [{ type: ContextMenuOptionType.NoResults }]
9697

0 commit comments

Comments
 (0)