Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/vscode-e2e/src/suite/modes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 })
Expand Down
72 changes: 68 additions & 4 deletions webview-ui/src/components/chat/ChatTextArea.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -293,7 +293,7 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
}, [showContextMenu, setShowContextMenu])

const handleMentionSelect = useCallback(
(type: ContextMenuOptionType, value?: string) => {
(type: ContextMenuOptionType, value?: string, trigger?: "Enter" | "Tab" | "Click") => {
if (type === ContextMenuOptionType.NoResults) {
return
}
Expand Down Expand Up @@ -341,6 +341,69 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
}
}

// 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)

Expand Down Expand Up @@ -454,7 +517,7 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
selectedOption.type !== ContextMenuOptionType.NoResults &&
selectedOption.type !== ContextMenuOptionType.SectionHeader
) {
handleMentionSelect(selectedOption.type, selectedOption.value)
handleMentionSelect(selectedOption.type, selectedOption.value, event.key as "Enter" | "Tab")
}
return
}
Expand Down Expand Up @@ -575,7 +638,8 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
} 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.
Expand Down