From 92eb0d147c5d1c344756597b632ed595e87fc21c Mon Sep 17 00:00:00 2001 From: "matter-code-review[bot]" <150888575+matter-code-review[bot]@users.noreply.github.com> Date: Mon, 19 Jan 2026 02:50:20 +0000 Subject: [PATCH] fix: webview-ui/src/components/chat/ChatTextArea.tsx - address review comments --- .../src/components/chat/ChatTextArea.tsx | 835 +----------------- 1 file changed, 1 insertion(+), 834 deletions(-) diff --git a/webview-ui/src/components/chat/ChatTextArea.tsx b/webview-ui/src/components/chat/ChatTextArea.tsx index e43db0715..587b21914 100644 --- a/webview-ui/src/components/chat/ChatTextArea.tsx +++ b/webview-ui/src/components/chat/ChatTextArea.tsx @@ -973,837 +973,4 @@ export const ChatTextArea = forwardRef( if ( charBeforeIsWhitespace && - inputValue.slice(0, cursorPosition - 1).match(new RegExp(mentionRegex.source + "$")) - ) { - const newCursorPosition = cursorPosition - 1 - if (!charAfterIsWhitespace) { - event.preventDefault() - setCaretPosition(newCursorPosition) - setCursorPosition(newCursorPosition) - } - - setCursorPosition(newCursorPosition) - setJustDeletedSpaceAfterMention(true) - } else if (justDeletedSpaceAfterMention) { - const { newText, newPosition } = removeMention(inputValue, cursorPosition) - - if (newText !== inputValue) { - event.preventDefault() - setInputValue(newText) - intendedCursorPositionRef.current = newPosition - } - - setJustDeletedSpaceAfterMention(false) - setShowContextMenu(false) - } else { - setJustDeletedSpaceAfterMention(false) - } - } - }, - [ - showSlashCommandsMenu, - localWorkflows, - globalWorkflows, - customModes, - handleSlashCommandsSelect, - selectedSlashCommandsIndex, - slashCommandsQuery, - handleSend, - showContextMenu, - searchQuery, - selectedMenuIndex, - handleMentionSelect, - selectedType, - inputValue, - cursorPosition, - setInputValue, - justDeletedSpaceAfterMention, - queryItems, - allModes, - fileSearchResults, - handleHistoryNavigation, - resetHistoryNavigation, - sendMessageOnEnter, - setCaretPosition, - ], - ) - - const searchTimeoutRef = useRef(null) - - const handleInputChange = useCallback(() => { - const newValue = getPlainTextFromInput() - setInputValue(newValue) - isUserInputRef.current = true // Mark this as user input using ref - resetOnInputChange() - - const newCursorPosition = getCaretPosition() - setCursorPosition(newCursorPosition) - intendedCursorPositionRef.current = newCursorPosition - - let showMenu = shouldShowContextMenu(newValue, newCursorPosition) - const slashMenuVisible = shouldShowSlashCommandsMenu(newValue, newCursorPosition) - - if (slashMenuVisible) { - showMenu = false - } - - setShowSlashCommandsMenu(slashMenuVisible) - setShowContextMenu(showMenu) - - if (slashMenuVisible) { - const slashIndex = newValue.indexOf("/") - const query = newValue.slice(slashIndex + 1, newCursorPosition) - setSlashCommandsQuery(query) - setSelectedSlashCommandsIndex(0) - } else { - setSlashCommandsQuery("") - setSelectedSlashCommandsIndex(0) - } - - if (showMenu) { - const lastAtIndex = newValue.lastIndexOf("@", newCursorPosition - 1) - - if (newValue.startsWith("/") && lastAtIndex === -1) { - const query = newValue - setSearchQuery(query) - setSelectedMenuIndex(0) - } else { - const query = newValue.slice(lastAtIndex + 1, newCursorPosition) - setSearchQuery(query) - - if (query.length > 0) { - setSelectedMenuIndex(0) - - if (searchTimeoutRef.current) { - clearTimeout(searchTimeoutRef.current) - } - - searchTimeoutRef.current = setTimeout(() => { - const reqId = Math.random().toString(36).substring(2, 9) - setSearchRequestId(reqId) - setSearchLoading(true) - - vscode.postMessage({ - type: "searchFiles", - query: unescapeSpaces(query), - requestId: reqId, - }) - }, 200) - } else { - setSelectedMenuIndex(-1) - } - } - } else { - setSearchQuery("") - setSelectedMenuIndex(-1) - setFileSearchResults([]) - } - - if (textAreaRef.current) { - onHeightChange?.(textAreaRef.current.clientHeight) - } - }, [ - getPlainTextFromInput, - getCaretPosition, - resetOnInputChange, - setInputValue, - setCursorPosition, - setShowSlashCommandsMenu, - setShowContextMenu, - setSlashCommandsQuery, - setSelectedSlashCommandsIndex, - setSearchQuery, - setSelectedMenuIndex, - setSearchRequestId, - setSearchLoading, - setFileSearchResults, - onHeightChange, - ]) - - // Handle enhanced prompt response and search results. - useEffect(() => { - const messageHandler = (event: MessageEvent) => { - const message = event.data - - if (message.type === "enhancedPrompt") { - if (message.text && textAreaRef.current) { - try { - // Use execCommand to replace text while preserving undo history - if (document.execCommand) { - // Use native browser methods to preserve undo stack - const textarea = textAreaRef.current - - // Focus the textarea to ensure it's the active element - textarea.focus() - - // Select all text first - const selection = window.getSelection() - if (selection) { - selection.removeAllRanges() - const range = document.createRange() - range.selectNodeContents(textarea) - selection.addRange(range) - } - document.execCommand("insertText", false, message.text) - handleInputChange() - } else { - setInputValue(message.text) - } - } catch { - setInputValue(message.text) - } - } - } else if (message.type === "commitSearchResults") { - const commits = message.commits.map((commit: any) => ({ - type: ContextMenuOptionType.Git, - value: commit.hash, - label: commit.subject, - description: `${commit.shortHash} by ${commit.author} on ${commit.date}`, - icon: "$(git-commit)", - })) - - setGitCommits(commits) - } else if (message.type === "fileSearchResults") { - setSearchLoading(false) - if (message.requestId === searchRequestId) { - setFileSearchResults(message.results || []) - } - // kilocode_change start - } else if (message.type === "insertTextToChatArea") { - if (message.text) { - setInputValue(message.text) - setTimeout(() => { - if (textAreaRef.current) { - textAreaRef.current.focus() - } - }, 0) - } - } - // kilocode_change end - } - - window.addEventListener("message", messageHandler) - return () => window.removeEventListener("message", messageHandler) - }, [handleInputChange, searchRequestId, setInputValue]) - - const handleDrop = useCallback( - async (e: React.DragEvent) => { - e.preventDefault() - setIsDraggingOver(false) - - const textFieldList = e.dataTransfer.getData("text") - const textUriList = e.dataTransfer.getData("application/vnd.code.uri-list") - // When textFieldList is empty, it may attempt to use textUriList obtained from drag-and-drop tabs; if not empty, it will use textFieldList. - const text = textFieldList || textUriList - if (text) { - // Split text on newlines to handle multiple files - const lines = text.split(/\r?\n/).filter((line) => line.trim() !== "") - - if (lines.length > 0) { - // Process each line as a separate file path - let newValue = inputValue.slice(0, cursorPosition) - let totalLength = 0 - - // Using a standard for loop instead of forEach for potential performance gains. - for (let i = 0; i < lines.length; i++) { - const line = lines[i] - // Convert each path to a mention-friendly format - const fullMention = convertToMentionPath(line, cwd) - // Extract filename for compact display - let mentionText = fullMention - if (fullMention.startsWith("@/")) { - const pathWithoutAt = fullMention.slice(1) // Remove @ - const segments = pathWithoutAt.split("/").filter(Boolean) - const filename = segments.pop() || pathWithoutAt - mentionText = `${filename}` - // Store mapping for expansion - mentionMapRef.current.set(filename, pathWithoutAt) - } - newValue += mentionText - totalLength += mentionText.length - - // Add space after each mention except the last one - if (i < lines.length - 1) { - newValue += " " - totalLength += 1 - } - } - - // Add space after the last mention and append the rest of the input - newValue += " " + inputValue.slice(cursorPosition) - totalLength += 1 - - setInputValue(newValue) - const newCursorPosition = cursorPosition + totalLength + 1 - setCursorPosition(newCursorPosition) - intendedCursorPositionRef.current = newCursorPosition - } - - return - } - - const files = Array.from(e.dataTransfer.files) - - if (files.length > 0) { - const acceptedTypes = ["png", "jpeg", "webp"] - - const imageFiles = files.filter((file) => { - const [type, subtype] = file.type.split("/") - return type === "image" && acceptedTypes.includes(subtype) - }) - - // kilocode_change start: Image validation with warning messages for drag and drop - if (imageFiles.length > 0) { - if (shouldDisableImages) { - showImageWarning("kilocode:imageWarnings.modelNoImageSupport") - return - } - if (selectedImages.length >= MAX_IMAGES_PER_MESSAGE) { - showImageWarning("kilocode:imageWarnings.maxImagesReached") - return - } - // kilocode_change end: Image validation with warning messages for drag and drop - - const imagePromises = imageFiles.map((file) => { - return new Promise((resolve) => { - const reader = new FileReader() - - reader.onloadend = () => { - if (reader.error) { - console.error(t("chat:errorReadingFile"), reader.error) - resolve(null) - } else { - const result = reader.result - resolve(typeof result === "string" ? result : null) - } - } - - reader.readAsDataURL(file) - }) - }) - - const imageDataArray = await Promise.all(imagePromises) - const dataUrls = imageDataArray.filter((dataUrl): dataUrl is string => dataUrl !== null) - - if (dataUrls.length > 0) { - setSelectedImages((prevImages) => - [...prevImages, ...dataUrls].slice(0, MAX_IMAGES_PER_MESSAGE), - ) - - if (typeof vscode !== "undefined") { - vscode.postMessage({ type: "draggedImages", dataUrls: dataUrls }) - } - } else { - console.warn(t("chat:noValidImages")) - } - } - } - }, - [ - cursorPosition, - cwd, - inputValue, - setInputValue, - setCursorPosition, - shouldDisableImages, - setSelectedImages, - t, - selectedImages.length, // kilocode_change - added selectedImages.length - showImageWarning, // kilocode_change - added showImageWarning - ], - ) - - const [isTtsPlaying, setIsTtsPlaying] = useState(false) - - useEvent("message", (event: MessageEvent) => { - const message: ExtensionMessage = event.data - - if (message.type === "ttsStart") { - setIsTtsPlaying(true) - } else if (message.type === "ttsStop") { - setIsTtsPlaying(false) - } - }) - - const placeholderBottomText = `${t("chat:addContext")}${shouldDisableImages ? `, ${t("chat:dragFiles")}` : `, ${t("chat:dragFilesImages")}`}` - - // Common mode selector handler - // const handleModeChange = useCallback( - // (value: Mode) => { - // setMode(value) - // vscode.postMessage({ type: "mode", text: value }) - // }, - // [setMode], - // ) - - // // Helper function to get API config dropdown options - // // kilocode_change: unused - // const _getApiConfigOptions = useMemo(() => { - // const pinnedConfigs = (listApiConfigMeta || []) - // .filter((config) => pinnedApiConfigs && pinnedApiConfigs[config.id]) - // .map((config) => ({ - // value: config.id, - // label: config.name, - // name: config.name, - // type: DropdownOptionType.ITEM, - // pinned: true, - // })) - // .sort((a, b) => a.label.localeCompare(b.label)) - - // const unpinnedConfigs = (listApiConfigMeta || []) - // .filter((config) => !pinnedApiConfigs || !pinnedApiConfigs[config.id]) - // .map((config) => ({ - // value: config.id, - // label: config.name, - // name: config.name, - // type: DropdownOptionType.ITEM, - // pinned: false, - // })) - // .sort((a, b) => a.label.localeCompare(b.label)) - - // const hasPinnedAndUnpinned = pinnedConfigs.length > 0 && unpinnedConfigs.length > 0 - - // return [ - // ...pinnedConfigs, - // ...(hasPinnedAndUnpinned - // ? [ - // { - // value: "sep-pinned", - // label: t("chat:separator"), - // type: DropdownOptionType.SEPARATOR, - // }, - // ] - // : []), - // ...unpinnedConfigs, - // { - // value: "sep-2", - // label: t("chat:separator"), - // type: DropdownOptionType.SEPARATOR, - // }, - // { - // value: "settingsButtonClicked", - // label: t("chat:edit"), - // type: DropdownOptionType.ACTION, - // }, - // ] - // }, [listApiConfigMeta, pinnedApiConfigs, t]) - - // Helper function to handle API config change - // kilocode_change: unused - // const _handleApiConfigChange = useCallback((value: string) => { - // if (value === "settingsButtonClicked") { - // vscode.postMessage({ - // type: "loadApiConfiguration", - // text: value, - // values: { section: "providers" }, - // }) - // } else { - // vscode.postMessage({ type: "loadApiConfigurationById", text: value }) - // } - // }, []) - - // Helper function to render API config item - // kilocode_change: unused - // const _renderApiConfigItem = useCallback( - // ({ type, value, label, pinned }: any) => { - // if (type !== DropdownOptionType.ITEM) { - // return label - // } - - // const config = listApiConfigMeta?.find((c) => c.id === value) - // const isCurrentConfig = config?.name === currentApiConfigName - - // return ( - //
- //
- // {label} - //
- //
- //
- // - //
- // - // - // - //
- //
- // ) - // }, - // [listApiConfigMeta, currentApiConfigName, t, togglePinnedApiConfig], - // ) - - // Helper function to render the text area section - const renderTextAreaSection = () => ( -
-
{ - if (typeof ref === "function") { - ref(el) - } else if (ref) { - ref.current = el - } - textAreaRef.current = el - }} - role="textbox" - contentEditable - suppressContentEditableWarning - aria-multiline="true" - data-testid="chat-input" - onInput={handleInputChange} - onFocus={() => setIsFocused(true)} - onKeyDown={(e) => { - if (isEditMode && e.key === "Escape" && !e.nativeEvent?.isComposing) { - e.preventDefault() - onCancel?.() - return - } - handleKeyDown(e) - }} - onKeyUp={handleKeyUp} - onBlur={handleBlur} - onPaste={handlePaste} - onSelect={updateCursorPosition} - onMouseUp={updateCursorPosition} - onScroll={updateCursorPosition} - spellCheck={false} - autoFocus - className={cn( - "w-full", - "text-vscode-input-foreground", - "font-vscode-font-family", - "text-vscode-editor-font-size", - "leading-vscode-editor-line-height", - "cursor-text", - "outline-none", - isEditMode ? "pt-1.5 pb-2 px-2" : "py-1.5 px-2", - "min-h-[80px]", - "max-h-[calc(100vh/2.5)]", - "box-border", - "overflow-x-hidden", - "overflow-y-auto", - "flex-grow", - "scrollbar-none", - "scrollbar-hide", - "whitespace-pre-wrap", - "break-words", - )} - style={{ - caretColor: "var(--vscode-input-foreground)", - }} - /> - - {isTtsPlaying && ( - - - - )} - - {!inputValue && ( -
- {placeholderBottomText} -
- )} - - {/* Bottom controls section */} -
-
-
- -
- {apiConfiguration && ( -
- -
- )} -
-
- {!isEditMode && ( - <> - - - - )} - - - - {isEditMode && ( - - - - )} - - - -
-
-
- ) - - return ( -
- {/* Pinned file review actions (not a chat row) */} - {!isEditMode && ( -
- {}} /> -
- )} -
-
{ - // Only allowed to drop images/files on shift key pressed. - if (!e.shiftKey) { - setIsDraggingOver(false) - return - } - - e.preventDefault() - setIsDraggingOver(true) - e.dataTransfer.dropEffect = "copy" - }} - onDragLeave={(e) => { - e.preventDefault() - const rect = e.currentTarget.getBoundingClientRect() - - if ( - e.clientX <= rect.left || - e.clientX >= rect.right || - e.clientY <= rect.top || - e.clientY >= rect.bottom - ) { - setIsDraggingOver(false) - } - }}> - {/* kilocode_change start: ImageWarningBanner integration */} - - {/* kilocode_change end: ImageWarningBanner integration */} - {/* kilocode_change start: pull slash commands from Cline */} - {showSlashCommandsMenu && ( -
- -
- )} - {/* kilocode_change end: pull slash commands from Cline */} - {showContextMenu && ( -
- -
- )} - - {renderTextAreaSection()} -
-
- - {selectedImages.length > 0 && ( - - )} -
- ) - }, -) + \ No newline at end of file