mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
fix(webview-ui): keep @ context picker open when selecting a folder via Enter/Tab
Insert folder mentions without trailing space so the picker remains active. Keep the picker open and immediately query folder children to enable keyboard drilldown. Add a focused test for folder drilldown behavior. Fixes #8076.
This commit is contained in:
parent
205f3e4242
commit
3cbb73bc86
2 changed files with 162 additions and 0 deletions
|
|
@ -342,6 +342,72 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
|||
}
|
||||
}
|
||||
|
||||
// Special handling for concrete folder selections:
|
||||
// - Keep the picker open
|
||||
// - Ensure trailing slash
|
||||
// - Insert without trailing space
|
||||
// - Trigger a follow-up search to show folder contents
|
||||
if (type === ContextMenuOptionType.Folder && value && textAreaRef.current) {
|
||||
// Normalize folder path to end with a trailing slash
|
||||
let folderPath = value
|
||||
if (!folderPath.endsWith("/")) {
|
||||
folderPath = folderPath + "/"
|
||||
}
|
||||
|
||||
// Manually build insertion without a trailing space (more deterministic than insertMention)
|
||||
const original = textAreaRef.current.value
|
||||
const beforeCursor = original.slice(0, cursorPosition)
|
||||
const afterCursor = original.slice(cursorPosition)
|
||||
const lastAtIndex = beforeCursor.lastIndexOf("@")
|
||||
|
||||
let beforeMention = beforeCursor
|
||||
let afterCursorContent = afterCursor
|
||||
|
||||
if (lastAtIndex !== -1) {
|
||||
// Replace everything from '@' to cursor with the folder path
|
||||
beforeMention = original.slice(0, lastAtIndex)
|
||||
// Match insertMention behavior for non-space-delimited languages
|
||||
const isAlphaNumSpace = /^[a-zA-Z0-9\s]*$/.test(afterCursor)
|
||||
afterCursorContent = isAlphaNumSpace ? afterCursor.replace(/^[^\s]*/, "") : afterCursor
|
||||
}
|
||||
|
||||
const updatedValue = beforeMention + "@" + folderPath + afterCursorContent
|
||||
const afterMentionPos =
|
||||
(lastAtIndex !== -1 ? lastAtIndex : beforeCursor.length) + 1 + folderPath.length
|
||||
|
||||
setInputValue(updatedValue)
|
||||
setCursorPosition(afterMentionPos)
|
||||
setIntendedCursorPosition(afterMentionPos)
|
||||
|
||||
// Keep the context menu open for drill-down
|
||||
setShowContextMenu(true)
|
||||
setSelectedType(null)
|
||||
|
||||
// Compute the next-level query (text after '@' up to the caret)
|
||||
const nextQuery = updatedValue.slice(
|
||||
(lastAtIndex !== -1 ? lastAtIndex : beforeCursor.length) + 1,
|
||||
afterMentionPos,
|
||||
)
|
||||
setSearchQuery(nextQuery)
|
||||
|
||||
// Kick off a search to populate folder children
|
||||
const reqId = Math.random().toString(36).substring(2, 9)
|
||||
setSearchRequestId(reqId)
|
||||
setSearchLoading(true)
|
||||
vscode.postMessage({
|
||||
type: "searchFiles",
|
||||
query: unescapeSpaces(nextQuery),
|
||||
requestId: reqId,
|
||||
})
|
||||
|
||||
// Keep focus in the textarea for continued typing
|
||||
setTimeout(() => {
|
||||
textAreaRef.current?.focus()
|
||||
}, 0)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
setShowContextMenu(false)
|
||||
setSelectedType(null)
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,96 @@
|
|||
import React from "react"
|
||||
import { render, fireEvent, screen } from "@src/utils/test-utils"
|
||||
import { useExtensionState } from "@src/context/ExtensionStateContext"
|
||||
import { vscode } from "@src/utils/vscode"
|
||||
import { ContextMenuOptionType } from "@src/utils/context-mentions"
|
||||
import { ChatTextArea } from "../ChatTextArea"
|
||||
|
||||
// Mock VS Code messaging
|
||||
vi.mock("@src/utils/vscode", () => ({
|
||||
vscode: {
|
||||
postMessage: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
// Capture the last props passed to ContextMenu so we can invoke onSelect directly
|
||||
let lastContextMenuProps: any = null
|
||||
vi.mock("../ContextMenu", () => {
|
||||
return {
|
||||
__esModule: true,
|
||||
default: (props: any) => {
|
||||
lastContextMenuProps = props
|
||||
return <div data-testid="context-menu" />
|
||||
},
|
||||
__getLastProps: () => lastContextMenuProps,
|
||||
}
|
||||
})
|
||||
|
||||
// Mock ExtensionStateContext
|
||||
vi.mock("@src/context/ExtensionStateContext")
|
||||
|
||||
describe("ChatTextArea - folder drilldown behavior", () => {
|
||||
const defaultProps = {
|
||||
inputValue: "",
|
||||
setInputValue: vi.fn(),
|
||||
onSend: vi.fn(),
|
||||
sendingDisabled: false,
|
||||
selectApiConfigDisabled: false,
|
||||
onSelectImages: vi.fn(),
|
||||
shouldDisableImages: false,
|
||||
placeholderText: "Type a message...",
|
||||
selectedImages: [],
|
||||
setSelectedImages: vi.fn(),
|
||||
onHeightChange: vi.fn(),
|
||||
mode: "architect",
|
||||
setMode: vi.fn(),
|
||||
modeShortcutText: "(⌘. for next mode)",
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
;(useExtensionState as ReturnType<typeof vi.fn>).mockReturnValue({
|
||||
filePaths: ["src/", "src/index.ts"],
|
||||
openedTabs: [],
|
||||
taskHistory: [],
|
||||
cwd: "/test/workspace",
|
||||
})
|
||||
})
|
||||
|
||||
it("keeps picker open and triggers folder children search when selecting a folder", () => {
|
||||
const setInputValue = vi.fn()
|
||||
|
||||
const { container } = render(<ChatTextArea {...defaultProps} setInputValue={setInputValue} />)
|
||||
|
||||
// Type to open the @-context menu and set a query
|
||||
const textarea = container.querySelector("textarea")!
|
||||
fireEvent.change(textarea, {
|
||||
target: { value: "@s", selectionStart: 2 },
|
||||
})
|
||||
|
||||
// Ensure our mocked ContextMenu rendered and captured props
|
||||
expect(screen.getByTestId("context-menu")).toBeInTheDocument()
|
||||
const props = lastContextMenuProps
|
||||
expect(props).toBeTruthy()
|
||||
expect(typeof props.onSelect).toBe("function")
|
||||
|
||||
// Simulate selecting a concrete folder suggestion (e.g. "/src")
|
||||
props.onSelect(ContextMenuOptionType.Folder, "/src")
|
||||
|
||||
// The input should contain "@/src/" with NO trailing space and the picker should remain open
|
||||
expect(setInputValue).toHaveBeenCalled()
|
||||
const finalValue = setInputValue.mock.calls.at(-1)?.[0]
|
||||
expect(finalValue).toBe("@/src/")
|
||||
|
||||
// Context menu should still be present (picker remains open)
|
||||
expect(screen.getByTestId("context-menu")).toBeInTheDocument()
|
||||
|
||||
// It should have kicked off a searchFiles request for the folder children
|
||||
const pm = vscode.postMessage as ReturnType<typeof vi.fn>
|
||||
expect(pm).toHaveBeenCalled()
|
||||
const lastMsg = pm.mock.calls.at(-1)?.[0]
|
||||
expect(lastMsg).toMatchObject({ type: "searchFiles" })
|
||||
// Query mirrors substring after '@' including leading slash per existing logic
|
||||
expect(lastMsg.query).toBe("/src/")
|
||||
expect(typeof lastMsg.requestId).toBe("string")
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Reference in a new issue