diff --git a/webview-ui/src/components/chat/ChatTextArea.tsx b/webview-ui/src/components/chat/ChatTextArea.tsx index 84bc7634e2..3ce6155dc0 100644 --- a/webview-ui/src/components/chat/ChatTextArea.tsx +++ b/webview-ui/src/components/chat/ChatTextArea.tsx @@ -305,6 +305,7 @@ const ChatTextArea = forwardRef( const direction = event.key === "ArrowUp" ? -1 : 1 const options = getContextMenuOptions( searchQuery, + inputValue, selectedType, queryItems, fileSearchResults, @@ -341,6 +342,7 @@ const ChatTextArea = forwardRef( event.preventDefault() const selectedOption = getContextMenuOptions( searchQuery, + inputValue, selectedType, queryItems, fileSearchResults, @@ -780,6 +782,7 @@ const ChatTextArea = forwardRef( void searchQuery: string + inputValue: string onMouseDown: () => void selectedIndex: number setSelectedIndex: (index: number) => void @@ -24,6 +25,7 @@ interface ContextMenuProps { const ContextMenu: React.FC = ({ onSelect, searchQuery, + inputValue, onMouseDown, selectedIndex, setSelectedIndex, @@ -36,8 +38,8 @@ const ContextMenu: React.FC = ({ const menuRef = useRef(null) const filteredOptions = useMemo(() => { - return getContextMenuOptions(searchQuery, selectedType, queryItems, dynamicSearchResults, modes) - }, [searchQuery, selectedType, queryItems, dynamicSearchResults, modes]) + return getContextMenuOptions(searchQuery, inputValue, selectedType, queryItems, dynamicSearchResults, modes) + }, [searchQuery, inputValue, selectedType, queryItems, dynamicSearchResults, modes]) useEffect(() => { if (menuRef.current) { diff --git a/webview-ui/src/utils/__tests__/context-mentions.test.ts b/webview-ui/src/utils/__tests__/context-mentions.test.ts index c72a26d911..1e69964a94 100644 --- a/webview-ui/src/utils/__tests__/context-mentions.test.ts +++ b/webview-ui/src/utils/__tests__/context-mentions.test.ts @@ -90,7 +90,7 @@ describe("getContextMenuOptions", () => { ] it("should return all option types for empty query", () => { - const result = getContextMenuOptions("", null, []) + const result = getContextMenuOptions("", "", null, []) expect(result).toHaveLength(6) expect(result.map((item) => item.type)).toEqual([ ContextMenuOptionType.Problems, @@ -103,7 +103,7 @@ describe("getContextMenuOptions", () => { }) it("should filter by selected type when query is empty", () => { - const result = getContextMenuOptions("", ContextMenuOptionType.File, mockQueryItems) + const result = getContextMenuOptions("", "", ContextMenuOptionType.File, mockQueryItems) expect(result).toHaveLength(2) expect(result.map((item) => item.type)).toContain(ContextMenuOptionType.File) expect(result.map((item) => item.type)).toContain(ContextMenuOptionType.OpenedFile) @@ -112,19 +112,19 @@ describe("getContextMenuOptions", () => { }) it("should match git commands", () => { - const result = getContextMenuOptions("git", null, mockQueryItems) + const result = getContextMenuOptions("git", "git", null, mockQueryItems) expect(result[0].type).toBe(ContextMenuOptionType.Git) expect(result[0].label).toBe("Git Commits") }) it("should match git commit hashes", () => { - const result = getContextMenuOptions("abc1234", null, mockQueryItems) + const result = getContextMenuOptions("abc1234", "abc1234", null, mockQueryItems) expect(result[0].type).toBe(ContextMenuOptionType.Git) expect(result[0].value).toBe("abc1234") }) it("should return NoResults when no matches found", () => { - const result = getContextMenuOptions("nonexistent", null, mockQueryItems) + const result = getContextMenuOptions("nonexistent", "nonexistent", null, mockQueryItems) expect(result).toHaveLength(1) expect(result[0].type).toBe(ContextMenuOptionType.NoResults) }) @@ -145,7 +145,7 @@ describe("getContextMenuOptions", () => { }, ] - const result = getContextMenuOptions("test", null, testItems, mockDynamicSearchResults) + const result = getContextMenuOptions("test", "test", null, testItems, mockDynamicSearchResults) // Check if opened files and dynamic search results are included expect(result.some((item) => item.type === ContextMenuOptionType.OpenedFile)).toBe(true) @@ -154,7 +154,7 @@ describe("getContextMenuOptions", () => { it("should maintain correct result ordering according to implementation", () => { // Add multiple item types to test ordering - const result = getContextMenuOptions("t", null, mockQueryItems, mockDynamicSearchResults) + const result = getContextMenuOptions("t", "t", null, mockQueryItems, mockDynamicSearchResults) // Find the different result types const fileResults = result.filter( @@ -185,7 +185,7 @@ describe("getContextMenuOptions", () => { }) it("should include opened files when dynamic search results exist", () => { - const result = getContextMenuOptions("open", null, mockQueryItems, mockDynamicSearchResults) + const result = getContextMenuOptions("open", "open", null, mockQueryItems, mockDynamicSearchResults) // Verify opened files are included expect(result.some((item) => item.type === ContextMenuOptionType.OpenedFile)).toBe(true) @@ -194,7 +194,7 @@ describe("getContextMenuOptions", () => { }) it("should include git results when dynamic search results exist", () => { - const result = getContextMenuOptions("commit", null, mockQueryItems, mockDynamicSearchResults) + const result = getContextMenuOptions("commit", "commit", null, mockQueryItems, mockDynamicSearchResults) // Verify git results are included expect(result.some((item) => item.type === ContextMenuOptionType.Git)).toBe(true) @@ -215,7 +215,7 @@ describe("getContextMenuOptions", () => { }, ] - const result = getContextMenuOptions("test", null, mockQueryItems, duplicateSearchResults) + const result = getContextMenuOptions("test", "test", null, mockQueryItems, duplicateSearchResults) // Count occurrences of src/test.ts in results const duplicateCount = result.filter( @@ -233,6 +233,7 @@ describe("getContextMenuOptions", () => { it("should return NoResults when all combined results are empty with dynamic search", () => { // Use a query that won't match anything const result = getContextMenuOptions( + "nonexistentquery123456", "nonexistentquery123456", null, mockQueryItems, @@ -281,7 +282,7 @@ describe("getContextMenuOptions", () => { ] // Get results for "test" query - const result = getContextMenuOptions(testQuery, null, testItems, testSearchResults) + const result = getContextMenuOptions(testQuery, testQuery, null, testItems, testSearchResults) // Verify we have results expect(result.length).toBeGreaterThan(0) @@ -310,6 +311,40 @@ describe("getContextMenuOptions", () => { expect(firstGitResultIndex).toBeGreaterThan(firstSearchResultIndex) } }) + + it("should process slash commands when both query and inputValue start with slash", () => { + const mockModes = [ + { + slug: "code", + name: "Code", + roleDefinition: "You are a coding assistant", + groups: ["read" as const, "edit" as const], + }, + { + slug: "architect", + name: "Architect", + roleDefinition: "You are an architecture assistant", + groups: ["read" as const], + }, + ] + + const result = getContextMenuOptions("/co", "/co", null, [], [], mockModes) + + // Verify mode results are returned + expect(result[0].type).toBe(ContextMenuOptionType.Mode) + expect(result[0].value).toBe("code") + }) + + it("should not process slash commands when query starts with slash but inputValue doesn't", () => { + // Use a completely non-matching query to ensure we get NoResults + // and provide empty query items to avoid any matches + const result = getContextMenuOptions("/nonexistentquery", "Hello /code", null, [], []) + + // Should not process as a mode command + expect(result[0].type).not.toBe(ContextMenuOptionType.Mode) + // Should return NoResults since it won't match anything + expect(result[0].type).toBe(ContextMenuOptionType.NoResults) + }) }) describe("shouldShowContextMenu", () => { diff --git a/webview-ui/src/utils/context-mentions.ts b/webview-ui/src/utils/context-mentions.ts index f294f0a030..12478864f6 100644 --- a/webview-ui/src/utils/context-mentions.ts +++ b/webview-ui/src/utils/context-mentions.ts @@ -84,13 +84,14 @@ export interface ContextMenuQueryItem { export function getContextMenuOptions( query: string, + inputValue: string, selectedType: ContextMenuOptionType | null = null, queryItems: ContextMenuQueryItem[], dynamicSearchResults: SearchResult[] = [], modes?: ModeConfig[], ): ContextMenuQueryItem[] { // Handle slash commands for modes - if (query.startsWith("/")) { + if (query.startsWith("/") && inputValue.startsWith("/")) { const modeQuery = query.slice(1) if (!modes?.length) return [{ type: ContextMenuOptionType.NoResults }]