From 048863e03991a3b49429a77827da2f96b32889f9 Mon Sep 17 00:00:00 2001
From: Piotr Rogowski
Date: Sun, 26 Jan 2025 08:31:07 +0100
Subject: [PATCH 01/15] Do not exclude whole project dir when listing in case
where project is places inside excluded dir (like /tmp or ~/tmp)
---
src/services/glob/list-files.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
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,
From 1cd90a655b0db197b6dd248e854eef366d148849 Mon Sep 17 00:00:00 2001
From: kohii
Date: Sun, 2 Feb 2025 10:17:57 +0900
Subject: [PATCH 02/15] feat: Add Kotlin support in list_code_definition_names
---
esbuild.js | 1 +
.../tree-sitter/__tests__/index.test.ts | 6 ++++
.../__tests__/languageParser.test.ts | 11 ++++++++
src/services/tree-sitter/index.ts | 3 ++
src/services/tree-sitter/languageParser.ts | 6 ++++
src/services/tree-sitter/queries/index.ts | 1 +
src/services/tree-sitter/queries/kotlin.ts | 28 +++++++++++++++++++
7 files changed, 56 insertions(+)
create mode 100644 src/services/tree-sitter/queries/kotlin.ts
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/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 83e02ac615..5b48da885d 100644
--- a/src/services/tree-sitter/index.ts
+++ b/src/services/tree-sitter/index.ts
@@ -73,6 +73,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
+`
From fbf65bfc6c1eeef107e9aa27245ea7bd64a5adf7 Mon Sep 17 00:00:00 2001
From: axb
Date: Wed, 12 Feb 2025 17:32:55 +0800
Subject: [PATCH 03/15] Reduce the probability of errors when the model tries
to fix the problem due to mismatched line numbers after applying diff
---
src/core/mentions/index.ts | 4 ++--
src/integrations/diagnostics/index.ts | 10 +++++++---
src/integrations/editor/DiffViewProvider.ts | 2 +-
3 files changed, 10 insertions(+), 6 deletions(-)
diff --git a/src/core/mentions/index.ts b/src/core/mentions/index.ts
index cf5bdeaae0..cf87241f23 100644
--- a/src/core/mentions/index.ts
+++ b/src/core/mentions/index.ts
@@ -186,9 +186,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/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..8f7e387c7a 100644
--- a/src/integrations/editor/DiffViewProvider.ts
+++ b/src/integrations/editor/DiffViewProvider.ts
@@ -172,7 +172,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)
From e14b1b2daba6d2ef7060846f32ca5d747bac630e Mon Sep 17 00:00:00 2001
From: Daniel Trugman
Date: Fri, 7 Mar 2025 13:53:32 +0000
Subject: [PATCH 04/15] Add OpenAI-style cost calculation
---
src/api/providers/vscode-lm.ts | 4 +-
src/core/Cline.ts | 6 +-
src/utils/__tests__/cost.test.ts | 113 ++++++++++++++++++++++++++++---
src/utils/cost.ts | 57 ++++++++++++----
4 files changed, 152 insertions(+), 28 deletions(-)
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 16f1d4e99d..4f27a89cc0 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.calculateApiCostAntrhopic(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}`)
@@ -3159,7 +3159,7 @@ export class Cline {
cacheReads: cacheReadTokens,
cost:
totalCost ??
- calculateApiCost(
+ calculateApiCostAnthropic(
this.api.getModel().info,
inputTokens,
outputTokens,
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)
From c51f59e50b6642494ed5c5addf6897e9a9827ae5 Mon Sep 17 00:00:00 2001
From: Daniel Trugman
Date: Fri, 7 Mar 2025 14:57:11 +0000
Subject: [PATCH 05/15] Requesty: Correctly calculate request costs
---
src/api/providers/__tests__/requesty.test.ts | 26 +++++++++++-----
src/api/providers/openai.ts | 6 ++--
src/api/providers/requesty.ts | 32 ++++++++++++++++----
3 files changed, 47 insertions(+), 17 deletions(-)
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 9262f3b75a..5d85a86a5a 100644
--- a/src/api/providers/openai.ts
+++ b/src/api/providers/openai.ts
@@ -111,7 +111,7 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
}
}
if (chunk.usage) {
- yield this.processUsageMetrics(chunk.usage)
+ yield this.processUsageMetrics(chunk.usage, modelInfo)
}
}
} else {
@@ -134,11 +134,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,
}
}
}
From 9f40c4d57a737db353be350ffe7c963466ed0b28 Mon Sep 17 00:00:00 2001
From: cte
Date: Sun, 9 Mar 2025 15:07:59 -0700
Subject: [PATCH 06/15] App tab layout fixes
---
.env.sample | 3 +-
webview-ui/src/App.tsx | 50 +++++-------
.../src/components/chat/Announcement.tsx | 4 +-
.../chat/checkpoints/CheckpointMenu.tsx | 14 +---
webview-ui/src/components/common/Alert.tsx | 15 ++++
webview-ui/src/components/common/Tab.tsx | 23 ++++++
.../src/components/history/HistoryView.tsx | 15 ++--
webview-ui/src/components/mcp/McpView.tsx | 26 +++---
.../src/components/prompts/PromptsView.tsx | 18 +++--
.../src/components/settings/SettingsView.tsx | 81 ++++++++-----------
.../src/components/ui/dropdown-menu.tsx | 36 +++++----
webview-ui/src/components/ui/popover.tsx | 36 +++++----
.../src/components/ui/select-dropdown.tsx | 14 +---
webview-ui/src/components/ui/select.tsx | 4 +-
.../src/components/welcome/WelcomeView.tsx | 38 ++++-----
webview-ui/src/index.css | 4 +
16 files changed, 198 insertions(+), 183 deletions(-)
create mode 100644 webview-ui/src/components/common/Alert.tsx
create mode 100644 webview-ui/src/components/common/Tab.tsx
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/webview-ui/src/App.tsx b/webview-ui/src/App.tsx
index b537b9298e..99dda495d0 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 })}
/>
>
)
@@ -147,6 +132,7 @@ const App = () => {
const AppWithProviders = () => (
+
)
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/checkpoints/CheckpointMenu.tsx b/webview-ui/src/components/chat/checkpoints/CheckpointMenu.tsx
index 1910fc0bb6..63867c9858 100644
--- a/webview-ui/src/components/chat/checkpoints/CheckpointMenu.tsx
+++ b/webview-ui/src/components/chat/checkpoints/CheckpointMenu.tsx
@@ -1,4 +1,4 @@
-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"
@@ -14,7 +14,6 @@ type CheckpointMenuProps = {
}
export const CheckpointMenu = ({ ts, commitHash, currentHash, checkpoint }: CheckpointMenuProps) => {
- const [portalContainer, setPortalContainer] = useState()
const [isOpen, setIsOpen] = useState(false)
const [isConfirming, setIsConfirming] = useState(false)
@@ -42,15 +41,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 && (
@@ -70,7 +60,7 @@ export const CheckpointMenu = ({ ts, commitHash, currentHash, checkpoint }: Chec
-
+
{!isCurrent && (
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..d51d7d8909 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)
@@ -406,12 +407,13 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
}
return (
-
-
+
+
Prompts
Done
-
-
+
+
+
Preferred Language
@@ -934,6 +936,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
+
{
)}
-
+
+
{isCreateModeDialogOpen && (
{
)}
+
{isDialogOpen && (
{
)}
+
{isCustomLanguage && (
@@ -1497,7 +1503,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..3193f497ca 100644
--- a/webview-ui/src/components/ui/dropdown-menu.tsx
+++ b/webview-ui/src/components/ui/dropdown-menu.tsx
@@ -53,23 +53,25 @@ DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayNam
const DropdownMenuContent = React.forwardRef<
React.ElementRef,
- React.ComponentPropsWithoutRef & {
- container?: HTMLElement
- }
->(({ className, sideOffset = 4, container, ...props }, ref) => (
-
-
-
-))
+ React.ComponentPropsWithoutRef
+>(({ className, sideOffset = 4, ...props }, ref) => {
+ const container = React.useMemo(() => document.getElementById("roo-portal"), [])
+
+ return (
+
+
+
+ )
+})
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
const DropdownMenuItem = React.forwardRef<
diff --git a/webview-ui/src/components/ui/popover.tsx b/webview-ui/src/components/ui/popover.tsx
index 3ab3344689..b6235853ca 100644
--- a/webview-ui/src/components/ui/popover.tsx
+++ b/webview-ui/src/components/ui/popover.tsx
@@ -11,23 +11,25 @@ const PopoverAnchor = PopoverPrimitive.Anchor
const PopoverContent = React.forwardRef<
React.ElementRef,
- React.ComponentPropsWithoutRef & {
- container?: HTMLElement
- }
->(({ className, align = "center", sideOffset = 4, container, ...props }, ref) => (
-
-
-
-))
+ React.ComponentPropsWithoutRef
+>(({ className, align = "center", sideOffset = 4, ...props }, ref) => {
+ const container = React.useMemo(() => document.getElementById("roo-portal"), [])
+
+ return (
+
+
+
+ )
+})
PopoverContent.displayName = PopoverPrimitive.Content.displayName
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
diff --git a/webview-ui/src/components/ui/select-dropdown.tsx b/webview-ui/src/components/ui/select-dropdown.tsx
index 775066732d..eef474cd02 100644
--- a/webview-ui/src/components/ui/select-dropdown.tsx
+++ b/webview-ui/src/components/ui/select-dropdown.tsx
@@ -7,7 +7,6 @@ import {
DropdownMenuSeparator,
} from "./dropdown-menu"
import { cn } from "@/lib/utils"
-import { useEffect, useState } from "react"
// Constants for option types
export enum DropdownOptionType {
@@ -39,6 +38,8 @@ export interface SelectDropdownProps {
shortcutText?: string
}
+// TODO: Get rid of this and use the native @shadcn/ui `Select` component.
+
export const SelectDropdown = React.forwardRef, SelectDropdownProps>(
(
{
@@ -60,16 +61,6 @@ export const SelectDropdown = React.forwardRef {
// Track open state
const [open, setOpen] = React.useState(false)
- const [portalContainer, setPortalContainer] = useState()
-
- 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)
@@ -130,7 +121,6 @@ export const SelectDropdown = React.forwardRef setOpen(false)}
onInteractOutside={() => setOpen(false)}
- container={portalContainer}
className={cn(
"bg-vscode-dropdown-background text-vscode-dropdown-foreground border border-vscode-dropdown-border z-50",
contentClassName,
diff --git a/webview-ui/src/components/ui/select.tsx b/webview-ui/src/components/ui/select.tsx
index 50f89b3760..5e673257cf 100644
--- a/webview-ui/src/components/ui/select.tsx
+++ b/webview-ui/src/components/ui/select.tsx
@@ -39,8 +39,10 @@ function SelectContent({
position = "popper",
...props
}: React.ComponentProps) {
+ const container = React.useMemo(() => document.getElementById("roo-portal"), [])
+
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 {
From 5f20fbbf44126f097abb64cf06b52cc399a66086 Mon Sep 17 00:00:00 2001
From: cte
Date: Sun, 9 Mar 2025 15:08:36 -0700
Subject: [PATCH 07/15] Add changeset
---
.changeset/lemon-bulldogs-unite.md | 5 +++++
1 file changed, 5 insertions(+)
create mode 100644 .changeset/lemon-bulldogs-unite.md
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
From 847d8f57f0022a39fad8edb8fca408f785dc0d22 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
Date: Sun, 9 Mar 2025 23:44:37 +0000
Subject: [PATCH 08/15] changeset version bump
---
.changeset/empty-bees-suffer.md | 5 -----
.changeset/sixty-ants-begin.md | 5 -----
CHANGELOG.md | 7 +++++++
package-lock.json | 4 ++--
package.json | 2 +-
5 files changed, 10 insertions(+), 13 deletions(-)
delete mode 100644 .changeset/empty-bees-suffer.md
delete mode 100644 .changeset/sixty-ants-begin.md
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/sixty-ants-begin.md
deleted file mode 100644
index 5ef9e04868..0000000000
--- a/.changeset/sixty-ants-begin.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"roo-cline": patch
----
-
-v3.8.4
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7afcfae2d9..ae74b346d0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,12 @@
# Roo Code Changelog
+## 3.8.4
+
+### Patch Changes
+
+- Revert tool progress for now
+- v3.8.4
+
## [3.8.3] - 2025-03-09
- Fix VS Code LM API model picker truncation issue
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",
From 831371cfa9d4354fe2d6f69c113bf3af78761d93 Mon Sep 17 00:00:00 2001
From: Matt Rubens
Date: Sun, 9 Mar 2025 19:47:22 -0400
Subject: [PATCH 09/15] Update CHANGELOG.md
---
CHANGELOG.md | 8 +++-----
1 file changed, 3 insertions(+), 5 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ae74b346d0..fd6a574dc6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,11 +1,9 @@
# Roo Code Changelog
-## 3.8.4
+## [3.8.4] - 2025-03-09
-### Patch Changes
-
-- Revert tool progress for now
-- v3.8.4
+- 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
From 9c08d044b39afb766e012d311ef984be2dba0fdf Mon Sep 17 00:00:00 2001
From: Matt Rubens
Date: Sun, 9 Mar 2025 22:55:23 -0400
Subject: [PATCH 10/15] Add telemetry for checkpoint save/restore/diff
---
src/core/Cline.ts | 6 ++++++
src/services/telemetry/TelemetryService.ts | 15 +++++++++++++++
2 files changed, 21 insertions(+)
diff --git a/src/core/Cline.ts b/src/core/Cline.ts
index 3d1f980c7d..65e053c3d1 100644
--- a/src/core/Cline.ts
+++ b/src/core/Cline.ts
@@ -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/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
From 732eeddc6c87b9135900bb6770ec4409381ba6b7 Mon Sep 17 00:00:00 2001
From: Matt Rubens
Date: Sun, 9 Mar 2025 23:18:44 -0400
Subject: [PATCH 11/15] Add model telemetry too
---
src/core/webview/ClineProvider.ts | 9 +++
.../webview/__tests__/ClineProvider.test.ts | 59 +++++++++++++++++++
2 files changed, 68 insertions(+)
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")
+ })
+})
From 9fdc546f0189dbd21999b18227cea289cab9089f Mon Sep 17 00:00:00 2001
From: cte
Date: Sun, 9 Mar 2025 21:49:29 -0700
Subject: [PATCH 12/15] Fix portal stuff
---
webview-ui/src/App.tsx | 1 -
.../src/components/chat/ChatTextArea.tsx | 19 +++++-----
webview-ui/src/components/chat/ChatView.tsx | 2 +-
.../chat/checkpoints/CheckpointMenu.tsx | 4 ++-
.../src/components/ui/dropdown-menu.tsx | 35 +++++++++----------
webview-ui/src/components/ui/hooks/index.ts | 1 +
.../src/components/ui/hooks/useRooPortal.ts | 10 ++++++
webview-ui/src/components/ui/popover.tsx | 35 +++++++++----------
.../src/components/ui/select-dropdown.tsx | 27 ++++++--------
webview-ui/src/components/ui/select.tsx | 6 ++--
10 files changed, 71 insertions(+), 69 deletions(-)
create mode 100644 webview-ui/src/components/ui/hooks/useRooPortal.ts
diff --git a/webview-ui/src/App.tsx b/webview-ui/src/App.tsx
index 99dda495d0..389f5709fc 100644
--- a/webview-ui/src/App.tsx
+++ b/webview-ui/src/App.tsx
@@ -132,7 +132,6 @@ const App = () => {
const AppWithProviders = () => (
-
)
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 63867c9858..b6aaebd518 100644
--- a/webview-ui/src/components/chat/checkpoints/CheckpointMenu.tsx
+++ b/webview-ui/src/components/chat/checkpoints/CheckpointMenu.tsx
@@ -2,6 +2,7 @@ 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"
@@ -16,6 +17,7 @@ type CheckpointMenuProps = {
export const CheckpointMenu = ({ ts, commitHash, currentHash, checkpoint }: CheckpointMenuProps) => {
const [isOpen, setIsOpen] = useState(false)
const [isConfirming, setIsConfirming] = useState(false)
+ const portalContainer = useRooPortal("roo-portal")
const isCurrent = currentHash === commitHash
const isFirst = checkpoint.isFirst
@@ -60,7 +62,7 @@ export const CheckpointMenu = ({ ts, commitHash, currentHash, checkpoint }: Chec
-
+
{!isCurrent && (
diff --git a/webview-ui/src/components/ui/dropdown-menu.tsx b/webview-ui/src/components/ui/dropdown-menu.tsx
index 3193f497ca..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,25 +54,21 @@ DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayNam
const DropdownMenuContent = React.forwardRef<
React.ElementRef
,
- React.ComponentPropsWithoutRef
->(({ className, sideOffset = 4, ...props }, ref) => {
- const container = React.useMemo(() => document.getElementById("roo-portal"), [])
-
- return (
-
-
-
- )
-})
+ React.ComponentPropsWithoutRef & Pick
+>(({ className, sideOffset = 4, container, ...props }, ref) => (
+
+
+
+))
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
const DropdownMenuItem = React.forwardRef<
diff --git a/webview-ui/src/components/ui/hooks/index.ts b/webview-ui/src/components/ui/hooks/index.ts
index 0ca9075f59..46aff4f28d 100644
--- a/webview-ui/src/components/ui/hooks/index.ts
+++ b/webview-ui/src/components/ui/hooks/index.ts
@@ -1 +1,2 @@
export * from "./useClipboard"
+export * from "./useRooPortal"
diff --git a/webview-ui/src/components/ui/hooks/useRooPortal.ts b/webview-ui/src/components/ui/hooks/useRooPortal.ts
new file mode 100644
index 0000000000..25ef139e64
--- /dev/null
+++ b/webview-ui/src/components/ui/hooks/useRooPortal.ts
@@ -0,0 +1,10 @@
+import { useState } from "react"
+import { useMount } from "react-use"
+
+export const useRooPortal = (id: string) => {
+ 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 b6235853ca..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,25 +12,21 @@ const PopoverAnchor = PopoverPrimitive.Anchor
const PopoverContent = React.forwardRef<
React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, align = "center", sideOffset = 4, ...props }, ref) => {
- const container = React.useMemo(() => document.getElementById("roo-portal"), [])
-
- return (
-
-
-
- )
-})
+ React.ComponentPropsWithoutRef & Pick
+>(({ className, align = "center", sideOffset = 4, container, ...props }, ref) => (
+
+
+
+))
PopoverContent.displayName = PopoverPrimitive.Content.displayName
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
diff --git a/webview-ui/src/components/ui/select-dropdown.tsx b/webview-ui/src/components/ui/select-dropdown.tsx
index eef474cd02..bec496ed50 100644
--- a/webview-ui/src/components/ui/select-dropdown.tsx
+++ b/webview-ui/src/components/ui/select-dropdown.tsx
@@ -1,4 +1,8 @@
import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+import { useRooPortal } from "./hooks/useRooPortal"
import {
DropdownMenu,
DropdownMenuContent,
@@ -6,9 +10,7 @@ import {
DropdownMenuTrigger,
DropdownMenuSeparator,
} from "./dropdown-menu"
-import { cn } from "@/lib/utils"
-// Constants for option types
export enum DropdownOptionType {
ITEM = "item",
SEPARATOR = "separator",
@@ -19,7 +21,7 @@ export interface DropdownOption {
value: string
label: string
disabled?: boolean
- type?: DropdownOptionType // Optional type to specify special behaviors
+ type?: DropdownOptionType
}
export interface SelectDropdownProps {
@@ -38,8 +40,6 @@ export interface SelectDropdownProps {
shortcutText?: string
}
-// TODO: Get rid of this and use the native @shadcn/ui `Select` component.
-
export const SelectDropdown = React.forwardRef, SelectDropdownProps>(
(
{
@@ -59,24 +59,19 @@ export const SelectDropdown = React.forwardRef {
- // Track open state
const [open, setOpen] = React.useState(false)
+ const portalContainer = useRooPortal("roo-portal")
- // 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)
}
@@ -94,7 +89,7 @@ export const SelectDropdown = React.forwardRef
@@ -121,17 +116,16 @@ export const SelectDropdown = React.forwardRef setOpen(false)}
onInteractOutside={() => setOpen(false)}
+ container={portalContainer}
className={cn(
"bg-vscode-dropdown-background text-vscode-dropdown-foreground border border-vscode-dropdown-border z-50",
contentClassName,
)}>
{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))
@@ -143,7 +137,6 @@ export const SelectDropdown = React.forwardRef) {
- const container = React.useMemo(() => document.getElementById("roo-portal"), [])
-
+}: React.ComponentProps & Pick) {
return (
Date: Mon, 10 Mar 2025 11:51:47 +0100
Subject: [PATCH 13/15] ci: publish git tags to Github (resolves #444)
---
.changeset/automatic-tags-publish.md | 5 +++++
.github/workflows/marketplace-publish.yml | 9 +++++++++
2 files changed, 14 insertions(+)
create mode 100644 .changeset/automatic-tags-publish.md
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/.github/workflows/marketplace-publish.yml b/.github/workflows/marketplace-publish.yml
index c6fd66b1b3..7d71b2f86f 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' &&
@@ -51,3 +53,10 @@ jobs:
npm run publish:marketplace
echo "Successfully published version $current_package_version to VS Code Marketplace"
+
+ - 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}"
From 03045d8e752c4e693f7ba652cc09d97c11c5aea5 Mon Sep 17 00:00:00 2001
From: Matt Rubens
Date: Mon, 10 Mar 2025 09:54:45 -0400
Subject: [PATCH 14/15] Update src/core/Cline.ts
---
src/core/Cline.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/core/Cline.ts b/src/core/Cline.ts
index 4f27a89cc0..7f2b77ec1e 100644
--- a/src/core/Cline.ts
+++ b/src/core/Cline.ts
@@ -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.calculateApiCostAntrhopic(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}`)
From f99fc59460f1049f53ac82b809d9b11dc9d2663f Mon Sep 17 00:00:00 2001
From: Patrick Decat
Date: Mon, 10 Mar 2025 15:29:14 +0100
Subject: [PATCH 15/15] ci: tag after packaging but before publishing
---
.github/workflows/marketplace-publish.yml | 22 ++++++++++++++--------
1 file changed, 14 insertions(+), 8 deletions(-)
diff --git a/.github/workflows/marketplace-publish.yml b/.github/workflows/marketplace-publish.yml
index 7d71b2f86f..4ecd2af7a2 100644
--- a/.github/workflows/marketplace-publish.yml
+++ b/.github/workflows/marketplace-publish.yml
@@ -25,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"
@@ -51,12 +51,18 @@ jobs:
echo "$package" | grep -q "extension/node_modules/@vscode/codicons/dist/codicon.ttf" || exit 1
echo "$package" | grep -q ".env" || exit 1
- npm run publish:marketplace
- echo "Successfully published version $current_package_version to VS Code Marketplace"
-
- 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"