diff --git a/.review/pr-8274 b/.review/pr-8274 new file mode 160000 index 000000000000..e46929b8d8ad --- /dev/null +++ b/.review/pr-8274 @@ -0,0 +1 @@ +Subproject commit e46929b8d8add0cd3c412d69f8ac882c405a4ba9 diff --git a/tmp/pr-8287-Roo-Code b/tmp/pr-8287-Roo-Code new file mode 160000 index 000000000000..88a473b017af --- /dev/null +++ b/tmp/pr-8287-Roo-Code @@ -0,0 +1 @@ +Subproject commit 88a473b017af37091c85ce3056e444e856f80d6e diff --git a/webview-ui/src/components/chat/ChatTextArea.tsx b/webview-ui/src/components/chat/ChatTextArea.tsx index 900cb5081e5d..3e2ec1d8cf17 100644 --- a/webview-ui/src/components/chat/ChatTextArea.tsx +++ b/webview-ui/src/components/chat/ChatTextArea.tsx @@ -342,6 +342,70 @@ export const ChatTextArea = forwardRef( } } + // Special handling for folder selection with concrete value + if (type === ContextMenuOptionType.Folder && value) { + if (textAreaRef.current) { + // Ensure the path ends with "/" + let folderPath = value + if (!folderPath.endsWith("/")) { + folderPath += "/" + } + + // Insert the folder mention without a trailing space + const beforeCursor = textAreaRef.current.value.slice(0, cursorPosition) + const afterCursor = textAreaRef.current.value.slice(cursorPosition) + const lastAtIndex = beforeCursor.lastIndexOf("@") + + let newValue: string + let newCursorPosition: number + + if (lastAtIndex !== -1) { + // Replace everything after the @ with the folder path + const beforeMention = textAreaRef.current.value.slice(0, lastAtIndex) + // Don't add a trailing space after the folder path + newValue = beforeMention + "@" + folderPath + afterCursor + newCursorPosition = lastAtIndex + 1 + folderPath.length + } else { + // Insert at cursor position + newValue = beforeCursor + "@" + folderPath + afterCursor + newCursorPosition = cursorPosition + 1 + folderPath.length + } + + setInputValue(newValue) + setCursorPosition(newCursorPosition) + setIntendedCursorPosition(newCursorPosition) + + // Keep the menu open and search for folder contents + // Don't call setShowContextMenu(false) + setSelectedType(null) + setSelectedMenuIndex(0) + + // Extract the query for searching folder contents + const query = folderPath + setSearchQuery(query) + + // Trigger file search for the folder contents + const reqId = Math.random().toString(36).substring(2, 9) + setSearchRequestId(reqId) + setSearchLoading(true) + + // Send message to extension to search files in the folder + vscode.postMessage({ + type: "searchFiles", + query: query, + requestId: reqId, + }) + + // Focus the textarea + setTimeout(() => { + if (textAreaRef.current) { + textAreaRef.current.focus() + } + }, 0) + } + return + } + setShowContextMenu(false) setSelectedType(null) @@ -387,7 +451,7 @@ export const ChatTextArea = forwardRef( } }, // eslint-disable-next-line react-hooks/exhaustive-deps - [setInputValue, cursorPosition], + [setInputValue, cursorPosition, setSearchRequestId, setSearchLoading], ) const handleKeyDown = useCallback( diff --git a/webview-ui/src/utils/__tests__/context-mentions.spec.ts b/webview-ui/src/utils/__tests__/context-mentions.spec.ts index 48b03907c341..ca4f395977f4 100644 --- a/webview-ui/src/utils/__tests__/context-mentions.spec.ts +++ b/webview-ui/src/utils/__tests__/context-mentions.spec.ts @@ -145,6 +145,43 @@ describe("insertMention", () => { expect(result.mentionIndex).toBe(6) }) }) + + // --- Tests for Folder Drill-Down --- + describe("folder drill-down behavior", () => { + it("should not add trailing space when inserting folder path ending with /", () => { + // This test documents the expected behavior for folder drill-down + // When a folder path ends with /, we don't want a trailing space + const folderPath = "/path/to/folder/" + const result = insertMention("Browse @", 8, folderPath) + + // The current implementation adds a space, but for folder drill-down + // we need to handle this specially in the component + expect(result.newValue).toBe("Browse @/path/to/folder/ ") + expect(result.mentionIndex).toBe(7) + }) + + it("should handle folder paths with spaces correctly", () => { + const folderPath = "/my documents/project folder/" + const expectedEscapedPath = "/my\\ documents/project\\ folder/" + const result = insertMention("Open @", 6, folderPath) + + expect(result.newValue).toBe(`Open @${expectedEscapedPath} `) + expect(result.mentionIndex).toBe(5) + }) + + it("should ensure folder paths end with / for consistency", () => { + // This documents that the component should ensure folder paths end with / + const folderPathWithoutSlash = "/path/to/folder" + const folderPathWithSlash = "/path/to/folder/" + + const result1 = insertMention("@", 1, folderPathWithoutSlash) + const result2 = insertMention("@", 1, folderPathWithSlash) + + // Both should have the path, but the component should normalize to have trailing / + expect(result1.newValue).toBe("@/path/to/folder ") + expect(result2.newValue).toBe("@/path/to/folder/ ") + }) + }) }) describe("removeMention", () => { @@ -585,4 +622,37 @@ describe("shouldShowContextMenu", () => { // This case means the regex wouldn't match anyway, but confirms context menu logic expect(shouldShowContextMenu("@/path/with space", 13)).toBe(false) // Cursor after unescaped space }) + + // --- Tests for Folder Drill-Down --- + describe("folder drill-down behavior", () => { + it("should keep menu open when path ends with / and no trailing space", () => { + // When drilling into a folder, the path ends with / and no space after + expect(shouldShowContextMenu("@/path/to/folder/", 17)).toBe(true) + }) + + it("should keep menu open for nested folder paths", () => { + expect(shouldShowContextMenu("@/src/components/", 17)).toBe(true) + expect(shouldShowContextMenu("@/src/components/chat/", 22)).toBe(true) + }) + + it("should close menu when space is added after folder path", () => { + // Normal behavior - space after path closes the menu + expect(shouldShowContextMenu("@/path/to/folder/ ", 18)).toBe(false) + }) + + it("should handle folder paths with escaped spaces", () => { + expect(shouldShowContextMenu("@/my\\ documents/", 16)).toBe(true) + expect(shouldShowContextMenu("@/my\\ documents/folder/", 23)).toBe(true) + }) + + it("should return true for slash commands without spaces", () => { + expect(shouldShowContextMenu("/code", 5)).toBe(true) + expect(shouldShowContextMenu("/debug", 6)).toBe(true) + }) + + it("should return false for slash commands with spaces", () => { + expect(shouldShowContextMenu("/code ", 6)).toBe(false) + expect(shouldShowContextMenu("/debug some text", 7)).toBe(false) + }) + }) })