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/empty-bees-suffer.md b/.changeset/empty-bees-suffer.md
deleted file mode 100644
index e3d87a51ad..0000000000
--- a/.changeset/empty-bees-suffer.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"roo-cline": patch
----
-
-Revert tool progress for now
diff --git a/.changeset/sixty-ants-begin.md b/.changeset/lemon-bulldogs-unite.md
similarity index 57%
rename from .changeset/sixty-ants-begin.md
rename to .changeset/lemon-bulldogs-unite.md
index 5ef9e04868..b87abc1bd2 100644
--- a/.changeset/sixty-ants-begin.md
+++ b/.changeset/lemon-bulldogs-unite.md
@@ -2,4 +2,4 @@
"roo-cline": patch
---
-v3.8.4
+App tab layout fixes
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 7afcfae2d9..fd6a574dc6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,10 @@
# 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
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/package-lock.json b/package-lock.json
index 6884f2e626..cd961b0a21 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "roo-cline",
- "version": "3.8.3",
+ "version": "3.8.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "roo-cline",
- "version": "3.8.3",
+ "version": "3.8.4",
"dependencies": {
"@anthropic-ai/bedrock-sdk": "^0.10.2",
"@anthropic-ai/sdk": "^0.37.0",
diff --git a/package.json b/package.json
index e4adffd1d7..b32a81071d 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.3",
+ "version": "3.8.4",
"icon": "assets/icons/rocket.png",
"galleryBanner": {
"color": "#617A91",
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 3d1f980c7d..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}`)
@@ -3173,7 +3173,7 @@ export class Cline {
cacheReads: cacheReadTokens,
cost:
totalCost ??
- calculateApiCost(
+ calculateApiCostAnthropic(
this.api.getModel().info,
inputTokens,
outputTokens,
@@ -3798,6 +3798,8 @@ export class Cline {
return
}
+ telemetryService.captureCheckpointDiffed(this.taskId)
+
if (!previousCommitHash && mode === "checkpoint") {
const previousCheckpoint = this.clineMessages
.filter(({ say }) => say === "checkpoint_saved")
@@ -3849,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)
@@ -3880,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/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts
index b82a6a62e0..72cf56d35b 100644
--- a/src/core/webview/ClineProvider.ts
+++ b/src/core/webview/ClineProvider.ts
@@ -2567,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 2aab8f9115..0bf494854a 100644
--- a/src/integrations/editor/DiffViewProvider.ts
+++ b/src/integrations/editor/DiffViewProvider.ts
@@ -177,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)
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/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/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 5ac7f50559..09dcdd3ca1 100644
--- a/webview-ui/src/components/chat/ChatView.tsx
+++ b/webview-ui/src/components/chat/ChatView.tsx
@@ -1275,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 6c69bc4229..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)
@@ -419,12 +420,13 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
}
return (
-
-
+
+
Prompts
Done
-
-
+
+
+
Preferred Language
@@ -946,6 +948,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
+
{
)}
-
+
+
{isCreateModeDialogOpen && (
{
)}
+
{isDialogOpen && (
{
)}
+
{isCustomLanguage && (
@@ -1538,7 +1544,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
)}
-
+
)
}
diff --git a/webview-ui/src/components/settings/SettingsView.tsx b/webview-ui/src/components/settings/SettingsView.tsx
index 7cafbd6663..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"
@@ -263,53 +264,41 @@ const SettingsView = forwardRef(({ onDone },
const scrollToSection = (ref: React.RefObject) => ref.current?.scrollIntoView()
return (
-
-
-
-
-
-
Settings
-
- {sections.map(({ id, icon: Icon, ref }) => (
-
- ))}
-
-
-
-
- Save
-
- checkUnsaveChanges(onDone)}>
- Done
-
-
+
+
+
+
Settings
+
+ {sections.map(({ id, icon: Icon, ref }) => (
+
+ ))}
-
+
+
+ Save
+
+ checkUnsaveChanges(onDone)}>
+ Done
+
+
+
-
+
@@ -425,7 +414,7 @@ const SettingsView = forwardRef(({ onDone },
telemetrySetting={telemetrySetting}
setTelemetrySetting={setTelemetrySetting}
/>
-
+
@@ -442,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/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 {