diff --git a/apps/vscode-e2e/src/suite/modes.test.ts b/apps/vscode-e2e/src/suite/modes.test.ts index 7982f3cf22ba..eab21f66127a 100644 --- a/apps/vscode-e2e/src/suite/modes.test.ts +++ b/apps/vscode-e2e/src/suite/modes.test.ts @@ -15,7 +15,7 @@ suite("Roo Code Modes", function () { const switchModesTaskId = await globalThis.api.startNewTask({ configuration: { mode: "code", alwaysAllowModeSwitch: true, autoApprovalEnabled: true }, - text: "For each of `architect`, `ask`, and `debug` use the `switch_mode` tool to switch to that mode.", + text: "For each of `architect`, `ask`, and `debug` use the `switch_mode` tool to switch to that mode. After switching all three, call the `attempt_completion` tool with the result: 'Mode switches completed.'", }) await waitUntilCompleted({ api: globalThis.api, taskId: switchModesTaskId }) diff --git a/webview-ui/src/components/chat/ChatTextArea.tsx b/webview-ui/src/components/chat/ChatTextArea.tsx index c7813372fa79..3be7ba05a05d 100644 --- a/webview-ui/src/components/chat/ChatTextArea.tsx +++ b/webview-ui/src/components/chat/ChatTextArea.tsx @@ -20,7 +20,7 @@ import { SearchResult, } from "@src/utils/context-mentions" import { cn } from "@src/lib/utils" -import { convertToMentionPath } from "@src/utils/path-mentions" +import { convertToMentionPath, escapeSpaces } from "@src/utils/path-mentions" import { StandardTooltip } from "@src/components/ui" import Thumbnails from "../common/Thumbnails" @@ -293,7 +293,7 @@ export const ChatTextArea = forwardRef( }, [showContextMenu, setShowContextMenu]) const handleMentionSelect = useCallback( - (type: ContextMenuOptionType, value?: string) => { + (type: ContextMenuOptionType, value?: string, trigger?: "Enter" | "Tab" | "Click") => { if (type === ContextMenuOptionType.NoResults) { return } @@ -341,6 +341,69 @@ export const ChatTextArea = forwardRef( } } + // Special handling for folder drill-in via Tab only: keep picker open and do not add trailing space + if (type === ContextMenuOptionType.Folder && value && textAreaRef.current && trigger === "Tab") { + // Ensure trailing slash and escape spaces like insertMention does + let insertValue = value.endsWith("/") ? value : value + "/" + if (insertValue.startsWith("/") && insertValue.includes(" ") && !insertValue.includes("\\ ")) { + insertValue = escapeSpaces(insertValue) + } + + const text = textAreaRef.current.value + const position = cursorPosition + const beforeCursor = text.slice(0, position) + const afterCursor = text.slice(position) + const lastAtIndex = beforeCursor.lastIndexOf("@") + + // Mirror insertMention behavior for replacing after '@' without adding a trailing space + const afterCursorContent = /^[a-zA-Z0-9\s]*$/.test(afterCursor) + ? afterCursor.replace(/^[^\s]*/, "") + : afterCursor + + let newValue: string + let mentionIndex: number + + if (lastAtIndex !== -1) { + const beforeMention = text.slice(0, lastAtIndex) + newValue = beforeMention + "@" + insertValue + afterCursorContent + mentionIndex = lastAtIndex + } else { + newValue = beforeCursor + "@" + insertValue + afterCursor + mentionIndex = position + } + + setInputValue(newValue) + + // Place caret right after the inserted folder path (after trailing slash) + const newCursorPos = mentionIndex + 1 + insertValue.length + setCursorPosition(newCursorPos) + setIntendedCursorPosition(newCursorPos) + + // Keep the context menu open and immediately search within the folder + setShowContextMenu(true) + setSelectedType(null) + const folderQuery = insertValue.slice(1) + setSearchQuery(folderQuery) + setSelectedMenuIndex(0) + + // Trigger immediate search (no debounce) to repopulate with folder children + const reqId = Math.random().toString(36).substring(2, 9) + setSearchRequestId(reqId) + setSearchLoading(true) + vscode.postMessage({ + type: "searchFiles", + query: unescapeSpaces(folderQuery), + requestId: reqId, + }) + + // Position caret without blurring to avoid closing the menu + if (textAreaRef.current) { + textAreaRef.current.setSelectionRange(newCursorPos, newCursorPos) + } + + return + } + setShowContextMenu(false) setSelectedType(null) @@ -454,7 +517,7 @@ export const ChatTextArea = forwardRef( selectedOption.type !== ContextMenuOptionType.NoResults && selectedOption.type !== ContextMenuOptionType.SectionHeader ) { - handleMentionSelect(selectedOption.type, selectedOption.value) + handleMentionSelect(selectedOption.type, selectedOption.value, event.key as "Enter" | "Tab") } return } @@ -575,7 +638,8 @@ export const ChatTextArea = forwardRef( } else { // Existing @ mention handling. const lastAtIndex = newValue.lastIndexOf("@", newCursorPosition - 1) - const query = newValue.slice(lastAtIndex + 1, newCursorPosition) + const rawQuery = newValue.slice(lastAtIndex + 1, newCursorPosition) + const query = rawQuery.startsWith("/") ? rawQuery.slice(1) : rawQuery setSearchQuery(query) // Send file search request if query is not empty.