Browse Source

Merge pull request #1 from cannuri/update-from-upstream

Update from upstream
cannuri 11 months ago
parent
commit
58532bc6db

+ 5 - 0
.changeset/empty-bees-suffer.md

@@ -0,0 +1,5 @@
+---
+"roo-cline": patch
+---
+
+Revert tool progress for now

+ 5 - 0
.changeset/sixty-ants-begin.md

@@ -0,0 +1,5 @@
+---
+"roo-cline": patch
+---
+
+v3.8.4

+ 12 - 0
CHANGELOG.md

@@ -1,5 +1,17 @@
 # Roo Code Changelog
 
+## [3.8.3] - 2025-03-09
+
+- Fix VS Code LM API model picker truncation issue
+
+## [3.8.2] - 2025-03-08
+
+- Create an auto-approval toggle for subtask creation and completion (thanks @shaybc!)
+- Show a progress indicator when using the multi-diff editing strategy (thanks @qdaxb!)
+- Add o3-mini support to the OpenAI-compatible provider (thanks @yt3trees!)
+- Fix encoding issue where unreadable characters were sometimes getting added to the beginning of files
+- Fix issue where settings dropdowns were getting truncated in some cases
+
 ## [3.8.1] - 2025-03-07
 
 - Show the reserved output tokens in the context window visualization

+ 2 - 1
jest.config.js

@@ -30,9 +30,10 @@ module.exports = {
 		"^strip-ansi$": "<rootDir>/src/__mocks__/strip-ansi.js",
 		"^default-shell$": "<rootDir>/src/__mocks__/default-shell.js",
 		"^os-name$": "<rootDir>/src/__mocks__/os-name.js",
+		"^strip-bom$": "<rootDir>/src/__mocks__/strip-bom.js",
 	},
 	transformIgnorePatterns: [
-		"node_modules/(?!(@modelcontextprotocol|delay|p-wait-for|globby|serialize-error|strip-ansi|default-shell|os-name)/)",
+		"node_modules/(?!(@modelcontextprotocol|delay|p-wait-for|globby|serialize-error|strip-ansi|default-shell|os-name|strip-bom)/)",
 	],
 	roots: ["<rootDir>/src", "<rootDir>/webview-ui/src"],
 	modulePathIgnorePatterns: [".vscode-test"],

+ 19 - 7
package-lock.json

@@ -1,12 +1,12 @@
 {
 	"name": "roo-cline",
-	"version": "3.8.1",
+	"version": "3.8.3",
 	"lockfileVersion": 3,
 	"requires": true,
 	"packages": {
 		"": {
 			"name": "roo-cline",
-			"version": "3.8.1",
+			"version": "3.8.3",
 			"dependencies": {
 				"@anthropic-ai/bedrock-sdk": "^0.10.2",
 				"@anthropic-ai/sdk": "^0.37.0",
@@ -51,6 +51,7 @@
 				"sound-play": "^1.1.0",
 				"string-similarity": "^4.0.4",
 				"strip-ansi": "^7.1.0",
+				"strip-bom": "^5.0.0",
 				"tmp": "^0.2.3",
 				"tree-sitter-wasms": "^0.1.11",
 				"turndown": "^7.2.0",
@@ -10782,6 +10783,15 @@
 				"node": ">=8"
 			}
 		},
+		"node_modules/jest-runtime/node_modules/strip-bom": {
+			"version": "4.0.0",
+			"resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz",
+			"integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==",
+			"dev": true,
+			"engines": {
+				"node": ">=8"
+			}
+		},
 		"node_modules/jest-simple-dot-reporter": {
 			"version": "1.0.5",
 			"resolved": "https://registry.npmjs.org/jest-simple-dot-reporter/-/jest-simple-dot-reporter-1.0.5.tgz",
@@ -14170,12 +14180,14 @@
 			}
 		},
 		"node_modules/strip-bom": {
-			"version": "4.0.0",
-			"resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz",
-			"integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==",
-			"dev": true,
+			"version": "5.0.0",
+			"resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-5.0.0.tgz",
+			"integrity": "sha512-p+byADHF7SzEcVnLvc/r3uognM1hUhObuHXxJcgLCfD194XAkaLbjq3Wzb0N5G2tgIjH0dgT708Z51QxMeu60A==",
 			"engines": {
-				"node": ">=8"
+				"node": ">=12"
+			},
+			"funding": {
+				"url": "https://github.com/sponsors/sindresorhus"
 			}
 		},
 		"node_modules/strip-final-newline": {

+ 3 - 2
package.json

@@ -3,7 +3,7 @@
 	"displayName": "Roo Code (prev. Roo Cline)",
 	"description": "A whole dev team of AI agents in your editor.",
 	"publisher": "RooVeterinaryInc",
-	"version": "3.8.1",
+	"version": "3.8.3",
 	"icon": "assets/icons/rocket.png",
 	"galleryBanner": {
 		"color": "#617A91",
@@ -265,8 +265,8 @@
 		"@anthropic-ai/sdk": "^0.37.0",
 		"@anthropic-ai/vertex-sdk": "^0.7.0",
 		"@aws-sdk/client-bedrock-runtime": "^3.706.0",
-		"@google/generative-ai": "^0.18.0",
 		"@google-cloud/vertexai": "^1.9.3",
+		"@google/generative-ai": "^0.18.0",
 		"@mistralai/mistralai": "^1.3.6",
 		"@modelcontextprotocol/sdk": "^1.0.1",
 		"@types/clone-deep": "^4.0.4",
@@ -304,6 +304,7 @@
 		"sound-play": "^1.1.0",
 		"string-similarity": "^4.0.4",
 		"strip-ansi": "^7.1.0",
+		"strip-bom": "^5.0.0",
 		"tmp": "^0.2.3",
 		"tree-sitter-wasms": "^0.1.11",
 		"turndown": "^7.2.0",

+ 13 - 0
src/__mocks__/strip-bom.js

@@ -0,0 +1,13 @@
+// Mock implementation of strip-bom
+module.exports = function stripBom(string) {
+	if (typeof string !== "string") {
+		throw new TypeError("Expected a string")
+	}
+
+	// Removes UTF-8 BOM
+	if (string.charCodeAt(0) === 0xfeff) {
+		return string.slice(1)
+	}
+
+	return string
+}

+ 68 - 0
src/api/providers/openai.ts

@@ -66,6 +66,11 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
 		const deepseekReasoner = modelId.includes("deepseek-reasoner")
 		const ark = modelUrl.includes(".volces.com")
 
+		if (modelId.startsWith("o3-mini")) {
+			yield* this.handleO3FamilyMessage(modelId, systemPrompt, messages)
+			return
+		}
+
 		if (this.options.openAiStreamingEnabled ?? true) {
 			const systemMessage: OpenAI.Chat.ChatCompletionSystemMessageParam = {
 				role: "system",
@@ -169,6 +174,69 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
 			throw error
 		}
 	}
+
+	private async *handleO3FamilyMessage(
+		modelId: string,
+		systemPrompt: string,
+		messages: Anthropic.Messages.MessageParam[],
+	): ApiStream {
+		if (this.options.openAiStreamingEnabled ?? true) {
+			const stream = await this.client.chat.completions.create({
+				model: "o3-mini",
+				messages: [
+					{
+						role: "developer",
+						content: `Formatting re-enabled\n${systemPrompt}`,
+					},
+					...convertToOpenAiMessages(messages),
+				],
+				stream: true,
+				stream_options: { include_usage: true },
+				reasoning_effort: this.getModel().info.reasoningEffort,
+			})
+
+			yield* this.handleStreamResponse(stream)
+		} else {
+			const requestOptions: OpenAI.Chat.Completions.ChatCompletionCreateParamsNonStreaming = {
+				model: modelId,
+				messages: [
+					{
+						role: "developer",
+						content: `Formatting re-enabled\n${systemPrompt}`,
+					},
+					...convertToOpenAiMessages(messages),
+				],
+			}
+
+			const response = await this.client.chat.completions.create(requestOptions)
+
+			yield {
+				type: "text",
+				text: response.choices[0]?.message.content || "",
+			}
+			yield this.processUsageMetrics(response.usage)
+		}
+	}
+
+	private async *handleStreamResponse(stream: AsyncIterable<OpenAI.Chat.Completions.ChatCompletionChunk>): ApiStream {
+		for await (const chunk of stream) {
+			const delta = chunk.choices[0]?.delta
+			if (delta?.content) {
+				yield {
+					type: "text",
+					text: delta.content,
+				}
+			}
+
+			if (chunk.usage) {
+				yield {
+					type: "usage",
+					inputTokens: chunk.usage.prompt_tokens || 0,
+					outputTokens: chunk.usage.completion_tokens || 0,
+				}
+			}
+		}
+	}
 }
 
 export async function getOpenAiModels(baseUrl?: string, apiKey?: string) {

+ 21 - 12
src/core/Cline.ts

@@ -1417,6 +1417,18 @@ export class Cline {
 					return true
 				}
 
+				const askFinishSubTaskApproval = async () => {
+					// ask the user to approve this task has completed, and he has reviewd it, and we can declare task is finished
+					// and return control to the parent task to continue running the rest of the sub-tasks
+					const toolMessage = JSON.stringify({
+						tool: "finishTask",
+						content:
+							"Subtask completed! You can review the results and suggest any corrections or next steps. If everything looks good, confirm to return the result to the parent task.",
+					})
+
+					return await askApproval("tool", toolMessage)
+				}
+
 				const handleError = async (action: string, error: Error) => {
 					const errorString = `Error ${action}: ${JSON.stringify(serializeError(error))}`
 					await this.say(
@@ -2945,13 +2957,6 @@ export class Cline {
 										// havent sent a command message yet so first send completion_result then command
 										await this.say("completion_result", result, undefined, false)
 										telemetryService.captureTaskCompleted(this.taskId)
-										if (this.isSubTask) {
-											// tell the provider to remove the current subtask and resume the previous task in the stack
-											await this.providerRef
-												.deref()
-												?.finishSubTask(`Task complete: ${lastMessage?.text}`)
-											break
-										}
 									}
 
 									// complete command message
@@ -2970,13 +2975,17 @@ export class Cline {
 								} else {
 									await this.say("completion_result", result, undefined, false)
 									telemetryService.captureTaskCompleted(this.taskId)
-									if (this.isSubTask) {
-										// tell the provider to remove the current subtask and resume the previous task in the stack
-										await this.providerRef
-											.deref()
-											?.finishSubTask(`Task complete: ${lastMessage?.text}`)
+								}
+
+								if (this.isSubTask) {
+									const didApprove = await askFinishSubTaskApproval()
+									if (!didApprove) {
 										break
 									}
+
+									// tell the provider to remove the current subtask and resume the previous task in the stack
+									await this.providerRef.deref()?.finishSubTask(`Task complete: ${lastMessage?.text}`)
+									break
 								}
 
 								// we already sent completion_result says, an empty string asks relinquishes control over button and field

+ 12 - 2
src/core/prompts/sections/modes.ts

@@ -11,12 +11,19 @@ export async function getModesSection(context: vscode.ExtensionContext): Promise
 	// Get all modes with their overrides from extension state
 	const allModes = await getAllModesWithPrompts(context)
 
-	return `====
+	// Get enableCustomModeCreation setting from extension state
+	const shouldEnableCustomModeCreation = await context.globalState.get<boolean>("enableCustomModeCreation") ?? true
+
+	let modesContent = `====
 
 MODES
 
 - These are the currently available modes:
-${allModes.map((mode: ModeConfig) => `  * "${mode.name}" mode (${mode.slug}) - ${mode.roleDefinition.split(".")[0]}`).join("\n")}
+${allModes.map((mode: ModeConfig) => `  * "${mode.name}" mode (${mode.slug}) - ${mode.roleDefinition.split(".")[0]}`).join("\n")}`
+
+	// Only include custom modes documentation if the feature is enabled
+	if (shouldEnableCustomModeCreation) {
+		modesContent += `
 
 - Custom modes can be configured in two ways:
   1. Globally via '${customModesPath}' (created automatically on startup)
@@ -56,4 +63,7 @@ Both files should follow this structure:
     }
   ]
 }`
+	}
+
+	return modesContent
 }

+ 12 - 3
src/core/webview/ClineProvider.ts

@@ -984,6 +984,10 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 						await this.updateGlobalState("alwaysAllowModeSwitch", message.bool)
 						await this.postStateToWebview()
 						break
+					case "alwaysAllowSubtasks":
+						await this.updateGlobalState("alwaysAllowSubtasks", message.bool)
+						await this.postStateToWebview()
+						break
 					case "askResponse":
 						this.getCurrentCline()?.handleWebviewAskResponse(
 							message.askResponse!,
@@ -993,9 +997,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 						break
 					case "clearTask":
 						// clear task resets the current session and allows for a new task to be started, if this session is a subtask - it allows the parent task to be resumed
-						await this.finishSubTask(
-							`new_task finished with an error!, it was stopped and canceled by the user.`,
-						)
+						await this.finishSubTask(`Task error: It was stopped and canceled by the user.`)
 						await this.postStateToWebview()
 						break
 					case "didShowAnnouncement":
@@ -1476,6 +1478,10 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 						await this.updateGlobalState("enhancementApiConfigId", message.text)
 						await this.postStateToWebview()
 						break
+					case "enableCustomModeCreation":
+						await this.updateGlobalState("enableCustomModeCreation", message.bool ?? true)
+						await this.postStateToWebview()
+						break
 					case "autoApprovalEnabled":
 						await this.updateGlobalState("autoApprovalEnabled", message.bool ?? false)
 						await this.postStateToWebview()
@@ -2177,6 +2183,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 			alwaysAllowBrowser,
 			alwaysAllowMcp,
 			alwaysAllowModeSwitch,
+			alwaysAllowSubtasks,
 			soundEnabled,
 			diffEnabled,
 			enableCheckpoints,
@@ -2224,6 +2231,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 			alwaysAllowBrowser: alwaysAllowBrowser ?? false,
 			alwaysAllowMcp: alwaysAllowMcp ?? false,
 			alwaysAllowModeSwitch: alwaysAllowModeSwitch ?? false,
+			alwaysAllowSubtasks: alwaysAllowSubtasks ?? false,
 			uriScheme: vscode.env.uriScheme,
 			currentTaskItem: this.getCurrentCline()?.taskId
 				? (taskHistory || []).find((item: HistoryItem) => item.id === this.getCurrentCline()?.taskId)
@@ -2385,6 +2393,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 			alwaysAllowBrowser: stateValues.alwaysAllowBrowser ?? false,
 			alwaysAllowMcp: stateValues.alwaysAllowMcp ?? false,
 			alwaysAllowModeSwitch: stateValues.alwaysAllowModeSwitch ?? false,
+			alwaysAllowSubtasks: stateValues.alwaysAllowSubtasks ?? false,
 			taskHistory: stateValues.taskHistory,
 			allowedCommands: stateValues.allowedCommands,
 			soundEnabled: stateValues.soundEnabled ?? false,

+ 17 - 2
src/integrations/editor/DiffViewProvider.ts

@@ -7,6 +7,7 @@ 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"
 
 export const DIFF_VIEW_URI_SCHEME = "cline-diff"
 
@@ -104,7 +105,7 @@ export class DiffViewProvider {
 		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, contentToReplace)
+		edit.replace(document.uri, rangeToReplace, this.stripAllBOMs(contentToReplace))
 		await vscode.workspace.applyEdit(edit)
 		// Update decorations
 		this.activeLineController.setActiveLine(endLine)
@@ -128,7 +129,11 @@ export class DiffViewProvider {
 			}
 			// Apply the final content
 			const finalEdit = new vscode.WorkspaceEdit()
-			finalEdit.replace(document.uri, new vscode.Range(0, 0, document.lineCount, 0), accumulatedContent)
+			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)
 			this.fadedOverlayController.clear()
@@ -336,6 +341,16 @@ 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() {
 		this.editType = undefined

+ 4 - 0
src/shared/ExtensionMessage.ts

@@ -109,6 +109,7 @@ export interface ExtensionState {
 	alwaysAllowMcp?: boolean
 	alwaysApproveResubmit?: boolean
 	alwaysAllowModeSwitch?: boolean
+	alwaysAllowSubtasks?: boolean
 	browserToolEnabled?: boolean
 	requestDelaySeconds: number
 	rateLimitSeconds: number // Minimum time between successive requests (0 = disabled)
@@ -128,6 +129,7 @@ export interface ExtensionState {
 	terminalOutputLimit?: number
 	mcpEnabled: boolean
 	enableMcpServerCreation: boolean
+	enableCustomModeCreation?: boolean
 	mode: Mode
 	modeApiConfigs?: Record<Mode, string>
 	enhancementApiConfigId?: string
@@ -168,6 +170,7 @@ export type ClineAsk =
 	| "mistake_limit_reached"
 	| "browser_action_launch"
 	| "use_mcp_server"
+	| "finishTask"
 
 export type ClineSay =
 	| "task"
@@ -207,6 +210,7 @@ export interface ClineSayTool {
 		| "searchFiles"
 		| "switchMode"
 		| "newTask"
+		| "finishTask"
 	path?: string
 	diff?: string
 	content?: string

+ 2 - 0
src/shared/WebviewMessage.ts

@@ -48,6 +48,7 @@ export interface WebviewMessage {
 		| "alwaysAllowBrowser"
 		| "alwaysAllowMcp"
 		| "alwaysAllowModeSwitch"
+		| "alwaysAllowSubtasks"
 		| "playSound"
 		| "soundEnabled"
 		| "soundVolume"
@@ -71,6 +72,7 @@ export interface WebviewMessage {
 		| "terminalOutputLimit"
 		| "mcpEnabled"
 		| "enableMcpServerCreation"
+		| "enableCustomModeCreation"
 		| "searchCommits"
 		| "alwaysApproveResubmit"
 		| "requestDelaySeconds"

+ 2 - 0
src/shared/globalState.ts

@@ -40,6 +40,7 @@ export const GLOBAL_STATE_KEYS = [
 	"alwaysAllowBrowser",
 	"alwaysAllowMcp",
 	"alwaysAllowModeSwitch",
+	"alwaysAllowSubtasks",
 	"taskHistory",
 	"openAiBaseUrl",
 	"openAiModelId",
@@ -84,6 +85,7 @@ export const GLOBAL_STATE_KEYS = [
 	"enhancementApiConfigId",
 	"experiments", // Map of experiment IDs to their enabled state
 	"autoApprovalEnabled",
+	"enableCustomModeCreation", // Enable the ability for Roo to create custom modes
 	"customModes", // Array of custom modes
 	"unboundModelId",
 	"requestyModelId",

+ 18 - 3
webview-ui/src/components/chat/AutoApproveMenu.tsx

@@ -30,6 +30,8 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => {
 		setAlwaysAllowMcp,
 		alwaysAllowModeSwitch,
 		setAlwaysAllowModeSwitch,
+		alwaysAllowSubtasks,
+		setAlwaysAllowSubtasks,
 		alwaysApproveResubmit,
 		setAlwaysApproveResubmit,
 		autoApprovalEnabled,
@@ -75,11 +77,17 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => {
 		},
 		{
 			id: "switchModes",
-			label: "Switch modes & create tasks",
+			label: "Switch modes",
 			shortName: "Modes",
 			enabled: alwaysAllowModeSwitch ?? false,
-			description:
-				"Allows automatic switching between different AI modes and creating new tasks without requiring approval.",
+			description: "Allows automatic switching between different modes without requiring approval.",
+		},
+		{
+			id: "subtasks",
+			label: "Create & complete subtasks",
+			shortName: "Subtasks",
+			enabled: alwaysAllowSubtasks ?? false,
+			description: "Allow creation and completion of subtasks without requiring approval.",
 		},
 		{
 			id: "retryRequests",
@@ -136,6 +144,12 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => {
 		vscode.postMessage({ type: "alwaysAllowModeSwitch", bool: newValue })
 	}, [alwaysAllowModeSwitch, setAlwaysAllowModeSwitch])
 
+	const handleSubtasksChange = useCallback(() => {
+		const newValue = !(alwaysAllowSubtasks ?? false)
+		setAlwaysAllowSubtasks(newValue)
+		vscode.postMessage({ type: "alwaysAllowSubtasks", bool: newValue })
+	}, [alwaysAllowSubtasks, setAlwaysAllowSubtasks])
+
 	const handleRetryChange = useCallback(() => {
 		const newValue = !(alwaysApproveResubmit ?? false)
 		setAlwaysApproveResubmit(newValue)
@@ -150,6 +164,7 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => {
 		useBrowser: handleBrowserChange,
 		useMcp: handleMcpChange,
 		switchModes: handleModeSwitchChange,
+		subtasks: handleSubtasksChange,
 		retryRequests: handleRetryChange,
 	}
 

+ 13 - 1
webview-ui/src/components/chat/ChatRow.tsx

@@ -451,7 +451,7 @@ export const ChatRowContent = ({
 						<div style={headerStyle}>
 							{toolIcon("new-file")}
 							<span style={{ fontWeight: "bold" }}>
-								Roo wants to create a new task in <code>{tool.mode}</code> mode:
+								Roo wants to create a new subtask in <code>{tool.mode}</code> mode:
 							</span>
 						</div>
 						<div style={{ paddingLeft: "26px", marginTop: "4px" }}>
@@ -459,6 +459,18 @@ export const ChatRowContent = ({
 						</div>
 					</>
 				)
+			case "finishTask":
+				return (
+					<>
+						<div style={headerStyle}>
+							{toolIcon("checklist")}
+							<span style={{ fontWeight: "bold" }}>Roo wants to finish this subtask</span>
+						</div>
+						<div style={{ paddingLeft: "26px", marginTop: "4px" }}>
+							<code>{tool.content}</code>
+						</div>
+					</>
+				)
 			default:
 				return null
 		}

+ 10 - 2
webview-ui/src/components/chat/ChatView.tsx

@@ -61,6 +61,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
 		setMode,
 		autoApprovalEnabled,
 		alwaysAllowModeSwitch,
+		alwaysAllowSubtasks,
 		customModes,
 		telemetrySetting,
 	} = useExtensionState()
@@ -148,6 +149,10 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
 									setPrimaryButtonText("Save")
 									setSecondaryButtonText("Reject")
 									break
+								case "finishTask":
+									setPrimaryButtonText("Complete Subtask and Return")
+									setSecondaryButtonText(undefined)
+									break
 								default:
 									setPrimaryButtonText("Approve")
 									setSecondaryButtonText("Reject")
@@ -641,8 +646,10 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
 				(alwaysAllowMcp && message.ask === "use_mcp_server" && isMcpToolAlwaysAllowed(message)) ||
 				(alwaysAllowModeSwitch &&
 					message.ask === "tool" &&
-					(JSON.parse(message.text || "{}")?.tool === "switchMode" ||
-						JSON.parse(message.text || "{}")?.tool === "newTask"))
+					JSON.parse(message.text || "{}")?.tool === "switchMode") ||
+				(alwaysAllowSubtasks &&
+					message.ask === "tool" &&
+					["newTask", "finishTask"].includes(JSON.parse(message.text || "{}")?.tool))
 			)
 		},
 		[
@@ -657,6 +664,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
 			alwaysAllowMcp,
 			isMcpToolAlwaysAllowed,
 			alwaysAllowModeSwitch,
+			alwaysAllowSubtasks,
 		],
 	)
 

+ 42 - 1
webview-ui/src/components/prompts/PromptsView.tsx

@@ -71,6 +71,8 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
 		preferredLanguage,
 		setPreferredLanguage,
 		customModes,
+		enableCustomModeCreation,
+		setEnableCustomModeCreation,
 	} = useExtensionState()
 
 	// Memoize modes to preserve array order
@@ -341,6 +343,17 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
 		return () => document.removeEventListener("click", handleClickOutside)
 	}, [showConfigMenu])
 
+	// Add effect to sync enableCustomModeCreation with backend
+	useEffect(() => {
+		if (enableCustomModeCreation !== undefined) {
+			// Send the value to the extension's global state
+			vscode.postMessage({
+				type: "enableCustomModeCreation", // Using dedicated message type
+				bool: enableCustomModeCreation,
+			})
+		}
+	}, [enableCustomModeCreation])
+
 	useEffect(() => {
 		const handler = (event: MessageEvent) => {
 			const message = event.data
@@ -541,7 +554,6 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
 						in your workspace.
 					</div>
 				</div>
-
 				<div className="mt-5">
 					<div onClick={(e) => e.stopPropagation()} className="flex justify-between items-center mb-3">
 						<h3 className="text-vscode-foreground m-0">Modes</h3>
@@ -1010,6 +1022,35 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
 							</div>
 						)}
 					</div>
+
+					{/*
+			NOTE: This setting is placed in PromptsView rather than SettingsView since it
+			directly affects the functionality related to modes and custom mode creation,
+			which are managed in this component. This is an intentional deviation from
+			the standard pattern described in cline_docs/settings.md.
+	*/}
+					<div className="mb-4 mt-4">
+						<VSCodeCheckbox
+							checked={enableCustomModeCreation ?? true}
+							onChange={(e: any) => {
+								// Just update the local state through React context
+								// The React context will update the global state
+								setEnableCustomModeCreation(e.target.checked)
+							}}>
+							<span style={{ fontWeight: "500" }}>Enable Custom Mode Creation Through Prompts</span>
+						</VSCodeCheckbox>
+						<p
+							style={{
+								fontSize: "12px",
+								marginTop: "5px",
+								color: "var(--vscode-descriptionForeground)",
+							}}>
+							When enabled, Roo allows you to create custom modes using prompts like ‘Make me a custom
+							mode that…’. Disabling this reduces your system prompt by about 700 tokens when this feature
+							isn’t needed. When disabled you can still manually create custom modes using the + button
+							above or by editing the related config JSON.
+						</p>
+					</div>
 				</div>
 
 				<div

+ 18 - 14
webview-ui/src/components/settings/ApiOptions.tsx

@@ -1076,27 +1076,31 @@ const ApiOptions = ({
 							Language Model
 						</label>
 						{vsCodeLmModels.length > 0 ? (
-							<Dropdown
-								id="vscode-lm-model"
+							<Select
 								value={
 									apiConfiguration?.vsCodeLmModelSelector
 										? `${apiConfiguration.vsCodeLmModelSelector.vendor ?? ""}/${apiConfiguration.vsCodeLmModelSelector.family ?? ""}`
 										: ""
 								}
-								onChange={handleInputChange("vsCodeLmModelSelector", (e) => {
-									const valueStr = (e as DropdownOption)?.value
+								onValueChange={handleInputChange("vsCodeLmModelSelector", (valueStr) => {
 									const [vendor, family] = valueStr.split("/")
 									return { vendor, family }
-								})}
-								options={[
-									{ value: "", label: "Select a model..." },
-									...vsCodeLmModels.map((model) => ({
-										value: `${model.vendor}/${model.family}`,
-										label: `${model.vendor} - ${model.family}`,
-									})),
-								]}
-								className="w-full"
-							/>
+								})}>
+								<SelectTrigger className="w-full">
+									<SelectValue placeholder="Select a model..." />
+								</SelectTrigger>
+								<SelectContent>
+									<SelectGroup>
+										{vsCodeLmModels.map((model) => (
+											<SelectItem
+												key={`${model.vendor}/${model.family}`}
+												value={`${model.vendor}/${model.family}`}>
+												{`${model.vendor} - ${model.family}`}
+											</SelectItem>
+										))}
+									</SelectGroup>
+								</SelectContent>
+							</Select>
 						) : (
 							<div className="text-sm text-vscode-descriptionForeground">
 								The VS Code Language Model API allows you to run models provided by other VS Code

+ 15 - 2
webview-ui/src/components/settings/AutoApproveSettings.tsx

@@ -18,6 +18,7 @@ type AutoApproveSettingsProps = HTMLAttributes<HTMLDivElement> & {
 	requestDelaySeconds: number
 	alwaysAllowMcp?: boolean
 	alwaysAllowModeSwitch?: boolean
+	alwaysAllowSubtasks?: boolean
 	alwaysAllowExecute?: boolean
 	allowedCommands?: string[]
 	setCachedStateField: SetCachedStateField<keyof ExtensionStateContextType>
@@ -32,6 +33,7 @@ export const AutoApproveSettings = ({
 	requestDelaySeconds,
 	alwaysAllowMcp,
 	alwaysAllowModeSwitch,
+	alwaysAllowSubtasks,
 	alwaysAllowExecute,
 	allowedCommands,
 	setCachedStateField,
@@ -173,10 +175,21 @@ export const AutoApproveSettings = ({
 					<VSCodeCheckbox
 						checked={alwaysAllowModeSwitch}
 						onChange={(e: any) => setCachedStateField("alwaysAllowModeSwitch", e.target.checked)}>
-						<span className="font-medium">Always approve mode switching & task creation</span>
+						<span className="font-medium">Always approve mode switching</span>
 					</VSCodeCheckbox>
 					<p className="text-vscode-descriptionForeground text-sm mt-0">
-						Automatically switch between different AI modes and create new tasks without requiring approval
+						Automatically switch between different modes without requiring approval
+					</p>
+				</div>
+
+				<div>
+					<VSCodeCheckbox
+						checked={alwaysAllowSubtasks}
+						onChange={(e: any) => setCachedStateField("alwaysAllowSubtasks", e.target.checked)}>
+						<span className="font-medium">Always approve creation & completion of subtasks</span>
+					</VSCodeCheckbox>
+					<p className="text-vscode-descriptionForeground text-sm mt-0">
+						Allow creation and completion of subtasks without requiring approval
 					</p>
 				</div>
 

+ 3 - 0
webview-ui/src/components/settings/SettingsView.tsx

@@ -63,6 +63,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone },
 		alwaysAllowExecute,
 		alwaysAllowMcp,
 		alwaysAllowModeSwitch,
+		alwaysAllowSubtasks,
 		alwaysAllowWrite,
 		alwaysApproveResubmit,
 		browserToolEnabled,
@@ -184,6 +185,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone },
 			vscode.postMessage({ type: "currentApiConfigName", text: currentApiConfigName })
 			vscode.postMessage({ type: "updateExperimental", values: experiments })
 			vscode.postMessage({ type: "alwaysAllowModeSwitch", bool: alwaysAllowModeSwitch })
+			vscode.postMessage({ type: "alwaysAllowSubtasks", bool: alwaysAllowSubtasks })
 			vscode.postMessage({ type: "upsertApiConfiguration", text: currentApiConfigName, apiConfiguration })
 			vscode.postMessage({ type: "telemetrySetting", text: telemetrySetting })
 			setChangeDetected(false)
@@ -364,6 +366,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone },
 						requestDelaySeconds={requestDelaySeconds}
 						alwaysAllowMcp={alwaysAllowMcp}
 						alwaysAllowModeSwitch={alwaysAllowModeSwitch}
+						alwaysAllowSubtasks={alwaysAllowSubtasks}
 						alwaysAllowExecute={alwaysAllowExecute}
 						allowedCommands={allowedCommands}
 						setCachedStateField={setCachedStateField}

+ 7 - 0
webview-ui/src/context/ExtensionStateContext.tsx

@@ -31,6 +31,7 @@ export interface ExtensionStateContextType extends ExtensionState {
 	setAlwaysAllowBrowser: (value: boolean) => void
 	setAlwaysAllowMcp: (value: boolean) => void
 	setAlwaysAllowModeSwitch: (value: boolean) => void
+	setAlwaysAllowSubtasks: (value: boolean) => void
 	setBrowserToolEnabled: (value: boolean) => void
 	setShowRooIgnoredFiles: (value: boolean) => void
 	setShowAnnouncement: (value: boolean) => void
@@ -52,6 +53,8 @@ export interface ExtensionStateContextType extends ExtensionState {
 	setMcpEnabled: (value: boolean) => void
 	enableMcpServerCreation: boolean
 	setEnableMcpServerCreation: (value: boolean) => void
+	enableCustomModeCreation?: boolean
+	setEnableCustomModeCreation: (value: boolean) => void
 	alwaysApproveResubmit?: boolean
 	setAlwaysApproveResubmit: (value: boolean) => void
 	requestDelaySeconds: number
@@ -117,6 +120,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
 		checkpointStorage: "task",
 		fuzzyMatchThreshold: 1.0,
 		preferredLanguage: "English",
+		enableCustomModeCreation: true,
 		writeDelayMs: 1000,
 		browserViewportSize: "900x600",
 		screenshotQuality: 75,
@@ -247,6 +251,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
 		setAlwaysAllowBrowser: (value) => setState((prevState) => ({ ...prevState, alwaysAllowBrowser: value })),
 		setAlwaysAllowMcp: (value) => setState((prevState) => ({ ...prevState, alwaysAllowMcp: value })),
 		setAlwaysAllowModeSwitch: (value) => setState((prevState) => ({ ...prevState, alwaysAllowModeSwitch: value })),
+		setAlwaysAllowSubtasks: (value) => setState((prevState) => ({ ...prevState, alwaysAllowSubtasks: value })),
 		setShowAnnouncement: (value) => setState((prevState) => ({ ...prevState, shouldShowAnnouncement: value })),
 		setAllowedCommands: (value) => setState((prevState) => ({ ...prevState, allowedCommands: value })),
 		setSoundEnabled: (value) => setState((prevState) => ({ ...prevState, soundEnabled: value })),
@@ -273,6 +278,8 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
 		setCustomSupportPrompts: (value) => setState((prevState) => ({ ...prevState, customSupportPrompts: value })),
 		setEnhancementApiConfigId: (value) =>
 			setState((prevState) => ({ ...prevState, enhancementApiConfigId: value })),
+		setEnableCustomModeCreation: (value) =>
+			setState((prevState) => ({ ...prevState, enableCustomModeCreation: value })),
 		setAutoApprovalEnabled: (value) => setState((prevState) => ({ ...prevState, autoApprovalEnabled: value })),
 		setCustomModes: (value) => setState((prevState) => ({ ...prevState, customModes: value })),
 		setMaxOpenTabsContext: (value) => setState((prevState) => ({ ...prevState, maxOpenTabsContext: value })),