2
0
Chris Estreich 7 сар өмнө
parent
commit
9a358ab0f5

+ 131 - 58
src/integrations/editor/DiffViewProvider.ts

@@ -1,16 +1,19 @@
 import * as vscode from "vscode"
 import * as path from "path"
 import * as fs from "fs/promises"
+import * as diff from "diff"
+import stripBom from "strip-bom"
+
 import { createDirectoriesForFile } from "../../utils/fs"
 import { arePathsEqual } from "../../utils/path"
 import { formatResponse } from "../../core/prompts/responses"
-import { DecorationController } from "./DecorationController"
-import * as diff from "diff"
 import { diagnosticsToProblemsString, getNewDiagnostics } from "../diagnostics"
-import stripBom from "strip-bom"
+
+import { DecorationController } from "./DecorationController"
 
 export const DIFF_VIEW_URI_SCHEME = "cline-diff"
 
+// TODO: https://github.com/cline/cline/pull/3354
 export class DiffViewProvider {
 	editType?: "create" | "modify"
 	isEditing = false
@@ -32,17 +35,21 @@ export class DiffViewProvider {
 		const fileExists = this.editType === "modify"
 		const absolutePath = path.resolve(this.cwd, relPath)
 		this.isEditing = true
-		// if the file is already open, ensure it's not dirty before getting its contents
+
+		// If the file is already open, ensure it's not dirty before getting its
+		// contents.
 		if (fileExists) {
 			const existingDocument = vscode.workspace.textDocuments.find((doc) =>
 				arePathsEqual(doc.uri.fsPath, absolutePath),
 			)
+
 			if (existingDocument && existingDocument.isDirty) {
 				await existingDocument.save()
 			}
 		}
 
-		// get diagnostics before editing the file, we'll compare to diagnostics after editing to see if cline needs to fix anything
+		// Get diagnostics before editing the file, we'll compare to diagnostics
+		// after editing to see if cline needs to fix anything.
 		this.preDiagnostics = vscode.languages.getDiagnostics()
 
 		if (fileExists) {
@@ -50,33 +57,41 @@ export class DiffViewProvider {
 		} else {
 			this.originalContent = ""
 		}
-		// for new files, create any necessary directories and keep track of new directories to delete if the user denies the operation
+
+		// For new files, create any necessary directories and keep track of new
+		// directories to delete if the user denies the operation.
 		this.createdDirs = await createDirectoriesForFile(absolutePath)
-		// make sure the file exists before we open it
+
+		// Make sure the file exists before we open it.
 		if (!fileExists) {
 			await fs.writeFile(absolutePath, "")
 		}
-		// if the file was already open, close it (must happen after showing the diff view since if it's the only tab the column will close)
+
+		// If the file was already open, close it (must happen after showing the
+		// diff view since if it's the only tab the column will close).
 		this.documentWasOpen = false
-		// close the tab if it's open (it's already saved above)
+
+		// Close the tab if it's open (it's already saved above).
 		const tabs = vscode.window.tabGroups.all
 			.map((tg) => tg.tabs)
 			.flat()
 			.filter(
 				(tab) => tab.input instanceof vscode.TabInputText && arePathsEqual(tab.input.uri.fsPath, absolutePath),
 			)
+
 		for (const tab of tabs) {
 			if (!tab.isDirty) {
 				await vscode.window.tabGroups.close(tab)
 			}
 			this.documentWasOpen = true
 		}
+
 		this.activeDiffEditor = await this.openDiffEditor()
 		this.fadedOverlayController = new DecorationController("fadedOverlay", this.activeDiffEditor)
 		this.activeLineController = new DecorationController("activeLine", this.activeDiffEditor)
-		// Apply faded overlay to all lines initially
+		// Apply faded overlay to all lines initially.
 		this.fadedOverlayController.addLines(0, this.activeDiffEditor.document.lineCount)
-		this.scrollEditorToLine(0) // will this crash for new files?
+		this.scrollEditorToLine(0) // Will this crash for new files?
 		this.streamedLines = []
 	}
 
@@ -84,58 +99,70 @@ export class DiffViewProvider {
 		if (!this.relPath || !this.activeLineController || !this.fadedOverlayController) {
 			throw new Error("Required values not set")
 		}
+
 		this.newContent = accumulatedContent
 		const accumulatedLines = accumulatedContent.split("\n")
+
 		if (!isFinal) {
-			accumulatedLines.pop() // remove the last partial line only if it's not the final update
+			accumulatedLines.pop() // Remove the last partial line only if it's not the final update.
 		}
 
 		const diffEditor = this.activeDiffEditor
 		const document = diffEditor?.document
+
 		if (!diffEditor || !document) {
 			throw new Error("User closed text editor, unable to edit file...")
 		}
 
-		// Place cursor at the beginning of the diff editor to keep it out of the way of the stream animation
+		// Place cursor at the beginning of the diff editor to keep it out of
+		// the way of the stream animation.
 		const beginningOfDocument = new vscode.Position(0, 0)
 		diffEditor.selection = new vscode.Selection(beginningOfDocument, beginningOfDocument)
 
 		const endLine = accumulatedLines.length
-		// Replace all content up to the current line with accumulated lines
+		// Replace all content up to the current line with accumulated lines.
 		const edit = new vscode.WorkspaceEdit()
 		const rangeToReplace = new vscode.Range(0, 0, endLine + 1, 0)
 		const contentToReplace = accumulatedLines.slice(0, endLine + 1).join("\n") + "\n"
 		edit.replace(document.uri, rangeToReplace, this.stripAllBOMs(contentToReplace))
 		await vscode.workspace.applyEdit(edit)
-		// Update decorations
+		// Update decorations.
 		this.activeLineController.setActiveLine(endLine)
 		this.fadedOverlayController.updateOverlayAfterLine(endLine, document.lineCount)
-		// Scroll to the current line
+		// Scroll to the current line.
 		this.scrollEditorToLine(endLine)
 
-		// Update the streamedLines with the new accumulated content
+		// Update the streamedLines with the new accumulated content.
 		this.streamedLines = accumulatedLines
+
 		if (isFinal) {
-			// Handle any remaining lines if the new content is shorter than the original
+			// Handle any remaining lines if the new content is shorter than the
+			// original.
 			if (this.streamedLines.length < document.lineCount) {
 				const edit = new vscode.WorkspaceEdit()
 				edit.delete(document.uri, new vscode.Range(this.streamedLines.length, 0, document.lineCount, 0))
 				await vscode.workspace.applyEdit(edit)
 			}
-			// Preserve empty last line if original content had one
+
+			// Preserve empty last line if original content had one.
 			const hasEmptyLastLine = this.originalContent?.endsWith("\n")
+
 			if (hasEmptyLastLine && !accumulatedContent.endsWith("\n")) {
 				accumulatedContent += "\n"
 			}
-			// Apply the final content
+
+			// Apply the final content.
 			const finalEdit = new vscode.WorkspaceEdit()
+
 			finalEdit.replace(
 				document.uri,
 				new vscode.Range(0, 0, document.lineCount, 0),
 				this.stripAllBOMs(accumulatedContent),
 			)
+
 			await vscode.workspace.applyEdit(finalEdit)
-			// Clear all decorations at the end (after applying final edit)
+
+			// Clear all decorations at the end (after applying final edit).
 			this.fadedOverlayController.clear()
 			this.activeLineController.clear()
 		}
@@ -149,59 +176,68 @@ export class DiffViewProvider {
 		if (!this.relPath || !this.newContent || !this.activeDiffEditor) {
 			return { newProblemsMessage: undefined, userEdits: undefined, finalContent: undefined }
 		}
+
 		const absolutePath = path.resolve(this.cwd, this.relPath)
 		const updatedDocument = this.activeDiffEditor.document
 		const editedContent = updatedDocument.getText()
+
 		if (updatedDocument.isDirty) {
 			await updatedDocument.save()
 		}
 
-		await vscode.window.showTextDocument(vscode.Uri.file(absolutePath), { preview: false })
+		await vscode.window.showTextDocument(vscode.Uri.file(absolutePath), { preview: false, preserveFocus: true })
 		await this.closeAllDiffViews()
 
-		/*
-		Getting diagnostics before and after the file edit is a better approach than
-		automatically tracking problems in real-time. This method ensures we only
-		report new problems that are a direct result of this specific edit.
-		Since these are new problems resulting from Roo's edit, we know they're
-		directly related to the work he's doing. This eliminates the risk of Roo
-		going off-task or getting distracted by unrelated issues, which was a problem
-		with the previous auto-debug approach. Some users' machines may be slow to
-		update diagnostics, so this approach provides a good balance between automation
-		and avoiding potential issues where Roo might get stuck in loops due to
-		outdated problem information. If no new problems show up by the time the user
-		accepts the changes, they can always debug later using the '@problems' mention.
-		This way, Roo only becomes aware of new problems resulting from his edits
-		and can address them accordingly. If problems don't change immediately after
-		applying a fix, won't be notified, which is generally fine since the
-		initial fix is usually correct and it may just take time for linters to catch up.
-		*/
+		// Getting diagnostics before and after the file edit is a better approach than
+		// automatically tracking problems in real-time. This method ensures we only
+		// report new problems that are a direct result of this specific edit.
+		// Since these are new problems resulting from Roo's edit, we know they're
+		// directly related to the work he's doing. This eliminates the risk of Roo
+		// going off-task or getting distracted by unrelated issues, which was a problem
+		// with the previous auto-debug approach. Some users' machines may be slow to
+		// update diagnostics, so this approach provides a good balance between automation
+		// and avoiding potential issues where Roo might get stuck in loops due to
+		// outdated problem information. If no new problems show up by the time the user
+		// accepts the changes, they can always debug later using the '@problems' mention.
+		// This way, Roo only becomes aware of new problems resulting from his edits
+		// and can address them accordingly. If problems don't change immediately after
+		// applying a fix, won't be notified, which is generally fine since the
+		// initial fix is usually correct and it may just take time for linters to catch up.
 		const postDiagnostics = vscode.languages.getDiagnostics()
+
 		const newProblems = await diagnosticsToProblemsString(
 			getNewDiagnostics(this.preDiagnostics, postDiagnostics),
 			[
 				vscode.DiagnosticSeverity.Error, // only including errors since warnings can be distracting (if user wants to fix warnings they can use the @problems mention)
 			],
 			this.cwd,
-		) // will be empty string if no errors
+		) // Will be empty string if no errors.
+
 		const newProblemsMessage =
 			newProblems.length > 0 ? `\n\nNew problems detected after saving the file:\n${newProblems}` : ""
 
-		// If the edited content has different EOL characters, we don't want to show a diff with all the EOL differences.
+		// If the edited content has different EOL characters, we don't want to
+		// show a diff with all the EOL differences.
 		const newContentEOL = this.newContent.includes("\r\n") ? "\r\n" : "\n"
-		const normalizedEditedContent = editedContent.replace(/\r\n|\n/g, newContentEOL).trimEnd() + newContentEOL // trimEnd to fix issue where editor adds in extra new line automatically
-		// just in case the new content has a mix of varying EOL characters
+
+		// `trimEnd` to fix issue where editor adds in extra new line
+		// automatically.
+		const normalizedEditedContent = editedContent.replace(/\r\n|\n/g, newContentEOL).trimEnd() + newContentEOL
+
+		// Just in case the new content has a mix of varying EOL characters.
 		const normalizedNewContent = this.newContent.replace(/\r\n|\n/g, newContentEOL).trimEnd() + newContentEOL
+
 		if (normalizedEditedContent !== normalizedNewContent) {
-			// user made changes before approving edit
+			// User made changes before approving edit.
 			const userEdits = formatResponse.createPrettyPatch(
 				this.relPath.toPosix(),
 				normalizedNewContent,
 				normalizedEditedContent,
 			)
+
 			return { newProblemsMessage, userEdits, finalContent: normalizedEditedContent }
 		} else {
-			// no changes to cline's edits
+			// No changes to Roo's edits.
 			return { newProblemsMessage, userEdits: undefined, finalContent: normalizedEditedContent }
 		}
 	}
@@ -210,42 +246,55 @@ export class DiffViewProvider {
 		if (!this.relPath || !this.activeDiffEditor) {
 			return
 		}
+
 		const fileExists = this.editType === "modify"
 		const updatedDocument = this.activeDiffEditor.document
 		const absolutePath = path.resolve(this.cwd, this.relPath)
+
 		if (!fileExists) {
 			if (updatedDocument.isDirty) {
 				await updatedDocument.save()
 			}
+
 			await this.closeAllDiffViews()
 			await fs.unlink(absolutePath)
-			// Remove only the directories we created, in reverse order
+
+			// Remove only the directories we created, in reverse order.
 			for (let i = this.createdDirs.length - 1; i >= 0; i--) {
 				await fs.rmdir(this.createdDirs[i])
 				console.log(`Directory ${this.createdDirs[i]} has been deleted.`)
 			}
+
 			console.log(`File ${absolutePath} has been deleted.`)
 		} else {
-			// revert document
+			// Revert document.
 			const edit = new vscode.WorkspaceEdit()
+
 			const fullRange = new vscode.Range(
 				updatedDocument.positionAt(0),
 				updatedDocument.positionAt(updatedDocument.getText().length),
 			)
+
 			edit.replace(updatedDocument.uri, fullRange, this.originalContent ?? "")
-			// Apply the edit and save, since contents shouldnt have changed this wont show in local history unless of course the user made changes and saved during the edit
+
+			// Apply the edit and save, since contents shouldnt have changed
+			// this won't show in local history unless of course the user made
+			// changes and saved during the edit.
 			await vscode.workspace.applyEdit(edit)
 			await updatedDocument.save()
 			console.log(`File ${absolutePath} has been reverted to its original content.`)
+
 			if (this.documentWasOpen) {
 				await vscode.window.showTextDocument(vscode.Uri.file(absolutePath), {
 					preview: false,
+					preserveFocus: true,
 				})
 			}
+
 			await this.closeAllDiffViews()
 		}
 
-		// edit is done
+		// Edit is done.
 		await this.reset()
 	}
 
@@ -257,8 +306,9 @@ export class DiffViewProvider {
 					tab.input instanceof vscode.TabInputTextDiff &&
 					tab.input?.original?.scheme === DIFF_VIEW_URI_SCHEME,
 			)
+
 		for (const tab of tabs) {
-			// trying to close dirty views results in save popup
+			// Trying to close dirty views results in save popup.
 			if (!tab.isDirty) {
 				await vscode.window.tabGroups.close(tab)
 			}
@@ -269,8 +319,12 @@ export class DiffViewProvider {
 		if (!this.relPath) {
 			throw new Error("No file path set")
 		}
+
 		const uri = vscode.Uri.file(path.resolve(this.cwd, this.relPath))
-		// If this diff editor is already open (ie if a previous write file was interrupted) then we should activate that instead of opening a new diff
+
+		// If this diff editor is already open (ie if a previous write file was
+		// interrupted) then we should activate that instead of opening a new
+		// diff.
 		const diffTab = vscode.window.tabGroups.all
 			.flatMap((group) => group.tabs)
 			.find(
@@ -279,20 +333,24 @@ export class DiffViewProvider {
 					tab.input?.original?.scheme === DIFF_VIEW_URI_SCHEME &&
 					arePathsEqual(tab.input.modified.fsPath, uri.fsPath),
 			)
+
 		if (diffTab && diffTab.input instanceof vscode.TabInputTextDiff) {
-			const editor = await vscode.window.showTextDocument(diffTab.input.modified)
+			const editor = await vscode.window.showTextDocument(diffTab.input.modified, { preserveFocus: true })
 			return editor
 		}
-		// Open new diff editor
+
+		// Open new diff editor.
 		return new Promise<vscode.TextEditor>((resolve, reject) => {
 			const fileName = path.basename(uri.fsPath)
 			const fileExists = this.editType === "modify"
+
 			const disposable = vscode.window.onDidChangeActiveTextEditor((editor) => {
 				if (editor && arePathsEqual(editor.document.uri.fsPath, uri.fsPath)) {
 					disposable.dispose()
 					resolve(editor)
 				}
 			})
+
 			vscode.commands.executeCommand(
 				"vscode.diff",
 				vscode.Uri.parse(`${DIFF_VIEW_URI_SCHEME}:${fileName}`).with({
@@ -300,8 +358,10 @@ export class DiffViewProvider {
 				}),
 				uri,
 				`${fileName}: ${fileExists ? "Original ↔ Roo's Changes" : "New File"} (Editable)`,
+				{ preserveFocus: true },
 			)
-			// This may happen on very slow machines ie project idx
+
+			// This may happen on very slow machines i.e. project idx.
 			setTimeout(() => {
 				disposable.dispose()
 				reject(new Error("Failed to open diff editor, please try again..."))
@@ -312,6 +372,7 @@ export class DiffViewProvider {
 	private scrollEditorToLine(line: number) {
 		if (this.activeDiffEditor) {
 			const scrollLine = line + 4
+
 			this.activeDiffEditor.revealRange(
 				new vscode.Range(scrollLine, 0, scrollLine, 0),
 				vscode.TextEditorRevealType.InCenter,
@@ -323,18 +384,23 @@ export class DiffViewProvider {
 		if (!this.activeDiffEditor) {
 			return
 		}
+
 		const currentContent = this.activeDiffEditor.document.getText()
 		const diffs = diff.diffLines(this.originalContent || "", currentContent)
+
 		let lineCount = 0
+
 		for (const part of diffs) {
 			if (part.added || part.removed) {
-				// Found the first diff, scroll to it
+				// Found the first diff, scroll to it.
 				this.activeDiffEditor.revealRange(
 					new vscode.Range(lineCount, 0, lineCount, 0),
 					vscode.TextEditorRevealType.InCenter,
 				)
+
 				return
 			}
+
 			if (!part.removed) {
 				lineCount += part.count || 0
 			}
@@ -344,17 +410,24 @@ export class DiffViewProvider {
 	private stripAllBOMs(input: string): string {
 		let result = input
 		let previous
+
 		do {
 			previous = result
 			result = stripBom(result)
 		} while (result !== previous)
+
 		return result
 	}
 
-	// close editor if open?
 	async reset() {
-		// Ensure any diff views opened by this provider are closed to release memory
-		await this.closeAllDiffViews()
+		// Ensure any diff views opened by this provider are closed to release
+		// memory.
+		try {
+			await this.closeAllDiffViews()
+		} catch (error) {
+			console.error("Error closing diff views", error)
+		}
+
 		this.editType = undefined
 		this.isEditing = false
 		this.originalContent = undefined

+ 82 - 57
webview-ui/src/components/chat/ChatTextArea.tsx

@@ -31,7 +31,7 @@ import { cn } from "@/lib/utils"
 interface ChatTextAreaProps {
 	inputValue: string
 	setInputValue: (value: string) => void
-	textAreaDisabled: boolean
+	sendingDisabled: boolean
 	selectApiConfigDisabled: boolean
 	placeholderText: string
 	selectedImages: string[]
@@ -50,7 +50,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 		{
 			inputValue,
 			setInputValue,
-			textAreaDisabled,
+			sendingDisabled,
 			selectApiConfigDisabled,
 			placeholderText,
 			selectedImages,
@@ -165,21 +165,19 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 		}, [selectedType, searchQuery])
 
 		const handleEnhancePrompt = useCallback(() => {
-			if (!textAreaDisabled) {
-				const trimmedInput = inputValue.trim()
-				if (trimmedInput) {
-					setIsEnhancingPrompt(true)
-					const message = {
-						type: "enhancePrompt" as const,
-						text: trimmedInput,
-					}
-					vscode.postMessage(message)
-				} else {
-					const promptDescription = t("chat:enhancePromptDescription")
-					setInputValue(promptDescription)
-				}
+			if (sendingDisabled) {
+				return
+			}
+
+			const trimmedInput = inputValue.trim()
+
+			if (trimmedInput) {
+				setIsEnhancingPrompt(true)
+				vscode.postMessage({ type: "enhancePrompt" as const, text: trimmedInput })
+			} else {
+				setInputValue(t("chat:enhancePromptDescription"))
 			}
-		}, [inputValue, textAreaDisabled, setInputValue, t])
+		}, [inputValue, sendingDisabled, setInputValue, t])
 
 		const queryItems = useMemo(() => {
 			return [
@@ -361,9 +359,13 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 				}
 
 				const isComposing = event.nativeEvent?.isComposing ?? false
+
 				if (event.key === "Enter" && !event.shiftKey && !isComposing) {
 					event.preventDefault()
-					onSend()
+
+					if (!sendingDisabled) {
+						onSend()
+					}
 				}
 
 				if (event.key === "Backspace" && !isComposing) {
@@ -372,29 +374,37 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 
 					const charBeforeIsWhitespace =
 						charBeforeCursor === " " || charBeforeCursor === "\n" || charBeforeCursor === "\r\n"
+
 					const charAfterIsWhitespace =
 						charAfterCursor === " " || charAfterCursor === "\n" || charAfterCursor === "\r\n"
-					// checks if char before cusor is whitespace after a mention
+
+					// Checks if char before cusor is whitespace after a mention.
 					if (
 						charBeforeIsWhitespace &&
-						inputValue.slice(0, cursorPosition - 1).match(new RegExp(mentionRegex.source + "$")) // "$" is added to ensure the match occurs at the end of the string
+						// "$" is added to ensure the match occurs at the end of the string.
+						inputValue.slice(0, cursorPosition - 1).match(new RegExp(mentionRegex.source + "$"))
 					) {
 						const newCursorPosition = cursorPosition - 1
-						// if mention is followed by another word, then instead of deleting the space separating them we just move the cursor to the end of the mention
+						// If mention is followed by another word, then instead
+						// of deleting the space separating them we just move
+						// the cursor to the end of the mention.
 						if (!charAfterIsWhitespace) {
 							event.preventDefault()
 							textAreaRef.current?.setSelectionRange(newCursorPosition, newCursorPosition)
 							setCursorPosition(newCursorPosition)
 						}
+
 						setCursorPosition(newCursorPosition)
 						setJustDeletedSpaceAfterMention(true)
 					} else if (justDeletedSpaceAfterMention) {
 						const { newText, newPosition } = removeMention(inputValue, cursorPosition)
+
 						if (newText !== inputValue) {
 							event.preventDefault()
 							setInputValue(newText)
 							setIntendedCursorPosition(newPosition) // Store the new cursor position in state
 						}
+
 						setJustDeletedSpaceAfterMention(false)
 						setShowContextMenu(false)
 					} else {
@@ -403,6 +413,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 				}
 			},
 			[
+				sendingDisabled,
 				onSend,
 				showContextMenu,
 				searchQuery,
@@ -425,63 +436,67 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 				setIntendedCursorPosition(null) // Reset the state.
 			}
 		}, [inputValue, intendedCursorPosition])
-		// Ref to store the search timeout
+
+		// Ref to store the search timeout.
 		const searchTimeoutRef = useRef<NodeJS.Timeout | null>(null)
 
 		const handleInputChange = useCallback(
 			(e: React.ChangeEvent<HTMLTextAreaElement>) => {
 				const newValue = e.target.value
-				const newCursorPosition = e.target.selectionStart
 				setInputValue(newValue)
+
+				const newCursorPosition = e.target.selectionStart
 				setCursorPosition(newCursorPosition)
-				const showMenu = shouldShowContextMenu(newValue, newCursorPosition)
 
+				const showMenu = shouldShowContextMenu(newValue, newCursorPosition)
 				setShowContextMenu(showMenu)
+
 				if (showMenu) {
 					if (newValue.startsWith("/")) {
-						// Handle slash command
+						// Handle slash command.
 						const query = newValue
 						setSearchQuery(query)
 						setSelectedMenuIndex(0)
 					} else {
-						// Existing @ mention handling
+						// Existing @ mention handling.
 						const lastAtIndex = newValue.lastIndexOf("@", newCursorPosition - 1)
 						const query = newValue.slice(lastAtIndex + 1, newCursorPosition)
 						setSearchQuery(query)
 
-						// Send file search request if query is not empty
+						// Send file search request if query is not empty.
 						if (query.length > 0) {
 							setSelectedMenuIndex(0)
-							// Don't clear results until we have new ones
-							// This prevents flickering
 
-							// Clear any existing timeout
+							// Don't clear results until we have new ones. This
+							// prevents flickering.
+
+							// Clear any existing timeout.
 							if (searchTimeoutRef.current) {
 								clearTimeout(searchTimeoutRef.current)
 							}
 
-							// Set a timeout to debounce the search requests
+							// Set a timeout to debounce the search requests.
 							searchTimeoutRef.current = setTimeout(() => {
-								// Generate a request ID for this search
+								// Generate a request ID for this search.
 								const reqId = Math.random().toString(36).substring(2, 9)
 								setSearchRequestId(reqId)
 								setSearchLoading(true)
 
-								// Send message to extension to search files
+								// Send message to extension to search files.
 								vscode.postMessage({
 									type: "searchFiles",
 									query: unescapeSpaces(query),
 									requestId: reqId,
 								})
-							}, 200) // 200ms debounce
+							}, 200) // 200ms debounce.
 						} else {
-							setSelectedMenuIndex(3) // Set to "File" option by default
+							setSelectedMenuIndex(3) // Set to "File" option by default.
 						}
 					}
 				} else {
 					setSearchQuery("")
 					setSelectedMenuIndex(-1)
-					setFileSearchResults([]) // Clear file search results
+					setFileSearchResults([]) // Clear file search results.
 				}
 			},
 			[setInputValue, setSearchRequestId, setFileSearchResults, setSearchLoading],
@@ -541,14 +556,18 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 
 				if (!shouldDisableImages && imageItems.length > 0) {
 					e.preventDefault()
+
 					const imagePromises = imageItems.map((item) => {
 						return new Promise<string | null>((resolve) => {
 							const blob = item.getAsFile()
+
 							if (!blob) {
 								resolve(null)
 								return
 							}
+
 							const reader = new FileReader()
+
 							reader.onloadend = () => {
 								if (reader.error) {
 									console.error(t("chat:errorReadingFile"), reader.error)
@@ -558,11 +577,14 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 									resolve(typeof result === "string" ? result : null)
 								}
 							}
+
 							reader.readAsDataURL(blob)
 						})
 					})
+
 					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))
 					} else {
@@ -657,8 +679,10 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 				}
 
 				const files = Array.from(e.dataTransfer.files)
-				if (!textAreaDisabled && files.length > 0) {
+
+				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)
@@ -668,6 +692,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 						const imagePromises = imageFiles.map((file) => {
 							return new Promise<string | null>((resolve) => {
 								const reader = new FileReader()
+
 								reader.onloadend = () => {
 									if (reader.error) {
 										console.error(t("chat:errorReadingFile"), reader.error)
@@ -677,20 +702,21 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 										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,
-								})
+								vscode.postMessage({ type: "draggedImages", dataUrls: dataUrls })
 							}
 						} else {
 							console.warn(t("chat:noValidImages"))
@@ -705,7 +731,6 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 				setInputValue,
 				setCursorPosition,
 				setIntendedCursorPosition,
-				textAreaDisabled,
 				shouldDisableImages,
 				setSelectedImages,
 				t,
@@ -749,11 +774,12 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 						className={cn("chat-text-area", "relative", "flex", "flex-col", "outline-none")}
 						onDrop={handleDrop}
 						onDragOver={(e) => {
-							//Only allowed to drop images/files on shift key pressed
+							// Only allowed to drop images/files on shift key pressed.
 							if (!e.shiftKey) {
 								setIsDraggingOver(false)
 								return
 							}
+
 							e.preventDefault()
 							setIsDraggingOver(true)
 							e.dataTransfer.dropEffect = "copy"
@@ -761,6 +787,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 						onDragLeave={(e) => {
 							e.preventDefault()
 							const rect = e.currentTarget.getBoundingClientRect()
+
 							if (
 								e.clientX <= rect.left ||
 								e.clientX >= rect.right ||
@@ -840,7 +867,6 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 									textAreaRef.current = el
 								}}
 								value={inputValue}
-								disabled={textAreaDisabled}
 								onChange={(e) => {
 									handleInputChange(e)
 									updateHighlights()
@@ -856,6 +882,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 									if (textAreaBaseHeight === undefined || height < textAreaBaseHeight) {
 										setTextAreaBaseHeight(height)
 									}
+
 									onHeightChange?.(height)
 								}}
 								placeholder={placeholderText}
@@ -868,14 +895,13 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 									"font-vscode-font-family",
 									"text-vscode-editor-font-size",
 									"leading-vscode-editor-line-height",
-									textAreaDisabled ? "cursor-not-allowed" : "cursor-text",
+									"cursor-text",
 									"py-1.5 px-2",
 									isFocused
 										? "border border-vscode-focusBorder outline outline-vscode-focusBorder"
 										: isDraggingOver
 											? "border-2 border-dashed border-vscode-focusBorder"
 											: "border border-transparent",
-									textAreaDisabled ? "opacity-50" : "opacity-100",
 									isDraggingOver
 										? "bg-[color-mix(in_srgb,var(--vscode-input-background)_95%,var(--vscode-focusBorder))]"
 										: "bg-vscode-input-background",
@@ -894,6 +920,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 								)}
 								onScroll={() => updateHighlights()}
 							/>
+
 							{isTtsPlaying && (
 								<Button
 									variant="ghost"
@@ -903,6 +930,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 									<VolumeX className="size-4" />
 								</Button>
 							)}
+
 							{!inputValue && (
 								<div
 									className={cn(
@@ -919,7 +947,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 										"transition-opacity",
 										"duration-200",
 										"ease-in-out",
-										textAreaDisabled ? "opacity-35" : "opacity-70",
+										"opacity-70",
 									)}>
 									{placeholderBottomText}
 								</div>
@@ -942,7 +970,6 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 
 				<div className={cn("flex", "justify-between", "items-center", "mt-auto", "pt-0.5")}>
 					<div className={cn("flex", "items-center", "gap-1", "min-w-0")}>
-						{/* Mode selector - fixed width */}
 						<div className="shrink-0">
 							<SelectDropdown
 								value={mode}
@@ -979,26 +1006,25 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 							/>
 						</div>
 
-						{/* API configuration selector - flexible width */}
 						<div className={cn("flex-1", "min-w-0", "overflow-hidden")}>
 							<SelectDropdown
 								value={currentConfigId}
 								disabled={selectApiConfigDisabled}
 								title={t("chat:selectApiConfig")}
-								placeholder={displayName} // Always show the current name
+								placeholder={displayName}
 								options={[
-									// Pinned items first
+									// Pinned items first.
 									...(listApiConfigMeta || [])
 										.filter((config) => pinnedApiConfigs && pinnedApiConfigs[config.id])
 										.map((config) => ({
 											value: config.id,
 											label: config.name,
-											name: config.name, // Keep name for comparison with currentApiConfigName
+											name: config.name, // Keep name for comparison with currentApiConfigName.
 											type: DropdownOptionType.ITEM,
 											pinned: true,
 										}))
 										.sort((a, b) => a.label.localeCompare(b.label)),
-									// If we have pinned items and unpinned items, add a separator
+									// If we have pinned items and unpinned items, add a separator.
 									...(pinnedApiConfigs &&
 									Object.keys(pinnedApiConfigs).length > 0 &&
 									(listApiConfigMeta || []).some((config) => !pinnedApiConfigs[config.id])
@@ -1010,13 +1036,13 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 												},
 											]
 										: []),
-									// Unpinned items sorted alphabetically
+									// Unpinned items sorted alphabetically.
 									...(listApiConfigMeta || [])
 										.filter((config) => !pinnedApiConfigs || !pinnedApiConfigs[config.id])
 										.map((config) => ({
 											value: config.id,
 											label: config.name,
-											name: config.name, // Keep name for comparison with currentApiConfigName
+											name: config.name, // Keep name for comparison with currentApiConfigName.
 											type: DropdownOptionType.ITEM,
 											pinned: false,
 										}))
@@ -1093,12 +1119,11 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 						</div>
 					</div>
 
-					{/* Right side - action buttons */}
 					<div className={cn("flex", "items-center", "gap-0.5", "shrink-0")}>
 						<IconButton
 							iconClass={isEnhancingPrompt ? "codicon-loading" : "codicon-sparkle"}
 							title={t("chat:enhancePrompt")}
-							disabled={textAreaDisabled}
+							disabled={sendingDisabled}
 							isLoading={isEnhancingPrompt}
 							onClick={handleEnhancePrompt}
 						/>
@@ -1111,7 +1136,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 						<IconButton
 							iconClass="codicon-send"
 							title={t("chat:sendMessage")}
-							disabled={textAreaDisabled}
+							disabled={sendingDisabled}
 							onClick={onSend}
 						/>
 					</div>

+ 27 - 27
webview-ui/src/components/chat/ChatView.tsx

@@ -112,7 +112,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
 
 	const [inputValue, setInputValue] = useState("")
 	const textAreaRef = useRef<HTMLTextAreaElement>(null)
-	const [textAreaDisabled, setTextAreaDisabled] = useState(false)
+	const [sendingDisabled, setSendingDisabled] = useState(false)
 	const [selectedImages, setSelectedImages] = useState<string[]>([])
 
 	// we need to hold on to the ask because useEffect > lastMessage will always let us know when an ask comes in and handle it, but by the time handleMessage is called, the last message might not be the ask anymore (it could be a say that followed)
@@ -155,7 +155,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
 					switch (lastMessage.ask) {
 						case "api_req_failed":
 							playSound("progress_loop")
-							setTextAreaDisabled(true)
+							setSendingDisabled(true)
 							setClineAsk("api_req_failed")
 							setEnableButtons(true)
 							setPrimaryButtonText(t("chat:retry.title"))
@@ -163,7 +163,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
 							break
 						case "mistake_limit_reached":
 							playSound("progress_loop")
-							setTextAreaDisabled(false)
+							setSendingDisabled(false)
 							setClineAsk("mistake_limit_reached")
 							setEnableButtons(true)
 							setPrimaryButtonText(t("chat:proceedAnyways.title"))
@@ -173,7 +173,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
 							if (!isPartial) {
 								playSound("notification")
 							}
-							setTextAreaDisabled(isPartial)
+							setSendingDisabled(isPartial)
 							setClineAsk("followup")
 							// setting enable buttons to `false` would trigger a focus grab when
 							// the text area is enabled which is undesirable.
@@ -187,7 +187,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
 							if (!isAutoApproved(lastMessage) && !isPartial) {
 								playSound("notification")
 							}
-							setTextAreaDisabled(isPartial)
+							setSendingDisabled(isPartial)
 							setClineAsk("tool")
 							setEnableButtons(!isPartial)
 							const tool = JSON.parse(lastMessage.text || "{}") as ClineSayTool
@@ -213,7 +213,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
 							if (!isAutoApproved(lastMessage) && !isPartial) {
 								playSound("notification")
 							}
-							setTextAreaDisabled(isPartial)
+							setSendingDisabled(isPartial)
 							setClineAsk("browser_action_launch")
 							setEnableButtons(!isPartial)
 							setPrimaryButtonText(t("chat:approve.title"))
@@ -223,14 +223,14 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
 							if (!isAutoApproved(lastMessage) && !isPartial) {
 								playSound("notification")
 							}
-							setTextAreaDisabled(isPartial)
+							setSendingDisabled(isPartial)
 							setClineAsk("command")
 							setEnableButtons(!isPartial)
 							setPrimaryButtonText(t("chat:runCommand.title"))
 							setSecondaryButtonText(t("chat:reject.title"))
 							break
 						case "command_output":
-							setTextAreaDisabled(false)
+							setSendingDisabled(false)
 							setClineAsk("command_output")
 							setEnableButtons(true)
 							setPrimaryButtonText(t("chat:proceedWhileRunning.title"))
@@ -240,7 +240,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
 							if (!isAutoApproved(lastMessage) && !isPartial) {
 								playSound("notification")
 							}
-							setTextAreaDisabled(isPartial)
+							setSendingDisabled(isPartial)
 							setClineAsk("use_mcp_server")
 							setEnableButtons(!isPartial)
 							setPrimaryButtonText(t("chat:approve.title"))
@@ -251,7 +251,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
 							if (!isPartial) {
 								playSound("celebration")
 							}
-							setTextAreaDisabled(isPartial)
+							setSendingDisabled(isPartial)
 							setClineAsk("completion_result")
 							setEnableButtons(!isPartial)
 							setPrimaryButtonText(t("chat:startNewTask.title"))
@@ -261,7 +261,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
 							if (!isAutoApproved(lastMessage) && !isPartial) {
 								playSound("notification")
 							}
-							setTextAreaDisabled(false)
+							setSendingDisabled(false)
 							setClineAsk("resume_task")
 							setEnableButtons(true)
 							setPrimaryButtonText(t("chat:resumeTask.title"))
@@ -272,7 +272,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
 							if (!isPartial) {
 								playSound("celebration")
 							}
-							setTextAreaDisabled(false)
+							setSendingDisabled(false)
 							setClineAsk("resume_completed_task")
 							setEnableButtons(true)
 							setPrimaryButtonText(t("chat:startNewTask.title"))
@@ -286,7 +286,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
 					// an "ask" while ask is waiting for response.
 					switch (lastMessage.say) {
 						case "api_req_retry_delayed":
-							setTextAreaDisabled(true)
+							setSendingDisabled(true)
 							break
 						case "api_req_started":
 							if (secondLastMessage?.ask === "command_output") {
@@ -298,7 +298,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
 								// field or buttons to continue, which does the
 								// following automatically).
 								setInputValue("")
-								setTextAreaDisabled(true)
+								setSendingDisabled(true)
 								setSelectedImages([])
 								setClineAsk(undefined)
 								setEnableButtons(false)
@@ -322,7 +322,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
 
 	useEffect(() => {
 		if (messages.length === 0) {
-			setTextAreaDisabled(false)
+			setSendingDisabled(false)
 			setClineAsk(undefined)
 			setEnableButtons(false)
 			setPrimaryButtonText(undefined)
@@ -376,7 +376,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
 	const handleChatReset = useCallback(() => {
 		// Only reset message-specific state, preserving mode.
 		setInputValue("")
-		setTextAreaDisabled(true)
+		setSendingDisabled(true)
 		setSelectedImages([])
 		setClineAsk(undefined)
 		setEnableButtons(false)
@@ -474,7 +474,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
 					break
 			}
 
-			setTextAreaDisabled(true)
+			setSendingDisabled(true)
 			setClineAsk(undefined)
 			setEnableButtons(false)
 		},
@@ -521,7 +521,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
 					vscode.postMessage({ type: "terminalOperation", terminalOperation: "abort" })
 					break
 			}
-			setTextAreaDisabled(true)
+			setSendingDisabled(true)
 			setClineAsk(undefined)
 			setEnableButtons(false)
 		},
@@ -535,7 +535,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
 	const selectImages = useCallback(() => vscode.postMessage({ type: "selectImages" }), [])
 
 	const shouldDisableImages =
-		!model?.supportsImages || textAreaDisabled || selectedImages.length >= MAX_IMAGES_PER_MESSAGE
+		!model?.supportsImages || sendingDisabled || selectedImages.length >= MAX_IMAGES_PER_MESSAGE
 
 	const handleMessage = useCallback(
 		(e: MessageEvent) => {
@@ -545,7 +545,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
 				case "action":
 					switch (message.action!) {
 						case "didBecomeVisible":
-							if (!isHidden && !textAreaDisabled && !enableButtons) {
+							if (!isHidden && !sendingDisabled && !enableButtons) {
 								textAreaRef.current?.focus()
 							}
 							break
@@ -587,7 +587,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
 		},
 		[
 			isHidden,
-			textAreaDisabled,
+			sendingDisabled,
 			enableButtons,
 			handleChatReset,
 			handleSendMessage,
@@ -604,7 +604,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
 
 	useEffect(() => {
 		const timer = setTimeout(() => {
-			if (!isHidden && !textAreaDisabled && !enableButtons) {
+			if (!isHidden && !sendingDisabled && !enableButtons) {
 				textAreaRef.current?.focus()
 			}
 		}, 50)
@@ -612,7 +612,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
 		return () => {
 			clearTimeout(timer)
 		}
-	}, [isHidden, textAreaDisabled, enableButtons])
+	}, [isHidden, sendingDisabled, enableButtons])
 
 	const visibleMessages = useMemo(() => {
 		return modifiedMessages.filter((message) => {
@@ -1134,7 +1134,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
 				// things are actually needed.
 				setInputValue("")
 				setSelectedImages([])
-				setTextAreaDisabled(true)
+				setSendingDisabled(true)
 				setClineAsk(undefined)
 				setEnableButtons(false)
 			}
@@ -1197,7 +1197,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
 		acceptInput: () => {
 			if (enableButtons && primaryButtonText) {
 				handlePrimaryButtonClick(inputValue, selectedImages)
-			} else if (!textAreaDisabled && (inputValue.trim() || selectedImages.length > 0)) {
+			} else if (!sendingDisabled && (inputValue.trim() || selectedImages.length > 0)) {
 				handleSendMessage(inputValue, selectedImages)
 			}
 		},
@@ -1398,8 +1398,8 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
 				ref={textAreaRef}
 				inputValue={inputValue}
 				setInputValue={setInputValue}
-				textAreaDisabled={textAreaDisabled}
-				selectApiConfigDisabled={textAreaDisabled && clineAsk !== "api_req_failed"}
+				sendingDisabled={sendingDisabled}
+				selectApiConfigDisabled={sendingDisabled && clineAsk !== "api_req_failed"}
 				placeholderText={placeholderText}
 				selectedImages={selectedImages}
 				setSelectedImages={setSelectedImages}

+ 6 - 6
webview-ui/src/components/chat/__tests__/ChatTextArea.test.tsx

@@ -46,7 +46,7 @@ describe("ChatTextArea", () => {
 		inputValue: "",
 		setInputValue: jest.fn(),
 		onSend: jest.fn(),
-		textAreaDisabled: false,
+		sendingDisabled: false,
 		selectApiConfigDisabled: false,
 		onSelectImages: jest.fn(),
 		shouldDisableImages: false,
@@ -72,12 +72,12 @@ describe("ChatTextArea", () => {
 	})
 
 	describe("enhance prompt button", () => {
-		it("should be disabled when textAreaDisabled is true", () => {
+		it("should be disabled when sendingDisabled is true", () => {
 			;(useExtensionState as jest.Mock).mockReturnValue({
 				filePaths: [],
 				openedTabs: [],
 			})
-			render(<ChatTextArea {...defaultProps} textAreaDisabled={true} />)
+			render(<ChatTextArea {...defaultProps} sendingDisabled={true} />)
 			const enhanceButton = getEnhancePromptButton()
 			expect(enhanceButton).toHaveClass("cursor-not-allowed")
 		})
@@ -415,13 +415,13 @@ describe("ChatTextArea", () => {
 		const getApiConfigDropdown = () => {
 			return screen.getByTitle("chat:selectApiConfig")
 		}
-		it("should be enabled independently of textAreaDisabled", () => {
-			render(<ChatTextArea {...defaultProps} textAreaDisabled={true} selectApiConfigDisabled={false} />)
+		it("should be enabled independently of sendingDisabled", () => {
+			render(<ChatTextArea {...defaultProps} sendingDisabled={true} selectApiConfigDisabled={false} />)
 			const apiConfigDropdown = getApiConfigDropdown()
 			expect(apiConfigDropdown).not.toHaveAttribute("disabled")
 		})
 		it("should be disabled when selectApiConfigDisabled is true", () => {
-			render(<ChatTextArea {...defaultProps} textAreaDisabled={true} selectApiConfigDisabled={true} />)
+			render(<ChatTextArea {...defaultProps} sendingDisabled={true} selectApiConfigDisabled={true} />)
 			const apiConfigDropdown = getApiConfigDropdown()
 			expect(apiConfigDropdown).toHaveAttribute("disabled")
 		})

+ 1 - 1
webview-ui/src/components/chat/__tests__/ChatView.test.tsx

@@ -59,7 +59,7 @@ jest.mock("../AutoApproveMenu", () => ({
 interface ChatTextAreaProps {
 	onSend: (value: string) => void
 	inputValue?: string
-	textAreaDisabled?: boolean
+	sendingDisabled?: boolean
 	placeholderText?: string
 	selectedImages?: string[]
 	shouldDisableImages?: boolean