From eaf3974aa15b2b1636fe7e848f970f98af78f6fd Mon Sep 17 00:00:00 2001 From: Hannes Rudolph Date: Mon, 3 Nov 2025 16:26:00 -0700 Subject: [PATCH] fix(webview-ui): use crypto.randomUUID for searchFiles requestId; escape spaces for folder mentions and add coverage. Addresses review feedback by @daniel-lxs --- .../src/components/chat/ChatTextArea.tsx | 19 ++++++++--- .../ChatTextArea.folder-drilldown.spec.tsx | 33 +++++++++++++++++++ 2 files changed, 47 insertions(+), 5 deletions(-) diff --git a/webview-ui/src/components/chat/ChatTextArea.tsx b/webview-ui/src/components/chat/ChatTextArea.tsx index bdd64a750c..52d2c7c94c 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" @@ -354,6 +354,12 @@ export const ChatTextArea = forwardRef( folderPath = folderPath + "/" } + // Escape spaces for display (match insertMention behavior) + let displayFolderPath = folderPath + if (displayFolderPath.includes(" ") && !displayFolderPath.includes("\\ ")) { + displayFolderPath = escapeSpaces(displayFolderPath) + } + // Manually build insertion without a trailing space (more deterministic than insertMention) const original = textAreaRef.current.value const beforeCursor = original.slice(0, cursorPosition) @@ -371,9 +377,9 @@ export const ChatTextArea = forwardRef( afterCursorContent = isAlphaNumSpace ? afterCursor.replace(/^[^\s]*/, "") : afterCursor } - const updatedValue = beforeMention + "@" + folderPath + afterCursorContent + const updatedValue = beforeMention + "@" + displayFolderPath + afterCursorContent const afterMentionPos = - (lastAtIndex !== -1 ? lastAtIndex : beforeCursor.length) + 1 + folderPath.length + (lastAtIndex !== -1 ? lastAtIndex : beforeCursor.length) + 1 + displayFolderPath.length setInputValue(updatedValue) setCursorPosition(afterMentionPos) @@ -391,7 +397,8 @@ export const ChatTextArea = forwardRef( setSearchQuery(nextQuery) // Kick off a search to populate folder children - const reqId = Math.random().toString(36).substring(2, 9) + const reqId = + globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random().toString(36).slice(2)}` setSearchRequestId(reqId) setSearchLoading(true) vscode.postMessage({ @@ -660,7 +667,9 @@ export const ChatTextArea = forwardRef( // Set a timeout to debounce the search requests. searchTimeoutRef.current = setTimeout(() => { // Generate a request ID for this search. - const reqId = Math.random().toString(36).substring(2, 9) + const reqId = + globalThis.crypto?.randomUUID?.() ?? + `${Date.now()}-${Math.random().toString(36).slice(2)}` setSearchRequestId(reqId) setSearchLoading(true) diff --git a/webview-ui/src/components/chat/__tests__/ChatTextArea.folder-drilldown.spec.tsx b/webview-ui/src/components/chat/__tests__/ChatTextArea.folder-drilldown.spec.tsx index bafc4f55d8..00327ada50 100644 --- a/webview-ui/src/components/chat/__tests__/ChatTextArea.folder-drilldown.spec.tsx +++ b/webview-ui/src/components/chat/__tests__/ChatTextArea.folder-drilldown.spec.tsx @@ -93,4 +93,37 @@ describe("ChatTextArea - folder drilldown behavior", () => { expect(lastMsg.query).toBe("/src/") expect(typeof lastMsg.requestId).toBe("string") }) + + it("escapes spaces in input and sends unescaped query for folder with spaces", () => { + const setInputValue = vi.fn() + + const { container } = render() + + // Type to open the @-context menu and set a query + const textarea = container.querySelector("textarea")! + fireEvent.change(textarea, { + target: { value: "@m", 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 with a space + props.onSelect(ContextMenuOptionType.Folder, "/my folder") + + // The input should contain the escaped path and NO trailing space + expect(setInputValue).toHaveBeenCalled() + const finalValue2 = setInputValue.mock.calls.at(-1)?.[0] + expect(finalValue2).toBe("@/my\\ folder/") + + // It should have kicked off a searchFiles request with unescaped query + const pm2 = vscode.postMessage as ReturnType + const lastMsg2 = pm2.mock.calls.at(-1)?.[0] + expect(lastMsg2).toMatchObject({ type: "searchFiles" }) + expect(lastMsg2.query).toBe("/my folder/") + expect(typeof lastMsg2.requestId).toBe("string") + }) })