diff --git a/.changeset/empty-bees-suffer.md b/.changeset/empty-bees-suffer.md new file mode 100644 index 0000000000..e3d87a51ad --- /dev/null +++ b/.changeset/empty-bees-suffer.md @@ -0,0 +1,5 @@ +--- +"roo-cline": patch +--- + +Revert tool progress for now diff --git a/.changeset/sixty-ants-begin.md b/.changeset/sixty-ants-begin.md new file mode 100644 index 0000000000..5ef9e04868 --- /dev/null +++ b/.changeset/sixty-ants-begin.md @@ -0,0 +1,5 @@ +--- +"roo-cline": patch +--- + +v3.8.4 diff --git a/CHANGELOG.md b/CHANGELOG.md index ff2aa8de27..7afcfae2d9 100644 --- a/CHANGELOG.md +++ b/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 diff --git a/jest.config.js b/jest.config.js index dbe5ee54eb..c18b6e9eff 100644 --- a/jest.config.js +++ b/jest.config.js @@ -30,9 +30,10 @@ module.exports = { "^strip-ansi$": "/src/__mocks__/strip-ansi.js", "^default-shell$": "/src/__mocks__/default-shell.js", "^os-name$": "/src/__mocks__/os-name.js", + "^strip-bom$": "/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: ["/src", "/webview-ui/src"], modulePathIgnorePatterns: [".vscode-test"], diff --git a/package-lock.json b/package-lock.json index 3b8f47c2e2..6884f2e626 100644 --- a/package-lock.json +++ b/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": { diff --git a/package.json b/package.json index 5ddd9320f1..e4adffd1d7 100644 --- a/package.json +++ b/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", diff --git a/src/__mocks__/strip-bom.js b/src/__mocks__/strip-bom.js new file mode 100644 index 0000000000..64bb0dac4f --- /dev/null +++ b/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 +} diff --git a/src/api/providers/openai.ts b/src/api/providers/openai.ts index 9262f3b75a..caa99def09 100644 --- a/src/api/providers/openai.ts +++ b/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): 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) { diff --git a/src/core/Cline.ts b/src/core/Cline.ts index fd8ce3e9a2..3d1f980c7d 100644 --- a/src/core/Cline.ts +++ b/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 diff --git a/src/core/prompts/sections/modes.ts b/src/core/prompts/sections/modes.ts index f3863870db..788c7d7ebe 100644 --- a/src/core/prompts/sections/modes.ts +++ b/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("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 } diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index e1d67b5a28..b82a6a62e0 100644 --- a/src/core/webview/ClineProvider.ts +++ b/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, diff --git a/src/integrations/editor/DiffViewProvider.ts b/src/integrations/editor/DiffViewProvider.ts index ee24d7db4e..2aab8f9115 100644 --- a/src/integrations/editor/DiffViewProvider.ts +++ b/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 diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index 98ff9b36e1..73f8127c53 100644 --- a/src/shared/ExtensionMessage.ts +++ b/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 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 diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index 10af6f7a94..724d7e5983 100644 --- a/src/shared/WebviewMessage.ts +++ b/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" diff --git a/src/shared/globalState.ts b/src/shared/globalState.ts index bfd24f4298..540b7e72be 100644 --- a/src/shared/globalState.ts +++ b/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", diff --git a/webview-ui/src/components/chat/AutoApproveMenu.tsx b/webview-ui/src/components/chat/AutoApproveMenu.tsx index 161f3032b0..692cf1d44c 100644 --- a/webview-ui/src/components/chat/AutoApproveMenu.tsx +++ b/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, } diff --git a/webview-ui/src/components/chat/ChatRow.tsx b/webview-ui/src/components/chat/ChatRow.tsx index 1533bba3a8..259c03fa21 100644 --- a/webview-ui/src/components/chat/ChatRow.tsx +++ b/webview-ui/src/components/chat/ChatRow.tsx @@ -451,7 +451,7 @@ export const ChatRowContent = ({
{toolIcon("new-file")} - Roo wants to create a new task in {tool.mode} mode: + Roo wants to create a new subtask in {tool.mode} mode:
@@ -459,6 +459,18 @@ export const ChatRowContent = ({
) + case "finishTask": + return ( + <> +
+ {toolIcon("checklist")} + Roo wants to finish this subtask +
+
+ {tool.content} +
+ + ) default: return null } diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx index 35e63dd332..5ac7f50559 100644 --- a/webview-ui/src/components/chat/ChatView.tsx +++ b/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, ], ) diff --git a/webview-ui/src/components/prompts/PromptsView.tsx b/webview-ui/src/components/prompts/PromptsView.tsx index ccf1e6d700..6c69bc4229 100644 --- a/webview-ui/src/components/prompts/PromptsView.tsx +++ b/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. -
e.stopPropagation()} className="flex justify-between items-center mb-3">

Modes

@@ -1010,6 +1022,35 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
)}
+ + {/* + 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. + */} +
+ { + // Just update the local state through React context + // The React context will update the global state + setEnableCustomModeCreation(e.target.checked) + }}> + Enable Custom Mode Creation Through Prompts + +

+ 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. +

+
{vsCodeLmModels.length > 0 ? ( - { - 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" - /> + })}> + + + + + + {vsCodeLmModels.map((model) => ( + + {`${model.vendor} - ${model.family}`} + + ))} + + + ) : (
The VS Code Language Model API allows you to run models provided by other VS Code diff --git a/webview-ui/src/components/settings/AutoApproveSettings.tsx b/webview-ui/src/components/settings/AutoApproveSettings.tsx index b2da2cab75..d26fc33a7c 100644 --- a/webview-ui/src/components/settings/AutoApproveSettings.tsx +++ b/webview-ui/src/components/settings/AutoApproveSettings.tsx @@ -18,6 +18,7 @@ type AutoApproveSettingsProps = HTMLAttributes & { requestDelaySeconds: number alwaysAllowMcp?: boolean alwaysAllowModeSwitch?: boolean + alwaysAllowSubtasks?: boolean alwaysAllowExecute?: boolean allowedCommands?: string[] setCachedStateField: SetCachedStateField @@ -32,6 +33,7 @@ export const AutoApproveSettings = ({ requestDelaySeconds, alwaysAllowMcp, alwaysAllowModeSwitch, + alwaysAllowSubtasks, alwaysAllowExecute, allowedCommands, setCachedStateField, @@ -173,10 +175,21 @@ export const AutoApproveSettings = ({ setCachedStateField("alwaysAllowModeSwitch", e.target.checked)}> - Always approve mode switching & task creation + Always approve mode switching

- Automatically switch between different AI modes and create new tasks without requiring approval + Automatically switch between different modes without requiring approval +

+
+ +
+ setCachedStateField("alwaysAllowSubtasks", e.target.checked)}> + Always approve creation & completion of subtasks + +

+ Allow creation and completion of subtasks without requiring approval

diff --git a/webview-ui/src/components/settings/SettingsView.tsx b/webview-ui/src/components/settings/SettingsView.tsx index 604ef92df3..7cafbd6663 100644 --- a/webview-ui/src/components/settings/SettingsView.tsx +++ b/webview-ui/src/components/settings/SettingsView.tsx @@ -63,6 +63,7 @@ const SettingsView = forwardRef(({ onDone }, alwaysAllowExecute, alwaysAllowMcp, alwaysAllowModeSwitch, + alwaysAllowSubtasks, alwaysAllowWrite, alwaysApproveResubmit, browserToolEnabled, @@ -184,6 +185,7 @@ const SettingsView = forwardRef(({ 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(({ onDone }, requestDelaySeconds={requestDelaySeconds} alwaysAllowMcp={alwaysAllowMcp} alwaysAllowModeSwitch={alwaysAllowModeSwitch} + alwaysAllowSubtasks={alwaysAllowSubtasks} alwaysAllowExecute={alwaysAllowExecute} allowedCommands={allowedCommands} setCachedStateField={setCachedStateField} diff --git a/webview-ui/src/context/ExtensionStateContext.tsx b/webview-ui/src/context/ExtensionStateContext.tsx index c4daf426ca..b2172df642 100644 --- a/webview-ui/src/context/ExtensionStateContext.tsx +++ b/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 })),