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"