Skip to content

Commit eaf3974

Browse files
committed
fix(webview-ui): use crypto.randomUUID for searchFiles requestId; escape spaces for folder mentions and add coverage. Addresses review feedback by @daniel-lxs
1 parent 3cbb73b commit eaf3974

File tree

2 files changed

+47
-5
lines changed

2 files changed

+47
-5
lines changed

webview-ui/src/components/chat/ChatTextArea.tsx

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import {
2020
SearchResult,
2121
} from "@src/utils/context-mentions"
2222
import { cn } from "@src/lib/utils"
23-
import { convertToMentionPath } from "@src/utils/path-mentions"
23+
import { convertToMentionPath, escapeSpaces } from "@src/utils/path-mentions"
2424
import { StandardTooltip } from "@src/components/ui"
2525

2626
import Thumbnails from "../common/Thumbnails"
@@ -354,6 +354,12 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
354354
folderPath = folderPath + "/"
355355
}
356356

357+
// Escape spaces for display (match insertMention behavior)
358+
let displayFolderPath = folderPath
359+
if (displayFolderPath.includes(" ") && !displayFolderPath.includes("\\ ")) {
360+
displayFolderPath = escapeSpaces(displayFolderPath)
361+
}
362+
357363
// Manually build insertion without a trailing space (more deterministic than insertMention)
358364
const original = textAreaRef.current.value
359365
const beforeCursor = original.slice(0, cursorPosition)
@@ -371,9 +377,9 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
371377
afterCursorContent = isAlphaNumSpace ? afterCursor.replace(/^[^\s]*/, "") : afterCursor
372378
}
373379

374-
const updatedValue = beforeMention + "@" + folderPath + afterCursorContent
380+
const updatedValue = beforeMention + "@" + displayFolderPath + afterCursorContent
375381
const afterMentionPos =
376-
(lastAtIndex !== -1 ? lastAtIndex : beforeCursor.length) + 1 + folderPath.length
382+
(lastAtIndex !== -1 ? lastAtIndex : beforeCursor.length) + 1 + displayFolderPath.length
377383

378384
setInputValue(updatedValue)
379385
setCursorPosition(afterMentionPos)
@@ -391,7 +397,8 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
391397
setSearchQuery(nextQuery)
392398

393399
// Kick off a search to populate folder children
394-
const reqId = Math.random().toString(36).substring(2, 9)
400+
const reqId =
401+
globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random().toString(36).slice(2)}`
395402
setSearchRequestId(reqId)
396403
setSearchLoading(true)
397404
vscode.postMessage({
@@ -660,7 +667,9 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
660667
// Set a timeout to debounce the search requests.
661668
searchTimeoutRef.current = setTimeout(() => {
662669
// Generate a request ID for this search.
663-
const reqId = Math.random().toString(36).substring(2, 9)
670+
const reqId =
671+
globalThis.crypto?.randomUUID?.() ??
672+
`${Date.now()}-${Math.random().toString(36).slice(2)}`
664673
setSearchRequestId(reqId)
665674
setSearchLoading(true)
666675

webview-ui/src/components/chat/__tests__/ChatTextArea.folder-drilldown.spec.tsx

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,4 +93,37 @@ describe("ChatTextArea - folder drilldown behavior", () => {
9393
expect(lastMsg.query).toBe("/src/")
9494
expect(typeof lastMsg.requestId).toBe("string")
9595
})
96+
97+
it("escapes spaces in input and sends unescaped query for folder with spaces", () => {
98+
const setInputValue = vi.fn()
99+
100+
const { container } = render(<ChatTextArea {...defaultProps} setInputValue={setInputValue} />)
101+
102+
// Type to open the @-context menu and set a query
103+
const textarea = container.querySelector("textarea")!
104+
fireEvent.change(textarea, {
105+
target: { value: "@m", selectionStart: 2 },
106+
})
107+
108+
// Ensure our mocked ContextMenu rendered and captured props
109+
expect(screen.getByTestId("context-menu")).toBeInTheDocument()
110+
const props = lastContextMenuProps
111+
expect(props).toBeTruthy()
112+
expect(typeof props.onSelect).toBe("function")
113+
114+
// Simulate selecting a concrete folder with a space
115+
props.onSelect(ContextMenuOptionType.Folder, "/my folder")
116+
117+
// The input should contain the escaped path and NO trailing space
118+
expect(setInputValue).toHaveBeenCalled()
119+
const finalValue2 = setInputValue.mock.calls.at(-1)?.[0]
120+
expect(finalValue2).toBe("@/my\\ folder/")
121+
122+
// It should have kicked off a searchFiles request with unescaped query
123+
const pm2 = vscode.postMessage as ReturnType<typeof vi.fn>
124+
const lastMsg2 = pm2.mock.calls.at(-1)?.[0]
125+
expect(lastMsg2).toMatchObject({ type: "searchFiles" })
126+
expect(lastMsg2.query).toBe("/my folder/")
127+
expect(typeof lastMsg2.requestId).toBe("string")
128+
})
96129
})

0 commit comments

Comments
 (0)