Chris Estreich 7 месяцев назад
Родитель
Сommit
9a358ab0f5

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

@@ -1,16 +1,19 @@
 import * as vscode from "vscode"
 import * as vscode from "vscode"
 import * as path from "path"
 import * as path from "path"
 import * as fs from "fs/promises"
 import * as fs from "fs/promises"
+import * as diff from "diff"
+import stripBom from "strip-bom"
+
 import { createDirectoriesForFile } from "../../utils/fs"
 import { createDirectoriesForFile } from "../../utils/fs"
 import { arePathsEqual } from "../../utils/path"
 import { arePathsEqual } from "../../utils/path"
 import { formatResponse } from "../../core/prompts/responses"
 import { formatResponse } from "../../core/prompts/responses"
-import { DecorationController } from "./DecorationController"
-import * as diff from "diff"
 import { diagnosticsToProblemsString, getNewDiagnostics } from "../diagnostics"
 import { diagnosticsToProblemsString, getNewDiagnostics } from "../diagnostics"
-import stripBom from "strip-bom"
+
+import { DecorationController } from "./DecorationController"
 
 
 export const DIFF_VIEW_URI_SCHEME = "cline-diff"
 export const DIFF_VIEW_URI_SCHEME = "cline-diff"
 
 
+// TODO: https://github.com/cline/cline/pull/3354
 export class DiffViewProvider {
 export class DiffViewProvider {
 	editType?: "create" | "modify"
 	editType?: "create" | "modify"
 	isEditing = false
 	isEditing = false
@@ -32,17 +35,21 @@ export class DiffViewProvider {
 		const fileExists = this.editType === "modify"
 		const fileExists = this.editType === "modify"
 		const absolutePath = path.resolve(this.cwd, relPath)
 		const absolutePath = path.resolve(this.cwd, relPath)
 		this.isEditing = true
 		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) {
 		if (fileExists) {
 			const existingDocument = vscode.workspace.textDocuments.find((doc) =>
 			const existingDocument = vscode.workspace.textDocuments.find((doc) =>
 				arePathsEqual(doc.uri.fsPath, absolutePath),
 				arePathsEqual(doc.uri.fsPath, absolutePath),
 			)
 			)
+
 			if (existingDocument && existingDocument.isDirty) {
 			if (existingDocument && existingDocument.isDirty) {
 				await existingDocument.save()
 				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()
 		this.preDiagnostics = vscode.languages.getDiagnostics()
 
 
 		if (fileExists) {
 		if (fileExists) {
@@ -50,33 +57,41 @@ export class DiffViewProvider {
 		} else {
 		} else {
 			this.originalContent = ""
 			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)
 		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) {
 		if (!fileExists) {
 			await fs.writeFile(absolutePath, "")
 			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
 		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
 		const tabs = vscode.window.tabGroups.all
 			.map((tg) => tg.tabs)
 			.map((tg) => tg.tabs)
 			.flat()
 			.flat()
 			.filter(
 			.filter(
 				(tab) => tab.input instanceof vscode.TabInputText && arePathsEqual(tab.input.uri.fsPath, absolutePath),
 				(tab) => tab.input instanceof vscode.TabInputText && arePathsEqual(tab.input.uri.fsPath, absolutePath),
 			)
 			)
+
 		for (const tab of tabs) {
 		for (const tab of tabs) {
 			if (!tab.isDirty) {
 			if (!tab.isDirty) {
 				await vscode.window.tabGroups.close(tab)
 				await vscode.window.tabGroups.close(tab)
 			}
 			}
 			this.documentWasOpen = true
 			this.documentWasOpen = true
 		}
 		}
+
 		this.activeDiffEditor = await this.openDiffEditor()
 		this.activeDiffEditor = await this.openDiffEditor()
 		this.fadedOverlayController = new DecorationController("fadedOverlay", this.activeDiffEditor)
 		this.fadedOverlayController = new DecorationController("fadedOverlay", this.activeDiffEditor)
 		this.activeLineController = new DecorationController("activeLine", 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.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 = []
 		this.streamedLines = []
 	}
 	}
 
 
@@ -84,58 +99,70 @@ export class DiffViewProvider {
 		if (!this.relPath || !this.activeLineController || !this.fadedOverlayController) {
 		if (!this.relPath || !this.activeLineController || !this.fadedOverlayController) {
 			throw new Error("Required values not set")
 			throw new Error("Required values not set")
 		}
 		}
+
 		this.newContent = accumulatedContent
 		this.newContent = accumulatedContent
 		const accumulatedLines = accumulatedContent.split("\n")
 		const accumulatedLines = accumulatedContent.split("\n")
+
 		if (!isFinal) {
 		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 diffEditor = this.activeDiffEditor
 		const document = diffEditor?.document
 		const document = diffEditor?.document
+
 		if (!diffEditor || !document) {
 		if (!diffEditor || !document) {
 			throw new Error("User closed text editor, unable to edit file...")
 			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)
 		const beginningOfDocument = new vscode.Position(0, 0)
 		diffEditor.selection = new vscode.Selection(beginningOfDocument, beginningOfDocument)
 		diffEditor.selection = new vscode.Selection(beginningOfDocument, beginningOfDocument)
 
 
 		const endLine = accumulatedLines.length
 		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 edit = new vscode.WorkspaceEdit()
 		const rangeToReplace = new vscode.Range(0, 0, endLine + 1, 0)
 		const rangeToReplace = new vscode.Range(0, 0, endLine + 1, 0)
 		const contentToReplace = accumulatedLines.slice(0, endLine + 1).join("\n") + "\n"
 		const contentToReplace = accumulatedLines.slice(0, endLine + 1).join("\n") + "\n"
 		edit.replace(document.uri, rangeToReplace, this.stripAllBOMs(contentToReplace))
 		edit.replace(document.uri, rangeToReplace, this.stripAllBOMs(contentToReplace))
 		await vscode.workspace.applyEdit(edit)
 		await vscode.workspace.applyEdit(edit)
-		// Update decorations
+		// Update decorations.
 		this.activeLineController.setActiveLine(endLine)
 		this.activeLineController.setActiveLine(endLine)
 		this.fadedOverlayController.updateOverlayAfterLine(endLine, document.lineCount)
 		this.fadedOverlayController.updateOverlayAfterLine(endLine, document.lineCount)
-		// Scroll to the current line
+		// Scroll to the current line.
 		this.scrollEditorToLine(endLine)
 		this.scrollEditorToLine(endLine)
 
 
-		// Update the streamedLines with the new accumulated content
+		// Update the streamedLines with the new accumulated content.
 		this.streamedLines = accumulatedLines
 		this.streamedLines = accumulatedLines
+
 		if (isFinal) {
 		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) {
 			if (this.streamedLines.length < document.lineCount) {
 				const edit = new vscode.WorkspaceEdit()
 				const edit = new vscode.WorkspaceEdit()
 				edit.delete(document.uri, new vscode.Range(this.streamedLines.length, 0, document.lineCount, 0))
 				edit.delete(document.uri, new vscode.Range(this.streamedLines.length, 0, document.lineCount, 0))
 				await vscode.workspace.applyEdit(edit)
 				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")
 			const hasEmptyLastLine = this.originalContent?.endsWith("\n")
+
 			if (hasEmptyLastLine && !accumulatedContent.endsWith("\n")) {
 			if (hasEmptyLastLine && !accumulatedContent.endsWith("\n")) {
 				accumulatedContent += "\n"
 				accumulatedContent += "\n"
 			}
 			}
-			// Apply the final content
+
+			// Apply the final content.
 			const finalEdit = new vscode.WorkspaceEdit()
 			const finalEdit = new vscode.WorkspaceEdit()
+
 			finalEdit.replace(
 			finalEdit.replace(
 				document.uri,
 				document.uri,
 				new vscode.Range(0, 0, document.lineCount, 0),
 				new vscode.Range(0, 0, document.lineCount, 0),
 				this.stripAllBOMs(accumulatedContent),
 				this.stripAllBOMs(accumulatedContent),
 			)
 			)
+
 			await vscode.workspace.applyEdit(finalEdit)
 			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.fadedOverlayController.clear()
 			this.activeLineController.clear()
 			this.activeLineController.clear()
 		}
 		}
@@ -149,59 +176,68 @@ export class DiffViewProvider {
 		if (!this.relPath || !this.newContent || !this.activeDiffEditor) {
 		if (!this.relPath || !this.newContent || !this.activeDiffEditor) {
 			return { newProblemsMessage: undefined, userEdits: undefined, finalContent: undefined }
 			return { newProblemsMessage: undefined, userEdits: undefined, finalContent: undefined }
 		}
 		}
+
 		const absolutePath = path.resolve(this.cwd, this.relPath)
 		const absolutePath = path.resolve(this.cwd, this.relPath)
 		const updatedDocument = this.activeDiffEditor.document
 		const updatedDocument = this.activeDiffEditor.document
 		const editedContent = updatedDocument.getText()
 		const editedContent = updatedDocument.getText()
+
 		if (updatedDocument.isDirty) {
 		if (updatedDocument.isDirty) {
 			await updatedDocument.save()
 			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()
 		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 postDiagnostics = vscode.languages.getDiagnostics()
+
 		const newProblems = await diagnosticsToProblemsString(
 		const newProblems = await diagnosticsToProblemsString(
 			getNewDiagnostics(this.preDiagnostics, postDiagnostics),
 			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)
 				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,
 			this.cwd,
-		) // will be empty string if no errors
+		) // Will be empty string if no errors.
+
 		const newProblemsMessage =
 		const newProblemsMessage =
 			newProblems.length > 0 ? `\n\nNew problems detected after saving the file:\n${newProblems}` : ""
 			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 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
 		const normalizedNewContent = this.newContent.replace(/\r\n|\n/g, newContentEOL).trimEnd() + newContentEOL
+
 		if (normalizedEditedContent !== normalizedNewContent) {
 		if (normalizedEditedContent !== normalizedNewContent) {
-			// user made changes before approving edit
+			// User made changes before approving edit.
 			const userEdits = formatResponse.createPrettyPatch(
 			const userEdits = formatResponse.createPrettyPatch(
 				this.relPath.toPosix(),
 				this.relPath.toPosix(),
 				normalizedNewContent,
 				normalizedNewContent,
 				normalizedEditedContent,
 				normalizedEditedContent,
 			)
 			)
+
 			return { newProblemsMessage, userEdits, finalContent: normalizedEditedContent }
 			return { newProblemsMessage, userEdits, finalContent: normalizedEditedContent }
 		} else {
 		} else {
-			// no changes to cline's edits
+			// No changes to Roo's edits.
 			return { newProblemsMessage, userEdits: undefined, finalContent: normalizedEditedContent }
 			return { newProblemsMessage, userEdits: undefined, finalContent: normalizedEditedContent }
 		}
 		}
 	}
 	}
@@ -210,42 +246,55 @@ export class DiffViewProvider {
 		if (!this.relPath || !this.activeDiffEditor) {
 		if (!this.relPath || !this.activeDiffEditor) {
 			return
 			return
 		}
 		}
+
 		const fileExists = this.editType === "modify"
 		const fileExists = this.editType === "modify"
 		const updatedDocument = this.activeDiffEditor.document
 		const updatedDocument = this.activeDiffEditor.document
 		const absolutePath = path.resolve(this.cwd, this.relPath)
 		const absolutePath = path.resolve(this.cwd, this.relPath)
+
 		if (!fileExists) {
 		if (!fileExists) {
 			if (updatedDocument.isDirty) {
 			if (updatedDocument.isDirty) {
 				await updatedDocument.save()
 				await updatedDocument.save()
 			}
 			}
+
 			await this.closeAllDiffViews()
 			await this.closeAllDiffViews()
 			await fs.unlink(absolutePath)
 			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--) {
 			for (let i = this.createdDirs.length - 1; i >= 0; i--) {
 				await fs.rmdir(this.createdDirs[i])
 				await fs.rmdir(this.createdDirs[i])
 				console.log(`Directory ${this.createdDirs[i]} has been deleted.`)
 				console.log(`Directory ${this.createdDirs[i]} has been deleted.`)
 			}
 			}
+
 			console.log(`File ${absolutePath} has been deleted.`)
 			console.log(`File ${absolutePath} has been deleted.`)
 		} else {
 		} else {
-			// revert document
+			// Revert document.
 			const edit = new vscode.WorkspaceEdit()
 			const edit = new vscode.WorkspaceEdit()
+
 			const fullRange = new vscode.Range(
 			const fullRange = new vscode.Range(
 				updatedDocument.positionAt(0),
 				updatedDocument.positionAt(0),
 				updatedDocument.positionAt(updatedDocument.getText().length),
 				updatedDocument.positionAt(updatedDocument.getText().length),
 			)
 			)
+
 			edit.replace(updatedDocument.uri, fullRange, this.originalContent ?? "")
 			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 vscode.workspace.applyEdit(edit)
 			await updatedDocument.save()
 			await updatedDocument.save()
 			console.log(`File ${absolutePath} has been reverted to its original content.`)
 			console.log(`File ${absolutePath} has been reverted to its original content.`)
+
 			if (this.documentWasOpen) {
 			if (this.documentWasOpen) {
 				await vscode.window.showTextDocument(vscode.Uri.file(absolutePath), {
 				await vscode.window.showTextDocument(vscode.Uri.file(absolutePath), {
 					preview: false,
 					preview: false,
+					preserveFocus: true,
 				})
 				})
 			}
 			}
+
 			await this.closeAllDiffViews()
 			await this.closeAllDiffViews()
 		}
 		}
 
 
-		// edit is done
+		// Edit is done.
 		await this.reset()
 		await this.reset()
 	}
 	}
 
 
@@ -257,8 +306,9 @@ export class DiffViewProvider {
 					tab.input instanceof vscode.TabInputTextDiff &&
 					tab.input instanceof vscode.TabInputTextDiff &&
 					tab.input?.original?.scheme === DIFF_VIEW_URI_SCHEME,
 					tab.input?.original?.scheme === DIFF_VIEW_URI_SCHEME,
 			)
 			)
+
 		for (const tab of tabs) {
 		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) {
 			if (!tab.isDirty) {
 				await vscode.window.tabGroups.close(tab)
 				await vscode.window.tabGroups.close(tab)
 			}
 			}
@@ -269,8 +319,12 @@ export class DiffViewProvider {
 		if (!this.relPath) {
 		if (!this.relPath) {
 			throw new Error("No file path set")
 			throw new Error("No file path set")
 		}
 		}
+
 		const uri = vscode.Uri.file(path.resolve(this.cwd, this.relPath))
 		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
 		const diffTab = vscode.window.tabGroups.all
 			.flatMap((group) => group.tabs)
 			.flatMap((group) => group.tabs)
 			.find(
 			.find(
@@ -279,20 +333,24 @@ export class DiffViewProvider {
 					tab.input?.original?.scheme === DIFF_VIEW_URI_SCHEME &&
 					tab.input?.original?.scheme === DIFF_VIEW_URI_SCHEME &&
 					arePathsEqual(tab.input.modified.fsPath, uri.fsPath),
 					arePathsEqual(tab.input.modified.fsPath, uri.fsPath),
 			)
 			)
+
 		if (diffTab && diffTab.input instanceof vscode.TabInputTextDiff) {
 		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
 			return editor
 		}
 		}
-		// Open new diff editor
+
+		// Open new diff editor.
 		return new Promise<vscode.TextEditor>((resolve, reject) => {
 		return new Promise<vscode.TextEditor>((resolve, reject) => {
 			const fileName = path.basename(uri.fsPath)
 			const fileName = path.basename(uri.fsPath)
 			const fileExists = this.editType === "modify"
 			const fileExists = this.editType === "modify"
+
 			const disposable = vscode.window.onDidChangeActiveTextEditor((editor) => {
 			const disposable = vscode.window.onDidChangeActiveTextEditor((editor) => {
 				if (editor && arePathsEqual(editor.document.uri.fsPath, uri.fsPath)) {
 				if (editor && arePathsEqual(editor.document.uri.fsPath, uri.fsPath)) {
 					disposable.dispose()
 					disposable.dispose()
 					resolve(editor)
 					resolve(editor)
 				}
 				}
 			})
 			})
+
 			vscode.commands.executeCommand(
 			vscode.commands.executeCommand(
 				"vscode.diff",
 				"vscode.diff",
 				vscode.Uri.parse(`${DIFF_VIEW_URI_SCHEME}:${fileName}`).with({
 				vscode.Uri.parse(`${DIFF_VIEW_URI_SCHEME}:${fileName}`).with({
@@ -300,8 +358,10 @@ export class DiffViewProvider {
 				}),
 				}),
 				uri,
 				uri,
 				`${fileName}: ${fileExists ? "Original ↔ Roo's Changes" : "New File"} (Editable)`,
 				`${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(() => {
 			setTimeout(() => {
 				disposable.dispose()
 				disposable.dispose()
 				reject(new Error("Failed to open diff editor, please try again..."))
 				reject(new Error("Failed to open diff editor, please try again..."))
@@ -312,6 +372,7 @@ export class DiffViewProvider {
 	private scrollEditorToLine(line: number) {
 	private scrollEditorToLine(line: number) {
 		if (this.activeDiffEditor) {
 		if (this.activeDiffEditor) {
 			const scrollLine = line + 4
 			const scrollLine = line + 4
+
 			this.activeDiffEditor.revealRange(
 			this.activeDiffEditor.revealRange(
 				new vscode.Range(scrollLine, 0, scrollLine, 0),
 				new vscode.Range(scrollLine, 0, scrollLine, 0),
 				vscode.TextEditorRevealType.InCenter,
 				vscode.TextEditorRevealType.InCenter,
@@ -323,18 +384,23 @@ export class DiffViewProvider {
 		if (!this.activeDiffEditor) {
 		if (!this.activeDiffEditor) {
 			return
 			return
 		}
 		}
+
 		const currentContent = this.activeDiffEditor.document.getText()
 		const currentContent = this.activeDiffEditor.document.getText()
 		const diffs = diff.diffLines(this.originalContent || "", currentContent)
 		const diffs = diff.diffLines(this.originalContent || "", currentContent)
+
 		let lineCount = 0
 		let lineCount = 0
+
 		for (const part of diffs) {
 		for (const part of diffs) {
 			if (part.added || part.removed) {
 			if (part.added || part.removed) {
-				// Found the first diff, scroll to it
+				// Found the first diff, scroll to it.
 				this.activeDiffEditor.revealRange(
 				this.activeDiffEditor.revealRange(
 					new vscode.Range(lineCount, 0, lineCount, 0),
 					new vscode.Range(lineCount, 0, lineCount, 0),
 					vscode.TextEditorRevealType.InCenter,
 					vscode.TextEditorRevealType.InCenter,
 				)
 				)
+
 				return
 				return
 			}
 			}
+
 			if (!part.removed) {
 			if (!part.removed) {
 				lineCount += part.count || 0
 				lineCount += part.count || 0
 			}
 			}
@@ -344,17 +410,24 @@ export class DiffViewProvider {
 	private stripAllBOMs(input: string): string {
 	private stripAllBOMs(input: string): string {
 		let result = input
 		let result = input
 		let previous
 		let previous
+
 		do {
 		do {
 			previous = result
 			previous = result
 			result = stripBom(result)
 			result = stripBom(result)
 		} while (result !== previous)
 		} while (result !== previous)
+
 		return result
 		return result
 	}
 	}
 
 
-	// close editor if open?
 	async reset() {
 	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.editType = undefined
 		this.isEditing = false
 		this.isEditing = false
 		this.originalContent = undefined
 		this.originalContent = undefined

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

@@ -31,7 +31,7 @@ import { cn } from "@/lib/utils"
 interface ChatTextAreaProps {
 interface ChatTextAreaProps {
 	inputValue: string
 	inputValue: string
 	setInputValue: (value: string) => void
 	setInputValue: (value: string) => void
-	textAreaDisabled: boolean
+	sendingDisabled: boolean
 	selectApiConfigDisabled: boolean
 	selectApiConfigDisabled: boolean
 	placeholderText: string
 	placeholderText: string
 	selectedImages: string[]
 	selectedImages: string[]
@@ -50,7 +50,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 		{
 		{
 			inputValue,
 			inputValue,
 			setInputValue,
 			setInputValue,
-			textAreaDisabled,
+			sendingDisabled,
 			selectApiConfigDisabled,
 			selectApiConfigDisabled,
 			placeholderText,
 			placeholderText,
 			selectedImages,
 			selectedImages,
@@ -165,21 +165,19 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 		}, [selectedType, searchQuery])
 		}, [selectedType, searchQuery])
 
 
 		const handleEnhancePrompt = useCallback(() => {
 		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(() => {
 		const queryItems = useMemo(() => {
 			return [
 			return [
@@ -361,9 +359,13 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 				}
 				}
 
 
 				const isComposing = event.nativeEvent?.isComposing ?? false
 				const isComposing = event.nativeEvent?.isComposing ?? false
+
 				if (event.key === "Enter" && !event.shiftKey && !isComposing) {
 				if (event.key === "Enter" && !event.shiftKey && !isComposing) {
 					event.preventDefault()
 					event.preventDefault()
-					onSend()
+
+					if (!sendingDisabled) {
+						onSend()
+					}
 				}
 				}
 
 
 				if (event.key === "Backspace" && !isComposing) {
 				if (event.key === "Backspace" && !isComposing) {
@@ -372,29 +374,37 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 
 
 					const charBeforeIsWhitespace =
 					const charBeforeIsWhitespace =
 						charBeforeCursor === " " || charBeforeCursor === "\n" || charBeforeCursor === "\r\n"
 						charBeforeCursor === " " || charBeforeCursor === "\n" || charBeforeCursor === "\r\n"
+
 					const charAfterIsWhitespace =
 					const charAfterIsWhitespace =
 						charAfterCursor === " " || charAfterCursor === "\n" || charAfterCursor === "\r\n"
 						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 (
 					if (
 						charBeforeIsWhitespace &&
 						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
 						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) {
 						if (!charAfterIsWhitespace) {
 							event.preventDefault()
 							event.preventDefault()
 							textAreaRef.current?.setSelectionRange(newCursorPosition, newCursorPosition)
 							textAreaRef.current?.setSelectionRange(newCursorPosition, newCursorPosition)
 							setCursorPosition(newCursorPosition)
 							setCursorPosition(newCursorPosition)
 						}
 						}
+
 						setCursorPosition(newCursorPosition)
 						setCursorPosition(newCursorPosition)
 						setJustDeletedSpaceAfterMention(true)
 						setJustDeletedSpaceAfterMention(true)
 					} else if (justDeletedSpaceAfterMention) {
 					} else if (justDeletedSpaceAfterMention) {
 						const { newText, newPosition } = removeMention(inputValue, cursorPosition)
 						const { newText, newPosition } = removeMention(inputValue, cursorPosition)
+
 						if (newText !== inputValue) {
 						if (newText !== inputValue) {
 							event.preventDefault()
 							event.preventDefault()
 							setInputValue(newText)
 							setInputValue(newText)
 							setIntendedCursorPosition(newPosition) // Store the new cursor position in state
 							setIntendedCursorPosition(newPosition) // Store the new cursor position in state
 						}
 						}
+
 						setJustDeletedSpaceAfterMention(false)
 						setJustDeletedSpaceAfterMention(false)
 						setShowContextMenu(false)
 						setShowContextMenu(false)
 					} else {
 					} else {
@@ -403,6 +413,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 				}
 				}
 			},
 			},
 			[
 			[
+				sendingDisabled,
 				onSend,
 				onSend,
 				showContextMenu,
 				showContextMenu,
 				searchQuery,
 				searchQuery,
@@ -425,63 +436,67 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 				setIntendedCursorPosition(null) // Reset the state.
 				setIntendedCursorPosition(null) // Reset the state.
 			}
 			}
 		}, [inputValue, intendedCursorPosition])
 		}, [inputValue, intendedCursorPosition])
-		// Ref to store the search timeout
+
+		// Ref to store the search timeout.
 		const searchTimeoutRef = useRef<NodeJS.Timeout | null>(null)
 		const searchTimeoutRef = useRef<NodeJS.Timeout | null>(null)
 
 
 		const handleInputChange = useCallback(
 		const handleInputChange = useCallback(
 			(e: React.ChangeEvent<HTMLTextAreaElement>) => {
 			(e: React.ChangeEvent<HTMLTextAreaElement>) => {
 				const newValue = e.target.value
 				const newValue = e.target.value
-				const newCursorPosition = e.target.selectionStart
 				setInputValue(newValue)
 				setInputValue(newValue)
+
+				const newCursorPosition = e.target.selectionStart
 				setCursorPosition(newCursorPosition)
 				setCursorPosition(newCursorPosition)
-				const showMenu = shouldShowContextMenu(newValue, newCursorPosition)
 
 
+				const showMenu = shouldShowContextMenu(newValue, newCursorPosition)
 				setShowContextMenu(showMenu)
 				setShowContextMenu(showMenu)
+
 				if (showMenu) {
 				if (showMenu) {
 					if (newValue.startsWith("/")) {
 					if (newValue.startsWith("/")) {
-						// Handle slash command
+						// Handle slash command.
 						const query = newValue
 						const query = newValue
 						setSearchQuery(query)
 						setSearchQuery(query)
 						setSelectedMenuIndex(0)
 						setSelectedMenuIndex(0)
 					} else {
 					} else {
-						// Existing @ mention handling
+						// Existing @ mention handling.
 						const lastAtIndex = newValue.lastIndexOf("@", newCursorPosition - 1)
 						const lastAtIndex = newValue.lastIndexOf("@", newCursorPosition - 1)
 						const query = newValue.slice(lastAtIndex + 1, newCursorPosition)
 						const query = newValue.slice(lastAtIndex + 1, newCursorPosition)
 						setSearchQuery(query)
 						setSearchQuery(query)
 
 
-						// Send file search request if query is not empty
+						// Send file search request if query is not empty.
 						if (query.length > 0) {
 						if (query.length > 0) {
 							setSelectedMenuIndex(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) {
 							if (searchTimeoutRef.current) {
 								clearTimeout(searchTimeoutRef.current)
 								clearTimeout(searchTimeoutRef.current)
 							}
 							}
 
 
-							// Set a timeout to debounce the search requests
+							// Set a timeout to debounce the search requests.
 							searchTimeoutRef.current = setTimeout(() => {
 							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)
 								const reqId = Math.random().toString(36).substring(2, 9)
 								setSearchRequestId(reqId)
 								setSearchRequestId(reqId)
 								setSearchLoading(true)
 								setSearchLoading(true)
 
 
-								// Send message to extension to search files
+								// Send message to extension to search files.
 								vscode.postMessage({
 								vscode.postMessage({
 									type: "searchFiles",
 									type: "searchFiles",
 									query: unescapeSpaces(query),
 									query: unescapeSpaces(query),
 									requestId: reqId,
 									requestId: reqId,
 								})
 								})
-							}, 200) // 200ms debounce
+							}, 200) // 200ms debounce.
 						} else {
 						} else {
-							setSelectedMenuIndex(3) // Set to "File" option by default
+							setSelectedMenuIndex(3) // Set to "File" option by default.
 						}
 						}
 					}
 					}
 				} else {
 				} else {
 					setSearchQuery("")
 					setSearchQuery("")
 					setSelectedMenuIndex(-1)
 					setSelectedMenuIndex(-1)
-					setFileSearchResults([]) // Clear file search results
+					setFileSearchResults([]) // Clear file search results.
 				}
 				}
 			},
 			},
 			[setInputValue, setSearchRequestId, setFileSearchResults, setSearchLoading],
 			[setInputValue, setSearchRequestId, setFileSearchResults, setSearchLoading],
@@ -541,14 +556,18 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 
 
 				if (!shouldDisableImages && imageItems.length > 0) {
 				if (!shouldDisableImages && imageItems.length > 0) {
 					e.preventDefault()
 					e.preventDefault()
+
 					const imagePromises = imageItems.map((item) => {
 					const imagePromises = imageItems.map((item) => {
 						return new Promise<string | null>((resolve) => {
 						return new Promise<string | null>((resolve) => {
 							const blob = item.getAsFile()
 							const blob = item.getAsFile()
+
 							if (!blob) {
 							if (!blob) {
 								resolve(null)
 								resolve(null)
 								return
 								return
 							}
 							}
+
 							const reader = new FileReader()
 							const reader = new FileReader()
+
 							reader.onloadend = () => {
 							reader.onloadend = () => {
 								if (reader.error) {
 								if (reader.error) {
 									console.error(t("chat:errorReadingFile"), reader.error)
 									console.error(t("chat:errorReadingFile"), reader.error)
@@ -558,11 +577,14 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 									resolve(typeof result === "string" ? result : null)
 									resolve(typeof result === "string" ? result : null)
 								}
 								}
 							}
 							}
+
 							reader.readAsDataURL(blob)
 							reader.readAsDataURL(blob)
 						})
 						})
 					})
 					})
+
 					const imageDataArray = await Promise.all(imagePromises)
 					const imageDataArray = await Promise.all(imagePromises)
 					const dataUrls = imageDataArray.filter((dataUrl): dataUrl is string => dataUrl !== null)
 					const dataUrls = imageDataArray.filter((dataUrl): dataUrl is string => dataUrl !== null)
+
 					if (dataUrls.length > 0) {
 					if (dataUrls.length > 0) {
 						setSelectedImages((prevImages) => [...prevImages, ...dataUrls].slice(0, MAX_IMAGES_PER_MESSAGE))
 						setSelectedImages((prevImages) => [...prevImages, ...dataUrls].slice(0, MAX_IMAGES_PER_MESSAGE))
 					} else {
 					} else {
@@ -657,8 +679,10 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 				}
 				}
 
 
 				const files = Array.from(e.dataTransfer.files)
 				const files = Array.from(e.dataTransfer.files)
-				if (!textAreaDisabled && files.length > 0) {
+
+				if (files.length > 0) {
 					const acceptedTypes = ["png", "jpeg", "webp"]
 					const acceptedTypes = ["png", "jpeg", "webp"]
+
 					const imageFiles = files.filter((file) => {
 					const imageFiles = files.filter((file) => {
 						const [type, subtype] = file.type.split("/")
 						const [type, subtype] = file.type.split("/")
 						return type === "image" && acceptedTypes.includes(subtype)
 						return type === "image" && acceptedTypes.includes(subtype)
@@ -668,6 +692,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 						const imagePromises = imageFiles.map((file) => {
 						const imagePromises = imageFiles.map((file) => {
 							return new Promise<string | null>((resolve) => {
 							return new Promise<string | null>((resolve) => {
 								const reader = new FileReader()
 								const reader = new FileReader()
+
 								reader.onloadend = () => {
 								reader.onloadend = () => {
 									if (reader.error) {
 									if (reader.error) {
 										console.error(t("chat:errorReadingFile"), reader.error)
 										console.error(t("chat:errorReadingFile"), reader.error)
@@ -677,20 +702,21 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 										resolve(typeof result === "string" ? result : null)
 										resolve(typeof result === "string" ? result : null)
 									}
 									}
 								}
 								}
+
 								reader.readAsDataURL(file)
 								reader.readAsDataURL(file)
 							})
 							})
 						})
 						})
+
 						const imageDataArray = await Promise.all(imagePromises)
 						const imageDataArray = await Promise.all(imagePromises)
 						const dataUrls = imageDataArray.filter((dataUrl): dataUrl is string => dataUrl !== null)
 						const dataUrls = imageDataArray.filter((dataUrl): dataUrl is string => dataUrl !== null)
+
 						if (dataUrls.length > 0) {
 						if (dataUrls.length > 0) {
 							setSelectedImages((prevImages) =>
 							setSelectedImages((prevImages) =>
 								[...prevImages, ...dataUrls].slice(0, MAX_IMAGES_PER_MESSAGE),
 								[...prevImages, ...dataUrls].slice(0, MAX_IMAGES_PER_MESSAGE),
 							)
 							)
+
 							if (typeof vscode !== "undefined") {
 							if (typeof vscode !== "undefined") {
-								vscode.postMessage({
-									type: "draggedImages",
-									dataUrls: dataUrls,
-								})
+								vscode.postMessage({ type: "draggedImages", dataUrls: dataUrls })
 							}
 							}
 						} else {
 						} else {
 							console.warn(t("chat:noValidImages"))
 							console.warn(t("chat:noValidImages"))
@@ -705,7 +731,6 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 				setInputValue,
 				setInputValue,
 				setCursorPosition,
 				setCursorPosition,
 				setIntendedCursorPosition,
 				setIntendedCursorPosition,
-				textAreaDisabled,
 				shouldDisableImages,
 				shouldDisableImages,
 				setSelectedImages,
 				setSelectedImages,
 				t,
 				t,
@@ -749,11 +774,12 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 						className={cn("chat-text-area", "relative", "flex", "flex-col", "outline-none")}
 						className={cn("chat-text-area", "relative", "flex", "flex-col", "outline-none")}
 						onDrop={handleDrop}
 						onDrop={handleDrop}
 						onDragOver={(e) => {
 						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) {
 							if (!e.shiftKey) {
 								setIsDraggingOver(false)
 								setIsDraggingOver(false)
 								return
 								return
 							}
 							}
+
 							e.preventDefault()
 							e.preventDefault()
 							setIsDraggingOver(true)
 							setIsDraggingOver(true)
 							e.dataTransfer.dropEffect = "copy"
 							e.dataTransfer.dropEffect = "copy"
@@ -761,6 +787,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 						onDragLeave={(e) => {
 						onDragLeave={(e) => {
 							e.preventDefault()
 							e.preventDefault()
 							const rect = e.currentTarget.getBoundingClientRect()
 							const rect = e.currentTarget.getBoundingClientRect()
+
 							if (
 							if (
 								e.clientX <= rect.left ||
 								e.clientX <= rect.left ||
 								e.clientX >= rect.right ||
 								e.clientX >= rect.right ||
@@ -840,7 +867,6 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 									textAreaRef.current = el
 									textAreaRef.current = el
 								}}
 								}}
 								value={inputValue}
 								value={inputValue}
-								disabled={textAreaDisabled}
 								onChange={(e) => {
 								onChange={(e) => {
 									handleInputChange(e)
 									handleInputChange(e)
 									updateHighlights()
 									updateHighlights()
@@ -856,6 +882,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 									if (textAreaBaseHeight === undefined || height < textAreaBaseHeight) {
 									if (textAreaBaseHeight === undefined || height < textAreaBaseHeight) {
 										setTextAreaBaseHeight(height)
 										setTextAreaBaseHeight(height)
 									}
 									}
+
 									onHeightChange?.(height)
 									onHeightChange?.(height)
 								}}
 								}}
 								placeholder={placeholderText}
 								placeholder={placeholderText}
@@ -868,14 +895,13 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 									"font-vscode-font-family",
 									"font-vscode-font-family",
 									"text-vscode-editor-font-size",
 									"text-vscode-editor-font-size",
 									"leading-vscode-editor-line-height",
 									"leading-vscode-editor-line-height",
-									textAreaDisabled ? "cursor-not-allowed" : "cursor-text",
+									"cursor-text",
 									"py-1.5 px-2",
 									"py-1.5 px-2",
 									isFocused
 									isFocused
 										? "border border-vscode-focusBorder outline outline-vscode-focusBorder"
 										? "border border-vscode-focusBorder outline outline-vscode-focusBorder"
 										: isDraggingOver
 										: isDraggingOver
 											? "border-2 border-dashed border-vscode-focusBorder"
 											? "border-2 border-dashed border-vscode-focusBorder"
 											: "border border-transparent",
 											: "border border-transparent",
-									textAreaDisabled ? "opacity-50" : "opacity-100",
 									isDraggingOver
 									isDraggingOver
 										? "bg-[color-mix(in_srgb,var(--vscode-input-background)_95%,var(--vscode-focusBorder))]"
 										? "bg-[color-mix(in_srgb,var(--vscode-input-background)_95%,var(--vscode-focusBorder))]"
 										: "bg-vscode-input-background",
 										: "bg-vscode-input-background",
@@ -894,6 +920,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 								)}
 								)}
 								onScroll={() => updateHighlights()}
 								onScroll={() => updateHighlights()}
 							/>
 							/>
+
 							{isTtsPlaying && (
 							{isTtsPlaying && (
 								<Button
 								<Button
 									variant="ghost"
 									variant="ghost"
@@ -903,6 +930,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 									<VolumeX className="size-4" />
 									<VolumeX className="size-4" />
 								</Button>
 								</Button>
 							)}
 							)}
+
 							{!inputValue && (
 							{!inputValue && (
 								<div
 								<div
 									className={cn(
 									className={cn(
@@ -919,7 +947,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 										"transition-opacity",
 										"transition-opacity",
 										"duration-200",
 										"duration-200",
 										"ease-in-out",
 										"ease-in-out",
-										textAreaDisabled ? "opacity-35" : "opacity-70",
+										"opacity-70",
 									)}>
 									)}>
 									{placeholderBottomText}
 									{placeholderBottomText}
 								</div>
 								</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", "justify-between", "items-center", "mt-auto", "pt-0.5")}>
 					<div className={cn("flex", "items-center", "gap-1", "min-w-0")}>
 					<div className={cn("flex", "items-center", "gap-1", "min-w-0")}>
-						{/* Mode selector - fixed width */}
 						<div className="shrink-0">
 						<div className="shrink-0">
 							<SelectDropdown
 							<SelectDropdown
 								value={mode}
 								value={mode}
@@ -979,26 +1006,25 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 							/>
 							/>
 						</div>
 						</div>
 
 
-						{/* API configuration selector - flexible width */}
 						<div className={cn("flex-1", "min-w-0", "overflow-hidden")}>
 						<div className={cn("flex-1", "min-w-0", "overflow-hidden")}>
 							<SelectDropdown
 							<SelectDropdown
 								value={currentConfigId}
 								value={currentConfigId}
 								disabled={selectApiConfigDisabled}
 								disabled={selectApiConfigDisabled}
 								title={t("chat:selectApiConfig")}
 								title={t("chat:selectApiConfig")}
-								placeholder={displayName} // Always show the current name
+								placeholder={displayName}
 								options={[
 								options={[
-									// Pinned items first
+									// Pinned items first.
 									...(listApiConfigMeta || [])
 									...(listApiConfigMeta || [])
 										.filter((config) => pinnedApiConfigs && pinnedApiConfigs[config.id])
 										.filter((config) => pinnedApiConfigs && pinnedApiConfigs[config.id])
 										.map((config) => ({
 										.map((config) => ({
 											value: config.id,
 											value: config.id,
 											label: config.name,
 											label: config.name,
-											name: config.name, // Keep name for comparison with currentApiConfigName
+											name: config.name, // Keep name for comparison with currentApiConfigName.
 											type: DropdownOptionType.ITEM,
 											type: DropdownOptionType.ITEM,
 											pinned: true,
 											pinned: true,
 										}))
 										}))
 										.sort((a, b) => a.label.localeCompare(b.label)),
 										.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 &&
 									...(pinnedApiConfigs &&
 									Object.keys(pinnedApiConfigs).length > 0 &&
 									Object.keys(pinnedApiConfigs).length > 0 &&
 									(listApiConfigMeta || []).some((config) => !pinnedApiConfigs[config.id])
 									(listApiConfigMeta || []).some((config) => !pinnedApiConfigs[config.id])
@@ -1010,13 +1036,13 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 												},
 												},
 											]
 											]
 										: []),
 										: []),
-									// Unpinned items sorted alphabetically
+									// Unpinned items sorted alphabetically.
 									...(listApiConfigMeta || [])
 									...(listApiConfigMeta || [])
 										.filter((config) => !pinnedApiConfigs || !pinnedApiConfigs[config.id])
 										.filter((config) => !pinnedApiConfigs || !pinnedApiConfigs[config.id])
 										.map((config) => ({
 										.map((config) => ({
 											value: config.id,
 											value: config.id,
 											label: config.name,
 											label: config.name,
-											name: config.name, // Keep name for comparison with currentApiConfigName
+											name: config.name, // Keep name for comparison with currentApiConfigName.
 											type: DropdownOptionType.ITEM,
 											type: DropdownOptionType.ITEM,
 											pinned: false,
 											pinned: false,
 										}))
 										}))
@@ -1093,12 +1119,11 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 						</div>
 						</div>
 					</div>
 					</div>
 
 
-					{/* Right side - action buttons */}
 					<div className={cn("flex", "items-center", "gap-0.5", "shrink-0")}>
 					<div className={cn("flex", "items-center", "gap-0.5", "shrink-0")}>
 						<IconButton
 						<IconButton
 							iconClass={isEnhancingPrompt ? "codicon-loading" : "codicon-sparkle"}
 							iconClass={isEnhancingPrompt ? "codicon-loading" : "codicon-sparkle"}
 							title={t("chat:enhancePrompt")}
 							title={t("chat:enhancePrompt")}
-							disabled={textAreaDisabled}
+							disabled={sendingDisabled}
 							isLoading={isEnhancingPrompt}
 							isLoading={isEnhancingPrompt}
 							onClick={handleEnhancePrompt}
 							onClick={handleEnhancePrompt}
 						/>
 						/>
@@ -1111,7 +1136,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 						<IconButton
 						<IconButton
 							iconClass="codicon-send"
 							iconClass="codicon-send"
 							title={t("chat:sendMessage")}
 							title={t("chat:sendMessage")}
-							disabled={textAreaDisabled}
+							disabled={sendingDisabled}
 							onClick={onSend}
 							onClick={onSend}
 						/>
 						/>
 					</div>
 					</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 [inputValue, setInputValue] = useState("")
 	const textAreaRef = useRef<HTMLTextAreaElement>(null)
 	const textAreaRef = useRef<HTMLTextAreaElement>(null)
-	const [textAreaDisabled, setTextAreaDisabled] = useState(false)
+	const [sendingDisabled, setSendingDisabled] = useState(false)
 	const [selectedImages, setSelectedImages] = useState<string[]>([])
 	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)
 	// 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) {
 					switch (lastMessage.ask) {
 						case "api_req_failed":
 						case "api_req_failed":
 							playSound("progress_loop")
 							playSound("progress_loop")
-							setTextAreaDisabled(true)
+							setSendingDisabled(true)
 							setClineAsk("api_req_failed")
 							setClineAsk("api_req_failed")
 							setEnableButtons(true)
 							setEnableButtons(true)
 							setPrimaryButtonText(t("chat:retry.title"))
 							setPrimaryButtonText(t("chat:retry.title"))
@@ -163,7 +163,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
 							break
 							break
 						case "mistake_limit_reached":
 						case "mistake_limit_reached":
 							playSound("progress_loop")
 							playSound("progress_loop")
-							setTextAreaDisabled(false)
+							setSendingDisabled(false)
 							setClineAsk("mistake_limit_reached")
 							setClineAsk("mistake_limit_reached")
 							setEnableButtons(true)
 							setEnableButtons(true)
 							setPrimaryButtonText(t("chat:proceedAnyways.title"))
 							setPrimaryButtonText(t("chat:proceedAnyways.title"))
@@ -173,7 +173,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
 							if (!isPartial) {
 							if (!isPartial) {
 								playSound("notification")
 								playSound("notification")
 							}
 							}
-							setTextAreaDisabled(isPartial)
+							setSendingDisabled(isPartial)
 							setClineAsk("followup")
 							setClineAsk("followup")
 							// setting enable buttons to `false` would trigger a focus grab when
 							// setting enable buttons to `false` would trigger a focus grab when
 							// the text area is enabled which is undesirable.
 							// the text area is enabled which is undesirable.
@@ -187,7 +187,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
 							if (!isAutoApproved(lastMessage) && !isPartial) {
 							if (!isAutoApproved(lastMessage) && !isPartial) {
 								playSound("notification")
 								playSound("notification")
 							}
 							}
-							setTextAreaDisabled(isPartial)
+							setSendingDisabled(isPartial)
 							setClineAsk("tool")
 							setClineAsk("tool")
 							setEnableButtons(!isPartial)
 							setEnableButtons(!isPartial)
 							const tool = JSON.parse(lastMessage.text || "{}") as ClineSayTool
 							const tool = JSON.parse(lastMessage.text || "{}") as ClineSayTool
@@ -213,7 +213,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
 							if (!isAutoApproved(lastMessage) && !isPartial) {
 							if (!isAutoApproved(lastMessage) && !isPartial) {
 								playSound("notification")
 								playSound("notification")
 							}
 							}
-							setTextAreaDisabled(isPartial)
+							setSendingDisabled(isPartial)
 							setClineAsk("browser_action_launch")
 							setClineAsk("browser_action_launch")
 							setEnableButtons(!isPartial)
 							setEnableButtons(!isPartial)
 							setPrimaryButtonText(t("chat:approve.title"))
 							setPrimaryButtonText(t("chat:approve.title"))
@@ -223,14 +223,14 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
 							if (!isAutoApproved(lastMessage) && !isPartial) {
 							if (!isAutoApproved(lastMessage) && !isPartial) {
 								playSound("notification")
 								playSound("notification")
 							}
 							}
-							setTextAreaDisabled(isPartial)
+							setSendingDisabled(isPartial)
 							setClineAsk("command")
 							setClineAsk("command")
 							setEnableButtons(!isPartial)
 							setEnableButtons(!isPartial)
 							setPrimaryButtonText(t("chat:runCommand.title"))
 							setPrimaryButtonText(t("chat:runCommand.title"))
 							setSecondaryButtonText(t("chat:reject.title"))
 							setSecondaryButtonText(t("chat:reject.title"))
 							break
 							break
 						case "command_output":
 						case "command_output":
-							setTextAreaDisabled(false)
+							setSendingDisabled(false)
 							setClineAsk("command_output")
 							setClineAsk("command_output")
 							setEnableButtons(true)
 							setEnableButtons(true)
 							setPrimaryButtonText(t("chat:proceedWhileRunning.title"))
 							setPrimaryButtonText(t("chat:proceedWhileRunning.title"))
@@ -240,7 +240,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
 							if (!isAutoApproved(lastMessage) && !isPartial) {
 							if (!isAutoApproved(lastMessage) && !isPartial) {
 								playSound("notification")
 								playSound("notification")
 							}
 							}
-							setTextAreaDisabled(isPartial)
+							setSendingDisabled(isPartial)
 							setClineAsk("use_mcp_server")
 							setClineAsk("use_mcp_server")
 							setEnableButtons(!isPartial)
 							setEnableButtons(!isPartial)
 							setPrimaryButtonText(t("chat:approve.title"))
 							setPrimaryButtonText(t("chat:approve.title"))
@@ -251,7 +251,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
 							if (!isPartial) {
 							if (!isPartial) {
 								playSound("celebration")
 								playSound("celebration")
 							}
 							}
-							setTextAreaDisabled(isPartial)
+							setSendingDisabled(isPartial)
 							setClineAsk("completion_result")
 							setClineAsk("completion_result")
 							setEnableButtons(!isPartial)
 							setEnableButtons(!isPartial)
 							setPrimaryButtonText(t("chat:startNewTask.title"))
 							setPrimaryButtonText(t("chat:startNewTask.title"))
@@ -261,7 +261,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
 							if (!isAutoApproved(lastMessage) && !isPartial) {
 							if (!isAutoApproved(lastMessage) && !isPartial) {
 								playSound("notification")
 								playSound("notification")
 							}
 							}
-							setTextAreaDisabled(false)
+							setSendingDisabled(false)
 							setClineAsk("resume_task")
 							setClineAsk("resume_task")
 							setEnableButtons(true)
 							setEnableButtons(true)
 							setPrimaryButtonText(t("chat:resumeTask.title"))
 							setPrimaryButtonText(t("chat:resumeTask.title"))
@@ -272,7 +272,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
 							if (!isPartial) {
 							if (!isPartial) {
 								playSound("celebration")
 								playSound("celebration")
 							}
 							}
-							setTextAreaDisabled(false)
+							setSendingDisabled(false)
 							setClineAsk("resume_completed_task")
 							setClineAsk("resume_completed_task")
 							setEnableButtons(true)
 							setEnableButtons(true)
 							setPrimaryButtonText(t("chat:startNewTask.title"))
 							setPrimaryButtonText(t("chat:startNewTask.title"))
@@ -286,7 +286,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
 					// an "ask" while ask is waiting for response.
 					// an "ask" while ask is waiting for response.
 					switch (lastMessage.say) {
 					switch (lastMessage.say) {
 						case "api_req_retry_delayed":
 						case "api_req_retry_delayed":
-							setTextAreaDisabled(true)
+							setSendingDisabled(true)
 							break
 							break
 						case "api_req_started":
 						case "api_req_started":
 							if (secondLastMessage?.ask === "command_output") {
 							if (secondLastMessage?.ask === "command_output") {
@@ -298,7 +298,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
 								// field or buttons to continue, which does the
 								// field or buttons to continue, which does the
 								// following automatically).
 								// following automatically).
 								setInputValue("")
 								setInputValue("")
-								setTextAreaDisabled(true)
+								setSendingDisabled(true)
 								setSelectedImages([])
 								setSelectedImages([])
 								setClineAsk(undefined)
 								setClineAsk(undefined)
 								setEnableButtons(false)
 								setEnableButtons(false)
@@ -322,7 +322,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
 
 
 	useEffect(() => {
 	useEffect(() => {
 		if (messages.length === 0) {
 		if (messages.length === 0) {
-			setTextAreaDisabled(false)
+			setSendingDisabled(false)
 			setClineAsk(undefined)
 			setClineAsk(undefined)
 			setEnableButtons(false)
 			setEnableButtons(false)
 			setPrimaryButtonText(undefined)
 			setPrimaryButtonText(undefined)
@@ -376,7 +376,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
 	const handleChatReset = useCallback(() => {
 	const handleChatReset = useCallback(() => {
 		// Only reset message-specific state, preserving mode.
 		// Only reset message-specific state, preserving mode.
 		setInputValue("")
 		setInputValue("")
-		setTextAreaDisabled(true)
+		setSendingDisabled(true)
 		setSelectedImages([])
 		setSelectedImages([])
 		setClineAsk(undefined)
 		setClineAsk(undefined)
 		setEnableButtons(false)
 		setEnableButtons(false)
@@ -474,7 +474,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
 					break
 					break
 			}
 			}
 
 
-			setTextAreaDisabled(true)
+			setSendingDisabled(true)
 			setClineAsk(undefined)
 			setClineAsk(undefined)
 			setEnableButtons(false)
 			setEnableButtons(false)
 		},
 		},
@@ -521,7 +521,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
 					vscode.postMessage({ type: "terminalOperation", terminalOperation: "abort" })
 					vscode.postMessage({ type: "terminalOperation", terminalOperation: "abort" })
 					break
 					break
 			}
 			}
-			setTextAreaDisabled(true)
+			setSendingDisabled(true)
 			setClineAsk(undefined)
 			setClineAsk(undefined)
 			setEnableButtons(false)
 			setEnableButtons(false)
 		},
 		},
@@ -535,7 +535,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
 	const selectImages = useCallback(() => vscode.postMessage({ type: "selectImages" }), [])
 	const selectImages = useCallback(() => vscode.postMessage({ type: "selectImages" }), [])
 
 
 	const shouldDisableImages =
 	const shouldDisableImages =
-		!model?.supportsImages || textAreaDisabled || selectedImages.length >= MAX_IMAGES_PER_MESSAGE
+		!model?.supportsImages || sendingDisabled || selectedImages.length >= MAX_IMAGES_PER_MESSAGE
 
 
 	const handleMessage = useCallback(
 	const handleMessage = useCallback(
 		(e: MessageEvent) => {
 		(e: MessageEvent) => {
@@ -545,7 +545,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
 				case "action":
 				case "action":
 					switch (message.action!) {
 					switch (message.action!) {
 						case "didBecomeVisible":
 						case "didBecomeVisible":
-							if (!isHidden && !textAreaDisabled && !enableButtons) {
+							if (!isHidden && !sendingDisabled && !enableButtons) {
 								textAreaRef.current?.focus()
 								textAreaRef.current?.focus()
 							}
 							}
 							break
 							break
@@ -587,7 +587,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
 		},
 		},
 		[
 		[
 			isHidden,
 			isHidden,
-			textAreaDisabled,
+			sendingDisabled,
 			enableButtons,
 			enableButtons,
 			handleChatReset,
 			handleChatReset,
 			handleSendMessage,
 			handleSendMessage,
@@ -604,7 +604,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
 
 
 	useEffect(() => {
 	useEffect(() => {
 		const timer = setTimeout(() => {
 		const timer = setTimeout(() => {
-			if (!isHidden && !textAreaDisabled && !enableButtons) {
+			if (!isHidden && !sendingDisabled && !enableButtons) {
 				textAreaRef.current?.focus()
 				textAreaRef.current?.focus()
 			}
 			}
 		}, 50)
 		}, 50)
@@ -612,7 +612,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
 		return () => {
 		return () => {
 			clearTimeout(timer)
 			clearTimeout(timer)
 		}
 		}
-	}, [isHidden, textAreaDisabled, enableButtons])
+	}, [isHidden, sendingDisabled, enableButtons])
 
 
 	const visibleMessages = useMemo(() => {
 	const visibleMessages = useMemo(() => {
 		return modifiedMessages.filter((message) => {
 		return modifiedMessages.filter((message) => {
@@ -1134,7 +1134,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
 				// things are actually needed.
 				// things are actually needed.
 				setInputValue("")
 				setInputValue("")
 				setSelectedImages([])
 				setSelectedImages([])
-				setTextAreaDisabled(true)
+				setSendingDisabled(true)
 				setClineAsk(undefined)
 				setClineAsk(undefined)
 				setEnableButtons(false)
 				setEnableButtons(false)
 			}
 			}
@@ -1197,7 +1197,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
 		acceptInput: () => {
 		acceptInput: () => {
 			if (enableButtons && primaryButtonText) {
 			if (enableButtons && primaryButtonText) {
 				handlePrimaryButtonClick(inputValue, selectedImages)
 				handlePrimaryButtonClick(inputValue, selectedImages)
-			} else if (!textAreaDisabled && (inputValue.trim() || selectedImages.length > 0)) {
+			} else if (!sendingDisabled && (inputValue.trim() || selectedImages.length > 0)) {
 				handleSendMessage(inputValue, selectedImages)
 				handleSendMessage(inputValue, selectedImages)
 			}
 			}
 		},
 		},
@@ -1398,8 +1398,8 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
 				ref={textAreaRef}
 				ref={textAreaRef}
 				inputValue={inputValue}
 				inputValue={inputValue}
 				setInputValue={setInputValue}
 				setInputValue={setInputValue}
-				textAreaDisabled={textAreaDisabled}
-				selectApiConfigDisabled={textAreaDisabled && clineAsk !== "api_req_failed"}
+				sendingDisabled={sendingDisabled}
+				selectApiConfigDisabled={sendingDisabled && clineAsk !== "api_req_failed"}
 				placeholderText={placeholderText}
 				placeholderText={placeholderText}
 				selectedImages={selectedImages}
 				selectedImages={selectedImages}
 				setSelectedImages={setSelectedImages}
 				setSelectedImages={setSelectedImages}

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

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

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

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