diff --git a/.changeset/automatic-tags-publish.md b/.changeset/automatic-tags-publish.md new file mode 100644 index 0000000000..a3ff07fb4c --- /dev/null +++ b/.changeset/automatic-tags-publish.md @@ -0,0 +1,5 @@ +--- +"roo-cline": patch +--- + +Update GitHub Actions workflow to automatically create and push git tags during release diff --git a/.changeset/empty-bees-suffer.md b/.changeset/empty-bees-suffer.md deleted file mode 100644 index e3d87a51ad..0000000000 --- a/.changeset/empty-bees-suffer.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"roo-cline": patch ---- - -Revert tool progress for now diff --git a/.changeset/sixty-ants-begin.md b/.changeset/lemon-bulldogs-unite.md similarity index 57% rename from .changeset/sixty-ants-begin.md rename to .changeset/lemon-bulldogs-unite.md index 5ef9e04868..b87abc1bd2 100644 --- a/.changeset/sixty-ants-begin.md +++ b/.changeset/lemon-bulldogs-unite.md @@ -2,4 +2,4 @@ "roo-cline": patch --- -v3.8.4 +App tab layout fixes diff --git a/.env.sample b/.env.sample index 6cdaa1b3b1..4d6c24ac72 100644 --- a/.env.sample +++ b/.env.sample @@ -1,2 +1 @@ -# PostHog API Keys for telemetry -POSTHOG_API_KEY=key-goes-here \ No newline at end of file +POSTHOG_API_KEY=key-goes-here diff --git a/.github/workflows/marketplace-publish.yml b/.github/workflows/marketplace-publish.yml index c6fd66b1b3..4ecd2af7a2 100644 --- a/.github/workflows/marketplace-publish.yml +++ b/.github/workflows/marketplace-publish.yml @@ -10,6 +10,8 @@ env: jobs: publish-extension: runs-on: ubuntu-latest + permissions: + contents: write # Required for pushing tags if: > ( github.event_name == 'pull_request' && github.event.pull_request.base.ref == 'main' && @@ -23,24 +25,24 @@ jobs: - uses: actions/setup-node@v4 with: node-version: 18 + - run: | git config user.name github-actions git config user.email github-actions@github.com + - name: Install Dependencies run: | npm install -g vsce ovsx npm run install:ci + - name: Create .env file run: | echo "# PostHog API Keys for telemetry" > .env echo "POSTHOG_API_KEY=${{ secrets.POSTHOG_API_KEY }}" >> .env - - name: Package and Publish Extension - env: - VSCE_PAT: ${{ secrets.VSCE_PAT }} - OVSX_PAT: ${{ secrets.OVSX_PAT }} + + - name: Package Extension run: | current_package_version=$(node -p "require('./package.json').version") - npm run vsix package=$(unzip -l bin/roo-cline-${current_package_version}.vsix) echo "$package" @@ -49,5 +51,18 @@ jobs: echo "$package" | grep -q "extension/node_modules/@vscode/codicons/dist/codicon.ttf" || exit 1 echo "$package" | grep -q ".env" || exit 1 + - name: Create and Push Git Tag + run: | + current_package_version=$(node -p "require('./package.json').version") + git tag -a "v${current_package_version}" -m "Release v${current_package_version}" + git push origin "v${current_package_version}" + echo "Successfully created and pushed git tag v${current_package_version}" + + - name: Publish Extension + env: + VSCE_PAT: ${{ secrets.VSCE_PAT }} + OVSX_PAT: ${{ secrets.OVSX_PAT }} + run: | + current_package_version=$(node -p "require('./package.json').version") npm run publish:marketplace echo "Successfully published version $current_package_version to VS Code Marketplace" diff --git a/CHANGELOG.md b/CHANGELOG.md index 7afcfae2d9..fd6a574dc6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Roo Code Changelog +## [3.8.4] - 2025-03-09 + +- Roll back multi-diff progress indicator temporarily to fix a double-confirmation in saving edits +- Add an option in the prompts tab to save tokens by disabling the ability to ask Roo to create/edit custom modes for you (thanks @hannesrudolph!) + ## [3.8.3] - 2025-03-09 - Fix VS Code LM API model picker truncation issue diff --git a/esbuild.js b/esbuild.js index 8b203076e4..7907dd1c39 100644 --- a/esbuild.js +++ b/esbuild.js @@ -52,6 +52,7 @@ const copyWasmFiles = { "java", "php", "swift", + "kotlin", ] languages.forEach((lang) => { diff --git a/package-lock.json b/package-lock.json index 6884f2e626..cd961b0a21 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "roo-cline", - "version": "3.8.3", + "version": "3.8.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "roo-cline", - "version": "3.8.3", + "version": "3.8.4", "dependencies": { "@anthropic-ai/bedrock-sdk": "^0.10.2", "@anthropic-ai/sdk": "^0.37.0", diff --git a/package.json b/package.json index e4adffd1d7..b32a81071d 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "displayName": "Roo Code (prev. Roo Cline)", "description": "A whole dev team of AI agents in your editor.", "publisher": "RooVeterinaryInc", - "version": "3.8.3", + "version": "3.8.4", "icon": "assets/icons/rocket.png", "galleryBanner": { "color": "#617A91", diff --git a/src/api/providers/__tests__/requesty.test.ts b/src/api/providers/__tests__/requesty.test.ts index 7867b15ebc..47921a1c53 100644 --- a/src/api/providers/__tests__/requesty.test.ts +++ b/src/api/providers/__tests__/requesty.test.ts @@ -22,8 +22,10 @@ describe("RequestyHandler", () => { contextWindow: 4000, supportsPromptCache: false, supportsImages: true, - inputPrice: 0, - outputPrice: 0, + inputPrice: 1, + outputPrice: 10, + cacheReadsPrice: 0.1, + cacheWritesPrice: 1.5, }, openAiStreamingEnabled: true, includeMaxTokens: true, // Add this to match the implementation @@ -83,8 +85,12 @@ describe("RequestyHandler", () => { yield { choices: [{ delta: { content: " world" } }], usage: { - prompt_tokens: 10, - completion_tokens: 5, + prompt_tokens: 30, + completion_tokens: 10, + prompt_tokens_details: { + cached_tokens: 15, + caching_tokens: 5, + }, }, } }, @@ -105,10 +111,11 @@ describe("RequestyHandler", () => { { type: "text", text: " world" }, { type: "usage", - inputTokens: 10, - outputTokens: 5, - cacheWriteTokens: undefined, - cacheReadTokens: undefined, + inputTokens: 30, + outputTokens: 10, + cacheWriteTokens: 5, + cacheReadTokens: 15, + totalCost: 0.000119, // (10 * 1 / 1,000,000) + (5 * 1.5 / 1,000,000) + (15 * 0.1 / 1,000,000) + (10 * 10 / 1,000,000) }, ]) @@ -182,6 +189,9 @@ describe("RequestyHandler", () => { type: "usage", inputTokens: 10, outputTokens: 5, + cacheWriteTokens: 0, + cacheReadTokens: 0, + totalCost: 0.00006, // (10 * 1 / 1,000,000) + (5 * 10 / 1,000,000) }, ]) diff --git a/src/api/providers/openai.ts b/src/api/providers/openai.ts index caa99def09..2af3f2da05 100644 --- a/src/api/providers/openai.ts +++ b/src/api/providers/openai.ts @@ -116,7 +116,7 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl } } if (chunk.usage) { - yield this.processUsageMetrics(chunk.usage) + yield this.processUsageMetrics(chunk.usage, modelInfo) } } } else { @@ -139,11 +139,11 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl type: "text", text: response.choices[0]?.message.content || "", } - yield this.processUsageMetrics(response.usage) + yield this.processUsageMetrics(response.usage, modelInfo) } } - protected processUsageMetrics(usage: any): ApiStreamUsageChunk { + protected processUsageMetrics(usage: any, modelInfo?: ModelInfo): ApiStreamUsageChunk { return { type: "usage", inputTokens: usage?.prompt_tokens || 0, diff --git a/src/api/providers/requesty.ts b/src/api/providers/requesty.ts index 0060bfc5d8..434d6f4316 100644 --- a/src/api/providers/requesty.ts +++ b/src/api/providers/requesty.ts @@ -1,9 +1,20 @@ import axios from "axios" import { ModelInfo, requestyModelInfoSaneDefaults, requestyDefaultModelId } from "../../shared/api" -import { parseApiPrice } from "../../utils/cost" +import { calculateApiCostOpenAI, parseApiPrice } from "../../utils/cost" import { ApiStreamUsageChunk } from "../transform/stream" import { OpenAiHandler, OpenAiHandlerOptions } from "./openai" +import OpenAI from "openai" + +// Requesty usage includes an extra field for Anthropic use cases. +// Safely cast the prompt token details section to the appropriate structure. +interface RequestyUsage extends OpenAI.CompletionUsage { + prompt_tokens_details?: { + caching_tokens?: number + cached_tokens?: number + } + total_cost?: number +} export class RequestyHandler extends OpenAiHandler { constructor(options: OpenAiHandlerOptions) { @@ -27,13 +38,22 @@ export class RequestyHandler extends OpenAiHandler { } } - protected override processUsageMetrics(usage: any): ApiStreamUsageChunk { + protected override processUsageMetrics(usage: any, modelInfo?: ModelInfo): ApiStreamUsageChunk { + const requestyUsage = usage as RequestyUsage + const inputTokens = requestyUsage?.prompt_tokens || 0 + const outputTokens = requestyUsage?.completion_tokens || 0 + const cacheWriteTokens = requestyUsage?.prompt_tokens_details?.caching_tokens || 0 + const cacheReadTokens = requestyUsage?.prompt_tokens_details?.cached_tokens || 0 + const totalCost = modelInfo + ? calculateApiCostOpenAI(modelInfo, inputTokens, outputTokens, cacheWriteTokens, cacheReadTokens) + : 0 return { type: "usage", - inputTokens: usage?.prompt_tokens || 0, - outputTokens: usage?.completion_tokens || 0, - cacheWriteTokens: usage?.cache_creation_input_tokens, - cacheReadTokens: usage?.cache_read_input_tokens, + inputTokens: inputTokens, + outputTokens: outputTokens, + cacheWriteTokens: cacheWriteTokens, + cacheReadTokens: cacheReadTokens, + totalCost: totalCost, } } } diff --git a/src/api/providers/vscode-lm.ts b/src/api/providers/vscode-lm.ts index bf1215e238..0ce2a6e26a 100644 --- a/src/api/providers/vscode-lm.ts +++ b/src/api/providers/vscode-lm.ts @@ -2,7 +2,7 @@ import { Anthropic } from "@anthropic-ai/sdk" import * as vscode from "vscode" import { SingleCompletionHandler } from "../" -import { calculateApiCost } from "../../utils/cost" +import { calculateApiCostAnthropic } from "../../utils/cost" import { ApiStream } from "../transform/stream" import { convertToVsCodeLmMessages } from "../transform/vscode-lm-format" import { SELECTOR_SEPARATOR, stringifyVsCodeLmModelSelector } from "../../shared/vsCodeSelectorUtils" @@ -462,7 +462,7 @@ export class VsCodeLmHandler extends BaseProvider implements SingleCompletionHan type: "usage", inputTokens: totalInputTokens, outputTokens: totalOutputTokens, - totalCost: calculateApiCost(this.getModel().info, totalInputTokens, totalOutputTokens), + totalCost: calculateApiCostAnthropic(this.getModel().info, totalInputTokens, totalOutputTokens), } } catch (error: unknown) { this.ensureCleanState() diff --git a/src/core/Cline.ts b/src/core/Cline.ts index 3d1f980c7d..ba171ce3fa 100644 --- a/src/core/Cline.ts +++ b/src/core/Cline.ts @@ -55,7 +55,7 @@ import { ClineAskResponse } from "../shared/WebviewMessage" import { GlobalFileNames } from "../shared/globalFileNames" import { defaultModeSlug, getModeBySlug, getFullModeDetails } from "../shared/modes" import { EXPERIMENT_IDS, experiments as Experiments, ExperimentId } from "../shared/experiments" -import { calculateApiCost } from "../utils/cost" +import { calculateApiCostAnthropic } from "../utils/cost" import { fileExistsAtPath } from "../utils/fs" import { arePathsEqual, getReadablePath } from "../utils/path" import { parseMentions } from "./mentions" @@ -875,7 +875,7 @@ export class Cline { // The way this agentic loop works is that cline will be given a task that he then calls tools to complete. unless there's an attempt_completion call, we keep responding back to him with his tool's responses until he either attempt_completion or does not use anymore tools. If he does not use anymore tools, we ask him to consider if he's completed the task and then call attempt_completion, otherwise proceed with completing the task. // There is a MAX_REQUESTS_PER_TASK limit to prevent infinite requests, but Cline is prompted to finish the task as efficiently as he can. - //const totalCost = this.calculateApiCost(totalInputTokens, totalOutputTokens) + //const totalCost = this.calculateApiCostAnthropic(totalInputTokens, totalOutputTokens) if (didEndLoop) { // For now a task never 'completes'. This will only happen if the user hits max requests and denies resetting the count. //this.say("task_completed", `Task completed. Total API usage cost: ${totalCost}`) @@ -3173,7 +3173,7 @@ export class Cline { cacheReads: cacheReadTokens, cost: totalCost ?? - calculateApiCost( + calculateApiCostAnthropic( this.api.getModel().info, inputTokens, outputTokens, @@ -3798,6 +3798,8 @@ export class Cline { return } + telemetryService.captureCheckpointDiffed(this.taskId) + if (!previousCommitHash && mode === "checkpoint") { const previousCheckpoint = this.clineMessages .filter(({ say }) => say === "checkpoint_saved") @@ -3849,6 +3851,8 @@ export class Cline { return } + telemetryService.captureCheckpointCreated(this.taskId) + // Start the checkpoint process in the background. service.saveCheckpoint(`Task: ${this.taskId}, Time: ${Date.now()}`).catch((err) => { console.error("[Cline#checkpointSave] caught unexpected error, disabling checkpoints", err) @@ -3880,6 +3884,8 @@ export class Cline { try { await service.restoreCheckpoint(commitHash) + telemetryService.captureCheckpointRestored(this.taskId) + await this.providerRef.deref()?.postMessageToWebview({ type: "currentCheckpointUpdated", text: commitHash }) if (mode === "restore") { diff --git a/src/core/mentions/index.ts b/src/core/mentions/index.ts index d74ee0db97..445701c40f 100644 --- a/src/core/mentions/index.ts +++ b/src/core/mentions/index.ts @@ -198,9 +198,9 @@ async function getFileOrFolderContent(mentionPath: string, cwd: string): Promise } } -function getWorkspaceProblems(cwd: string): string { +async function getWorkspaceProblems(cwd: string): Promise { const diagnostics = vscode.languages.getDiagnostics() - const result = diagnosticsToProblemsString( + const result = await diagnosticsToProblemsString( diagnostics, [vscode.DiagnosticSeverity.Error, vscode.DiagnosticSeverity.Warning], cwd, diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index b82a6a62e0..72cf56d35b 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -2567,6 +2567,15 @@ export class ClineProvider implements vscode.WebviewViewProvider { properties.apiProvider = apiConfiguration.apiProvider } + // Add model ID if available + const currentCline = this.getCurrentCline() + if (currentCline?.api) { + const { id: modelId } = currentCline.api.getModel() + if (modelId) { + properties.modelId = modelId + } + } + return properties } } diff --git a/src/core/webview/__tests__/ClineProvider.test.ts b/src/core/webview/__tests__/ClineProvider.test.ts index f9fc5d3ece..2e9fcdf336 100644 --- a/src/core/webview/__tests__/ClineProvider.test.ts +++ b/src/core/webview/__tests__/ClineProvider.test.ts @@ -1652,3 +1652,62 @@ describe("ContextProxy integration", () => { expect(mockContextProxy.setValues).toBeDefined() }) }) + +describe("getTelemetryProperties", () => { + let provider: ClineProvider + let mockContext: vscode.ExtensionContext + let mockOutputChannel: vscode.OutputChannel + let mockCline: any + + beforeEach(() => { + // Reset mocks + jest.clearAllMocks() + + // Setup basic mocks + mockContext = { + globalState: { + get: jest.fn().mockImplementation((key: string) => { + if (key === "mode") return "code" + if (key === "apiProvider") return "anthropic" + return undefined + }), + update: jest.fn(), + keys: jest.fn().mockReturnValue([]), + }, + secrets: { get: jest.fn(), store: jest.fn(), delete: jest.fn() }, + extensionUri: {} as vscode.Uri, + globalStorageUri: { fsPath: "/test/path" }, + extension: { packageJSON: { version: "1.0.0" } }, + } as unknown as vscode.ExtensionContext + + mockOutputChannel = { appendLine: jest.fn() } as unknown as vscode.OutputChannel + provider = new ClineProvider(mockContext, mockOutputChannel) + + // Setup Cline instance with mocked getModel method + const { Cline } = require("../../Cline") + mockCline = new Cline() + mockCline.api = { + getModel: jest.fn().mockReturnValue({ + id: "claude-3-7-sonnet-20250219", + info: { contextWindow: 200000 }, + }), + } + }) + + test("includes basic properties in telemetry", async () => { + const properties = await provider.getTelemetryProperties() + + expect(properties).toHaveProperty("vscodeVersion") + expect(properties).toHaveProperty("platform") + expect(properties).toHaveProperty("appVersion", "1.0.0") + }) + + test("includes model ID from current Cline instance if available", async () => { + // Add mock Cline to stack + await provider.addClineToStack(mockCline) + + const properties = await provider.getTelemetryProperties() + + expect(properties).toHaveProperty("modelId", "claude-3-7-sonnet-20250219") + }) +}) diff --git a/src/integrations/diagnostics/index.ts b/src/integrations/diagnostics/index.ts index ad4ee7755c..2d829f26e7 100644 --- a/src/integrations/diagnostics/index.ts +++ b/src/integrations/diagnostics/index.ts @@ -70,11 +70,12 @@ export function getNewDiagnostics( // // - New error in file3 (1:1) // will return empty string if no problems with the given severity are found -export function diagnosticsToProblemsString( +export async function diagnosticsToProblemsString( diagnostics: [vscode.Uri, vscode.Diagnostic[]][], severities: vscode.DiagnosticSeverity[], cwd: string, -): string { +): Promise { + const documents = new Map() let result = "" for (const [uri, fileDiagnostics] of diagnostics) { const problems = fileDiagnostics.filter((d) => severities.includes(d.severity)) @@ -100,7 +101,10 @@ export function diagnosticsToProblemsString( } const line = diagnostic.range.start.line + 1 // VSCode lines are 0-indexed const source = diagnostic.source ? `${diagnostic.source} ` : "" - result += `\n- [${source}${label}] Line ${line}: ${diagnostic.message}` + const document = documents.get(uri) || (await vscode.workspace.openTextDocument(uri)) + documents.set(uri, document) + const lineContent = document.lineAt(diagnostic.range.start.line).text + result += `\n- [${source}${label}] ${line} | ${lineContent} : ${diagnostic.message}` } } } diff --git a/src/integrations/editor/DiffViewProvider.ts b/src/integrations/editor/DiffViewProvider.ts index 2aab8f9115..0bf494854a 100644 --- a/src/integrations/editor/DiffViewProvider.ts +++ b/src/integrations/editor/DiffViewProvider.ts @@ -177,7 +177,7 @@ export class DiffViewProvider { initial fix is usually correct and it may just take time for linters to catch up. */ const postDiagnostics = vscode.languages.getDiagnostics() - const newProblems = diagnosticsToProblemsString( + const newProblems = await diagnosticsToProblemsString( getNewDiagnostics(this.preDiagnostics, postDiagnostics), [ vscode.DiagnosticSeverity.Error, // only including errors since warnings can be distracting (if user wants to fix warnings they can use the @problems mention) diff --git a/src/services/glob/list-files.ts b/src/services/glob/list-files.ts index 8578b914d7..c7e3d41cf0 100644 --- a/src/services/glob/list-files.ts +++ b/src/services/glob/list-files.ts @@ -34,7 +34,7 @@ export async function listFiles(dirPath: string, recursive: boolean, limit: numb "pkg", "Pods", ".*", // '!**/.*' excludes hidden directories, while '!**/.*/**' excludes only their contents. This way we are at least aware of the existence of hidden directories. - ].map((dir) => `**/${dir}/**`) + ].map((dir) => `${dirPath}/**/${dir}/**`) const options = { cwd: dirPath, diff --git a/src/services/telemetry/TelemetryService.ts b/src/services/telemetry/TelemetryService.ts index 45a34bda4e..d3ea8bfb5f 100644 --- a/src/services/telemetry/TelemetryService.ts +++ b/src/services/telemetry/TelemetryService.ts @@ -22,6 +22,9 @@ class PostHogClient { CONVERSATION_MESSAGE: "Conversation Message", MODE_SWITCH: "Mode Switched", TOOL_USED: "Tool Used", + CHECKPOINT_CREATED: "Checkpoint Created", + CHECKPOINT_RESTORED: "Checkpoint Restored", + CHECKPOINT_DIFFED: "Checkpoint Diffed", }, } @@ -246,6 +249,18 @@ class TelemetryService { }) } + public captureCheckpointCreated(taskId: string): void { + this.captureEvent(PostHogClient.EVENTS.TASK.CHECKPOINT_CREATED, { taskId }) + } + + public captureCheckpointDiffed(taskId: string): void { + this.captureEvent(PostHogClient.EVENTS.TASK.CHECKPOINT_DIFFED, { taskId }) + } + + public captureCheckpointRestored(taskId: string): void { + this.captureEvent(PostHogClient.EVENTS.TASK.CHECKPOINT_RESTORED, { taskId }) + } + /** * Checks if telemetry is currently enabled * @returns Whether telemetry is enabled diff --git a/src/services/tree-sitter/__tests__/index.test.ts b/src/services/tree-sitter/__tests__/index.test.ts index 4a5782dcb1..8372e7e580 100644 --- a/src/services/tree-sitter/__tests__/index.test.ts +++ b/src/services/tree-sitter/__tests__/index.test.ts @@ -169,6 +169,8 @@ describe("Tree-sitter Service", () => { "/test/path/main.rs", "/test/path/program.cpp", "/test/path/code.go", + "/test/path/app.kt", + "/test/path/script.kts", ] ;(listFiles as jest.Mock).mockResolvedValue([mockFiles, new Set()]) @@ -197,6 +199,8 @@ describe("Tree-sitter Service", () => { rs: { parser: mockParser, query: mockQuery }, cpp: { parser: mockParser, query: mockQuery }, go: { parser: mockParser, query: mockQuery }, + kt: { parser: mockParser, query: mockQuery }, + kts: { parser: mockParser, query: mockQuery }, }) ;(fs.readFile as jest.Mock).mockResolvedValue("function test() {}") @@ -207,6 +211,8 @@ describe("Tree-sitter Service", () => { expect(result).toContain("main.rs") expect(result).toContain("program.cpp") expect(result).toContain("code.go") + expect(result).toContain("app.kt") + expect(result).toContain("script.kts") }) it("should normalize paths in output", async () => { diff --git a/src/services/tree-sitter/__tests__/languageParser.test.ts b/src/services/tree-sitter/__tests__/languageParser.test.ts index 1b92d81b6b..54271e30e8 100644 --- a/src/services/tree-sitter/__tests__/languageParser.test.ts +++ b/src/services/tree-sitter/__tests__/languageParser.test.ts @@ -92,6 +92,17 @@ describe("Language Parser", () => { expect(parsers.hpp).toBeDefined() }) + it("should handle Kotlin files correctly", async () => { + const files = ["test.kt", "test.kts"] + const parsers = await loadRequiredLanguageParsers(files) + + expect(ParserMock.Language.load).toHaveBeenCalledWith(expect.stringContaining("tree-sitter-kotlin.wasm")) + expect(parsers.kt).toBeDefined() + expect(parsers.kts).toBeDefined() + expect(parsers.kt.query).toBeDefined() + expect(parsers.kts.query).toBeDefined() + }) + it("should throw error for unsupported file extensions", async () => { const files = ["test.unsupported"] diff --git a/src/services/tree-sitter/index.ts b/src/services/tree-sitter/index.ts index bfdfa6c52c..9aaa672ce2 100644 --- a/src/services/tree-sitter/index.ts +++ b/src/services/tree-sitter/index.ts @@ -80,6 +80,9 @@ function separateFiles(allFiles: string[]): { filesToParse: string[]; remainingF "java", "php", "swift", + // Kotlin + "kt", + "kts", ].map((e) => `.${e}`) const filesToParse = allFiles.filter((file) => extensions.includes(path.extname(file))).slice(0, 50) // 50 files max const remainingFiles = allFiles.filter((file) => !filesToParse.includes(file)) diff --git a/src/services/tree-sitter/languageParser.ts b/src/services/tree-sitter/languageParser.ts index 2d791b39a8..f256b0b62a 100644 --- a/src/services/tree-sitter/languageParser.ts +++ b/src/services/tree-sitter/languageParser.ts @@ -13,6 +13,7 @@ import { javaQuery, phpQuery, swiftQuery, + kotlinQuery, } from "./queries" export interface LanguageParser { @@ -120,6 +121,11 @@ export async function loadRequiredLanguageParsers(filesToParse: string[]): Promi language = await loadLanguage("swift") query = language.query(swiftQuery) break + case "kt": + case "kts": + language = await loadLanguage("kotlin") + query = language.query(kotlinQuery) + break default: throw new Error(`Unsupported language: ${ext}`) } diff --git a/src/services/tree-sitter/queries/index.ts b/src/services/tree-sitter/queries/index.ts index 889210a8e5..818eacca01 100644 --- a/src/services/tree-sitter/queries/index.ts +++ b/src/services/tree-sitter/queries/index.ts @@ -10,3 +10,4 @@ export { default as cQuery } from "./c" export { default as csharpQuery } from "./c-sharp" export { default as goQuery } from "./go" export { default as swiftQuery } from "./swift" +export { default as kotlinQuery } from "./kotlin" diff --git a/src/services/tree-sitter/queries/kotlin.ts b/src/services/tree-sitter/queries/kotlin.ts new file mode 100644 index 0000000000..61eb112448 --- /dev/null +++ b/src/services/tree-sitter/queries/kotlin.ts @@ -0,0 +1,28 @@ +/* +- class declarations (including interfaces) +- function declarations +- object declarations +- property declarations +- type alias declarations +*/ +export default ` +(class_declaration + (type_identifier) @name.definition.class +) @definition.class + +(function_declaration + (simple_identifier) @name.definition.function +) @definition.function + +(object_declaration + (type_identifier) @name.definition.object +) @definition.object + +(property_declaration + (simple_identifier) @name.definition.property +) @definition.property + +(type_alias + (type_identifier) @name.definition.type +) @definition.type +` diff --git a/src/utils/__tests__/cost.test.ts b/src/utils/__tests__/cost.test.ts index e390c4af7f..4501f86b88 100644 --- a/src/utils/__tests__/cost.test.ts +++ b/src/utils/__tests__/cost.test.ts @@ -1,8 +1,8 @@ -import { calculateApiCost } from "../cost" +import { calculateApiCostAnthropic, calculateApiCostOpenAI } from "../cost" import { ModelInfo } from "../../shared/api" describe("Cost Utility", () => { - describe("calculateApiCost", () => { + describe("calculateApiCostAnthropic", () => { const mockModelInfo: ModelInfo = { maxTokens: 8192, contextWindow: 200_000, @@ -14,7 +14,7 @@ describe("Cost Utility", () => { } it("should calculate basic input/output costs correctly", () => { - const cost = calculateApiCost(mockModelInfo, 1000, 500) + const cost = calculateApiCostAnthropic(mockModelInfo, 1000, 500) // Input cost: (3.0 / 1_000_000) * 1000 = 0.003 // Output cost: (15.0 / 1_000_000) * 500 = 0.0075 @@ -23,7 +23,7 @@ describe("Cost Utility", () => { }) it("should handle cache writes cost", () => { - const cost = calculateApiCost(mockModelInfo, 1000, 500, 2000) + const cost = calculateApiCostAnthropic(mockModelInfo, 1000, 500, 2000) // Input cost: (3.0 / 1_000_000) * 1000 = 0.003 // Output cost: (15.0 / 1_000_000) * 500 = 0.0075 @@ -33,7 +33,7 @@ describe("Cost Utility", () => { }) it("should handle cache reads cost", () => { - const cost = calculateApiCost(mockModelInfo, 1000, 500, undefined, 3000) + const cost = calculateApiCostAnthropic(mockModelInfo, 1000, 500, undefined, 3000) // Input cost: (3.0 / 1_000_000) * 1000 = 0.003 // Output cost: (15.0 / 1_000_000) * 500 = 0.0075 @@ -43,7 +43,7 @@ describe("Cost Utility", () => { }) it("should handle all cost components together", () => { - const cost = calculateApiCost(mockModelInfo, 1000, 500, 2000, 3000) + const cost = calculateApiCostAnthropic(mockModelInfo, 1000, 500, 2000, 3000) // Input cost: (3.0 / 1_000_000) * 1000 = 0.003 // Output cost: (15.0 / 1_000_000) * 500 = 0.0075 @@ -60,17 +60,17 @@ describe("Cost Utility", () => { supportsPromptCache: true, } - const cost = calculateApiCost(modelWithoutPrices, 1000, 500, 2000, 3000) + const cost = calculateApiCostAnthropic(modelWithoutPrices, 1000, 500, 2000, 3000) expect(cost).toBe(0) }) it("should handle zero tokens", () => { - const cost = calculateApiCost(mockModelInfo, 0, 0, 0, 0) + const cost = calculateApiCostAnthropic(mockModelInfo, 0, 0, 0, 0) expect(cost).toBe(0) }) it("should handle undefined cache values", () => { - const cost = calculateApiCost(mockModelInfo, 1000, 500) + const cost = calculateApiCostAnthropic(mockModelInfo, 1000, 500) // Input cost: (3.0 / 1_000_000) * 1000 = 0.003 // Output cost: (15.0 / 1_000_000) * 500 = 0.0075 @@ -85,7 +85,7 @@ describe("Cost Utility", () => { cacheReadsPrice: undefined, } - const cost = calculateApiCost(modelWithoutCachePrices, 1000, 500, 2000, 3000) + const cost = calculateApiCostAnthropic(modelWithoutCachePrices, 1000, 500, 2000, 3000) // Should only include input and output costs // Input cost: (3.0 / 1_000_000) * 1000 = 0.003 @@ -94,4 +94,97 @@ describe("Cost Utility", () => { expect(cost).toBe(0.0105) }) }) + + describe("calculateApiCostOpenAI", () => { + const mockModelInfo: ModelInfo = { + maxTokens: 8192, + contextWindow: 200_000, + supportsPromptCache: true, + inputPrice: 3.0, // $3 per million tokens + outputPrice: 15.0, // $15 per million tokens + cacheWritesPrice: 3.75, // $3.75 per million tokens + cacheReadsPrice: 0.3, // $0.30 per million tokens + } + + it("should calculate basic input/output costs correctly", () => { + const cost = calculateApiCostOpenAI(mockModelInfo, 1000, 500) + + // Input cost: (3.0 / 1_000_000) * 1000 = 0.003 + // Output cost: (15.0 / 1_000_000) * 500 = 0.0075 + // Total: 0.003 + 0.0075 = 0.0105 + expect(cost).toBe(0.0105) + }) + + it("should handle cache writes cost", () => { + const cost = calculateApiCostOpenAI(mockModelInfo, 3000, 500, 2000) + + // Input cost: (3.0 / 1_000_000) * (3000 - 2000) = 0.003 + // Output cost: (15.0 / 1_000_000) * 500 = 0.0075 + // Cache writes: (3.75 / 1_000_000) * 2000 = 0.0075 + // Total: 0.003 + 0.0075 + 0.0075 = 0.018 + expect(cost).toBeCloseTo(0.018, 6) + }) + + it("should handle cache reads cost", () => { + const cost = calculateApiCostOpenAI(mockModelInfo, 4000, 500, undefined, 3000) + + // Input cost: (3.0 / 1_000_000) * (4000 - 3000) = 0.003 + // Output cost: (15.0 / 1_000_000) * 500 = 0.0075 + // Cache reads: (0.3 / 1_000_000) * 3000 = 0.0009 + // Total: 0.003 + 0.0075 + 0.0009 = 0.0114 + expect(cost).toBe(0.0114) + }) + + it("should handle all cost components together", () => { + const cost = calculateApiCostOpenAI(mockModelInfo, 6000, 500, 2000, 3000) + + // Input cost: (3.0 / 1_000_000) * (6000 - 2000 - 3000) = 0.003 + // Output cost: (15.0 / 1_000_000) * 500 = 0.0075 + // Cache writes: (3.75 / 1_000_000) * 2000 = 0.0075 + // Cache reads: (0.3 / 1_000_000) * 3000 = 0.0009 + // Total: 0.003 + 0.0075 + 0.0075 + 0.0009 = 0.0189 + expect(cost).toBe(0.0189) + }) + + it("should handle missing prices gracefully", () => { + const modelWithoutPrices: ModelInfo = { + maxTokens: 8192, + contextWindow: 200_000, + supportsPromptCache: true, + } + + const cost = calculateApiCostOpenAI(modelWithoutPrices, 1000, 500, 2000, 3000) + expect(cost).toBe(0) + }) + + it("should handle zero tokens", () => { + const cost = calculateApiCostOpenAI(mockModelInfo, 0, 0, 0, 0) + expect(cost).toBe(0) + }) + + it("should handle undefined cache values", () => { + const cost = calculateApiCostOpenAI(mockModelInfo, 1000, 500) + + // Input cost: (3.0 / 1_000_000) * 1000 = 0.003 + // Output cost: (15.0 / 1_000_000) * 500 = 0.0075 + // Total: 0.003 + 0.0075 = 0.0105 + expect(cost).toBe(0.0105) + }) + + it("should handle missing cache prices", () => { + const modelWithoutCachePrices: ModelInfo = { + ...mockModelInfo, + cacheWritesPrice: undefined, + cacheReadsPrice: undefined, + } + + const cost = calculateApiCostOpenAI(modelWithoutCachePrices, 6000, 500, 2000, 3000) + + // Should only include input and output costs + // Input cost: (3.0 / 1_000_000) * (6000 - 2000 - 3000) = 0.003 + // Output cost: (15.0 / 1_000_000) * 500 = 0.0075 + // Total: 0.003 + 0.0075 = 0.0105 + expect(cost).toBe(0.0105) + }) + }) }) diff --git a/src/utils/cost.ts b/src/utils/cost.ts index adc2ded0a8..48108b6348 100644 --- a/src/utils/cost.ts +++ b/src/utils/cost.ts @@ -1,26 +1,57 @@ import { ModelInfo } from "../shared/api" -export function calculateApiCost( +function calculateApiCostInternal( modelInfo: ModelInfo, inputTokens: number, outputTokens: number, - cacheCreationInputTokens?: number, - cacheReadInputTokens?: number, + cacheCreationInputTokens: number, + cacheReadInputTokens: number, ): number { - const modelCacheWritesPrice = modelInfo.cacheWritesPrice - let cacheWritesCost = 0 - if (cacheCreationInputTokens && modelCacheWritesPrice) { - cacheWritesCost = (modelCacheWritesPrice / 1_000_000) * cacheCreationInputTokens - } - const modelCacheReadsPrice = modelInfo.cacheReadsPrice - let cacheReadsCost = 0 - if (cacheReadInputTokens && modelCacheReadsPrice) { - cacheReadsCost = (modelCacheReadsPrice / 1_000_000) * cacheReadInputTokens - } + const cacheWritesCost = ((modelInfo.cacheWritesPrice || 0) / 1_000_000) * cacheCreationInputTokens + const cacheReadsCost = ((modelInfo.cacheReadsPrice || 0) / 1_000_000) * cacheReadInputTokens const baseInputCost = ((modelInfo.inputPrice || 0) / 1_000_000) * inputTokens const outputCost = ((modelInfo.outputPrice || 0) / 1_000_000) * outputTokens const totalCost = cacheWritesCost + cacheReadsCost + baseInputCost + outputCost return totalCost } +// For Anthropic compliant usage, the input tokens count does NOT include the cached tokens +export function calculateApiCostAnthropic( + modelInfo: ModelInfo, + inputTokens: number, + outputTokens: number, + cacheCreationInputTokens?: number, + cacheReadInputTokens?: number, +): number { + const cacheCreationInputTokensNum = cacheCreationInputTokens || 0 + const cacheReadInputTokensNum = cacheReadInputTokens || 0 + return calculateApiCostInternal( + modelInfo, + inputTokens, + outputTokens, + cacheCreationInputTokensNum, + cacheReadInputTokensNum, + ) +} + +// For OpenAI compliant usage, the input tokens count INCLUDES the cached tokens +export function calculateApiCostOpenAI( + modelInfo: ModelInfo, + inputTokens: number, + outputTokens: number, + cacheCreationInputTokens?: number, + cacheReadInputTokens?: number, +): number { + const cacheCreationInputTokensNum = cacheCreationInputTokens || 0 + const cacheReadInputTokensNum = cacheReadInputTokens || 0 + const nonCachedInputTokens = Math.max(0, inputTokens - cacheCreationInputTokensNum - cacheReadInputTokensNum) + return calculateApiCostInternal( + modelInfo, + nonCachedInputTokens, + outputTokens, + cacheCreationInputTokensNum, + cacheReadInputTokensNum, + ) +} + export const parseApiPrice = (price: any) => (price ? parseFloat(price) * 1_000_000 : undefined) diff --git a/webview-ui/src/App.tsx b/webview-ui/src/App.tsx index b537b9298e..389f5709fc 100644 --- a/webview-ui/src/App.tsx +++ b/webview-ui/src/App.tsx @@ -17,6 +17,12 @@ import { HumanRelayDialog } from "./components/human-relay/HumanRelayDialog" type Tab = "settings" | "history" | "mcp" | "prompts" | "chat" +type HumanRelayDialogState = { + isOpen: boolean + requestId: string + promptText: string +} + const tabsByMessageAction: Partial, Tab>> = { chatButtonClicked: "chat", settingsButtonClicked: "settings", @@ -24,24 +30,21 @@ const tabsByMessageAction: Partial { const { didHydrateState, showWelcome, shouldShowAnnouncement, telemetrySetting, telemetryKey, machineId } = useExtensionState() + const [showAnnouncement, setShowAnnouncement] = useState(false) const [tab, setTab] = useState("chat") - const settingsRef = useRef(null) - - // Human Relay Dialog Status - const [humanRelayDialogState, setHumanRelayDialogState] = useState<{ - isOpen: boolean - requestId: string - promptText: string - }>({ + const [humanRelayDialogState, setHumanRelayDialogState] = useState({ isOpen: false, requestId: "", promptText: "", }) + const settingsRef = useRef(null) + const switchTab = useCallback((newTab: Tab) => { if (settingsRef.current?.checkUnsaveChanges) { settingsRef.current.checkUnsaveChanges(() => setTab(newTab)) @@ -74,23 +77,6 @@ const App = () => { [switchTab], ) - // Processing Human Relay Dialog Submission - const handleHumanRelaySubmit = (requestId: string, text: string) => { - vscode.postMessage({ - type: "humanRelayResponse", - requestId, - text, - }) - } - - // Handle Human Relay dialog box cancel - const handleHumanRelayCancel = (requestId: string) => { - vscode.postMessage({ - type: "humanRelayCancel", - requestId, - }) - } - useEvent("message", onMessage) useEffect(() => { @@ -106,7 +92,7 @@ const App = () => { } }, [telemetrySetting, telemetryKey, machineId, didHydrateState]) - // Tell Extension that we are ready to receive messages + // Tell the extension that we are ready to receive messages. useEffect(() => { vscode.postMessage({ type: "webviewDidLaunch" }) }, []) @@ -121,24 +107,23 @@ const App = () => { ) : ( <> - {tab === "settings" && setTab("chat")} />} - {tab === "history" && switchTab("chat")} />} - {tab === "mcp" && switchTab("chat")} />} {tab === "prompts" && switchTab("chat")} />} + {tab === "mcp" && switchTab("chat")} />} + {tab === "history" && switchTab("chat")} />} + {tab === "settings" && setTab("chat")} />} setShowAnnouncement(false)} showHistoryView={() => switchTab("history")} /> - {/* Human Relay Dialog */} setHumanRelayDialogState((prev) => ({ ...prev, isOpen: false }))} - onSubmit={handleHumanRelaySubmit} - onCancel={handleHumanRelayCancel} + onSubmit={(requestId, text) => vscode.postMessage({ type: "humanRelayResponse", requestId, text })} + onCancel={(requestId) => vscode.postMessage({ type: "humanRelayCancel", requestId })} /> ) diff --git a/webview-ui/src/components/chat/Announcement.tsx b/webview-ui/src/components/chat/Announcement.tsx index 82fdb628eb..791ca26085 100644 --- a/webview-ui/src/components/chat/Announcement.tsx +++ b/webview-ui/src/components/chat/Announcement.tsx @@ -33,7 +33,7 @@ const Announcement = ({ version, hideAnnouncement }: AnnouncementProps) => {

What's New

-

+

  • • Faster asynchronous checkpoints
  • • Support for .rooignore files
  • @@ -44,7 +44,7 @@ const Announcement = ({ version, hideAnnouncement }: AnnouncementProps) => {
  • • Updated DeepSeek provider
  • • New "Human Relay" provider
-

+

Get more details and discuss in{" "} diff --git a/webview-ui/src/components/chat/ChatTextArea.tsx b/webview-ui/src/components/chat/ChatTextArea.tsx index 3df3e87e9b..1002788dbc 100644 --- a/webview-ui/src/components/chat/ChatTextArea.tsx +++ b/webview-ui/src/components/chat/ChatTextArea.tsx @@ -1,22 +1,25 @@ import React, { forwardRef, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react" import DynamicTextArea from "react-textarea-autosize" + import { mentionRegex, mentionRegexGlobal } from "../../../../src/shared/context-mentions" -import { useExtensionState } from "../../context/ExtensionStateContext" +import { WebviewMessage } from "../../../../src/shared/WebviewMessage" +import { Mode, getAllModes } from "../../../../src/shared/modes" + +import { vscode } from "@/utils/vscode" import { ContextMenuOptionType, getContextMenuOptions, insertMention, removeMention, shouldShowContextMenu, -} from "../../utils/context-mentions" +} from "@/utils/context-mentions" +import { SelectDropdown, DropdownOptionType } from "@/components/ui" + +import { useExtensionState } from "../../context/ExtensionStateContext" +import Thumbnails from "../common/Thumbnails" +import { convertToMentionPath } from "../../utils/path-mentions" import { MAX_IMAGES_PER_MESSAGE } from "./ChatView" import ContextMenu from "./ContextMenu" -import Thumbnails from "../common/Thumbnails" -import { vscode } from "../../utils/vscode" -import { WebviewMessage } from "../../../../src/shared/WebviewMessage" -import { Mode, getAllModes } from "../../../../src/shared/modes" -import { convertToMentionPath } from "../../utils/path-mentions" -import { SelectDropdown, DropdownOptionType } from "../ui" interface ChatTextAreaProps { inputValue: string diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx index 5ac7f50559..09dcdd3ca1 100644 --- a/webview-ui/src/components/chat/ChatView.tsx +++ b/webview-ui/src/components/chat/ChatView.tsx @@ -1275,7 +1275,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie modeShortcutText={modeShortcutText} /> -

+
) } diff --git a/webview-ui/src/components/chat/checkpoints/CheckpointMenu.tsx b/webview-ui/src/components/chat/checkpoints/CheckpointMenu.tsx index 1910fc0bb6..b6aaebd518 100644 --- a/webview-ui/src/components/chat/checkpoints/CheckpointMenu.tsx +++ b/webview-ui/src/components/chat/checkpoints/CheckpointMenu.tsx @@ -1,7 +1,8 @@ -import { useState, useEffect, useCallback } from "react" +import { useState, useCallback } from "react" import { CheckIcon, Cross2Icon } from "@radix-ui/react-icons" import { Button, Popover, PopoverContent, PopoverTrigger } from "@/components/ui" +import { useRooPortal } from "@/components/ui/hooks" import { vscode } from "../../../utils/vscode" import { Checkpoint } from "./schema" @@ -14,9 +15,9 @@ type CheckpointMenuProps = { } export const CheckpointMenu = ({ ts, commitHash, currentHash, checkpoint }: CheckpointMenuProps) => { - const [portalContainer, setPortalContainer] = useState() const [isOpen, setIsOpen] = useState(false) const [isConfirming, setIsConfirming] = useState(false) + const portalContainer = useRooPortal("roo-portal") const isCurrent = currentHash === commitHash const isFirst = checkpoint.isFirst @@ -42,15 +43,6 @@ export const CheckpointMenu = ({ ts, commitHash, currentHash, checkpoint }: Chec setIsOpen(false) }, [ts, commitHash]) - useEffect(() => { - // The dropdown menu uses a portal from @shadcn/ui which by default renders - // at the document root. This causes the menu to remain visible even when - // the parent ChatView component is hidden (during settings/history view). - // By moving the portal inside ChatView, the menu will properly hide when - // its parent is hidden. - setPortalContainer(document.getElementById("chat-view-portal") || undefined) - }, []) - return (
{isDiffAvailable && ( diff --git a/webview-ui/src/components/common/Alert.tsx b/webview-ui/src/components/common/Alert.tsx new file mode 100644 index 0000000000..b16e799b91 --- /dev/null +++ b/webview-ui/src/components/common/Alert.tsx @@ -0,0 +1,15 @@ +import { cn } from "@/lib/utils" +import { HTMLAttributes } from "react" + +type AlertProps = HTMLAttributes + +export const Alert = ({ className, children, ...props }: AlertProps) => ( +
+ {children} +
+) diff --git a/webview-ui/src/components/common/Tab.tsx b/webview-ui/src/components/common/Tab.tsx new file mode 100644 index 0000000000..982fb7e103 --- /dev/null +++ b/webview-ui/src/components/common/Tab.tsx @@ -0,0 +1,23 @@ +import { HTMLAttributes } from "react" + +import { cn } from "@/lib/utils" + +type TabProps = HTMLAttributes + +export const Tab = ({ className, children, ...props }: TabProps) => ( +
+ {children} +
+) + +export const TabHeader = ({ className, children, ...props }: TabProps) => ( +
+ {children} +
+) + +export const TabContent = ({ className, children, ...props }: TabProps) => ( +
+ {children} +
+) diff --git a/webview-ui/src/components/history/HistoryView.tsx b/webview-ui/src/components/history/HistoryView.tsx index e65a11a3ec..ec44f8eaca 100644 --- a/webview-ui/src/components/history/HistoryView.tsx +++ b/webview-ui/src/components/history/HistoryView.tsx @@ -9,6 +9,7 @@ import { formatLargeNumber, formatDate } from "@/utils/format" import { cn } from "@/lib/utils" import { Button } from "@/components/ui" +import { Tab, TabContent, TabHeader } from "../common/Tab" import { useTaskSearch } from "./useTaskSearch" import { ExportButton } from "./ExportButton" import { CopyButton } from "./CopyButton" @@ -25,8 +26,8 @@ const HistoryView = ({ onDone }: HistoryViewProps) => { const [deleteTaskId, setDeleteTaskId] = useState(null) return ( -
-
+ +

History

Done @@ -81,8 +82,9 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
-
-
+ + + {
)} /> -
+ + {deleteTaskId && ( !open && setDeleteTaskId(null)} open /> )} -
+ ) } diff --git a/webview-ui/src/components/mcp/McpView.tsx b/webview-ui/src/components/mcp/McpView.tsx index 7a24922d88..ce37a4c09d 100644 --- a/webview-ui/src/components/mcp/McpView.tsx +++ b/webview-ui/src/components/mcp/McpView.tsx @@ -1,3 +1,4 @@ +import { useState } from "react" import { VSCodeButton, VSCodeCheckbox, @@ -6,14 +7,17 @@ import { VSCodePanelTab, VSCodePanelView, } from "@vscode/webview-ui-toolkit/react" -import { useState } from "react" -import { vscode } from "../../utils/vscode" -import { useExtensionState } from "../../context/ExtensionStateContext" + import { McpServer } from "../../../../src/shared/mcp" + +import { vscode } from "@/utils/vscode" +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from "@/components/ui" + +import { useExtensionState } from "../../context/ExtensionStateContext" +import { Tab, TabContent, TabHeader } from "../common/Tab" import McpToolRow from "./McpToolRow" import McpResourceRow from "./McpResourceRow" import McpEnabledToggle from "./McpEnabledToggle" -import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from "../ui/dialog" type McpViewProps = { onDone: () => void @@ -29,12 +33,13 @@ const McpView = ({ onDone }: McpViewProps) => { } = useExtensionState() return ( -
-
+ +

MCP Servers

Done -
-
+ + +
{
)} -
-
+ + ) } -// Server Row Component const ServerRow = ({ server, alwaysAllowMcp }: { server: McpServer; alwaysAllowMcp?: boolean }) => { const [isExpanded, setIsExpanded] = useState(false) const [showDeleteConfirm, setShowDeleteConfirm] = useState(false) diff --git a/webview-ui/src/components/prompts/PromptsView.tsx b/webview-ui/src/components/prompts/PromptsView.tsx index 6c69bc4229..64e33ac70a 100644 --- a/webview-ui/src/components/prompts/PromptsView.tsx +++ b/webview-ui/src/components/prompts/PromptsView.tsx @@ -42,6 +42,7 @@ import { import { TOOL_GROUPS, GROUP_DISPLAY_NAMES, ToolGroup } from "../../../../src/shared/tool-groups" import { vscode } from "../../utils/vscode" +import { Tab, TabContent, TabHeader } from "../common/Tab" // Get all available groups that should show in prompts view const availableGroups = (Object.keys(TOOL_GROUPS) as ToolGroup[]).filter((group) => !TOOL_GROUPS[group].alwaysAvailable) @@ -419,12 +420,13 @@ const PromptsView = ({ onDone }: PromptsViewProps) => { } return ( -
-
+ +

Prompts

Done -
-
+ + +
Preferred Language
@@ -946,6 +948,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
+
{ )}
-
+ + {isCreateModeDialogOpen && (
{
)} + {isDialogOpen && (
{
)} + {isCustomLanguage && (
@@ -1538,7 +1544,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
)} - +
) } diff --git a/webview-ui/src/components/settings/SettingsView.tsx b/webview-ui/src/components/settings/SettingsView.tsx index 7cafbd6663..df08a03971 100644 --- a/webview-ui/src/components/settings/SettingsView.tsx +++ b/webview-ui/src/components/settings/SettingsView.tsx @@ -22,6 +22,7 @@ import { Button, } from "@/components/ui" +import { Tab, TabContent, TabHeader } from "../common/Tab" import { SetCachedStateField, SetExperimentEnabled } from "./types" import { SectionHeader } from "./SectionHeader" import ApiConfigManager from "./ApiConfigManager" @@ -263,53 +264,41 @@ const SettingsView = forwardRef(({ onDone }, const scrollToSection = (ref: React.RefObject) => ref.current?.scrollIntoView() return ( -
-
-
-
-
-

Settings

-
- {sections.map(({ id, icon: Icon, ref }) => ( - - ))} -
-
-
- - Save - - checkUnsaveChanges(onDone)}> - Done - -
+ + +
+

Settings

+
+ {sections.map(({ id, icon: Icon, ref }) => ( + + ))}
-
+
+ + Save + + checkUnsaveChanges(onDone)}> + Done + +
+ -
+
@@ -425,7 +414,7 @@ const SettingsView = forwardRef(({ onDone }, telemetrySetting={telemetrySetting} setTelemetrySetting={setTelemetrySetting} /> -
+ @@ -442,7 +431,7 @@ const SettingsView = forwardRef(({ onDone }, -
+ ) }) diff --git a/webview-ui/src/components/ui/dropdown-menu.tsx b/webview-ui/src/components/ui/dropdown-menu.tsx index fc5ad5b2b8..c65f18b1b5 100644 --- a/webview-ui/src/components/ui/dropdown-menu.tsx +++ b/webview-ui/src/components/ui/dropdown-menu.tsx @@ -1,5 +1,6 @@ import * as React from "react" import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" +import { PortalProps } from "@radix-ui/react-portal" import { CheckIcon, ChevronRightIcon, DotFilledIcon } from "@radix-ui/react-icons" import { cn } from "@/lib/utils" @@ -53,9 +54,7 @@ DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayNam const DropdownMenuContent = React.forwardRef< React.ElementRef, - React.ComponentPropsWithoutRef & { - container?: HTMLElement - } + React.ComponentPropsWithoutRef & Pick >(({ className, sideOffset = 4, container, ...props }, ref) => ( { + const [container, setContainer] = useState() + + useMount(() => setContainer(document.getElementById(id) ?? undefined)) + + return container +} diff --git a/webview-ui/src/components/ui/popover.tsx b/webview-ui/src/components/ui/popover.tsx index 3ab3344689..9fc035ad2a 100644 --- a/webview-ui/src/components/ui/popover.tsx +++ b/webview-ui/src/components/ui/popover.tsx @@ -1,4 +1,5 @@ import * as React from "react" +import { PortalProps } from "@radix-ui/react-portal" import * as PopoverPrimitive from "@radix-ui/react-popover" import { cn } from "@/lib/utils" @@ -11,9 +12,7 @@ const PopoverAnchor = PopoverPrimitive.Anchor const PopoverContent = React.forwardRef< React.ElementRef, - React.ComponentPropsWithoutRef & { - container?: HTMLElement - } + React.ComponentPropsWithoutRef & Pick >(({ className, align = "center", sideOffset = 4, container, ...props }, ref) => ( { - // Track open state const [open, setOpen] = React.useState(false) - const [portalContainer, setPortalContainer] = useState() + const portalContainer = useRooPortal("roo-portal") - useEffect(() => { - // The dropdown menu uses a portal from @shadcn/ui which by default renders - // at the document root. This causes the menu to remain visible even when - // the parent ChatView component is hidden (during settings/history view). - // By moving the portal inside ChatView, the menu will properly hide when - // its parent is hidden. - setPortalContainer(document.getElementById("chat-view-portal") || undefined) - }, []) - - // Find the selected option label const selectedOption = options.find((option) => option.value === value) const displayText = selectedOption?.label || placeholder || "" - // Handle menu item click const handleSelect = (option: DropdownOption) => { - // Check if this is an action option by its explicit type if (option.type === DropdownOptionType.ACTION) { - window.postMessage({ - type: "action", - action: option.value, - }) + window.postMessage({ type: "action", action: option.value }) setOpen(false) return } + onChange(option.value) setOpen(false) } @@ -103,7 +89,7 @@ export const SelectDropdown = React.forwardRef @@ -136,12 +122,10 @@ export const SelectDropdown = React.forwardRef {options.map((option, index) => { - // Handle separator type if (option.type === DropdownOptionType.SEPARATOR) { return } - // Handle shortcut text type (disabled label for keyboard shortcuts) if ( option.type === DropdownOptionType.SHORTCUT || (option.disabled && shortcutText && option.label.includes(shortcutText)) @@ -153,7 +137,6 @@ export const SelectDropdown = React.forwardRef) { +}: React.ComponentProps & Pick) { return ( - + { const { apiConfiguration, currentApiConfigName, setApiConfiguration, uriScheme } = useExtensionState() @@ -23,18 +26,16 @@ const WelcomeView = () => { }, [apiConfiguration, currentApiConfigName]) return ( -
-

Hi, I'm Roo!

-

- I can do all kinds of tasks thanks to the latest breakthroughs in agentic coding capabilities and access - to tools that let me create & edit files, explore complex projects, use the browser, and execute - terminal commands (with your permission, of course). I can even use MCP to create new tools and extend - my own capabilities. -

- - To get started, this extension needs an API provider. - -
+ + +

Hi, I'm Roo!

+
+ I can do all kinds of tasks thanks to the latest breakthroughs in agentic coding capabilities and + access to tools that let me create & edit files, explore complex projects, use the browser, and + execute terminal commands (with your permission, of course). I can even use MCP to create new tools + and extend my own capabilities. +
+ To get started, this extension needs an API provider. { errorMessage={errorMessage} setErrorMessage={setErrorMessage} /> -
- -
-
+ +
+
Let's go! - {errorMessage && {errorMessage}} + {errorMessage &&
{errorMessage}
}
-
+ ) } diff --git a/webview-ui/src/index.css b/webview-ui/src/index.css index 6cd405b93f..2144557a98 100644 --- a/webview-ui/src/index.css +++ b/webview-ui/src/index.css @@ -111,6 +111,10 @@ --color-vscode-charts-green: var(--vscode-charts-green); --color-vscode-charts-yellow: var(--vscode-charts-yellow); + + --color-vscode-inputValidation-infoForeground: var(--vscode-inputValidation-infoForeground); + --color-vscode-inputValidation-infoBackground: var(--vscode-inputValidation-infoBackground); + --color-vscode-inputValidation-infoBorder: var(--vscode-inputValidation-infoBorder); } @layer base {