diff --git a/.changeset/automatic-tags-publish.md b/.changeset/automatic-tags-publish.md
new file mode 100644
index 0000000000..a3ff07fb4c
--- /dev/null
+++ b/.changeset/automatic-tags-publish.md
@@ -0,0 +1,5 @@
+---
+"roo-cline": patch
+---
+
+Update GitHub Actions workflow to automatically create and push git tags during release
diff --git a/.changeset/lemon-bulldogs-unite.md b/.changeset/lemon-bulldogs-unite.md
new file mode 100644
index 0000000000..b87abc1bd2
--- /dev/null
+++ b/.changeset/lemon-bulldogs-unite.md
@@ -0,0 +1,5 @@
+---
+"roo-cline": patch
+---
+
+App tab layout fixes
diff --git a/.changeset/wild-dragons-leave.md b/.changeset/wild-dragons-leave.md
deleted file mode 100644
index 05320a4aa2..0000000000
--- a/.changeset/wild-dragons-leave.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"roo-cline": patch
----
-
-Add o3-mini support to openai compatible
diff --git a/.env.sample b/.env.sample
index 6cdaa1b3b1..4d6c24ac72 100644
--- a/.env.sample
+++ b/.env.sample
@@ -1,2 +1 @@
-# PostHog API Keys for telemetry
-POSTHOG_API_KEY=key-goes-here
\ No newline at end of file
+POSTHOG_API_KEY=key-goes-here
diff --git a/.github/workflows/marketplace-publish.yml b/.github/workflows/marketplace-publish.yml
index c6fd66b1b3..4ecd2af7a2 100644
--- a/.github/workflows/marketplace-publish.yml
+++ b/.github/workflows/marketplace-publish.yml
@@ -10,6 +10,8 @@ env:
jobs:
publish-extension:
runs-on: ubuntu-latest
+ permissions:
+ contents: write # Required for pushing tags
if: >
( github.event_name == 'pull_request' &&
github.event.pull_request.base.ref == 'main' &&
@@ -23,24 +25,24 @@ jobs:
- uses: actions/setup-node@v4
with:
node-version: 18
+
- run: |
git config user.name github-actions
git config user.email github-actions@github.com
+
- name: Install Dependencies
run: |
npm install -g vsce ovsx
npm run install:ci
+
- name: Create .env file
run: |
echo "# PostHog API Keys for telemetry" > .env
echo "POSTHOG_API_KEY=${{ secrets.POSTHOG_API_KEY }}" >> .env
- - name: Package and Publish Extension
- env:
- VSCE_PAT: ${{ secrets.VSCE_PAT }}
- OVSX_PAT: ${{ secrets.OVSX_PAT }}
+
+ - name: Package Extension
run: |
current_package_version=$(node -p "require('./package.json').version")
-
npm run vsix
package=$(unzip -l bin/roo-cline-${current_package_version}.vsix)
echo "$package"
@@ -49,5 +51,18 @@ jobs:
echo "$package" | grep -q "extension/node_modules/@vscode/codicons/dist/codicon.ttf" || exit 1
echo "$package" | grep -q ".env" || exit 1
+ - name: Create and Push Git Tag
+ run: |
+ current_package_version=$(node -p "require('./package.json').version")
+ git tag -a "v${current_package_version}" -m "Release v${current_package_version}"
+ git push origin "v${current_package_version}"
+ echo "Successfully created and pushed git tag v${current_package_version}"
+
+ - name: Publish Extension
+ env:
+ VSCE_PAT: ${{ secrets.VSCE_PAT }}
+ OVSX_PAT: ${{ secrets.OVSX_PAT }}
+ run: |
+ current_package_version=$(node -p "require('./package.json').version")
npm run publish:marketplace
echo "Successfully published version $current_package_version to VS Code Marketplace"
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ff2aa8de27..fd6a574dc6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,22 @@
# Roo Code Changelog
+## [3.8.4] - 2025-03-09
+
+- Roll back multi-diff progress indicator temporarily to fix a double-confirmation in saving edits
+- Add an option in the prompts tab to save tokens by disabling the ability to ask Roo to create/edit custom modes for you (thanks @hannesrudolph!)
+
+## [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/esbuild.js b/esbuild.js
index 8b203076e4..7907dd1c39 100644
--- a/esbuild.js
+++ b/esbuild.js
@@ -52,6 +52,7 @@ const copyWasmFiles = {
"java",
"php",
"swift",
+ "kotlin",
]
languages.forEach((lang) => {
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 63722a74f3..91a7148ad9 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "roo-cline",
- "version": "3.8.1",
+ "version": "3.8.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "roo-cline",
- "version": "3.8.1",
+ "version": "3.8.4",
"dependencies": {
"@anthropic-ai/bedrock-sdk": "^0.10.2",
"@anthropic-ai/sdk": "^0.37.0",
@@ -52,6 +52,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",
@@ -11971,6 +11972,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",
@@ -15653,13 +15663,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,
- "license": "MIT",
+ "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 efb934c241..35321a86b2 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.4",
"icon": "assets/icons/rocket.png",
"galleryBanner": {
"color": "#617A91",
@@ -305,6 +305,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/__tests__/requesty.test.ts b/src/api/providers/__tests__/requesty.test.ts
index 7867b15ebc..47921a1c53 100644
--- a/src/api/providers/__tests__/requesty.test.ts
+++ b/src/api/providers/__tests__/requesty.test.ts
@@ -22,8 +22,10 @@ describe("RequestyHandler", () => {
contextWindow: 4000,
supportsPromptCache: false,
supportsImages: true,
- inputPrice: 0,
- outputPrice: 0,
+ inputPrice: 1,
+ outputPrice: 10,
+ cacheReadsPrice: 0.1,
+ cacheWritesPrice: 1.5,
},
openAiStreamingEnabled: true,
includeMaxTokens: true, // Add this to match the implementation
@@ -83,8 +85,12 @@ describe("RequestyHandler", () => {
yield {
choices: [{ delta: { content: " world" } }],
usage: {
- prompt_tokens: 10,
- completion_tokens: 5,
+ prompt_tokens: 30,
+ completion_tokens: 10,
+ prompt_tokens_details: {
+ cached_tokens: 15,
+ caching_tokens: 5,
+ },
},
}
},
@@ -105,10 +111,11 @@ describe("RequestyHandler", () => {
{ type: "text", text: " world" },
{
type: "usage",
- inputTokens: 10,
- outputTokens: 5,
- cacheWriteTokens: undefined,
- cacheReadTokens: undefined,
+ inputTokens: 30,
+ outputTokens: 10,
+ cacheWriteTokens: 5,
+ cacheReadTokens: 15,
+ totalCost: 0.000119, // (10 * 1 / 1,000,000) + (5 * 1.5 / 1,000,000) + (15 * 0.1 / 1,000,000) + (10 * 10 / 1,000,000)
},
])
@@ -182,6 +189,9 @@ describe("RequestyHandler", () => {
type: "usage",
inputTokens: 10,
outputTokens: 5,
+ cacheWriteTokens: 0,
+ cacheReadTokens: 0,
+ totalCost: 0.00006, // (10 * 1 / 1,000,000) + (5 * 10 / 1,000,000)
},
])
diff --git a/src/api/providers/openai.ts b/src/api/providers/openai.ts
index caa99def09..2af3f2da05 100644
--- a/src/api/providers/openai.ts
+++ b/src/api/providers/openai.ts
@@ -116,7 +116,7 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
}
}
if (chunk.usage) {
- yield this.processUsageMetrics(chunk.usage)
+ yield this.processUsageMetrics(chunk.usage, modelInfo)
}
}
} else {
@@ -139,11 +139,11 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
type: "text",
text: response.choices[0]?.message.content || "",
}
- yield this.processUsageMetrics(response.usage)
+ yield this.processUsageMetrics(response.usage, modelInfo)
}
}
- protected processUsageMetrics(usage: any): ApiStreamUsageChunk {
+ protected processUsageMetrics(usage: any, modelInfo?: ModelInfo): ApiStreamUsageChunk {
return {
type: "usage",
inputTokens: usage?.prompt_tokens || 0,
diff --git a/src/api/providers/requesty.ts b/src/api/providers/requesty.ts
index 0060bfc5d8..434d6f4316 100644
--- a/src/api/providers/requesty.ts
+++ b/src/api/providers/requesty.ts
@@ -1,9 +1,20 @@
import axios from "axios"
import { ModelInfo, requestyModelInfoSaneDefaults, requestyDefaultModelId } from "../../shared/api"
-import { parseApiPrice } from "../../utils/cost"
+import { calculateApiCostOpenAI, parseApiPrice } from "../../utils/cost"
import { ApiStreamUsageChunk } from "../transform/stream"
import { OpenAiHandler, OpenAiHandlerOptions } from "./openai"
+import OpenAI from "openai"
+
+// Requesty usage includes an extra field for Anthropic use cases.
+// Safely cast the prompt token details section to the appropriate structure.
+interface RequestyUsage extends OpenAI.CompletionUsage {
+ prompt_tokens_details?: {
+ caching_tokens?: number
+ cached_tokens?: number
+ }
+ total_cost?: number
+}
export class RequestyHandler extends OpenAiHandler {
constructor(options: OpenAiHandlerOptions) {
@@ -27,13 +38,22 @@ export class RequestyHandler extends OpenAiHandler {
}
}
- protected override processUsageMetrics(usage: any): ApiStreamUsageChunk {
+ protected override processUsageMetrics(usage: any, modelInfo?: ModelInfo): ApiStreamUsageChunk {
+ const requestyUsage = usage as RequestyUsage
+ const inputTokens = requestyUsage?.prompt_tokens || 0
+ const outputTokens = requestyUsage?.completion_tokens || 0
+ const cacheWriteTokens = requestyUsage?.prompt_tokens_details?.caching_tokens || 0
+ const cacheReadTokens = requestyUsage?.prompt_tokens_details?.cached_tokens || 0
+ const totalCost = modelInfo
+ ? calculateApiCostOpenAI(modelInfo, inputTokens, outputTokens, cacheWriteTokens, cacheReadTokens)
+ : 0
return {
type: "usage",
- inputTokens: usage?.prompt_tokens || 0,
- outputTokens: usage?.completion_tokens || 0,
- cacheWriteTokens: usage?.cache_creation_input_tokens,
- cacheReadTokens: usage?.cache_read_input_tokens,
+ inputTokens: inputTokens,
+ outputTokens: outputTokens,
+ cacheWriteTokens: cacheWriteTokens,
+ cacheReadTokens: cacheReadTokens,
+ totalCost: totalCost,
}
}
}
diff --git a/src/api/providers/vscode-lm.ts b/src/api/providers/vscode-lm.ts
index bf1215e238..0ce2a6e26a 100644
--- a/src/api/providers/vscode-lm.ts
+++ b/src/api/providers/vscode-lm.ts
@@ -2,7 +2,7 @@ import { Anthropic } from "@anthropic-ai/sdk"
import * as vscode from "vscode"
import { SingleCompletionHandler } from "../"
-import { calculateApiCost } from "../../utils/cost"
+import { calculateApiCostAnthropic } from "../../utils/cost"
import { ApiStream } from "../transform/stream"
import { convertToVsCodeLmMessages } from "../transform/vscode-lm-format"
import { SELECTOR_SEPARATOR, stringifyVsCodeLmModelSelector } from "../../shared/vsCodeSelectorUtils"
@@ -462,7 +462,7 @@ export class VsCodeLmHandler extends BaseProvider implements SingleCompletionHan
type: "usage",
inputTokens: totalInputTokens,
outputTokens: totalOutputTokens,
- totalCost: calculateApiCost(this.getModel().info, totalInputTokens, totalOutputTokens),
+ totalCost: calculateApiCostAnthropic(this.getModel().info, totalInputTokens, totalOutputTokens),
}
} catch (error: unknown) {
this.ensureCleanState()
diff --git a/src/core/Cline.ts b/src/core/Cline.ts
index fd8ce3e9a2..ba171ce3fa 100644
--- a/src/core/Cline.ts
+++ b/src/core/Cline.ts
@@ -55,7 +55,7 @@ import { ClineAskResponse } from "../shared/WebviewMessage"
import { GlobalFileNames } from "../shared/globalFileNames"
import { defaultModeSlug, getModeBySlug, getFullModeDetails } from "../shared/modes"
import { EXPERIMENT_IDS, experiments as Experiments, ExperimentId } from "../shared/experiments"
-import { calculateApiCost } from "../utils/cost"
+import { calculateApiCostAnthropic } from "../utils/cost"
import { fileExistsAtPath } from "../utils/fs"
import { arePathsEqual, getReadablePath } from "../utils/path"
import { parseMentions } from "./mentions"
@@ -875,7 +875,7 @@ export class Cline {
// The way this agentic loop works is that cline will be given a task that he then calls tools to complete. unless there's an attempt_completion call, we keep responding back to him with his tool's responses until he either attempt_completion or does not use anymore tools. If he does not use anymore tools, we ask him to consider if he's completed the task and then call attempt_completion, otherwise proceed with completing the task.
// There is a MAX_REQUESTS_PER_TASK limit to prevent infinite requests, but Cline is prompted to finish the task as efficiently as he can.
- //const totalCost = this.calculateApiCost(totalInputTokens, totalOutputTokens)
+ //const totalCost = this.calculateApiCostAnthropic(totalInputTokens, totalOutputTokens)
if (didEndLoop) {
// For now a task never 'completes'. This will only happen if the user hits max requests and denies resetting the count.
//this.say("task_completed", `Task completed. Total API usage cost: ${totalCost}`)
@@ -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
@@ -3164,7 +3173,7 @@ export class Cline {
cacheReads: cacheReadTokens,
cost:
totalCost ??
- calculateApiCost(
+ calculateApiCostAnthropic(
this.api.getModel().info,
inputTokens,
outputTokens,
@@ -3789,6 +3798,8 @@ export class Cline {
return
}
+ telemetryService.captureCheckpointDiffed(this.taskId)
+
if (!previousCommitHash && mode === "checkpoint") {
const previousCheckpoint = this.clineMessages
.filter(({ say }) => say === "checkpoint_saved")
@@ -3840,6 +3851,8 @@ export class Cline {
return
}
+ telemetryService.captureCheckpointCreated(this.taskId)
+
// Start the checkpoint process in the background.
service.saveCheckpoint(`Task: ${this.taskId}, Time: ${Date.now()}`).catch((err) => {
console.error("[Cline#checkpointSave] caught unexpected error, disabling checkpoints", err)
@@ -3871,6 +3884,8 @@ export class Cline {
try {
await service.restoreCheckpoint(commitHash)
+ telemetryService.captureCheckpointRestored(this.taskId)
+
await this.providerRef.deref()?.postMessageToWebview({ type: "currentCheckpointUpdated", text: commitHash })
if (mode === "restore") {
diff --git a/src/core/mentions/index.ts b/src/core/mentions/index.ts
index d74ee0db97..445701c40f 100644
--- a/src/core/mentions/index.ts
+++ b/src/core/mentions/index.ts
@@ -198,9 +198,9 @@ async function getFileOrFolderContent(mentionPath: string, cwd: string): Promise
}
}
-function getWorkspaceProblems(cwd: string): string {
+async function getWorkspaceProblems(cwd: string): Promise {
const diagnostics = vscode.languages.getDiagnostics()
- const result = diagnosticsToProblemsString(
+ const result = await diagnosticsToProblemsString(
diagnostics,
[vscode.DiagnosticSeverity.Error, vscode.DiagnosticSeverity.Warning],
cwd,
diff --git a/src/core/prompts/sections/modes.ts b/src/core/prompts/sections/modes.ts
index f3863870db..78b94ec9e7 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..72cf56d35b 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,
@@ -2558,6 +2567,15 @@ export class ClineProvider implements vscode.WebviewViewProvider {
properties.apiProvider = apiConfiguration.apiProvider
}
+ // Add model ID if available
+ const currentCline = this.getCurrentCline()
+ if (currentCline?.api) {
+ const { id: modelId } = currentCline.api.getModel()
+ if (modelId) {
+ properties.modelId = modelId
+ }
+ }
+
return properties
}
}
diff --git a/src/core/webview/__tests__/ClineProvider.test.ts b/src/core/webview/__tests__/ClineProvider.test.ts
index f9fc5d3ece..2e9fcdf336 100644
--- a/src/core/webview/__tests__/ClineProvider.test.ts
+++ b/src/core/webview/__tests__/ClineProvider.test.ts
@@ -1652,3 +1652,62 @@ describe("ContextProxy integration", () => {
expect(mockContextProxy.setValues).toBeDefined()
})
})
+
+describe("getTelemetryProperties", () => {
+ let provider: ClineProvider
+ let mockContext: vscode.ExtensionContext
+ let mockOutputChannel: vscode.OutputChannel
+ let mockCline: any
+
+ beforeEach(() => {
+ // Reset mocks
+ jest.clearAllMocks()
+
+ // Setup basic mocks
+ mockContext = {
+ globalState: {
+ get: jest.fn().mockImplementation((key: string) => {
+ if (key === "mode") return "code"
+ if (key === "apiProvider") return "anthropic"
+ return undefined
+ }),
+ update: jest.fn(),
+ keys: jest.fn().mockReturnValue([]),
+ },
+ secrets: { get: jest.fn(), store: jest.fn(), delete: jest.fn() },
+ extensionUri: {} as vscode.Uri,
+ globalStorageUri: { fsPath: "/test/path" },
+ extension: { packageJSON: { version: "1.0.0" } },
+ } as unknown as vscode.ExtensionContext
+
+ mockOutputChannel = { appendLine: jest.fn() } as unknown as vscode.OutputChannel
+ provider = new ClineProvider(mockContext, mockOutputChannel)
+
+ // Setup Cline instance with mocked getModel method
+ const { Cline } = require("../../Cline")
+ mockCline = new Cline()
+ mockCline.api = {
+ getModel: jest.fn().mockReturnValue({
+ id: "claude-3-7-sonnet-20250219",
+ info: { contextWindow: 200000 },
+ }),
+ }
+ })
+
+ test("includes basic properties in telemetry", async () => {
+ const properties = await provider.getTelemetryProperties()
+
+ expect(properties).toHaveProperty("vscodeVersion")
+ expect(properties).toHaveProperty("platform")
+ expect(properties).toHaveProperty("appVersion", "1.0.0")
+ })
+
+ test("includes model ID from current Cline instance if available", async () => {
+ // Add mock Cline to stack
+ await provider.addClineToStack(mockCline)
+
+ const properties = await provider.getTelemetryProperties()
+
+ expect(properties).toHaveProperty("modelId", "claude-3-7-sonnet-20250219")
+ })
+})
diff --git a/src/integrations/diagnostics/index.ts b/src/integrations/diagnostics/index.ts
index ad4ee7755c..2d829f26e7 100644
--- a/src/integrations/diagnostics/index.ts
+++ b/src/integrations/diagnostics/index.ts
@@ -70,11 +70,12 @@ export function getNewDiagnostics(
// // - New error in file3 (1:1)
// will return empty string if no problems with the given severity are found
-export function diagnosticsToProblemsString(
+export async function diagnosticsToProblemsString(
diagnostics: [vscode.Uri, vscode.Diagnostic[]][],
severities: vscode.DiagnosticSeverity[],
cwd: string,
-): string {
+): Promise {
+ const documents = new Map()
let result = ""
for (const [uri, fileDiagnostics] of diagnostics) {
const problems = fileDiagnostics.filter((d) => severities.includes(d.severity))
@@ -100,7 +101,10 @@ export function diagnosticsToProblemsString(
}
const line = diagnostic.range.start.line + 1 // VSCode lines are 0-indexed
const source = diagnostic.source ? `${diagnostic.source} ` : ""
- result += `\n- [${source}${label}] Line ${line}: ${diagnostic.message}`
+ const document = documents.get(uri) || (await vscode.workspace.openTextDocument(uri))
+ documents.set(uri, document)
+ const lineContent = document.lineAt(diagnostic.range.start.line).text
+ result += `\n- [${source}${label}] ${line} | ${lineContent} : ${diagnostic.message}`
}
}
}
diff --git a/src/integrations/editor/DiffViewProvider.ts b/src/integrations/editor/DiffViewProvider.ts
index ee24d7db4e..0bf494854a 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()
@@ -172,7 +177,7 @@ export class DiffViewProvider {
initial fix is usually correct and it may just take time for linters to catch up.
*/
const postDiagnostics = vscode.languages.getDiagnostics()
- const newProblems = diagnosticsToProblemsString(
+ const newProblems = await diagnosticsToProblemsString(
getNewDiagnostics(this.preDiagnostics, postDiagnostics),
[
vscode.DiagnosticSeverity.Error, // only including errors since warnings can be distracting (if user wants to fix warnings they can use the @problems mention)
@@ -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/services/glob/list-files.ts b/src/services/glob/list-files.ts
index 8578b914d7..c7e3d41cf0 100644
--- a/src/services/glob/list-files.ts
+++ b/src/services/glob/list-files.ts
@@ -34,7 +34,7 @@ export async function listFiles(dirPath: string, recursive: boolean, limit: numb
"pkg",
"Pods",
".*", // '!**/.*' excludes hidden directories, while '!**/.*/**' excludes only their contents. This way we are at least aware of the existence of hidden directories.
- ].map((dir) => `**/${dir}/**`)
+ ].map((dir) => `${dirPath}/**/${dir}/**`)
const options = {
cwd: dirPath,
diff --git a/src/services/telemetry/TelemetryService.ts b/src/services/telemetry/TelemetryService.ts
index 45a34bda4e..d3ea8bfb5f 100644
--- a/src/services/telemetry/TelemetryService.ts
+++ b/src/services/telemetry/TelemetryService.ts
@@ -22,6 +22,9 @@ class PostHogClient {
CONVERSATION_MESSAGE: "Conversation Message",
MODE_SWITCH: "Mode Switched",
TOOL_USED: "Tool Used",
+ CHECKPOINT_CREATED: "Checkpoint Created",
+ CHECKPOINT_RESTORED: "Checkpoint Restored",
+ CHECKPOINT_DIFFED: "Checkpoint Diffed",
},
}
@@ -246,6 +249,18 @@ class TelemetryService {
})
}
+ public captureCheckpointCreated(taskId: string): void {
+ this.captureEvent(PostHogClient.EVENTS.TASK.CHECKPOINT_CREATED, { taskId })
+ }
+
+ public captureCheckpointDiffed(taskId: string): void {
+ this.captureEvent(PostHogClient.EVENTS.TASK.CHECKPOINT_DIFFED, { taskId })
+ }
+
+ public captureCheckpointRestored(taskId: string): void {
+ this.captureEvent(PostHogClient.EVENTS.TASK.CHECKPOINT_RESTORED, { taskId })
+ }
+
/**
* Checks if telemetry is currently enabled
* @returns Whether telemetry is enabled
diff --git a/src/services/tree-sitter/__tests__/index.test.ts b/src/services/tree-sitter/__tests__/index.test.ts
index 4a5782dcb1..8372e7e580 100644
--- a/src/services/tree-sitter/__tests__/index.test.ts
+++ b/src/services/tree-sitter/__tests__/index.test.ts
@@ -169,6 +169,8 @@ describe("Tree-sitter Service", () => {
"/test/path/main.rs",
"/test/path/program.cpp",
"/test/path/code.go",
+ "/test/path/app.kt",
+ "/test/path/script.kts",
]
;(listFiles as jest.Mock).mockResolvedValue([mockFiles, new Set()])
@@ -197,6 +199,8 @@ describe("Tree-sitter Service", () => {
rs: { parser: mockParser, query: mockQuery },
cpp: { parser: mockParser, query: mockQuery },
go: { parser: mockParser, query: mockQuery },
+ kt: { parser: mockParser, query: mockQuery },
+ kts: { parser: mockParser, query: mockQuery },
})
;(fs.readFile as jest.Mock).mockResolvedValue("function test() {}")
@@ -207,6 +211,8 @@ describe("Tree-sitter Service", () => {
expect(result).toContain("main.rs")
expect(result).toContain("program.cpp")
expect(result).toContain("code.go")
+ expect(result).toContain("app.kt")
+ expect(result).toContain("script.kts")
})
it("should normalize paths in output", async () => {
diff --git a/src/services/tree-sitter/__tests__/languageParser.test.ts b/src/services/tree-sitter/__tests__/languageParser.test.ts
index 1b92d81b6b..54271e30e8 100644
--- a/src/services/tree-sitter/__tests__/languageParser.test.ts
+++ b/src/services/tree-sitter/__tests__/languageParser.test.ts
@@ -92,6 +92,17 @@ describe("Language Parser", () => {
expect(parsers.hpp).toBeDefined()
})
+ it("should handle Kotlin files correctly", async () => {
+ const files = ["test.kt", "test.kts"]
+ const parsers = await loadRequiredLanguageParsers(files)
+
+ expect(ParserMock.Language.load).toHaveBeenCalledWith(expect.stringContaining("tree-sitter-kotlin.wasm"))
+ expect(parsers.kt).toBeDefined()
+ expect(parsers.kts).toBeDefined()
+ expect(parsers.kt.query).toBeDefined()
+ expect(parsers.kts.query).toBeDefined()
+ })
+
it("should throw error for unsupported file extensions", async () => {
const files = ["test.unsupported"]
diff --git a/src/services/tree-sitter/index.ts b/src/services/tree-sitter/index.ts
index bfdfa6c52c..9aaa672ce2 100644
--- a/src/services/tree-sitter/index.ts
+++ b/src/services/tree-sitter/index.ts
@@ -80,6 +80,9 @@ function separateFiles(allFiles: string[]): { filesToParse: string[]; remainingF
"java",
"php",
"swift",
+ // Kotlin
+ "kt",
+ "kts",
].map((e) => `.${e}`)
const filesToParse = allFiles.filter((file) => extensions.includes(path.extname(file))).slice(0, 50) // 50 files max
const remainingFiles = allFiles.filter((file) => !filesToParse.includes(file))
diff --git a/src/services/tree-sitter/languageParser.ts b/src/services/tree-sitter/languageParser.ts
index 2d791b39a8..f256b0b62a 100644
--- a/src/services/tree-sitter/languageParser.ts
+++ b/src/services/tree-sitter/languageParser.ts
@@ -13,6 +13,7 @@ import {
javaQuery,
phpQuery,
swiftQuery,
+ kotlinQuery,
} from "./queries"
export interface LanguageParser {
@@ -120,6 +121,11 @@ export async function loadRequiredLanguageParsers(filesToParse: string[]): Promi
language = await loadLanguage("swift")
query = language.query(swiftQuery)
break
+ case "kt":
+ case "kts":
+ language = await loadLanguage("kotlin")
+ query = language.query(kotlinQuery)
+ break
default:
throw new Error(`Unsupported language: ${ext}`)
}
diff --git a/src/services/tree-sitter/queries/index.ts b/src/services/tree-sitter/queries/index.ts
index 889210a8e5..818eacca01 100644
--- a/src/services/tree-sitter/queries/index.ts
+++ b/src/services/tree-sitter/queries/index.ts
@@ -10,3 +10,4 @@ export { default as cQuery } from "./c"
export { default as csharpQuery } from "./c-sharp"
export { default as goQuery } from "./go"
export { default as swiftQuery } from "./swift"
+export { default as kotlinQuery } from "./kotlin"
diff --git a/src/services/tree-sitter/queries/kotlin.ts b/src/services/tree-sitter/queries/kotlin.ts
new file mode 100644
index 0000000000..61eb112448
--- /dev/null
+++ b/src/services/tree-sitter/queries/kotlin.ts
@@ -0,0 +1,28 @@
+/*
+- class declarations (including interfaces)
+- function declarations
+- object declarations
+- property declarations
+- type alias declarations
+*/
+export default `
+(class_declaration
+ (type_identifier) @name.definition.class
+) @definition.class
+
+(function_declaration
+ (simple_identifier) @name.definition.function
+) @definition.function
+
+(object_declaration
+ (type_identifier) @name.definition.object
+) @definition.object
+
+(property_declaration
+ (simple_identifier) @name.definition.property
+) @definition.property
+
+(type_alias
+ (type_identifier) @name.definition.type
+) @definition.type
+`
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/src/utils/__tests__/cost.test.ts b/src/utils/__tests__/cost.test.ts
index e390c4af7f..4501f86b88 100644
--- a/src/utils/__tests__/cost.test.ts
+++ b/src/utils/__tests__/cost.test.ts
@@ -1,8 +1,8 @@
-import { calculateApiCost } from "../cost"
+import { calculateApiCostAnthropic, calculateApiCostOpenAI } from "../cost"
import { ModelInfo } from "../../shared/api"
describe("Cost Utility", () => {
- describe("calculateApiCost", () => {
+ describe("calculateApiCostAnthropic", () => {
const mockModelInfo: ModelInfo = {
maxTokens: 8192,
contextWindow: 200_000,
@@ -14,7 +14,7 @@ describe("Cost Utility", () => {
}
it("should calculate basic input/output costs correctly", () => {
- const cost = calculateApiCost(mockModelInfo, 1000, 500)
+ const cost = calculateApiCostAnthropic(mockModelInfo, 1000, 500)
// Input cost: (3.0 / 1_000_000) * 1000 = 0.003
// Output cost: (15.0 / 1_000_000) * 500 = 0.0075
@@ -23,7 +23,7 @@ describe("Cost Utility", () => {
})
it("should handle cache writes cost", () => {
- const cost = calculateApiCost(mockModelInfo, 1000, 500, 2000)
+ const cost = calculateApiCostAnthropic(mockModelInfo, 1000, 500, 2000)
// Input cost: (3.0 / 1_000_000) * 1000 = 0.003
// Output cost: (15.0 / 1_000_000) * 500 = 0.0075
@@ -33,7 +33,7 @@ describe("Cost Utility", () => {
})
it("should handle cache reads cost", () => {
- const cost = calculateApiCost(mockModelInfo, 1000, 500, undefined, 3000)
+ const cost = calculateApiCostAnthropic(mockModelInfo, 1000, 500, undefined, 3000)
// Input cost: (3.0 / 1_000_000) * 1000 = 0.003
// Output cost: (15.0 / 1_000_000) * 500 = 0.0075
@@ -43,7 +43,7 @@ describe("Cost Utility", () => {
})
it("should handle all cost components together", () => {
- const cost = calculateApiCost(mockModelInfo, 1000, 500, 2000, 3000)
+ const cost = calculateApiCostAnthropic(mockModelInfo, 1000, 500, 2000, 3000)
// Input cost: (3.0 / 1_000_000) * 1000 = 0.003
// Output cost: (15.0 / 1_000_000) * 500 = 0.0075
@@ -60,17 +60,17 @@ describe("Cost Utility", () => {
supportsPromptCache: true,
}
- const cost = calculateApiCost(modelWithoutPrices, 1000, 500, 2000, 3000)
+ const cost = calculateApiCostAnthropic(modelWithoutPrices, 1000, 500, 2000, 3000)
expect(cost).toBe(0)
})
it("should handle zero tokens", () => {
- const cost = calculateApiCost(mockModelInfo, 0, 0, 0, 0)
+ const cost = calculateApiCostAnthropic(mockModelInfo, 0, 0, 0, 0)
expect(cost).toBe(0)
})
it("should handle undefined cache values", () => {
- const cost = calculateApiCost(mockModelInfo, 1000, 500)
+ const cost = calculateApiCostAnthropic(mockModelInfo, 1000, 500)
// Input cost: (3.0 / 1_000_000) * 1000 = 0.003
// Output cost: (15.0 / 1_000_000) * 500 = 0.0075
@@ -85,7 +85,7 @@ describe("Cost Utility", () => {
cacheReadsPrice: undefined,
}
- const cost = calculateApiCost(modelWithoutCachePrices, 1000, 500, 2000, 3000)
+ const cost = calculateApiCostAnthropic(modelWithoutCachePrices, 1000, 500, 2000, 3000)
// Should only include input and output costs
// Input cost: (3.0 / 1_000_000) * 1000 = 0.003
@@ -94,4 +94,97 @@ describe("Cost Utility", () => {
expect(cost).toBe(0.0105)
})
})
+
+ describe("calculateApiCostOpenAI", () => {
+ const mockModelInfo: ModelInfo = {
+ maxTokens: 8192,
+ contextWindow: 200_000,
+ supportsPromptCache: true,
+ inputPrice: 3.0, // $3 per million tokens
+ outputPrice: 15.0, // $15 per million tokens
+ cacheWritesPrice: 3.75, // $3.75 per million tokens
+ cacheReadsPrice: 0.3, // $0.30 per million tokens
+ }
+
+ it("should calculate basic input/output costs correctly", () => {
+ const cost = calculateApiCostOpenAI(mockModelInfo, 1000, 500)
+
+ // Input cost: (3.0 / 1_000_000) * 1000 = 0.003
+ // Output cost: (15.0 / 1_000_000) * 500 = 0.0075
+ // Total: 0.003 + 0.0075 = 0.0105
+ expect(cost).toBe(0.0105)
+ })
+
+ it("should handle cache writes cost", () => {
+ const cost = calculateApiCostOpenAI(mockModelInfo, 3000, 500, 2000)
+
+ // Input cost: (3.0 / 1_000_000) * (3000 - 2000) = 0.003
+ // Output cost: (15.0 / 1_000_000) * 500 = 0.0075
+ // Cache writes: (3.75 / 1_000_000) * 2000 = 0.0075
+ // Total: 0.003 + 0.0075 + 0.0075 = 0.018
+ expect(cost).toBeCloseTo(0.018, 6)
+ })
+
+ it("should handle cache reads cost", () => {
+ const cost = calculateApiCostOpenAI(mockModelInfo, 4000, 500, undefined, 3000)
+
+ // Input cost: (3.0 / 1_000_000) * (4000 - 3000) = 0.003
+ // Output cost: (15.0 / 1_000_000) * 500 = 0.0075
+ // Cache reads: (0.3 / 1_000_000) * 3000 = 0.0009
+ // Total: 0.003 + 0.0075 + 0.0009 = 0.0114
+ expect(cost).toBe(0.0114)
+ })
+
+ it("should handle all cost components together", () => {
+ const cost = calculateApiCostOpenAI(mockModelInfo, 6000, 500, 2000, 3000)
+
+ // Input cost: (3.0 / 1_000_000) * (6000 - 2000 - 3000) = 0.003
+ // Output cost: (15.0 / 1_000_000) * 500 = 0.0075
+ // Cache writes: (3.75 / 1_000_000) * 2000 = 0.0075
+ // Cache reads: (0.3 / 1_000_000) * 3000 = 0.0009
+ // Total: 0.003 + 0.0075 + 0.0075 + 0.0009 = 0.0189
+ expect(cost).toBe(0.0189)
+ })
+
+ it("should handle missing prices gracefully", () => {
+ const modelWithoutPrices: ModelInfo = {
+ maxTokens: 8192,
+ contextWindow: 200_000,
+ supportsPromptCache: true,
+ }
+
+ const cost = calculateApiCostOpenAI(modelWithoutPrices, 1000, 500, 2000, 3000)
+ expect(cost).toBe(0)
+ })
+
+ it("should handle zero tokens", () => {
+ const cost = calculateApiCostOpenAI(mockModelInfo, 0, 0, 0, 0)
+ expect(cost).toBe(0)
+ })
+
+ it("should handle undefined cache values", () => {
+ const cost = calculateApiCostOpenAI(mockModelInfo, 1000, 500)
+
+ // Input cost: (3.0 / 1_000_000) * 1000 = 0.003
+ // Output cost: (15.0 / 1_000_000) * 500 = 0.0075
+ // Total: 0.003 + 0.0075 = 0.0105
+ expect(cost).toBe(0.0105)
+ })
+
+ it("should handle missing cache prices", () => {
+ const modelWithoutCachePrices: ModelInfo = {
+ ...mockModelInfo,
+ cacheWritesPrice: undefined,
+ cacheReadsPrice: undefined,
+ }
+
+ const cost = calculateApiCostOpenAI(modelWithoutCachePrices, 6000, 500, 2000, 3000)
+
+ // Should only include input and output costs
+ // Input cost: (3.0 / 1_000_000) * (6000 - 2000 - 3000) = 0.003
+ // Output cost: (15.0 / 1_000_000) * 500 = 0.0075
+ // Total: 0.003 + 0.0075 = 0.0105
+ expect(cost).toBe(0.0105)
+ })
+ })
})
diff --git a/src/utils/cost.ts b/src/utils/cost.ts
index adc2ded0a8..48108b6348 100644
--- a/src/utils/cost.ts
+++ b/src/utils/cost.ts
@@ -1,26 +1,57 @@
import { ModelInfo } from "../shared/api"
-export function calculateApiCost(
+function calculateApiCostInternal(
modelInfo: ModelInfo,
inputTokens: number,
outputTokens: number,
- cacheCreationInputTokens?: number,
- cacheReadInputTokens?: number,
+ cacheCreationInputTokens: number,
+ cacheReadInputTokens: number,
): number {
- const modelCacheWritesPrice = modelInfo.cacheWritesPrice
- let cacheWritesCost = 0
- if (cacheCreationInputTokens && modelCacheWritesPrice) {
- cacheWritesCost = (modelCacheWritesPrice / 1_000_000) * cacheCreationInputTokens
- }
- const modelCacheReadsPrice = modelInfo.cacheReadsPrice
- let cacheReadsCost = 0
- if (cacheReadInputTokens && modelCacheReadsPrice) {
- cacheReadsCost = (modelCacheReadsPrice / 1_000_000) * cacheReadInputTokens
- }
+ const cacheWritesCost = ((modelInfo.cacheWritesPrice || 0) / 1_000_000) * cacheCreationInputTokens
+ const cacheReadsCost = ((modelInfo.cacheReadsPrice || 0) / 1_000_000) * cacheReadInputTokens
const baseInputCost = ((modelInfo.inputPrice || 0) / 1_000_000) * inputTokens
const outputCost = ((modelInfo.outputPrice || 0) / 1_000_000) * outputTokens
const totalCost = cacheWritesCost + cacheReadsCost + baseInputCost + outputCost
return totalCost
}
+// For Anthropic compliant usage, the input tokens count does NOT include the cached tokens
+export function calculateApiCostAnthropic(
+ modelInfo: ModelInfo,
+ inputTokens: number,
+ outputTokens: number,
+ cacheCreationInputTokens?: number,
+ cacheReadInputTokens?: number,
+): number {
+ const cacheCreationInputTokensNum = cacheCreationInputTokens || 0
+ const cacheReadInputTokensNum = cacheReadInputTokens || 0
+ return calculateApiCostInternal(
+ modelInfo,
+ inputTokens,
+ outputTokens,
+ cacheCreationInputTokensNum,
+ cacheReadInputTokensNum,
+ )
+}
+
+// For OpenAI compliant usage, the input tokens count INCLUDES the cached tokens
+export function calculateApiCostOpenAI(
+ modelInfo: ModelInfo,
+ inputTokens: number,
+ outputTokens: number,
+ cacheCreationInputTokens?: number,
+ cacheReadInputTokens?: number,
+): number {
+ const cacheCreationInputTokensNum = cacheCreationInputTokens || 0
+ const cacheReadInputTokensNum = cacheReadInputTokens || 0
+ const nonCachedInputTokens = Math.max(0, inputTokens - cacheCreationInputTokensNum - cacheReadInputTokensNum)
+ return calculateApiCostInternal(
+ modelInfo,
+ nonCachedInputTokens,
+ outputTokens,
+ cacheCreationInputTokensNum,
+ cacheReadInputTokensNum,
+ )
+}
+
export const parseApiPrice = (price: any) => (price ? parseFloat(price) * 1_000_000 : undefined)
diff --git a/webview-ui/src/App.tsx b/webview-ui/src/App.tsx
index b537b9298e..389f5709fc 100644
--- a/webview-ui/src/App.tsx
+++ b/webview-ui/src/App.tsx
@@ -17,6 +17,12 @@ import { HumanRelayDialog } from "./components/human-relay/HumanRelayDialog"
type Tab = "settings" | "history" | "mcp" | "prompts" | "chat"
+type HumanRelayDialogState = {
+ isOpen: boolean
+ requestId: string
+ promptText: string
+}
+
const tabsByMessageAction: Partial, Tab>> = {
chatButtonClicked: "chat",
settingsButtonClicked: "settings",
@@ -24,24 +30,21 @@ const tabsByMessageAction: Partial {
const { didHydrateState, showWelcome, shouldShowAnnouncement, telemetrySetting, telemetryKey, machineId } =
useExtensionState()
+
const [showAnnouncement, setShowAnnouncement] = useState(false)
const [tab, setTab] = useState("chat")
- const settingsRef = useRef(null)
-
- // Human Relay Dialog Status
- const [humanRelayDialogState, setHumanRelayDialogState] = useState<{
- isOpen: boolean
- requestId: string
- promptText: string
- }>({
+ const [humanRelayDialogState, setHumanRelayDialogState] = useState({
isOpen: false,
requestId: "",
promptText: "",
})
+ const settingsRef = useRef(null)
+
const switchTab = useCallback((newTab: Tab) => {
if (settingsRef.current?.checkUnsaveChanges) {
settingsRef.current.checkUnsaveChanges(() => setTab(newTab))
@@ -74,23 +77,6 @@ const App = () => {
[switchTab],
)
- // Processing Human Relay Dialog Submission
- const handleHumanRelaySubmit = (requestId: string, text: string) => {
- vscode.postMessage({
- type: "humanRelayResponse",
- requestId,
- text,
- })
- }
-
- // Handle Human Relay dialog box cancel
- const handleHumanRelayCancel = (requestId: string) => {
- vscode.postMessage({
- type: "humanRelayCancel",
- requestId,
- })
- }
-
useEvent("message", onMessage)
useEffect(() => {
@@ -106,7 +92,7 @@ const App = () => {
}
}, [telemetrySetting, telemetryKey, machineId, didHydrateState])
- // Tell Extension that we are ready to receive messages
+ // Tell the extension that we are ready to receive messages.
useEffect(() => {
vscode.postMessage({ type: "webviewDidLaunch" })
}, [])
@@ -121,24 +107,23 @@ const App = () => {
) : (
<>
- {tab === "settings" && setTab("chat")} />}
- {tab === "history" && switchTab("chat")} />}
- {tab === "mcp" && switchTab("chat")} />}
{tab === "prompts" && switchTab("chat")} />}
+ {tab === "mcp" && switchTab("chat")} />}
+ {tab === "history" && switchTab("chat")} />}
+ {tab === "settings" && setTab("chat")} />}
setShowAnnouncement(false)}
showHistoryView={() => switchTab("history")}
/>
- {/* Human Relay Dialog */}
setHumanRelayDialogState((prev) => ({ ...prev, isOpen: false }))}
- onSubmit={handleHumanRelaySubmit}
- onCancel={handleHumanRelayCancel}
+ onSubmit={(requestId, text) => vscode.postMessage({ type: "humanRelayResponse", requestId, text })}
+ onCancel={(requestId) => vscode.postMessage({ type: "humanRelayCancel", requestId })}
/>
>
)
diff --git a/webview-ui/src/components/chat/Announcement.tsx b/webview-ui/src/components/chat/Announcement.tsx
index 82fdb628eb..791ca26085 100644
--- a/webview-ui/src/components/chat/Announcement.tsx
+++ b/webview-ui/src/components/chat/Announcement.tsx
@@ -33,7 +33,7 @@ const Announcement = ({ version, hideAnnouncement }: AnnouncementProps) => {
What's New
-
+
• Faster asynchronous checkpoints
• Support for .rooignore files
@@ -44,7 +44,7 @@ const Announcement = ({ version, hideAnnouncement }: AnnouncementProps) => {
• Updated DeepSeek provider
• New "Human Relay" provider
-
+
Get more details and discuss in{" "}
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/ChatTextArea.tsx b/webview-ui/src/components/chat/ChatTextArea.tsx
index 3df3e87e9b..1002788dbc 100644
--- a/webview-ui/src/components/chat/ChatTextArea.tsx
+++ b/webview-ui/src/components/chat/ChatTextArea.tsx
@@ -1,22 +1,25 @@
import React, { forwardRef, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"
import DynamicTextArea from "react-textarea-autosize"
+
import { mentionRegex, mentionRegexGlobal } from "../../../../src/shared/context-mentions"
-import { useExtensionState } from "../../context/ExtensionStateContext"
+import { WebviewMessage } from "../../../../src/shared/WebviewMessage"
+import { Mode, getAllModes } from "../../../../src/shared/modes"
+
+import { vscode } from "@/utils/vscode"
import {
ContextMenuOptionType,
getContextMenuOptions,
insertMention,
removeMention,
shouldShowContextMenu,
-} from "../../utils/context-mentions"
+} from "@/utils/context-mentions"
+import { SelectDropdown, DropdownOptionType } from "@/components/ui"
+
+import { useExtensionState } from "../../context/ExtensionStateContext"
+import Thumbnails from "../common/Thumbnails"
+import { convertToMentionPath } from "../../utils/path-mentions"
import { MAX_IMAGES_PER_MESSAGE } from "./ChatView"
import ContextMenu from "./ContextMenu"
-import Thumbnails from "../common/Thumbnails"
-import { vscode } from "../../utils/vscode"
-import { WebviewMessage } from "../../../../src/shared/WebviewMessage"
-import { Mode, getAllModes } from "../../../../src/shared/modes"
-import { convertToMentionPath } from "../../utils/path-mentions"
-import { SelectDropdown, DropdownOptionType } from "../ui"
interface ChatTextAreaProps {
inputValue: string
diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx
index 35e63dd332..09dcdd3ca1 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,
],
)
@@ -1267,7 +1275,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
modeShortcutText={modeShortcutText}
/>
-
+
)
}
diff --git a/webview-ui/src/components/chat/checkpoints/CheckpointMenu.tsx b/webview-ui/src/components/chat/checkpoints/CheckpointMenu.tsx
index 1910fc0bb6..b6aaebd518 100644
--- a/webview-ui/src/components/chat/checkpoints/CheckpointMenu.tsx
+++ b/webview-ui/src/components/chat/checkpoints/CheckpointMenu.tsx
@@ -1,7 +1,8 @@
-import { useState, useEffect, useCallback } from "react"
+import { useState, useCallback } from "react"
import { CheckIcon, Cross2Icon } from "@radix-ui/react-icons"
import { Button, Popover, PopoverContent, PopoverTrigger } from "@/components/ui"
+import { useRooPortal } from "@/components/ui/hooks"
import { vscode } from "../../../utils/vscode"
import { Checkpoint } from "./schema"
@@ -14,9 +15,9 @@ type CheckpointMenuProps = {
}
export const CheckpointMenu = ({ ts, commitHash, currentHash, checkpoint }: CheckpointMenuProps) => {
- const [portalContainer, setPortalContainer] = useState()
const [isOpen, setIsOpen] = useState(false)
const [isConfirming, setIsConfirming] = useState(false)
+ const portalContainer = useRooPortal("roo-portal")
const isCurrent = currentHash === commitHash
const isFirst = checkpoint.isFirst
@@ -42,15 +43,6 @@ export const CheckpointMenu = ({ ts, commitHash, currentHash, checkpoint }: Chec
setIsOpen(false)
}, [ts, commitHash])
- useEffect(() => {
- // The dropdown menu uses a portal from @shadcn/ui which by default renders
- // at the document root. This causes the menu to remain visible even when
- // the parent ChatView component is hidden (during settings/history view).
- // By moving the portal inside ChatView, the menu will properly hide when
- // its parent is hidden.
- setPortalContainer(document.getElementById("chat-view-portal") || undefined)
- }, [])
-
return (
{isDiffAvailable && (
diff --git a/webview-ui/src/components/common/Alert.tsx b/webview-ui/src/components/common/Alert.tsx
new file mode 100644
index 0000000000..b16e799b91
--- /dev/null
+++ b/webview-ui/src/components/common/Alert.tsx
@@ -0,0 +1,15 @@
+import { cn } from "@/lib/utils"
+import { HTMLAttributes } from "react"
+
+type AlertProps = HTMLAttributes
+
+export const Alert = ({ className, children, ...props }: AlertProps) => (
+
+ {children}
+
+)
diff --git a/webview-ui/src/components/common/Tab.tsx b/webview-ui/src/components/common/Tab.tsx
new file mode 100644
index 0000000000..982fb7e103
--- /dev/null
+++ b/webview-ui/src/components/common/Tab.tsx
@@ -0,0 +1,23 @@
+import { HTMLAttributes } from "react"
+
+import { cn } from "@/lib/utils"
+
+type TabProps = HTMLAttributes
+
+export const Tab = ({ className, children, ...props }: TabProps) => (
+
+ {children}
+
+)
+
+export const TabHeader = ({ className, children, ...props }: TabProps) => (
+
+ {children}
+
+)
+
+export const TabContent = ({ className, children, ...props }: TabProps) => (
+
+ {children}
+
+)
diff --git a/webview-ui/src/components/history/HistoryView.tsx b/webview-ui/src/components/history/HistoryView.tsx
index e65a11a3ec..ec44f8eaca 100644
--- a/webview-ui/src/components/history/HistoryView.tsx
+++ b/webview-ui/src/components/history/HistoryView.tsx
@@ -9,6 +9,7 @@ import { formatLargeNumber, formatDate } from "@/utils/format"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui"
+import { Tab, TabContent, TabHeader } from "../common/Tab"
import { useTaskSearch } from "./useTaskSearch"
import { ExportButton } from "./ExportButton"
import { CopyButton } from "./CopyButton"
@@ -25,8 +26,8 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
const [deleteTaskId, setDeleteTaskId] = useState(null)
return (
-
-
+
+
History
Done
@@ -81,8 +82,9 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
-
-
+
+
+
{
)}
/>
-
+
+
{deleteTaskId && (
!open && setDeleteTaskId(null)} open />
)}
-
+
)
}
diff --git a/webview-ui/src/components/mcp/McpView.tsx b/webview-ui/src/components/mcp/McpView.tsx
index 7a24922d88..ce37a4c09d 100644
--- a/webview-ui/src/components/mcp/McpView.tsx
+++ b/webview-ui/src/components/mcp/McpView.tsx
@@ -1,3 +1,4 @@
+import { useState } from "react"
import {
VSCodeButton,
VSCodeCheckbox,
@@ -6,14 +7,17 @@ import {
VSCodePanelTab,
VSCodePanelView,
} from "@vscode/webview-ui-toolkit/react"
-import { useState } from "react"
-import { vscode } from "../../utils/vscode"
-import { useExtensionState } from "../../context/ExtensionStateContext"
+
import { McpServer } from "../../../../src/shared/mcp"
+
+import { vscode } from "@/utils/vscode"
+import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from "@/components/ui"
+
+import { useExtensionState } from "../../context/ExtensionStateContext"
+import { Tab, TabContent, TabHeader } from "../common/Tab"
import McpToolRow from "./McpToolRow"
import McpResourceRow from "./McpResourceRow"
import McpEnabledToggle from "./McpEnabledToggle"
-import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from "../ui/dialog"
type McpViewProps = {
onDone: () => void
@@ -29,12 +33,13 @@ const McpView = ({ onDone }: McpViewProps) => {
} = useExtensionState()
return (
-
-
+
+
MCP Servers
Done
-
-
-
+
+
)
}
-// Server Row Component
const ServerRow = ({ server, alwaysAllowMcp }: { server: McpServer; alwaysAllowMcp?: boolean }) => {
const [isExpanded, setIsExpanded] = useState(false)
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)
diff --git a/webview-ui/src/components/prompts/PromptsView.tsx b/webview-ui/src/components/prompts/PromptsView.tsx
index ccf1e6d700..64e33ac70a 100644
--- a/webview-ui/src/components/prompts/PromptsView.tsx
+++ b/webview-ui/src/components/prompts/PromptsView.tsx
@@ -42,6 +42,7 @@ import {
import { TOOL_GROUPS, GROUP_DISPLAY_NAMES, ToolGroup } from "../../../../src/shared/tool-groups"
import { vscode } from "../../utils/vscode"
+import { Tab, TabContent, TabHeader } from "../common/Tab"
// Get all available groups that should show in prompts view
const availableGroups = (Object.keys(TOOL_GROUPS) as ToolGroup[]).filter((group) => !TOOL_GROUPS[group].alwaysAvailable)
@@ -71,6 +72,8 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
preferredLanguage,
setPreferredLanguage,
customModes,
+ enableCustomModeCreation,
+ setEnableCustomModeCreation,
} = useExtensionState()
// Memoize modes to preserve array order
@@ -341,6 +344,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
@@ -406,12 +420,13 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
}
return (
-
-
+
+
Prompts
Done
-
-
+
+
+
Preferred Language
@@ -541,7 +556,6 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
in your workspace.
-
e.stopPropagation()} className="flex justify-between items-center mb-3">
Modes
@@ -934,6 +948,7 @@ 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.
+
+
{
)}
-
+
+
{isCreateModeDialogOpen && (
{
)}
+
{isDialogOpen && (
{
)}
+
{isCustomLanguage && (
@@ -1497,7 +1544,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
)}
-
+
)
}
diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx
index c5a02dc117..f7982080c8 100644
--- a/webview-ui/src/components/settings/ApiOptions.tsx
+++ b/webview-ui/src/components/settings/ApiOptions.tsx
@@ -1076,27 +1076,31 @@ const ApiOptions = ({
Language Model
{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..df08a03971 100644
--- a/webview-ui/src/components/settings/SettingsView.tsx
+++ b/webview-ui/src/components/settings/SettingsView.tsx
@@ -22,6 +22,7 @@ import {
Button,
} from "@/components/ui"
+import { Tab, TabContent, TabHeader } from "../common/Tab"
import { SetCachedStateField, SetExperimentEnabled } from "./types"
import { SectionHeader } from "./SectionHeader"
import ApiConfigManager from "./ApiConfigManager"
@@ -63,6 +64,7 @@ const SettingsView = forwardRef(({ onDone },
alwaysAllowExecute,
alwaysAllowMcp,
alwaysAllowModeSwitch,
+ alwaysAllowSubtasks,
alwaysAllowWrite,
alwaysApproveResubmit,
browserToolEnabled,
@@ -184,6 +186,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)
@@ -261,53 +264,41 @@ const SettingsView = forwardRef(({ onDone },
const scrollToSection = (ref: React.RefObject) => ref.current?.scrollIntoView()
return (
-
-
-
-
-
-
Settings
-
- {sections.map(({ id, icon: Icon, ref }) => (
- scrollToSection(ref)}
- className={cn("w-6 h-6", activeSection === id ? "opacity-100" : "opacity-40")}>
-
-
- ))}
-
-
-
-
- Save
-
- checkUnsaveChanges(onDone)}>
- Done
-
-
+
+
+
+
Settings
+
+ {sections.map(({ id, icon: Icon, ref }) => (
+ scrollToSection(ref)}
+ className={cn("w-6 h-6", activeSection === id ? "opacity-100" : "opacity-40")}>
+
+
+ ))}
-
+
+
+ Save
+
+ checkUnsaveChanges(onDone)}>
+ Done
+
+
+
-
+
@@ -364,6 +355,7 @@ const SettingsView = forwardRef(({ onDone },
requestDelaySeconds={requestDelaySeconds}
alwaysAllowMcp={alwaysAllowMcp}
alwaysAllowModeSwitch={alwaysAllowModeSwitch}
+ alwaysAllowSubtasks={alwaysAllowSubtasks}
alwaysAllowExecute={alwaysAllowExecute}
allowedCommands={allowedCommands}
setCachedStateField={setCachedStateField}
@@ -422,7 +414,7 @@ const SettingsView = forwardRef(({ onDone },
telemetrySetting={telemetrySetting}
setTelemetrySetting={setTelemetrySetting}
/>
-
+
@@ -439,7 +431,7 @@ const SettingsView = forwardRef(({ onDone },
-
+
)
})
diff --git a/webview-ui/src/components/ui/dropdown-menu.tsx b/webview-ui/src/components/ui/dropdown-menu.tsx
index fc5ad5b2b8..c65f18b1b5 100644
--- a/webview-ui/src/components/ui/dropdown-menu.tsx
+++ b/webview-ui/src/components/ui/dropdown-menu.tsx
@@ -1,5 +1,6 @@
import * as React from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
+import { PortalProps } from "@radix-ui/react-portal"
import { CheckIcon, ChevronRightIcon, DotFilledIcon } from "@radix-ui/react-icons"
import { cn } from "@/lib/utils"
@@ -53,9 +54,7 @@ DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayNam
const DropdownMenuContent = React.forwardRef<
React.ElementRef,
- React.ComponentPropsWithoutRef & {
- container?: HTMLElement
- }
+ React.ComponentPropsWithoutRef & Pick
>(({ className, sideOffset = 4, container, ...props }, ref) => (
{
+ const [container, setContainer] = useState()
+
+ useMount(() => setContainer(document.getElementById(id) ?? undefined))
+
+ return container
+}
diff --git a/webview-ui/src/components/ui/popover.tsx b/webview-ui/src/components/ui/popover.tsx
index 3ab3344689..9fc035ad2a 100644
--- a/webview-ui/src/components/ui/popover.tsx
+++ b/webview-ui/src/components/ui/popover.tsx
@@ -1,4 +1,5 @@
import * as React from "react"
+import { PortalProps } from "@radix-ui/react-portal"
import * as PopoverPrimitive from "@radix-ui/react-popover"
import { cn } from "@/lib/utils"
@@ -11,9 +12,7 @@ const PopoverAnchor = PopoverPrimitive.Anchor
const PopoverContent = React.forwardRef<
React.ElementRef,
- React.ComponentPropsWithoutRef & {
- container?: HTMLElement
- }
+ React.ComponentPropsWithoutRef & Pick
>(({ className, align = "center", sideOffset = 4, container, ...props }, ref) => (
{
- // Track open state
const [open, setOpen] = React.useState(false)
- const [portalContainer, setPortalContainer] = useState()
+ const portalContainer = useRooPortal("roo-portal")
- useEffect(() => {
- // The dropdown menu uses a portal from @shadcn/ui which by default renders
- // at the document root. This causes the menu to remain visible even when
- // the parent ChatView component is hidden (during settings/history view).
- // By moving the portal inside ChatView, the menu will properly hide when
- // its parent is hidden.
- setPortalContainer(document.getElementById("chat-view-portal") || undefined)
- }, [])
-
- // Find the selected option label
const selectedOption = options.find((option) => option.value === value)
const displayText = selectedOption?.label || placeholder || ""
- // Handle menu item click
const handleSelect = (option: DropdownOption) => {
- // Check if this is an action option by its explicit type
if (option.type === DropdownOptionType.ACTION) {
- window.postMessage({
- type: "action",
- action: option.value,
- })
+ window.postMessage({ type: "action", action: option.value })
setOpen(false)
return
}
+
onChange(option.value)
setOpen(false)
}
@@ -103,7 +89,7 @@ export const SelectDropdown = React.forwardRef
@@ -136,12 +122,10 @@ export const SelectDropdown = React.forwardRef
{options.map((option, index) => {
- // Handle separator type
if (option.type === DropdownOptionType.SEPARATOR) {
return
}
- // Handle shortcut text type (disabled label for keyboard shortcuts)
if (
option.type === DropdownOptionType.SHORTCUT ||
(option.disabled && shortcutText && option.label.includes(shortcutText))
@@ -153,7 +137,6 @@ export const SelectDropdown = React.forwardRef) {
+}: React.ComponentProps & Pick) {
return (
-
+
{
const { apiConfiguration, currentApiConfigName, setApiConfiguration, uriScheme } = useExtensionState()
@@ -23,18 +26,16 @@ const WelcomeView = () => {
}, [apiConfiguration, currentApiConfigName])
return (
-
-
Hi, I'm Roo!
-
- I can do all kinds of tasks thanks to the latest breakthroughs in agentic coding capabilities and access
- to tools that let me create & edit files, explore complex projects, use the browser, and execute
- terminal commands (with your permission, of course). I can even use MCP to create new tools and extend
- my own capabilities.
-
-
-
To get started, this extension needs an API provider.
-
-
+
+
+ Hi, I'm Roo!
+
+ I can do all kinds of tasks thanks to the latest breakthroughs in agentic coding capabilities and
+ access to tools that let me create & edit files, explore complex projects, use the browser, and
+ execute terminal commands (with your permission, of course). I can even use MCP to create new tools
+ and extend my own capabilities.
+
+ To get started, this extension needs an API provider.
{
errorMessage={errorMessage}
setErrorMessage={setErrorMessage}
/>
-
-
-
-
+
+
+
Let's go!
- {errorMessage &&
{errorMessage} }
+ {errorMessage &&
{errorMessage}
}
-
+
)
}
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 })),
diff --git a/webview-ui/src/index.css b/webview-ui/src/index.css
index 6cd405b93f..2144557a98 100644
--- a/webview-ui/src/index.css
+++ b/webview-ui/src/index.css
@@ -111,6 +111,10 @@
--color-vscode-charts-green: var(--vscode-charts-green);
--color-vscode-charts-yellow: var(--vscode-charts-yellow);
+
+ --color-vscode-inputValidation-infoForeground: var(--vscode-inputValidation-infoForeground);
+ --color-vscode-inputValidation-infoBackground: var(--vscode-inputValidation-infoBackground);
+ --color-vscode-inputValidation-infoBorder: var(--vscode-inputValidation-infoBorder);
}
@layer base {