Browse Source

Merge pull request #2 from cannuri/feature/update-main

Feature/update main
cannuri 11 months ago
parent
commit
42287ad1a0
48 changed files with 584 additions and 244 deletions
  1. 5 0
      .changeset/automatic-tags-publish.md
  2. 0 5
      .changeset/empty-bees-suffer.md
  3. 1 1
      .changeset/lemon-bulldogs-unite.md
  4. 1 2
      .env.sample
  5. 20 5
      .github/workflows/marketplace-publish.yml
  6. 5 0
      CHANGELOG.md
  7. 1 0
      esbuild.js
  8. 2 2
      package-lock.json
  9. 1 1
      package.json
  10. 18 8
      src/api/providers/__tests__/requesty.test.ts
  11. 3 3
      src/api/providers/openai.ts
  12. 26 6
      src/api/providers/requesty.ts
  13. 2 2
      src/api/providers/vscode-lm.ts
  14. 9 3
      src/core/Cline.ts
  15. 2 2
      src/core/mentions/index.ts
  16. 9 0
      src/core/webview/ClineProvider.ts
  17. 59 0
      src/core/webview/__tests__/ClineProvider.test.ts
  18. 7 3
      src/integrations/diagnostics/index.ts
  19. 1 1
      src/integrations/editor/DiffViewProvider.ts
  20. 1 1
      src/services/glob/list-files.ts
  21. 15 0
      src/services/telemetry/TelemetryService.ts
  22. 6 0
      src/services/tree-sitter/__tests__/index.test.ts
  23. 11 0
      src/services/tree-sitter/__tests__/languageParser.test.ts
  24. 3 0
      src/services/tree-sitter/index.ts
  25. 6 0
      src/services/tree-sitter/languageParser.ts
  26. 1 0
      src/services/tree-sitter/queries/index.ts
  27. 28 0
      src/services/tree-sitter/queries/kotlin.ts
  28. 103 10
      src/utils/__tests__/cost.test.ts
  29. 44 13
      src/utils/cost.ts
  30. 17 32
      webview-ui/src/App.tsx
  31. 2 2
      webview-ui/src/components/chat/Announcement.tsx
  32. 11 8
      webview-ui/src/components/chat/ChatTextArea.tsx
  33. 1 1
      webview-ui/src/components/chat/ChatView.tsx
  34. 3 11
      webview-ui/src/components/chat/checkpoints/CheckpointMenu.tsx
  35. 15 0
      webview-ui/src/components/common/Alert.tsx
  36. 23 0
      webview-ui/src/components/common/Tab.tsx
  37. 9 6
      webview-ui/src/components/history/HistoryView.tsx
  38. 15 11
      webview-ui/src/components/mcp/McpView.tsx
  39. 12 6
      webview-ui/src/components/prompts/PromptsView.tsx
  40. 35 46
      webview-ui/src/components/settings/SettingsView.tsx
  41. 2 3
      webview-ui/src/components/ui/dropdown-menu.tsx
  42. 1 0
      webview-ui/src/components/ui/hooks/index.ts
  43. 10 0
      webview-ui/src/components/ui/hooks/useRooPortal.ts
  44. 2 3
      webview-ui/src/components/ui/popover.tsx
  45. 9 26
      webview-ui/src/components/ui/select-dropdown.tsx
  46. 4 2
      webview-ui/src/components/ui/select.tsx
  47. 19 19
      webview-ui/src/components/welcome/WelcomeView.tsx
  48. 4 0
      webview-ui/src/index.css

+ 5 - 0
.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

+ 0 - 5
.changeset/empty-bees-suffer.md

@@ -1,5 +0,0 @@
----
-"roo-cline": patch
----
-
-Revert tool progress for now

+ 1 - 1
.changeset/sixty-ants-begin.md → .changeset/lemon-bulldogs-unite.md

@@ -2,4 +2,4 @@
 "roo-cline": patch
 "roo-cline": patch
 ---
 ---
 
 
-v3.8.4
+App tab layout fixes

+ 1 - 2
.env.sample

@@ -1,2 +1 @@
-# PostHog API Keys for telemetry
-POSTHOG_API_KEY=key-goes-here
+POSTHOG_API_KEY=key-goes-here

+ 20 - 5
.github/workflows/marketplace-publish.yml

@@ -10,6 +10,8 @@ env:
 jobs:
 jobs:
   publish-extension:
   publish-extension:
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest
+    permissions:
+      contents: write  # Required for pushing tags
     if: >
     if: >
         ( github.event_name == 'pull_request' &&
         ( github.event_name == 'pull_request' &&
         github.event.pull_request.base.ref == 'main' &&
         github.event.pull_request.base.ref == 'main' &&
@@ -23,24 +25,24 @@ jobs:
       - uses: actions/setup-node@v4
       - uses: actions/setup-node@v4
         with:
         with:
           node-version: 18
           node-version: 18
+
       - run: |
       - run: |
           git config user.name github-actions
           git config user.name github-actions
           git config user.email [email protected]
           git config user.email [email protected]
+
       - name: Install Dependencies
       - name: Install Dependencies
         run: |
         run: |
           npm install -g vsce ovsx
           npm install -g vsce ovsx
           npm run install:ci
           npm run install:ci
+
       - name: Create .env file
       - name: Create .env file
         run: |
         run: |
           echo "# PostHog API Keys for telemetry" > .env
           echo "# PostHog API Keys for telemetry" > .env
           echo "POSTHOG_API_KEY=${{ secrets.POSTHOG_API_KEY }}" >> .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: |
         run: |
           current_package_version=$(node -p "require('./package.json').version")
           current_package_version=$(node -p "require('./package.json').version")
-
           npm run vsix
           npm run vsix
           package=$(unzip -l bin/roo-cline-${current_package_version}.vsix)
           package=$(unzip -l bin/roo-cline-${current_package_version}.vsix)
           echo "$package"
           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 "extension/node_modules/@vscode/codicons/dist/codicon.ttf" || exit 1
           echo "$package" | grep -q ".env" || 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
           npm run publish:marketplace
           echo "Successfully published version $current_package_version to VS Code Marketplace"
           echo "Successfully published version $current_package_version to VS Code Marketplace"

+ 5 - 0
CHANGELOG.md

@@ -1,5 +1,10 @@
 # Roo Code Changelog
 # 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
 ## [3.8.3] - 2025-03-09
 
 
 - Fix VS Code LM API model picker truncation issue
 - Fix VS Code LM API model picker truncation issue

+ 1 - 0
esbuild.js

@@ -52,6 +52,7 @@ const copyWasmFiles = {
 				"java",
 				"java",
 				"php",
 				"php",
 				"swift",
 				"swift",
+				"kotlin",
 			]
 			]
 
 
 			languages.forEach((lang) => {
 			languages.forEach((lang) => {

+ 2 - 2
package-lock.json

@@ -1,12 +1,12 @@
 {
 {
 	"name": "roo-cline",
 	"name": "roo-cline",
-	"version": "3.8.3",
+	"version": "3.8.4",
 	"lockfileVersion": 3,
 	"lockfileVersion": 3,
 	"requires": true,
 	"requires": true,
 	"packages": {
 	"packages": {
 		"": {
 		"": {
 			"name": "roo-cline",
 			"name": "roo-cline",
-			"version": "3.8.3",
+			"version": "3.8.4",
 			"dependencies": {
 			"dependencies": {
 				"@anthropic-ai/bedrock-sdk": "^0.10.2",
 				"@anthropic-ai/bedrock-sdk": "^0.10.2",
 				"@anthropic-ai/sdk": "^0.37.0",
 				"@anthropic-ai/sdk": "^0.37.0",

+ 1 - 1
package.json

@@ -3,7 +3,7 @@
 	"displayName": "Roo Code (prev. Roo Cline)",
 	"displayName": "Roo Code (prev. Roo Cline)",
 	"description": "A whole dev team of AI agents in your editor.",
 	"description": "A whole dev team of AI agents in your editor.",
 	"publisher": "RooVeterinaryInc",
 	"publisher": "RooVeterinaryInc",
-	"version": "3.8.3",
+	"version": "3.8.4",
 	"icon": "assets/icons/rocket.png",
 	"icon": "assets/icons/rocket.png",
 	"galleryBanner": {
 	"galleryBanner": {
 		"color": "#617A91",
 		"color": "#617A91",

+ 18 - 8
src/api/providers/__tests__/requesty.test.ts

@@ -22,8 +22,10 @@ describe("RequestyHandler", () => {
 			contextWindow: 4000,
 			contextWindow: 4000,
 			supportsPromptCache: false,
 			supportsPromptCache: false,
 			supportsImages: true,
 			supportsImages: true,
-			inputPrice: 0,
-			outputPrice: 0,
+			inputPrice: 1,
+			outputPrice: 10,
+			cacheReadsPrice: 0.1,
+			cacheWritesPrice: 1.5,
 		},
 		},
 		openAiStreamingEnabled: true,
 		openAiStreamingEnabled: true,
 		includeMaxTokens: true, // Add this to match the implementation
 		includeMaxTokens: true, // Add this to match the implementation
@@ -83,8 +85,12 @@ describe("RequestyHandler", () => {
 						yield {
 						yield {
 							choices: [{ delta: { content: " world" } }],
 							choices: [{ delta: { content: " world" } }],
 							usage: {
 							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: "text", text: " world" },
 					{
 					{
 						type: "usage",
 						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",
 						type: "usage",
 						inputTokens: 10,
 						inputTokens: 10,
 						outputTokens: 5,
 						outputTokens: 5,
+						cacheWriteTokens: 0,
+						cacheReadTokens: 0,
+						totalCost: 0.00006, // (10 * 1 / 1,000,000) + (5 * 10 / 1,000,000)
 					},
 					},
 				])
 				])
 
 

+ 3 - 3
src/api/providers/openai.ts

@@ -116,7 +116,7 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
 					}
 					}
 				}
 				}
 				if (chunk.usage) {
 				if (chunk.usage) {
-					yield this.processUsageMetrics(chunk.usage)
+					yield this.processUsageMetrics(chunk.usage, modelInfo)
 				}
 				}
 			}
 			}
 		} else {
 		} else {
@@ -139,11 +139,11 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
 				type: "text",
 				type: "text",
 				text: response.choices[0]?.message.content || "",
 				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 {
 		return {
 			type: "usage",
 			type: "usage",
 			inputTokens: usage?.prompt_tokens || 0,
 			inputTokens: usage?.prompt_tokens || 0,

+ 26 - 6
src/api/providers/requesty.ts

@@ -1,9 +1,20 @@
 import axios from "axios"
 import axios from "axios"
 
 
 import { ModelInfo, requestyModelInfoSaneDefaults, requestyDefaultModelId } from "../../shared/api"
 import { ModelInfo, requestyModelInfoSaneDefaults, requestyDefaultModelId } from "../../shared/api"
-import { parseApiPrice } from "../../utils/cost"
+import { calculateApiCostOpenAI, parseApiPrice } from "../../utils/cost"
 import { ApiStreamUsageChunk } from "../transform/stream"
 import { ApiStreamUsageChunk } from "../transform/stream"
 import { OpenAiHandler, OpenAiHandlerOptions } from "./openai"
 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 {
 export class RequestyHandler extends OpenAiHandler {
 	constructor(options: OpenAiHandlerOptions) {
 	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 {
 		return {
 			type: "usage",
 			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,
 		}
 		}
 	}
 	}
 }
 }

+ 2 - 2
src/api/providers/vscode-lm.ts

@@ -2,7 +2,7 @@ import { Anthropic } from "@anthropic-ai/sdk"
 import * as vscode from "vscode"
 import * as vscode from "vscode"
 
 
 import { SingleCompletionHandler } from "../"
 import { SingleCompletionHandler } from "../"
-import { calculateApiCost } from "../../utils/cost"
+import { calculateApiCostAnthropic } from "../../utils/cost"
 import { ApiStream } from "../transform/stream"
 import { ApiStream } from "../transform/stream"
 import { convertToVsCodeLmMessages } from "../transform/vscode-lm-format"
 import { convertToVsCodeLmMessages } from "../transform/vscode-lm-format"
 import { SELECTOR_SEPARATOR, stringifyVsCodeLmModelSelector } from "../../shared/vsCodeSelectorUtils"
 import { SELECTOR_SEPARATOR, stringifyVsCodeLmModelSelector } from "../../shared/vsCodeSelectorUtils"
@@ -462,7 +462,7 @@ export class VsCodeLmHandler extends BaseProvider implements SingleCompletionHan
 				type: "usage",
 				type: "usage",
 				inputTokens: totalInputTokens,
 				inputTokens: totalInputTokens,
 				outputTokens: totalOutputTokens,
 				outputTokens: totalOutputTokens,
-				totalCost: calculateApiCost(this.getModel().info, totalInputTokens, totalOutputTokens),
+				totalCost: calculateApiCostAnthropic(this.getModel().info, totalInputTokens, totalOutputTokens),
 			}
 			}
 		} catch (error: unknown) {
 		} catch (error: unknown) {
 			this.ensureCleanState()
 			this.ensureCleanState()

+ 9 - 3
src/core/Cline.ts

@@ -55,7 +55,7 @@ import { ClineAskResponse } from "../shared/WebviewMessage"
 import { GlobalFileNames } from "../shared/globalFileNames"
 import { GlobalFileNames } from "../shared/globalFileNames"
 import { defaultModeSlug, getModeBySlug, getFullModeDetails } from "../shared/modes"
 import { defaultModeSlug, getModeBySlug, getFullModeDetails } from "../shared/modes"
 import { EXPERIMENT_IDS, experiments as Experiments, ExperimentId } from "../shared/experiments"
 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 { fileExistsAtPath } from "../utils/fs"
 import { arePathsEqual, getReadablePath } from "../utils/path"
 import { arePathsEqual, getReadablePath } from "../utils/path"
 import { parseMentions } from "./mentions"
 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.
 			//  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.
 			// 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) {
 			if (didEndLoop) {
 				// For now a task never 'completes'. This will only happen if the user hits max requests and denies resetting the count.
 				// 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}`)
 				//this.say("task_completed", `Task completed. Total API usage cost: ${totalCost}`)
@@ -3173,7 +3173,7 @@ export class Cline {
 					cacheReads: cacheReadTokens,
 					cacheReads: cacheReadTokens,
 					cost:
 					cost:
 						totalCost ??
 						totalCost ??
-						calculateApiCost(
+						calculateApiCostAnthropic(
 							this.api.getModel().info,
 							this.api.getModel().info,
 							inputTokens,
 							inputTokens,
 							outputTokens,
 							outputTokens,
@@ -3798,6 +3798,8 @@ export class Cline {
 			return
 			return
 		}
 		}
 
 
+		telemetryService.captureCheckpointDiffed(this.taskId)
+
 		if (!previousCommitHash && mode === "checkpoint") {
 		if (!previousCommitHash && mode === "checkpoint") {
 			const previousCheckpoint = this.clineMessages
 			const previousCheckpoint = this.clineMessages
 				.filter(({ say }) => say === "checkpoint_saved")
 				.filter(({ say }) => say === "checkpoint_saved")
@@ -3849,6 +3851,8 @@ export class Cline {
 			return
 			return
 		}
 		}
 
 
+		telemetryService.captureCheckpointCreated(this.taskId)
+
 		// Start the checkpoint process in the background.
 		// Start the checkpoint process in the background.
 		service.saveCheckpoint(`Task: ${this.taskId}, Time: ${Date.now()}`).catch((err) => {
 		service.saveCheckpoint(`Task: ${this.taskId}, Time: ${Date.now()}`).catch((err) => {
 			console.error("[Cline#checkpointSave] caught unexpected error, disabling checkpoints", err)
 			console.error("[Cline#checkpointSave] caught unexpected error, disabling checkpoints", err)
@@ -3880,6 +3884,8 @@ export class Cline {
 		try {
 		try {
 			await service.restoreCheckpoint(commitHash)
 			await service.restoreCheckpoint(commitHash)
 
 
+			telemetryService.captureCheckpointRestored(this.taskId)
+
 			await this.providerRef.deref()?.postMessageToWebview({ type: "currentCheckpointUpdated", text: commitHash })
 			await this.providerRef.deref()?.postMessageToWebview({ type: "currentCheckpointUpdated", text: commitHash })
 
 
 			if (mode === "restore") {
 			if (mode === "restore") {

+ 2 - 2
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<string> {
 	const diagnostics = vscode.languages.getDiagnostics()
 	const diagnostics = vscode.languages.getDiagnostics()
-	const result = diagnosticsToProblemsString(
+	const result = await diagnosticsToProblemsString(
 		diagnostics,
 		diagnostics,
 		[vscode.DiagnosticSeverity.Error, vscode.DiagnosticSeverity.Warning],
 		[vscode.DiagnosticSeverity.Error, vscode.DiagnosticSeverity.Warning],
 		cwd,
 		cwd,

+ 9 - 0
src/core/webview/ClineProvider.ts

@@ -2567,6 +2567,15 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 			properties.apiProvider = apiConfiguration.apiProvider
 			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
 		return properties
 	}
 	}
 }
 }

+ 59 - 0
src/core/webview/__tests__/ClineProvider.test.ts

@@ -1652,3 +1652,62 @@ describe("ContextProxy integration", () => {
 		expect(mockContextProxy.setValues).toBeDefined()
 		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")
+	})
+})

+ 7 - 3
src/integrations/diagnostics/index.ts

@@ -70,11 +70,12 @@ export function getNewDiagnostics(
 // // - New error in file3 (1:1)
 // // - New error in file3 (1:1)
 
 
 // will return empty string if no problems with the given severity are found
 // 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[]][],
 	diagnostics: [vscode.Uri, vscode.Diagnostic[]][],
 	severities: vscode.DiagnosticSeverity[],
 	severities: vscode.DiagnosticSeverity[],
 	cwd: string,
 	cwd: string,
-): string {
+): Promise<string> {
+	const documents = new Map<vscode.Uri, vscode.TextDocument>()
 	let result = ""
 	let result = ""
 	for (const [uri, fileDiagnostics] of diagnostics) {
 	for (const [uri, fileDiagnostics] of diagnostics) {
 		const problems = fileDiagnostics.filter((d) => severities.includes(d.severity))
 		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 line = diagnostic.range.start.line + 1 // VSCode lines are 0-indexed
 				const source = diagnostic.source ? `${diagnostic.source} ` : ""
 				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}`
 			}
 			}
 		}
 		}
 	}
 	}

+ 1 - 1
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.
 		initial fix is usually correct and it may just take time for linters to catch up.
 		*/
 		*/
 		const postDiagnostics = vscode.languages.getDiagnostics()
 		const postDiagnostics = vscode.languages.getDiagnostics()
-		const newProblems = diagnosticsToProblemsString(
+		const newProblems = await diagnosticsToProblemsString(
 			getNewDiagnostics(this.preDiagnostics, postDiagnostics),
 			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)
 				vscode.DiagnosticSeverity.Error, // only including errors since warnings can be distracting (if user wants to fix warnings they can use the @problems mention)

+ 1 - 1
src/services/glob/list-files.ts

@@ -34,7 +34,7 @@ export async function listFiles(dirPath: string, recursive: boolean, limit: numb
 		"pkg",
 		"pkg",
 		"Pods",
 		"Pods",
 		".*", // '!**/.*' excludes hidden directories, while '!**/.*/**' excludes only their contents. This way we are at least aware of the existence of hidden directories.
 		".*", // '!**/.*' 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 = {
 	const options = {
 		cwd: dirPath,
 		cwd: dirPath,

+ 15 - 0
src/services/telemetry/TelemetryService.ts

@@ -22,6 +22,9 @@ class PostHogClient {
 			CONVERSATION_MESSAGE: "Conversation Message",
 			CONVERSATION_MESSAGE: "Conversation Message",
 			MODE_SWITCH: "Mode Switched",
 			MODE_SWITCH: "Mode Switched",
 			TOOL_USED: "Tool Used",
 			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
 	 * Checks if telemetry is currently enabled
 	 * @returns Whether telemetry is enabled
 	 * @returns Whether telemetry is enabled

+ 6 - 0
src/services/tree-sitter/__tests__/index.test.ts

@@ -169,6 +169,8 @@ describe("Tree-sitter Service", () => {
 				"/test/path/main.rs",
 				"/test/path/main.rs",
 				"/test/path/program.cpp",
 				"/test/path/program.cpp",
 				"/test/path/code.go",
 				"/test/path/code.go",
+				"/test/path/app.kt",
+				"/test/path/script.kts",
 			]
 			]
 
 
 			;(listFiles as jest.Mock).mockResolvedValue([mockFiles, new Set()])
 			;(listFiles as jest.Mock).mockResolvedValue([mockFiles, new Set()])
@@ -197,6 +199,8 @@ describe("Tree-sitter Service", () => {
 				rs: { parser: mockParser, query: mockQuery },
 				rs: { parser: mockParser, query: mockQuery },
 				cpp: { parser: mockParser, query: mockQuery },
 				cpp: { parser: mockParser, query: mockQuery },
 				go: { 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() {}")
 			;(fs.readFile as jest.Mock).mockResolvedValue("function test() {}")
 
 
@@ -207,6 +211,8 @@ describe("Tree-sitter Service", () => {
 			expect(result).toContain("main.rs")
 			expect(result).toContain("main.rs")
 			expect(result).toContain("program.cpp")
 			expect(result).toContain("program.cpp")
 			expect(result).toContain("code.go")
 			expect(result).toContain("code.go")
+			expect(result).toContain("app.kt")
+			expect(result).toContain("script.kts")
 		})
 		})
 
 
 		it("should normalize paths in output", async () => {
 		it("should normalize paths in output", async () => {

+ 11 - 0
src/services/tree-sitter/__tests__/languageParser.test.ts

@@ -92,6 +92,17 @@ describe("Language Parser", () => {
 			expect(parsers.hpp).toBeDefined()
 			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 () => {
 		it("should throw error for unsupported file extensions", async () => {
 			const files = ["test.unsupported"]
 			const files = ["test.unsupported"]
 
 

+ 3 - 0
src/services/tree-sitter/index.ts

@@ -80,6 +80,9 @@ function separateFiles(allFiles: string[]): { filesToParse: string[]; remainingF
 		"java",
 		"java",
 		"php",
 		"php",
 		"swift",
 		"swift",
+		// Kotlin
+		"kt",
+		"kts",
 	].map((e) => `.${e}`)
 	].map((e) => `.${e}`)
 	const filesToParse = allFiles.filter((file) => extensions.includes(path.extname(file))).slice(0, 50) // 50 files max
 	const filesToParse = allFiles.filter((file) => extensions.includes(path.extname(file))).slice(0, 50) // 50 files max
 	const remainingFiles = allFiles.filter((file) => !filesToParse.includes(file))
 	const remainingFiles = allFiles.filter((file) => !filesToParse.includes(file))

+ 6 - 0
src/services/tree-sitter/languageParser.ts

@@ -13,6 +13,7 @@ import {
 	javaQuery,
 	javaQuery,
 	phpQuery,
 	phpQuery,
 	swiftQuery,
 	swiftQuery,
+	kotlinQuery,
 } from "./queries"
 } from "./queries"
 
 
 export interface LanguageParser {
 export interface LanguageParser {
@@ -120,6 +121,11 @@ export async function loadRequiredLanguageParsers(filesToParse: string[]): Promi
 				language = await loadLanguage("swift")
 				language = await loadLanguage("swift")
 				query = language.query(swiftQuery)
 				query = language.query(swiftQuery)
 				break
 				break
+			case "kt":
+			case "kts":
+				language = await loadLanguage("kotlin")
+				query = language.query(kotlinQuery)
+				break
 			default:
 			default:
 				throw new Error(`Unsupported language: ${ext}`)
 				throw new Error(`Unsupported language: ${ext}`)
 		}
 		}

+ 1 - 0
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 csharpQuery } from "./c-sharp"
 export { default as goQuery } from "./go"
 export { default as goQuery } from "./go"
 export { default as swiftQuery } from "./swift"
 export { default as swiftQuery } from "./swift"
+export { default as kotlinQuery } from "./kotlin"

+ 28 - 0
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
+`

+ 103 - 10
src/utils/__tests__/cost.test.ts

@@ -1,8 +1,8 @@
-import { calculateApiCost } from "../cost"
+import { calculateApiCostAnthropic, calculateApiCostOpenAI } from "../cost"
 import { ModelInfo } from "../../shared/api"
 import { ModelInfo } from "../../shared/api"
 
 
 describe("Cost Utility", () => {
 describe("Cost Utility", () => {
-	describe("calculateApiCost", () => {
+	describe("calculateApiCostAnthropic", () => {
 		const mockModelInfo: ModelInfo = {
 		const mockModelInfo: ModelInfo = {
 			maxTokens: 8192,
 			maxTokens: 8192,
 			contextWindow: 200_000,
 			contextWindow: 200_000,
@@ -14,7 +14,7 @@ describe("Cost Utility", () => {
 		}
 		}
 
 
 		it("should calculate basic input/output costs correctly", () => {
 		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
 			// Input cost: (3.0 / 1_000_000) * 1000 = 0.003
 			// Output cost: (15.0 / 1_000_000) * 500 = 0.0075
 			// Output cost: (15.0 / 1_000_000) * 500 = 0.0075
@@ -23,7 +23,7 @@ describe("Cost Utility", () => {
 		})
 		})
 
 
 		it("should handle cache writes cost", () => {
 		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
 			// Input cost: (3.0 / 1_000_000) * 1000 = 0.003
 			// Output cost: (15.0 / 1_000_000) * 500 = 0.0075
 			// Output cost: (15.0 / 1_000_000) * 500 = 0.0075
@@ -33,7 +33,7 @@ describe("Cost Utility", () => {
 		})
 		})
 
 
 		it("should handle cache reads cost", () => {
 		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
 			// Input cost: (3.0 / 1_000_000) * 1000 = 0.003
 			// Output cost: (15.0 / 1_000_000) * 500 = 0.0075
 			// Output cost: (15.0 / 1_000_000) * 500 = 0.0075
@@ -43,7 +43,7 @@ describe("Cost Utility", () => {
 		})
 		})
 
 
 		it("should handle all cost components together", () => {
 		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
 			// Input cost: (3.0 / 1_000_000) * 1000 = 0.003
 			// Output cost: (15.0 / 1_000_000) * 500 = 0.0075
 			// Output cost: (15.0 / 1_000_000) * 500 = 0.0075
@@ -60,17 +60,17 @@ describe("Cost Utility", () => {
 				supportsPromptCache: true,
 				supportsPromptCache: true,
 			}
 			}
 
 
-			const cost = calculateApiCost(modelWithoutPrices, 1000, 500, 2000, 3000)
+			const cost = calculateApiCostAnthropic(modelWithoutPrices, 1000, 500, 2000, 3000)
 			expect(cost).toBe(0)
 			expect(cost).toBe(0)
 		})
 		})
 
 
 		it("should handle zero tokens", () => {
 		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)
 			expect(cost).toBe(0)
 		})
 		})
 
 
 		it("should handle undefined cache values", () => {
 		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
 			// Input cost: (3.0 / 1_000_000) * 1000 = 0.003
 			// Output cost: (15.0 / 1_000_000) * 500 = 0.0075
 			// Output cost: (15.0 / 1_000_000) * 500 = 0.0075
@@ -85,7 +85,7 @@ describe("Cost Utility", () => {
 				cacheReadsPrice: undefined,
 				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
 			// Should only include input and output costs
 			// Input cost: (3.0 / 1_000_000) * 1000 = 0.003
 			// Input cost: (3.0 / 1_000_000) * 1000 = 0.003
@@ -94,4 +94,97 @@ describe("Cost Utility", () => {
 			expect(cost).toBe(0.0105)
 			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)
+		})
+	})
 })
 })

+ 44 - 13
src/utils/cost.ts

@@ -1,26 +1,57 @@
 import { ModelInfo } from "../shared/api"
 import { ModelInfo } from "../shared/api"
 
 
-export function calculateApiCost(
+function calculateApiCostInternal(
 	modelInfo: ModelInfo,
 	modelInfo: ModelInfo,
 	inputTokens: number,
 	inputTokens: number,
 	outputTokens: number,
 	outputTokens: number,
-	cacheCreationInputTokens?: number,
-	cacheReadInputTokens?: number,
+	cacheCreationInputTokens: number,
+	cacheReadInputTokens: number,
 ): 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 baseInputCost = ((modelInfo.inputPrice || 0) / 1_000_000) * inputTokens
 	const outputCost = ((modelInfo.outputPrice || 0) / 1_000_000) * outputTokens
 	const outputCost = ((modelInfo.outputPrice || 0) / 1_000_000) * outputTokens
 	const totalCost = cacheWritesCost + cacheReadsCost + baseInputCost + outputCost
 	const totalCost = cacheWritesCost + cacheReadsCost + baseInputCost + outputCost
 	return totalCost
 	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)
 export const parseApiPrice = (price: any) => (price ? parseFloat(price) * 1_000_000 : undefined)

+ 17 - 32
webview-ui/src/App.tsx

@@ -17,6 +17,12 @@ import { HumanRelayDialog } from "./components/human-relay/HumanRelayDialog"
 
 
 type Tab = "settings" | "history" | "mcp" | "prompts" | "chat"
 type Tab = "settings" | "history" | "mcp" | "prompts" | "chat"
 
 
+type HumanRelayDialogState = {
+	isOpen: boolean
+	requestId: string
+	promptText: string
+}
+
 const tabsByMessageAction: Partial<Record<NonNullable<ExtensionMessage["action"]>, Tab>> = {
 const tabsByMessageAction: Partial<Record<NonNullable<ExtensionMessage["action"]>, Tab>> = {
 	chatButtonClicked: "chat",
 	chatButtonClicked: "chat",
 	settingsButtonClicked: "settings",
 	settingsButtonClicked: "settings",
@@ -24,24 +30,21 @@ const tabsByMessageAction: Partial<Record<NonNullable<ExtensionMessage["action"]
 	mcpButtonClicked: "mcp",
 	mcpButtonClicked: "mcp",
 	historyButtonClicked: "history",
 	historyButtonClicked: "history",
 }
 }
+
 const App = () => {
 const App = () => {
 	const { didHydrateState, showWelcome, shouldShowAnnouncement, telemetrySetting, telemetryKey, machineId } =
 	const { didHydrateState, showWelcome, shouldShowAnnouncement, telemetrySetting, telemetryKey, machineId } =
 		useExtensionState()
 		useExtensionState()
+
 	const [showAnnouncement, setShowAnnouncement] = useState(false)
 	const [showAnnouncement, setShowAnnouncement] = useState(false)
 	const [tab, setTab] = useState<Tab>("chat")
 	const [tab, setTab] = useState<Tab>("chat")
-	const settingsRef = useRef<SettingsViewRef>(null)
-
-	// Human Relay Dialog Status
-	const [humanRelayDialogState, setHumanRelayDialogState] = useState<{
-		isOpen: boolean
-		requestId: string
-		promptText: string
-	}>({
+	const [humanRelayDialogState, setHumanRelayDialogState] = useState<HumanRelayDialogState>({
 		isOpen: false,
 		isOpen: false,
 		requestId: "",
 		requestId: "",
 		promptText: "",
 		promptText: "",
 	})
 	})
 
 
+	const settingsRef = useRef<SettingsViewRef>(null)
+
 	const switchTab = useCallback((newTab: Tab) => {
 	const switchTab = useCallback((newTab: Tab) => {
 		if (settingsRef.current?.checkUnsaveChanges) {
 		if (settingsRef.current?.checkUnsaveChanges) {
 			settingsRef.current.checkUnsaveChanges(() => setTab(newTab))
 			settingsRef.current.checkUnsaveChanges(() => setTab(newTab))
@@ -74,23 +77,6 @@ const App = () => {
 		[switchTab],
 		[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)
 	useEvent("message", onMessage)
 
 
 	useEffect(() => {
 	useEffect(() => {
@@ -106,7 +92,7 @@ const App = () => {
 		}
 		}
 	}, [telemetrySetting, telemetryKey, machineId, didHydrateState])
 	}, [telemetrySetting, telemetryKey, machineId, didHydrateState])
 
 
-	// Tell Extension that we are ready to receive messages
+	// Tell the extension that we are ready to receive messages.
 	useEffect(() => {
 	useEffect(() => {
 		vscode.postMessage({ type: "webviewDidLaunch" })
 		vscode.postMessage({ type: "webviewDidLaunch" })
 	}, [])
 	}, [])
@@ -121,24 +107,23 @@ const App = () => {
 		<WelcomeView />
 		<WelcomeView />
 	) : (
 	) : (
 		<>
 		<>
-			{tab === "settings" && <SettingsView ref={settingsRef} onDone={() => setTab("chat")} />}
-			{tab === "history" && <HistoryView onDone={() => switchTab("chat")} />}
-			{tab === "mcp" && <McpView onDone={() => switchTab("chat")} />}
 			{tab === "prompts" && <PromptsView onDone={() => switchTab("chat")} />}
 			{tab === "prompts" && <PromptsView onDone={() => switchTab("chat")} />}
+			{tab === "mcp" && <McpView onDone={() => switchTab("chat")} />}
+			{tab === "history" && <HistoryView onDone={() => switchTab("chat")} />}
+			{tab === "settings" && <SettingsView ref={settingsRef} onDone={() => setTab("chat")} />}
 			<ChatView
 			<ChatView
 				isHidden={tab !== "chat"}
 				isHidden={tab !== "chat"}
 				showAnnouncement={showAnnouncement}
 				showAnnouncement={showAnnouncement}
 				hideAnnouncement={() => setShowAnnouncement(false)}
 				hideAnnouncement={() => setShowAnnouncement(false)}
 				showHistoryView={() => switchTab("history")}
 				showHistoryView={() => switchTab("history")}
 			/>
 			/>
-			{/* Human Relay Dialog */}
 			<HumanRelayDialog
 			<HumanRelayDialog
 				isOpen={humanRelayDialogState.isOpen}
 				isOpen={humanRelayDialogState.isOpen}
 				requestId={humanRelayDialogState.requestId}
 				requestId={humanRelayDialogState.requestId}
 				promptText={humanRelayDialogState.promptText}
 				promptText={humanRelayDialogState.promptText}
 				onClose={() => setHumanRelayDialogState((prev) => ({ ...prev, isOpen: false }))}
 				onClose={() => 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 })}
 			/>
 			/>
 		</>
 		</>
 	)
 	)

+ 2 - 2
webview-ui/src/components/chat/Announcement.tsx

@@ -33,7 +33,7 @@ const Announcement = ({ version, hideAnnouncement }: AnnouncementProps) => {
 			</p>
 			</p>
 
 
 			<h3 style={{ margin: "12px 0 8px" }}>What's New</h3>
 			<h3 style={{ margin: "12px 0 8px" }}>What's New</h3>
-			<p style={{ margin: "5px 0px" }}>
+			<div style={{ margin: "5px 0px" }}>
 				<ul style={{ margin: "4px 0 6px 20px", padding: 0 }}>
 				<ul style={{ margin: "4px 0 6px 20px", padding: 0 }}>
 					<li>• Faster asynchronous checkpoints</li>
 					<li>• Faster asynchronous checkpoints</li>
 					<li>• Support for .rooignore files</li>
 					<li>• Support for .rooignore files</li>
@@ -44,7 +44,7 @@ const Announcement = ({ version, hideAnnouncement }: AnnouncementProps) => {
 					<li>• Updated DeepSeek provider</li>
 					<li>• Updated DeepSeek provider</li>
 					<li>• New "Human Relay" provider</li>
 					<li>• New "Human Relay" provider</li>
 				</ul>
 				</ul>
-			</p>
+			</div>
 
 
 			<p style={{ margin: "10px 0px 0px" }}>
 			<p style={{ margin: "10px 0px 0px" }}>
 				Get more details and discuss in{" "}
 				Get more details and discuss in{" "}

+ 11 - 8
webview-ui/src/components/chat/ChatTextArea.tsx

@@ -1,22 +1,25 @@
 import React, { forwardRef, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"
 import React, { forwardRef, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"
 import DynamicTextArea from "react-textarea-autosize"
 import DynamicTextArea from "react-textarea-autosize"
+
 import { mentionRegex, mentionRegexGlobal } from "../../../../src/shared/context-mentions"
 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 {
 import {
 	ContextMenuOptionType,
 	ContextMenuOptionType,
 	getContextMenuOptions,
 	getContextMenuOptions,
 	insertMention,
 	insertMention,
 	removeMention,
 	removeMention,
 	shouldShowContextMenu,
 	shouldShowContextMenu,
-} from "../../utils/context-mentions"
-import { MAX_IMAGES_PER_MESSAGE } from "./ChatView"
-import ContextMenu from "./ContextMenu"
+} from "@/utils/context-mentions"
+import { SelectDropdown, DropdownOptionType } from "@/components/ui"
+
+import { useExtensionState } from "../../context/ExtensionStateContext"
 import Thumbnails from "../common/Thumbnails"
 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 { convertToMentionPath } from "../../utils/path-mentions"
-import { SelectDropdown, DropdownOptionType } from "../ui"
+import { MAX_IMAGES_PER_MESSAGE } from "./ChatView"
+import ContextMenu from "./ContextMenu"
 
 
 interface ChatTextAreaProps {
 interface ChatTextAreaProps {
 	inputValue: string
 	inputValue: string

+ 1 - 1
webview-ui/src/components/chat/ChatView.tsx

@@ -1275,7 +1275,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
 				modeShortcutText={modeShortcutText}
 				modeShortcutText={modeShortcutText}
 			/>
 			/>
 
 
-			<div id="chat-view-portal" />
+			<div id="roo-portal" />
 		</div>
 		</div>
 	)
 	)
 }
 }

+ 3 - 11
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 { CheckIcon, Cross2Icon } from "@radix-ui/react-icons"
 
 
 import { Button, Popover, PopoverContent, PopoverTrigger } from "@/components/ui"
 import { Button, Popover, PopoverContent, PopoverTrigger } from "@/components/ui"
+import { useRooPortal } from "@/components/ui/hooks"
 
 
 import { vscode } from "../../../utils/vscode"
 import { vscode } from "../../../utils/vscode"
 import { Checkpoint } from "./schema"
 import { Checkpoint } from "./schema"
@@ -14,9 +15,9 @@ type CheckpointMenuProps = {
 }
 }
 
 
 export const CheckpointMenu = ({ ts, commitHash, currentHash, checkpoint }: CheckpointMenuProps) => {
 export const CheckpointMenu = ({ ts, commitHash, currentHash, checkpoint }: CheckpointMenuProps) => {
-	const [portalContainer, setPortalContainer] = useState<HTMLElement>()
 	const [isOpen, setIsOpen] = useState(false)
 	const [isOpen, setIsOpen] = useState(false)
 	const [isConfirming, setIsConfirming] = useState(false)
 	const [isConfirming, setIsConfirming] = useState(false)
+	const portalContainer = useRooPortal("roo-portal")
 
 
 	const isCurrent = currentHash === commitHash
 	const isCurrent = currentHash === commitHash
 	const isFirst = checkpoint.isFirst
 	const isFirst = checkpoint.isFirst
@@ -42,15 +43,6 @@ export const CheckpointMenu = ({ ts, commitHash, currentHash, checkpoint }: Chec
 		setIsOpen(false)
 		setIsOpen(false)
 	}, [ts, commitHash])
 	}, [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 (
 	return (
 		<div className="flex flex-row gap-1">
 		<div className="flex flex-row gap-1">
 			{isDiffAvailable && (
 			{isDiffAvailable && (

+ 15 - 0
webview-ui/src/components/common/Alert.tsx

@@ -0,0 +1,15 @@
+import { cn } from "@/lib/utils"
+import { HTMLAttributes } from "react"
+
+type AlertProps = HTMLAttributes<HTMLDivElement>
+
+export const Alert = ({ className, children, ...props }: AlertProps) => (
+	<div
+		className={cn(
+			"text-vscode-inputValidation-infoForeground bg-vscode-inputValidation-infoBackground border border-vscode-inputValidation-infoBorder rounded-xs p-2",
+			className,
+		)}
+		{...props}>
+		{children}
+	</div>
+)

+ 23 - 0
webview-ui/src/components/common/Tab.tsx

@@ -0,0 +1,23 @@
+import { HTMLAttributes } from "react"
+
+import { cn } from "@/lib/utils"
+
+type TabProps = HTMLAttributes<HTMLDivElement>
+
+export const Tab = ({ className, children, ...props }: TabProps) => (
+	<div className={cn("fixed inset-0 flex flex-col overflow-hidden", className)} {...props}>
+		{children}
+	</div>
+)
+
+export const TabHeader = ({ className, children, ...props }: TabProps) => (
+	<div className={cn("px-5 py-2.5 border-b border-vscode-panel-border", className)} {...props}>
+		{children}
+	</div>
+)
+
+export const TabContent = ({ className, children, ...props }: TabProps) => (
+	<div className={cn("flex-1 overflow-auto p-5", className)} {...props}>
+		{children}
+	</div>
+)

+ 9 - 6
webview-ui/src/components/history/HistoryView.tsx

@@ -9,6 +9,7 @@ import { formatLargeNumber, formatDate } from "@/utils/format"
 import { cn } from "@/lib/utils"
 import { cn } from "@/lib/utils"
 import { Button } from "@/components/ui"
 import { Button } from "@/components/ui"
 
 
+import { Tab, TabContent, TabHeader } from "../common/Tab"
 import { useTaskSearch } from "./useTaskSearch"
 import { useTaskSearch } from "./useTaskSearch"
 import { ExportButton } from "./ExportButton"
 import { ExportButton } from "./ExportButton"
 import { CopyButton } from "./CopyButton"
 import { CopyButton } from "./CopyButton"
@@ -25,8 +26,8 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
 	const [deleteTaskId, setDeleteTaskId] = useState<string | null>(null)
 	const [deleteTaskId, setDeleteTaskId] = useState<string | null>(null)
 
 
 	return (
 	return (
-		<div className="fixed inset-0 flex flex-col">
-			<div className="flex flex-col gap-2 px-5 py-2.5 border-b border-vscode-panel-border">
+		<Tab>
+			<TabHeader className="flex flex-col gap-2">
 				<div className="flex justify-between items-center">
 				<div className="flex justify-between items-center">
 					<h3 className="text-vscode-foreground m-0">History</h3>
 					<h3 className="text-vscode-foreground m-0">History</h3>
 					<VSCodeButton onClick={onDone}>Done</VSCodeButton>
 					<VSCodeButton onClick={onDone}>Done</VSCodeButton>
@@ -81,8 +82,9 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
 						</VSCodeRadio>
 						</VSCodeRadio>
 					</VSCodeRadioGroup>
 					</VSCodeRadioGroup>
 				</div>
 				</div>
-			</div>
-			<div style={{ flexGrow: 1, overflowY: "auto", margin: 0 }}>
+			</TabHeader>
+
+			<TabContent className="p-0">
 				<Virtuoso
 				<Virtuoso
 					style={{
 					style={{
 						flexGrow: 1,
 						flexGrow: 1,
@@ -312,11 +314,12 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
 						</div>
 						</div>
 					)}
 					)}
 				/>
 				/>
-			</div>
+			</TabContent>
+
 			{deleteTaskId && (
 			{deleteTaskId && (
 				<DeleteTaskDialog taskId={deleteTaskId} onOpenChange={(open) => !open && setDeleteTaskId(null)} open />
 				<DeleteTaskDialog taskId={deleteTaskId} onOpenChange={(open) => !open && setDeleteTaskId(null)} open />
 			)}
 			)}
-		</div>
+		</Tab>
 	)
 	)
 }
 }
 
 

+ 15 - 11
webview-ui/src/components/mcp/McpView.tsx

@@ -1,3 +1,4 @@
+import { useState } from "react"
 import {
 import {
 	VSCodeButton,
 	VSCodeButton,
 	VSCodeCheckbox,
 	VSCodeCheckbox,
@@ -6,14 +7,17 @@ import {
 	VSCodePanelTab,
 	VSCodePanelTab,
 	VSCodePanelView,
 	VSCodePanelView,
 } from "@vscode/webview-ui-toolkit/react"
 } 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 { 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 McpToolRow from "./McpToolRow"
 import McpResourceRow from "./McpResourceRow"
 import McpResourceRow from "./McpResourceRow"
 import McpEnabledToggle from "./McpEnabledToggle"
 import McpEnabledToggle from "./McpEnabledToggle"
-import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from "../ui/dialog"
 
 
 type McpViewProps = {
 type McpViewProps = {
 	onDone: () => void
 	onDone: () => void
@@ -29,12 +33,13 @@ const McpView = ({ onDone }: McpViewProps) => {
 	} = useExtensionState()
 	} = useExtensionState()
 
 
 	return (
 	return (
-		<div className="fixed inset-0 flex flex-col">
-			<div className="flex justify-between items-center px-5 py-2.5 border-b border-vscode-panel-border">
+		<Tab>
+			<TabHeader className="flex justify-between items-center">
 				<h3 className="text-vscode-foreground m-0">MCP Servers</h3>
 				<h3 className="text-vscode-foreground m-0">MCP Servers</h3>
 				<VSCodeButton onClick={onDone}>Done</VSCodeButton>
 				<VSCodeButton onClick={onDone}>Done</VSCodeButton>
-			</div>
-			<div className="flex-1 overflow-auto p-5">
+			</TabHeader>
+
+			<TabContent>
 				<div
 				<div
 					style={{
 					style={{
 						color: "var(--vscode-foreground)",
 						color: "var(--vscode-foreground)",
@@ -103,12 +108,11 @@ const McpView = ({ onDone }: McpViewProps) => {
 						</div>
 						</div>
 					</>
 					</>
 				)}
 				)}
-			</div>
-		</div>
+			</TabContent>
+		</Tab>
 	)
 	)
 }
 }
 
 
-// Server Row Component
 const ServerRow = ({ server, alwaysAllowMcp }: { server: McpServer; alwaysAllowMcp?: boolean }) => {
 const ServerRow = ({ server, alwaysAllowMcp }: { server: McpServer; alwaysAllowMcp?: boolean }) => {
 	const [isExpanded, setIsExpanded] = useState(false)
 	const [isExpanded, setIsExpanded] = useState(false)
 	const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)
 	const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)

+ 12 - 6
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 { TOOL_GROUPS, GROUP_DISPLAY_NAMES, ToolGroup } from "../../../../src/shared/tool-groups"
 import { vscode } from "../../utils/vscode"
 import { vscode } from "../../utils/vscode"
+import { Tab, TabContent, TabHeader } from "../common/Tab"
 
 
 // Get all available groups that should show in prompts view
 // Get all available groups that should show in prompts view
 const availableGroups = (Object.keys(TOOL_GROUPS) as ToolGroup[]).filter((group) => !TOOL_GROUPS[group].alwaysAvailable)
 const availableGroups = (Object.keys(TOOL_GROUPS) as ToolGroup[]).filter((group) => !TOOL_GROUPS[group].alwaysAvailable)
@@ -419,12 +420,13 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
 	}
 	}
 
 
 	return (
 	return (
-		<div className="fixed inset-0 flex flex-col">
-			<div className="flex justify-between items-center px-5 py-2.5 border-b border-vscode-panel-border">
+		<Tab>
+			<TabHeader className="flex justify-between items-center">
 				<h3 className="text-vscode-foreground m-0">Prompts</h3>
 				<h3 className="text-vscode-foreground m-0">Prompts</h3>
 				<VSCodeButton onClick={onDone}>Done</VSCodeButton>
 				<VSCodeButton onClick={onDone}>Done</VSCodeButton>
-			</div>
-			<div className="flex-1 overflow-auto p-5">
+			</TabHeader>
+
+			<TabContent>
 				<div className="pb-5 border-b border-vscode-input-border">
 				<div className="pb-5 border-b border-vscode-input-border">
 					<div className="mb-5">
 					<div className="mb-5">
 						<div className="font-bold mb-1">Preferred Language</div>
 						<div className="font-bold mb-1">Preferred Language</div>
@@ -946,6 +948,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
 						</div>
 						</div>
 					</div>
 					</div>
 				</div>
 				</div>
+
 				<div
 				<div
 					style={{
 					style={{
 						paddingBottom: "40px",
 						paddingBottom: "40px",
@@ -1213,7 +1216,8 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
 						)}
 						)}
 					</div>
 					</div>
 				</div>
 				</div>
-			</div>
+			</TabContent>
+
 			{isCreateModeDialogOpen && (
 			{isCreateModeDialogOpen && (
 				<div
 				<div
 					style={{
 					style={{
@@ -1437,6 +1441,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
 					</div>
 					</div>
 				</div>
 				</div>
 			)}
 			)}
+
 			{isDialogOpen && (
 			{isDialogOpen && (
 				<div
 				<div
 					style={{
 					style={{
@@ -1504,6 +1509,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
 					</div>
 					</div>
 				</div>
 				</div>
 			)}
 			)}
+
 			{isCustomLanguage && (
 			{isCustomLanguage && (
 				<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
 				<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
 					<div className="bg-[var(--vscode-editor-background)] p-6 rounded-lg w-96">
 					<div className="bg-[var(--vscode-editor-background)] p-6 rounded-lg w-96">
@@ -1538,7 +1544,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
 					</div>
 					</div>
 				</div>
 				</div>
 			)}
 			)}
-		</div>
+		</Tab>
 	)
 	)
 }
 }
 
 

+ 35 - 46
webview-ui/src/components/settings/SettingsView.tsx

@@ -22,6 +22,7 @@ import {
 	Button,
 	Button,
 } from "@/components/ui"
 } from "@/components/ui"
 
 
+import { Tab, TabContent, TabHeader } from "../common/Tab"
 import { SetCachedStateField, SetExperimentEnabled } from "./types"
 import { SetCachedStateField, SetExperimentEnabled } from "./types"
 import { SectionHeader } from "./SectionHeader"
 import { SectionHeader } from "./SectionHeader"
 import ApiConfigManager from "./ApiConfigManager"
 import ApiConfigManager from "./ApiConfigManager"
@@ -263,53 +264,41 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone },
 	const scrollToSection = (ref: React.RefObject<HTMLDivElement>) => ref.current?.scrollIntoView()
 	const scrollToSection = (ref: React.RefObject<HTMLDivElement>) => ref.current?.scrollIntoView()
 
 
 	return (
 	return (
-		<div className="fixed inset-0 flex flex-col overflow-hidden">
-			<div className="px-5 py-2.5 border-b border-vscode-panel-border">
-				<div className="flex flex-col">
-					<div className="flex justify-between items-center">
-						<div className="flex items-center gap-2">
-							<h3 className="text-vscode-foreground m-0">Settings</h3>
-							<div className="hidden [@media(min-width:400px)]:flex items-center">
-								{sections.map(({ id, icon: Icon, ref }) => (
-									<Button
-										key={id}
-										variant="ghost"
-										onClick={() => scrollToSection(ref)}
-										className={cn("w-6 h-6", activeSection === id ? "opacity-100" : "opacity-40")}>
-										<Icon />
-									</Button>
-								))}
-							</div>
-						</div>
-						<div className="flex gap-2">
-							<VSCodeButton
-								appearance={isSettingValid ? "primary" : "secondary"}
-								className={!isSettingValid ? "!border-vscode-errorForeground" : ""}
-								title={
-									!isSettingValid
-										? errorMessage
-										: isChangeDetected
-											? "Save changes"
-											: "Nothing changed"
-								}
-								onClick={handleSubmit}
-								disabled={!isChangeDetected || !isSettingValid}>
-								Save
-							</VSCodeButton>
-							<VSCodeButton
-								appearance="secondary"
-								title="Discard unsaved changes and close settings panel"
-								onClick={() => checkUnsaveChanges(onDone)}>
-								Done
-							</VSCodeButton>
-						</div>
+		<Tab>
+			<TabHeader className="flex justify-between items-center gap-2">
+				<div className="flex items-center gap-2">
+					<h3 className="text-vscode-foreground m-0">Settings</h3>
+					<div className="hidden [@media(min-width:400px)]:flex items-center">
+						{sections.map(({ id, icon: Icon, ref }) => (
+							<Button
+								key={id}
+								variant="ghost"
+								onClick={() => scrollToSection(ref)}
+								className={cn("w-6 h-6", activeSection === id ? "opacity-100" : "opacity-40")}>
+								<Icon />
+							</Button>
+						))}
 					</div>
 					</div>
 				</div>
 				</div>
-			</div>
+				<div className="flex gap-2">
+					<VSCodeButton
+						appearance={isSettingValid ? "primary" : "secondary"}
+						className={!isSettingValid ? "!border-vscode-errorForeground" : ""}
+						title={!isSettingValid ? errorMessage : isChangeDetected ? "Save changes" : "Nothing changed"}
+						onClick={handleSubmit}
+						disabled={!isChangeDetected || !isSettingValid}>
+						Save
+					</VSCodeButton>
+					<VSCodeButton
+						appearance="secondary"
+						title="Discard unsaved changes and close settings panel"
+						onClick={() => checkUnsaveChanges(onDone)}>
+						Done
+					</VSCodeButton>
+				</div>
+			</TabHeader>
 
 
-			<div
-				className="flex flex-col flex-1 overflow-auto divide-y divide-vscode-sideBar-background"
-				onScroll={handleScroll}>
+			<TabContent className="p-0 divide-y divide-vscode-sideBar-background" onScroll={handleScroll}>
 				<div ref={providersRef}>
 				<div ref={providersRef}>
 					<SectionHeader>
 					<SectionHeader>
 						<div className="flex items-center gap-2">
 						<div className="flex items-center gap-2">
@@ -425,7 +414,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone },
 					telemetrySetting={telemetrySetting}
 					telemetrySetting={telemetrySetting}
 					setTelemetrySetting={setTelemetrySetting}
 					setTelemetrySetting={setTelemetrySetting}
 				/>
 				/>
-			</div>
+			</TabContent>
 
 
 			<AlertDialog open={isDiscardDialogShow} onOpenChange={setDiscardDialogShow}>
 			<AlertDialog open={isDiscardDialogShow} onOpenChange={setDiscardDialogShow}>
 				<AlertDialogContent>
 				<AlertDialogContent>
@@ -442,7 +431,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone },
 					</AlertDialogFooter>
 					</AlertDialogFooter>
 				</AlertDialogContent>
 				</AlertDialogContent>
 			</AlertDialog>
 			</AlertDialog>
-		</div>
+		</Tab>
 	)
 	)
 })
 })
 
 

+ 2 - 3
webview-ui/src/components/ui/dropdown-menu.tsx

@@ -1,5 +1,6 @@
 import * as React from "react"
 import * as React from "react"
 import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
 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 { CheckIcon, ChevronRightIcon, DotFilledIcon } from "@radix-ui/react-icons"
 
 
 import { cn } from "@/lib/utils"
 import { cn } from "@/lib/utils"
@@ -53,9 +54,7 @@ DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayNam
 
 
 const DropdownMenuContent = React.forwardRef<
 const DropdownMenuContent = React.forwardRef<
 	React.ElementRef<typeof DropdownMenuPrimitive.Content>,
 	React.ElementRef<typeof DropdownMenuPrimitive.Content>,
-	React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content> & {
-		container?: HTMLElement
-	}
+	React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content> & Pick<PortalProps, "container">
 >(({ className, sideOffset = 4, container, ...props }, ref) => (
 >(({ className, sideOffset = 4, container, ...props }, ref) => (
 	<DropdownMenuPrimitive.Portal container={container}>
 	<DropdownMenuPrimitive.Portal container={container}>
 		<DropdownMenuPrimitive.Content
 		<DropdownMenuPrimitive.Content

+ 1 - 0
webview-ui/src/components/ui/hooks/index.ts

@@ -1 +1,2 @@
 export * from "./useClipboard"
 export * from "./useClipboard"
+export * from "./useRooPortal"

+ 10 - 0
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<HTMLElement>()
+
+	useMount(() => setContainer(document.getElementById(id) ?? undefined))
+
+	return container
+}

+ 2 - 3
webview-ui/src/components/ui/popover.tsx

@@ -1,4 +1,5 @@
 import * as React from "react"
 import * as React from "react"
+import { PortalProps } from "@radix-ui/react-portal"
 import * as PopoverPrimitive from "@radix-ui/react-popover"
 import * as PopoverPrimitive from "@radix-ui/react-popover"
 
 
 import { cn } from "@/lib/utils"
 import { cn } from "@/lib/utils"
@@ -11,9 +12,7 @@ const PopoverAnchor = PopoverPrimitive.Anchor
 
 
 const PopoverContent = React.forwardRef<
 const PopoverContent = React.forwardRef<
 	React.ElementRef<typeof PopoverPrimitive.Content>,
 	React.ElementRef<typeof PopoverPrimitive.Content>,
-	React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content> & {
-		container?: HTMLElement
-	}
+	React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content> & Pick<PortalProps, "container">
 >(({ className, align = "center", sideOffset = 4, container, ...props }, ref) => (
 >(({ className, align = "center", sideOffset = 4, container, ...props }, ref) => (
 	<PopoverPrimitive.Portal container={container}>
 	<PopoverPrimitive.Portal container={container}>
 		<PopoverPrimitive.Content
 		<PopoverPrimitive.Content

+ 9 - 26
webview-ui/src/components/ui/select-dropdown.tsx

@@ -1,4 +1,8 @@
 import * as React from "react"
 import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+import { useRooPortal } from "./hooks/useRooPortal"
 import {
 import {
 	DropdownMenu,
 	DropdownMenu,
 	DropdownMenuContent,
 	DropdownMenuContent,
@@ -6,10 +10,7 @@ import {
 	DropdownMenuTrigger,
 	DropdownMenuTrigger,
 	DropdownMenuSeparator,
 	DropdownMenuSeparator,
 } from "./dropdown-menu"
 } from "./dropdown-menu"
-import { cn } from "@/lib/utils"
-import { useEffect, useState } from "react"
 
 
-// Constants for option types
 export enum DropdownOptionType {
 export enum DropdownOptionType {
 	ITEM = "item",
 	ITEM = "item",
 	SEPARATOR = "separator",
 	SEPARATOR = "separator",
@@ -20,7 +21,7 @@ export interface DropdownOption {
 	value: string
 	value: string
 	label: string
 	label: string
 	disabled?: boolean
 	disabled?: boolean
-	type?: DropdownOptionType // Optional type to specify special behaviors
+	type?: DropdownOptionType
 }
 }
 
 
 export interface SelectDropdownProps {
 export interface SelectDropdownProps {
@@ -58,34 +59,19 @@ export const SelectDropdown = React.forwardRef<React.ElementRef<typeof DropdownM
 		},
 		},
 		ref,
 		ref,
 	) => {
 	) => {
-		// Track open state
 		const [open, setOpen] = React.useState(false)
 		const [open, setOpen] = React.useState(false)
-		const [portalContainer, setPortalContainer] = useState<HTMLElement>()
-
-		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)
-		}, [])
+		const portalContainer = useRooPortal("roo-portal")
 
 
-		// Find the selected option label
 		const selectedOption = options.find((option) => option.value === value)
 		const selectedOption = options.find((option) => option.value === value)
 		const displayText = selectedOption?.label || placeholder || ""
 		const displayText = selectedOption?.label || placeholder || ""
 
 
-		// Handle menu item click
 		const handleSelect = (option: DropdownOption) => {
 		const handleSelect = (option: DropdownOption) => {
-			// Check if this is an action option by its explicit type
 			if (option.type === DropdownOptionType.ACTION) {
 			if (option.type === DropdownOptionType.ACTION) {
-				window.postMessage({
-					type: "action",
-					action: option.value,
-				})
+				window.postMessage({ type: "action", action: option.value })
 				setOpen(false)
 				setOpen(false)
 				return
 				return
 			}
 			}
+
 			onChange(option.value)
 			onChange(option.value)
 			setOpen(false)
 			setOpen(false)
 		}
 		}
@@ -103,7 +89,7 @@ export const SelectDropdown = React.forwardRef<React.ElementRef<typeof DropdownM
 						triggerClassName,
 						triggerClassName,
 					)}
 					)}
 					style={{
 					style={{
-						width: "100%", // Take full width of parent
+						width: "100%", // Take full width of parent.
 						minWidth: "0",
 						minWidth: "0",
 						maxWidth: "100%",
 						maxWidth: "100%",
 					}}>
 					}}>
@@ -136,12 +122,10 @@ export const SelectDropdown = React.forwardRef<React.ElementRef<typeof DropdownM
 						contentClassName,
 						contentClassName,
 					)}>
 					)}>
 					{options.map((option, index) => {
 					{options.map((option, index) => {
-						// Handle separator type
 						if (option.type === DropdownOptionType.SEPARATOR) {
 						if (option.type === DropdownOptionType.SEPARATOR) {
 							return <DropdownMenuSeparator key={`sep-${index}`} />
 							return <DropdownMenuSeparator key={`sep-${index}`} />
 						}
 						}
 
 
-						// Handle shortcut text type (disabled label for keyboard shortcuts)
 						if (
 						if (
 							option.type === DropdownOptionType.SHORTCUT ||
 							option.type === DropdownOptionType.SHORTCUT ||
 							(option.disabled && shortcutText && option.label.includes(shortcutText))
 							(option.disabled && shortcutText && option.label.includes(shortcutText))
@@ -153,7 +137,6 @@ export const SelectDropdown = React.forwardRef<React.ElementRef<typeof DropdownM
 							)
 							)
 						}
 						}
 
 
-						// Regular menu items
 						return (
 						return (
 							<DropdownMenuItem
 							<DropdownMenuItem
 								key={`item-${option.value}`}
 								key={`item-${option.value}`}

+ 4 - 2
webview-ui/src/components/ui/select.tsx

@@ -1,4 +1,5 @@
 import * as React from "react"
 import * as React from "react"
+import { PortalProps } from "@radix-ui/react-portal"
 import * as SelectPrimitive from "@radix-ui/react-select"
 import * as SelectPrimitive from "@radix-ui/react-select"
 import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
 import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
 
 
@@ -37,10 +38,11 @@ function SelectContent({
 	className,
 	className,
 	children,
 	children,
 	position = "popper",
 	position = "popper",
+	container,
 	...props
 	...props
-}: React.ComponentProps<typeof SelectPrimitive.Content>) {
+}: React.ComponentProps<typeof SelectPrimitive.Content> & Pick<PortalProps, "container">) {
 	return (
 	return (
-		<SelectPrimitive.Portal>
+		<SelectPrimitive.Portal container={container}>
 			<SelectPrimitive.Content
 			<SelectPrimitive.Content
 				data-slot="select-content"
 				data-slot="select-content"
 				className={cn(
 				className={cn(

+ 19 - 19
webview-ui/src/components/welcome/WelcomeView.tsx

@@ -1,9 +1,12 @@
-import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
 import { useCallback, useState } from "react"
 import { useCallback, useState } from "react"
+import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
+
 import { useExtensionState } from "../../context/ExtensionStateContext"
 import { useExtensionState } from "../../context/ExtensionStateContext"
 import { validateApiConfiguration } from "../../utils/validate"
 import { validateApiConfiguration } from "../../utils/validate"
 import { vscode } from "../../utils/vscode"
 import { vscode } from "../../utils/vscode"
 import ApiOptions from "../settings/ApiOptions"
 import ApiOptions from "../settings/ApiOptions"
+import { Tab, TabContent } from "../common/Tab"
+import { Alert } from "../common/Alert"
 
 
 const WelcomeView = () => {
 const WelcomeView = () => {
 	const { apiConfiguration, currentApiConfigName, setApiConfiguration, uriScheme } = useExtensionState()
 	const { apiConfiguration, currentApiConfigName, setApiConfiguration, uriScheme } = useExtensionState()
@@ -23,18 +26,16 @@ const WelcomeView = () => {
 	}, [apiConfiguration, currentApiConfigName])
 	}, [apiConfiguration, currentApiConfigName])
 
 
 	return (
 	return (
-		<div className="flex flex-col min-h-screen px-0 pb-5">
-			<h2>Hi, I'm Roo!</h2>
-			<p>
-				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.
-			</p>
-
-			<b>To get started, this extension needs an API provider.</b>
-
-			<div className="mt-3">
+		<Tab>
+			<TabContent className="flex flex-col gap-5">
+				<h2 className="m-0 p-0">Hi, I'm Roo!</h2>
+				<div>
+					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.
+				</div>
+				<Alert className="font-bold text-sm">To get started, this extension needs an API provider.</Alert>
 				<ApiOptions
 				<ApiOptions
 					fromWelcomeView
 					fromWelcomeView
 					apiConfiguration={apiConfiguration || {}}
 					apiConfiguration={apiConfiguration || {}}
@@ -43,15 +44,14 @@ const WelcomeView = () => {
 					errorMessage={errorMessage}
 					errorMessage={errorMessage}
 					setErrorMessage={setErrorMessage}
 					setErrorMessage={setErrorMessage}
 				/>
 				/>
-			</div>
-
-			<div className="sticky bottom-0 bg-[var(--vscode-sideBar-background)] py-3">
-				<div className="flex flex-col gap-1.5">
+			</TabContent>
+			<div className="sticky bottom-0 bg-vscode-sideBar-background p-5">
+				<div className="flex flex-col gap-1">
 					<VSCodeButton onClick={handleSubmit}>Let's go!</VSCodeButton>
 					<VSCodeButton onClick={handleSubmit}>Let's go!</VSCodeButton>
-					{errorMessage && <span className="text-destructive">{errorMessage}</span>}
+					{errorMessage && <div className="text-vscode-errorForeground">{errorMessage}</div>}
 				</div>
 				</div>
 			</div>
 			</div>
-		</div>
+		</Tab>
 	)
 	)
 }
 }
 
 

+ 4 - 0
webview-ui/src/index.css

@@ -111,6 +111,10 @@
 
 
 	--color-vscode-charts-green: var(--vscode-charts-green);
 	--color-vscode-charts-green: var(--vscode-charts-green);
 	--color-vscode-charts-yellow: var(--vscode-charts-yellow);
 	--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 {
 @layer base {