From e14b1b2daba6d2ef7060846f32ca5d747bac630e Mon Sep 17 00:00:00 2001
From: Daniel Trugman
Date: Fri, 7 Mar 2025 13:53:32 +0000
Subject: [PATCH 01/10] Add OpenAI-style cost calculation
---
src/api/providers/vscode-lm.ts | 4 +-
src/core/Cline.ts | 6 +-
src/utils/__tests__/cost.test.ts | 113 ++++++++++++++++++++++++++++---
src/utils/cost.ts | 57 ++++++++++++----
4 files changed, 152 insertions(+), 28 deletions(-)
diff --git a/src/api/providers/vscode-lm.ts b/src/api/providers/vscode-lm.ts
index bf1215e238..0ce2a6e26a 100644
--- a/src/api/providers/vscode-lm.ts
+++ b/src/api/providers/vscode-lm.ts
@@ -2,7 +2,7 @@ import { Anthropic } from "@anthropic-ai/sdk"
import * as vscode from "vscode"
import { SingleCompletionHandler } from "../"
-import { calculateApiCost } from "../../utils/cost"
+import { calculateApiCostAnthropic } from "../../utils/cost"
import { ApiStream } from "../transform/stream"
import { convertToVsCodeLmMessages } from "../transform/vscode-lm-format"
import { SELECTOR_SEPARATOR, stringifyVsCodeLmModelSelector } from "../../shared/vsCodeSelectorUtils"
@@ -462,7 +462,7 @@ export class VsCodeLmHandler extends BaseProvider implements SingleCompletionHan
type: "usage",
inputTokens: totalInputTokens,
outputTokens: totalOutputTokens,
- totalCost: calculateApiCost(this.getModel().info, totalInputTokens, totalOutputTokens),
+ totalCost: calculateApiCostAnthropic(this.getModel().info, totalInputTokens, totalOutputTokens),
}
} catch (error: unknown) {
this.ensureCleanState()
diff --git a/src/core/Cline.ts b/src/core/Cline.ts
index 16f1d4e99d..4f27a89cc0 100644
--- a/src/core/Cline.ts
+++ b/src/core/Cline.ts
@@ -55,7 +55,7 @@ import { ClineAskResponse } from "../shared/WebviewMessage"
import { GlobalFileNames } from "../shared/globalFileNames"
import { defaultModeSlug, getModeBySlug, getFullModeDetails } from "../shared/modes"
import { EXPERIMENT_IDS, experiments as Experiments, ExperimentId } from "../shared/experiments"
-import { calculateApiCost } from "../utils/cost"
+import { calculateApiCostAnthropic } from "../utils/cost"
import { fileExistsAtPath } from "../utils/fs"
import { arePathsEqual, getReadablePath } from "../utils/path"
import { parseMentions } from "./mentions"
@@ -875,7 +875,7 @@ export class Cline {
// The way this agentic loop works is that cline will be given a task that he then calls tools to complete. unless there's an attempt_completion call, we keep responding back to him with his tool's responses until he either attempt_completion or does not use anymore tools. If he does not use anymore tools, we ask him to consider if he's completed the task and then call attempt_completion, otherwise proceed with completing the task.
// There is a MAX_REQUESTS_PER_TASK limit to prevent infinite requests, but Cline is prompted to finish the task as efficiently as he can.
- //const totalCost = this.calculateApiCost(totalInputTokens, totalOutputTokens)
+ //const totalCost = this.calculateApiCostAntrhopic(totalInputTokens, totalOutputTokens)
if (didEndLoop) {
// For now a task never 'completes'. This will only happen if the user hits max requests and denies resetting the count.
//this.say("task_completed", `Task completed. Total API usage cost: ${totalCost}`)
@@ -3159,7 +3159,7 @@ export class Cline {
cacheReads: cacheReadTokens,
cost:
totalCost ??
- calculateApiCost(
+ calculateApiCostAnthropic(
this.api.getModel().info,
inputTokens,
outputTokens,
diff --git a/src/utils/__tests__/cost.test.ts b/src/utils/__tests__/cost.test.ts
index e390c4af7f..4501f86b88 100644
--- a/src/utils/__tests__/cost.test.ts
+++ b/src/utils/__tests__/cost.test.ts
@@ -1,8 +1,8 @@
-import { calculateApiCost } from "../cost"
+import { calculateApiCostAnthropic, calculateApiCostOpenAI } from "../cost"
import { ModelInfo } from "../../shared/api"
describe("Cost Utility", () => {
- describe("calculateApiCost", () => {
+ describe("calculateApiCostAnthropic", () => {
const mockModelInfo: ModelInfo = {
maxTokens: 8192,
contextWindow: 200_000,
@@ -14,7 +14,7 @@ describe("Cost Utility", () => {
}
it("should calculate basic input/output costs correctly", () => {
- const cost = calculateApiCost(mockModelInfo, 1000, 500)
+ const cost = calculateApiCostAnthropic(mockModelInfo, 1000, 500)
// Input cost: (3.0 / 1_000_000) * 1000 = 0.003
// Output cost: (15.0 / 1_000_000) * 500 = 0.0075
@@ -23,7 +23,7 @@ describe("Cost Utility", () => {
})
it("should handle cache writes cost", () => {
- const cost = calculateApiCost(mockModelInfo, 1000, 500, 2000)
+ const cost = calculateApiCostAnthropic(mockModelInfo, 1000, 500, 2000)
// Input cost: (3.0 / 1_000_000) * 1000 = 0.003
// Output cost: (15.0 / 1_000_000) * 500 = 0.0075
@@ -33,7 +33,7 @@ describe("Cost Utility", () => {
})
it("should handle cache reads cost", () => {
- const cost = calculateApiCost(mockModelInfo, 1000, 500, undefined, 3000)
+ const cost = calculateApiCostAnthropic(mockModelInfo, 1000, 500, undefined, 3000)
// Input cost: (3.0 / 1_000_000) * 1000 = 0.003
// Output cost: (15.0 / 1_000_000) * 500 = 0.0075
@@ -43,7 +43,7 @@ describe("Cost Utility", () => {
})
it("should handle all cost components together", () => {
- const cost = calculateApiCost(mockModelInfo, 1000, 500, 2000, 3000)
+ const cost = calculateApiCostAnthropic(mockModelInfo, 1000, 500, 2000, 3000)
// Input cost: (3.0 / 1_000_000) * 1000 = 0.003
// Output cost: (15.0 / 1_000_000) * 500 = 0.0075
@@ -60,17 +60,17 @@ describe("Cost Utility", () => {
supportsPromptCache: true,
}
- const cost = calculateApiCost(modelWithoutPrices, 1000, 500, 2000, 3000)
+ const cost = calculateApiCostAnthropic(modelWithoutPrices, 1000, 500, 2000, 3000)
expect(cost).toBe(0)
})
it("should handle zero tokens", () => {
- const cost = calculateApiCost(mockModelInfo, 0, 0, 0, 0)
+ const cost = calculateApiCostAnthropic(mockModelInfo, 0, 0, 0, 0)
expect(cost).toBe(0)
})
it("should handle undefined cache values", () => {
- const cost = calculateApiCost(mockModelInfo, 1000, 500)
+ const cost = calculateApiCostAnthropic(mockModelInfo, 1000, 500)
// Input cost: (3.0 / 1_000_000) * 1000 = 0.003
// Output cost: (15.0 / 1_000_000) * 500 = 0.0075
@@ -85,7 +85,7 @@ describe("Cost Utility", () => {
cacheReadsPrice: undefined,
}
- const cost = calculateApiCost(modelWithoutCachePrices, 1000, 500, 2000, 3000)
+ const cost = calculateApiCostAnthropic(modelWithoutCachePrices, 1000, 500, 2000, 3000)
// Should only include input and output costs
// Input cost: (3.0 / 1_000_000) * 1000 = 0.003
@@ -94,4 +94,97 @@ describe("Cost Utility", () => {
expect(cost).toBe(0.0105)
})
})
+
+ describe("calculateApiCostOpenAI", () => {
+ const mockModelInfo: ModelInfo = {
+ maxTokens: 8192,
+ contextWindow: 200_000,
+ supportsPromptCache: true,
+ inputPrice: 3.0, // $3 per million tokens
+ outputPrice: 15.0, // $15 per million tokens
+ cacheWritesPrice: 3.75, // $3.75 per million tokens
+ cacheReadsPrice: 0.3, // $0.30 per million tokens
+ }
+
+ it("should calculate basic input/output costs correctly", () => {
+ const cost = calculateApiCostOpenAI(mockModelInfo, 1000, 500)
+
+ // Input cost: (3.0 / 1_000_000) * 1000 = 0.003
+ // Output cost: (15.0 / 1_000_000) * 500 = 0.0075
+ // Total: 0.003 + 0.0075 = 0.0105
+ expect(cost).toBe(0.0105)
+ })
+
+ it("should handle cache writes cost", () => {
+ const cost = calculateApiCostOpenAI(mockModelInfo, 3000, 500, 2000)
+
+ // Input cost: (3.0 / 1_000_000) * (3000 - 2000) = 0.003
+ // Output cost: (15.0 / 1_000_000) * 500 = 0.0075
+ // Cache writes: (3.75 / 1_000_000) * 2000 = 0.0075
+ // Total: 0.003 + 0.0075 + 0.0075 = 0.018
+ expect(cost).toBeCloseTo(0.018, 6)
+ })
+
+ it("should handle cache reads cost", () => {
+ const cost = calculateApiCostOpenAI(mockModelInfo, 4000, 500, undefined, 3000)
+
+ // Input cost: (3.0 / 1_000_000) * (4000 - 3000) = 0.003
+ // Output cost: (15.0 / 1_000_000) * 500 = 0.0075
+ // Cache reads: (0.3 / 1_000_000) * 3000 = 0.0009
+ // Total: 0.003 + 0.0075 + 0.0009 = 0.0114
+ expect(cost).toBe(0.0114)
+ })
+
+ it("should handle all cost components together", () => {
+ const cost = calculateApiCostOpenAI(mockModelInfo, 6000, 500, 2000, 3000)
+
+ // Input cost: (3.0 / 1_000_000) * (6000 - 2000 - 3000) = 0.003
+ // Output cost: (15.0 / 1_000_000) * 500 = 0.0075
+ // Cache writes: (3.75 / 1_000_000) * 2000 = 0.0075
+ // Cache reads: (0.3 / 1_000_000) * 3000 = 0.0009
+ // Total: 0.003 + 0.0075 + 0.0075 + 0.0009 = 0.0189
+ expect(cost).toBe(0.0189)
+ })
+
+ it("should handle missing prices gracefully", () => {
+ const modelWithoutPrices: ModelInfo = {
+ maxTokens: 8192,
+ contextWindow: 200_000,
+ supportsPromptCache: true,
+ }
+
+ const cost = calculateApiCostOpenAI(modelWithoutPrices, 1000, 500, 2000, 3000)
+ expect(cost).toBe(0)
+ })
+
+ it("should handle zero tokens", () => {
+ const cost = calculateApiCostOpenAI(mockModelInfo, 0, 0, 0, 0)
+ expect(cost).toBe(0)
+ })
+
+ it("should handle undefined cache values", () => {
+ const cost = calculateApiCostOpenAI(mockModelInfo, 1000, 500)
+
+ // Input cost: (3.0 / 1_000_000) * 1000 = 0.003
+ // Output cost: (15.0 / 1_000_000) * 500 = 0.0075
+ // Total: 0.003 + 0.0075 = 0.0105
+ expect(cost).toBe(0.0105)
+ })
+
+ it("should handle missing cache prices", () => {
+ const modelWithoutCachePrices: ModelInfo = {
+ ...mockModelInfo,
+ cacheWritesPrice: undefined,
+ cacheReadsPrice: undefined,
+ }
+
+ const cost = calculateApiCostOpenAI(modelWithoutCachePrices, 6000, 500, 2000, 3000)
+
+ // Should only include input and output costs
+ // Input cost: (3.0 / 1_000_000) * (6000 - 2000 - 3000) = 0.003
+ // Output cost: (15.0 / 1_000_000) * 500 = 0.0075
+ // Total: 0.003 + 0.0075 = 0.0105
+ expect(cost).toBe(0.0105)
+ })
+ })
})
diff --git a/src/utils/cost.ts b/src/utils/cost.ts
index adc2ded0a8..48108b6348 100644
--- a/src/utils/cost.ts
+++ b/src/utils/cost.ts
@@ -1,26 +1,57 @@
import { ModelInfo } from "../shared/api"
-export function calculateApiCost(
+function calculateApiCostInternal(
modelInfo: ModelInfo,
inputTokens: number,
outputTokens: number,
- cacheCreationInputTokens?: number,
- cacheReadInputTokens?: number,
+ cacheCreationInputTokens: number,
+ cacheReadInputTokens: number,
): number {
- const modelCacheWritesPrice = modelInfo.cacheWritesPrice
- let cacheWritesCost = 0
- if (cacheCreationInputTokens && modelCacheWritesPrice) {
- cacheWritesCost = (modelCacheWritesPrice / 1_000_000) * cacheCreationInputTokens
- }
- const modelCacheReadsPrice = modelInfo.cacheReadsPrice
- let cacheReadsCost = 0
- if (cacheReadInputTokens && modelCacheReadsPrice) {
- cacheReadsCost = (modelCacheReadsPrice / 1_000_000) * cacheReadInputTokens
- }
+ const cacheWritesCost = ((modelInfo.cacheWritesPrice || 0) / 1_000_000) * cacheCreationInputTokens
+ const cacheReadsCost = ((modelInfo.cacheReadsPrice || 0) / 1_000_000) * cacheReadInputTokens
const baseInputCost = ((modelInfo.inputPrice || 0) / 1_000_000) * inputTokens
const outputCost = ((modelInfo.outputPrice || 0) / 1_000_000) * outputTokens
const totalCost = cacheWritesCost + cacheReadsCost + baseInputCost + outputCost
return totalCost
}
+// For Anthropic compliant usage, the input tokens count does NOT include the cached tokens
+export function calculateApiCostAnthropic(
+ modelInfo: ModelInfo,
+ inputTokens: number,
+ outputTokens: number,
+ cacheCreationInputTokens?: number,
+ cacheReadInputTokens?: number,
+): number {
+ const cacheCreationInputTokensNum = cacheCreationInputTokens || 0
+ const cacheReadInputTokensNum = cacheReadInputTokens || 0
+ return calculateApiCostInternal(
+ modelInfo,
+ inputTokens,
+ outputTokens,
+ cacheCreationInputTokensNum,
+ cacheReadInputTokensNum,
+ )
+}
+
+// For OpenAI compliant usage, the input tokens count INCLUDES the cached tokens
+export function calculateApiCostOpenAI(
+ modelInfo: ModelInfo,
+ inputTokens: number,
+ outputTokens: number,
+ cacheCreationInputTokens?: number,
+ cacheReadInputTokens?: number,
+): number {
+ const cacheCreationInputTokensNum = cacheCreationInputTokens || 0
+ const cacheReadInputTokensNum = cacheReadInputTokens || 0
+ const nonCachedInputTokens = Math.max(0, inputTokens - cacheCreationInputTokensNum - cacheReadInputTokensNum)
+ return calculateApiCostInternal(
+ modelInfo,
+ nonCachedInputTokens,
+ outputTokens,
+ cacheCreationInputTokensNum,
+ cacheReadInputTokensNum,
+ )
+}
+
export const parseApiPrice = (price: any) => (price ? parseFloat(price) * 1_000_000 : undefined)
From c51f59e50b6642494ed5c5addf6897e9a9827ae5 Mon Sep 17 00:00:00 2001
From: Daniel Trugman
Date: Fri, 7 Mar 2025 14:57:11 +0000
Subject: [PATCH 02/10] Requesty: Correctly calculate request costs
---
src/api/providers/__tests__/requesty.test.ts | 26 +++++++++++-----
src/api/providers/openai.ts | 6 ++--
src/api/providers/requesty.ts | 32 ++++++++++++++++----
3 files changed, 47 insertions(+), 17 deletions(-)
diff --git a/src/api/providers/__tests__/requesty.test.ts b/src/api/providers/__tests__/requesty.test.ts
index 7867b15ebc..47921a1c53 100644
--- a/src/api/providers/__tests__/requesty.test.ts
+++ b/src/api/providers/__tests__/requesty.test.ts
@@ -22,8 +22,10 @@ describe("RequestyHandler", () => {
contextWindow: 4000,
supportsPromptCache: false,
supportsImages: true,
- inputPrice: 0,
- outputPrice: 0,
+ inputPrice: 1,
+ outputPrice: 10,
+ cacheReadsPrice: 0.1,
+ cacheWritesPrice: 1.5,
},
openAiStreamingEnabled: true,
includeMaxTokens: true, // Add this to match the implementation
@@ -83,8 +85,12 @@ describe("RequestyHandler", () => {
yield {
choices: [{ delta: { content: " world" } }],
usage: {
- prompt_tokens: 10,
- completion_tokens: 5,
+ prompt_tokens: 30,
+ completion_tokens: 10,
+ prompt_tokens_details: {
+ cached_tokens: 15,
+ caching_tokens: 5,
+ },
},
}
},
@@ -105,10 +111,11 @@ describe("RequestyHandler", () => {
{ type: "text", text: " world" },
{
type: "usage",
- inputTokens: 10,
- outputTokens: 5,
- cacheWriteTokens: undefined,
- cacheReadTokens: undefined,
+ inputTokens: 30,
+ outputTokens: 10,
+ cacheWriteTokens: 5,
+ cacheReadTokens: 15,
+ totalCost: 0.000119, // (10 * 1 / 1,000,000) + (5 * 1.5 / 1,000,000) + (15 * 0.1 / 1,000,000) + (10 * 10 / 1,000,000)
},
])
@@ -182,6 +189,9 @@ describe("RequestyHandler", () => {
type: "usage",
inputTokens: 10,
outputTokens: 5,
+ cacheWriteTokens: 0,
+ cacheReadTokens: 0,
+ totalCost: 0.00006, // (10 * 1 / 1,000,000) + (5 * 10 / 1,000,000)
},
])
diff --git a/src/api/providers/openai.ts b/src/api/providers/openai.ts
index 9262f3b75a..5d85a86a5a 100644
--- a/src/api/providers/openai.ts
+++ b/src/api/providers/openai.ts
@@ -111,7 +111,7 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
}
}
if (chunk.usage) {
- yield this.processUsageMetrics(chunk.usage)
+ yield this.processUsageMetrics(chunk.usage, modelInfo)
}
}
} else {
@@ -134,11 +134,11 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
type: "text",
text: response.choices[0]?.message.content || "",
}
- yield this.processUsageMetrics(response.usage)
+ yield this.processUsageMetrics(response.usage, modelInfo)
}
}
- protected processUsageMetrics(usage: any): ApiStreamUsageChunk {
+ protected processUsageMetrics(usage: any, modelInfo?: ModelInfo): ApiStreamUsageChunk {
return {
type: "usage",
inputTokens: usage?.prompt_tokens || 0,
diff --git a/src/api/providers/requesty.ts b/src/api/providers/requesty.ts
index 0060bfc5d8..434d6f4316 100644
--- a/src/api/providers/requesty.ts
+++ b/src/api/providers/requesty.ts
@@ -1,9 +1,20 @@
import axios from "axios"
import { ModelInfo, requestyModelInfoSaneDefaults, requestyDefaultModelId } from "../../shared/api"
-import { parseApiPrice } from "../../utils/cost"
+import { calculateApiCostOpenAI, parseApiPrice } from "../../utils/cost"
import { ApiStreamUsageChunk } from "../transform/stream"
import { OpenAiHandler, OpenAiHandlerOptions } from "./openai"
+import OpenAI from "openai"
+
+// Requesty usage includes an extra field for Anthropic use cases.
+// Safely cast the prompt token details section to the appropriate structure.
+interface RequestyUsage extends OpenAI.CompletionUsage {
+ prompt_tokens_details?: {
+ caching_tokens?: number
+ cached_tokens?: number
+ }
+ total_cost?: number
+}
export class RequestyHandler extends OpenAiHandler {
constructor(options: OpenAiHandlerOptions) {
@@ -27,13 +38,22 @@ export class RequestyHandler extends OpenAiHandler {
}
}
- protected override processUsageMetrics(usage: any): ApiStreamUsageChunk {
+ protected override processUsageMetrics(usage: any, modelInfo?: ModelInfo): ApiStreamUsageChunk {
+ const requestyUsage = usage as RequestyUsage
+ const inputTokens = requestyUsage?.prompt_tokens || 0
+ const outputTokens = requestyUsage?.completion_tokens || 0
+ const cacheWriteTokens = requestyUsage?.prompt_tokens_details?.caching_tokens || 0
+ const cacheReadTokens = requestyUsage?.prompt_tokens_details?.cached_tokens || 0
+ const totalCost = modelInfo
+ ? calculateApiCostOpenAI(modelInfo, inputTokens, outputTokens, cacheWriteTokens, cacheReadTokens)
+ : 0
return {
type: "usage",
- inputTokens: usage?.prompt_tokens || 0,
- outputTokens: usage?.completion_tokens || 0,
- cacheWriteTokens: usage?.cache_creation_input_tokens,
- cacheReadTokens: usage?.cache_read_input_tokens,
+ inputTokens: inputTokens,
+ outputTokens: outputTokens,
+ cacheWriteTokens: cacheWriteTokens,
+ cacheReadTokens: cacheReadTokens,
+ totalCost: totalCost,
}
}
}
From 9f40c4d57a737db353be350ffe7c963466ed0b28 Mon Sep 17 00:00:00 2001
From: cte
Date: Sun, 9 Mar 2025 15:07:59 -0700
Subject: [PATCH 03/10] App tab layout fixes
---
.env.sample | 3 +-
webview-ui/src/App.tsx | 50 +++++-------
.../src/components/chat/Announcement.tsx | 4 +-
.../chat/checkpoints/CheckpointMenu.tsx | 14 +---
webview-ui/src/components/common/Alert.tsx | 15 ++++
webview-ui/src/components/common/Tab.tsx | 23 ++++++
.../src/components/history/HistoryView.tsx | 15 ++--
webview-ui/src/components/mcp/McpView.tsx | 26 +++---
.../src/components/prompts/PromptsView.tsx | 18 +++--
.../src/components/settings/SettingsView.tsx | 81 ++++++++-----------
.../src/components/ui/dropdown-menu.tsx | 36 +++++----
webview-ui/src/components/ui/popover.tsx | 36 +++++----
.../src/components/ui/select-dropdown.tsx | 14 +---
webview-ui/src/components/ui/select.tsx | 4 +-
.../src/components/welcome/WelcomeView.tsx | 38 ++++-----
webview-ui/src/index.css | 4 +
16 files changed, 198 insertions(+), 183 deletions(-)
create mode 100644 webview-ui/src/components/common/Alert.tsx
create mode 100644 webview-ui/src/components/common/Tab.tsx
diff --git a/.env.sample b/.env.sample
index 6cdaa1b3b1..4d6c24ac72 100644
--- a/.env.sample
+++ b/.env.sample
@@ -1,2 +1 @@
-# PostHog API Keys for telemetry
-POSTHOG_API_KEY=key-goes-here
\ No newline at end of file
+POSTHOG_API_KEY=key-goes-here
diff --git a/webview-ui/src/App.tsx b/webview-ui/src/App.tsx
index b537b9298e..99dda495d0 100644
--- a/webview-ui/src/App.tsx
+++ b/webview-ui/src/App.tsx
@@ -17,6 +17,12 @@ import { HumanRelayDialog } from "./components/human-relay/HumanRelayDialog"
type Tab = "settings" | "history" | "mcp" | "prompts" | "chat"
+type HumanRelayDialogState = {
+ isOpen: boolean
+ requestId: string
+ promptText: string
+}
+
const tabsByMessageAction: Partial, Tab>> = {
chatButtonClicked: "chat",
settingsButtonClicked: "settings",
@@ -24,24 +30,21 @@ const tabsByMessageAction: Partial {
const { didHydrateState, showWelcome, shouldShowAnnouncement, telemetrySetting, telemetryKey, machineId } =
useExtensionState()
+
const [showAnnouncement, setShowAnnouncement] = useState(false)
const [tab, setTab] = useState("chat")
- const settingsRef = useRef(null)
-
- // Human Relay Dialog Status
- const [humanRelayDialogState, setHumanRelayDialogState] = useState<{
- isOpen: boolean
- requestId: string
- promptText: string
- }>({
+ const [humanRelayDialogState, setHumanRelayDialogState] = useState({
isOpen: false,
requestId: "",
promptText: "",
})
+ const settingsRef = useRef(null)
+
const switchTab = useCallback((newTab: Tab) => {
if (settingsRef.current?.checkUnsaveChanges) {
settingsRef.current.checkUnsaveChanges(() => setTab(newTab))
@@ -74,23 +77,6 @@ const App = () => {
[switchTab],
)
- // Processing Human Relay Dialog Submission
- const handleHumanRelaySubmit = (requestId: string, text: string) => {
- vscode.postMessage({
- type: "humanRelayResponse",
- requestId,
- text,
- })
- }
-
- // Handle Human Relay dialog box cancel
- const handleHumanRelayCancel = (requestId: string) => {
- vscode.postMessage({
- type: "humanRelayCancel",
- requestId,
- })
- }
-
useEvent("message", onMessage)
useEffect(() => {
@@ -106,7 +92,7 @@ const App = () => {
}
}, [telemetrySetting, telemetryKey, machineId, didHydrateState])
- // Tell Extension that we are ready to receive messages
+ // Tell the extension that we are ready to receive messages.
useEffect(() => {
vscode.postMessage({ type: "webviewDidLaunch" })
}, [])
@@ -121,24 +107,23 @@ const App = () => {
) : (
<>
- {tab === "settings" && setTab("chat")} />}
- {tab === "history" && switchTab("chat")} />}
- {tab === "mcp" && switchTab("chat")} />}
{tab === "prompts" && switchTab("chat")} />}
+ {tab === "mcp" && switchTab("chat")} />}
+ {tab === "history" && switchTab("chat")} />}
+ {tab === "settings" && setTab("chat")} />}
setShowAnnouncement(false)}
showHistoryView={() => switchTab("history")}
/>
- {/* Human Relay Dialog */}
setHumanRelayDialogState((prev) => ({ ...prev, isOpen: false }))}
- onSubmit={handleHumanRelaySubmit}
- onCancel={handleHumanRelayCancel}
+ onSubmit={(requestId, text) => vscode.postMessage({ type: "humanRelayResponse", requestId, text })}
+ onCancel={(requestId) => vscode.postMessage({ type: "humanRelayCancel", requestId })}
/>
>
)
@@ -147,6 +132,7 @@ const App = () => {
const AppWithProviders = () => (
+
)
diff --git a/webview-ui/src/components/chat/Announcement.tsx b/webview-ui/src/components/chat/Announcement.tsx
index 82fdb628eb..791ca26085 100644
--- a/webview-ui/src/components/chat/Announcement.tsx
+++ b/webview-ui/src/components/chat/Announcement.tsx
@@ -33,7 +33,7 @@ const Announcement = ({ version, hideAnnouncement }: AnnouncementProps) => {
What's New
-
+
- • Faster asynchronous checkpoints
- • Support for .rooignore files
@@ -44,7 +44,7 @@ const Announcement = ({ version, hideAnnouncement }: AnnouncementProps) => {
- • Updated DeepSeek provider
- • New "Human Relay" provider
-
+
Get more details and discuss in{" "}
diff --git a/webview-ui/src/components/chat/checkpoints/CheckpointMenu.tsx b/webview-ui/src/components/chat/checkpoints/CheckpointMenu.tsx
index 1910fc0bb6..63867c9858 100644
--- a/webview-ui/src/components/chat/checkpoints/CheckpointMenu.tsx
+++ b/webview-ui/src/components/chat/checkpoints/CheckpointMenu.tsx
@@ -1,4 +1,4 @@
-import { useState, useEffect, useCallback } from "react"
+import { useState, useCallback } from "react"
import { CheckIcon, Cross2Icon } from "@radix-ui/react-icons"
import { Button, Popover, PopoverContent, PopoverTrigger } from "@/components/ui"
@@ -14,7 +14,6 @@ type CheckpointMenuProps = {
}
export const CheckpointMenu = ({ ts, commitHash, currentHash, checkpoint }: CheckpointMenuProps) => {
- const [portalContainer, setPortalContainer] = useState()
const [isOpen, setIsOpen] = useState(false)
const [isConfirming, setIsConfirming] = useState(false)
@@ -42,15 +41,6 @@ export const CheckpointMenu = ({ ts, commitHash, currentHash, checkpoint }: Chec
setIsOpen(false)
}, [ts, commitHash])
- useEffect(() => {
- // The dropdown menu uses a portal from @shadcn/ui which by default renders
- // at the document root. This causes the menu to remain visible even when
- // the parent ChatView component is hidden (during settings/history view).
- // By moving the portal inside ChatView, the menu will properly hide when
- // its parent is hidden.
- setPortalContainer(document.getElementById("chat-view-portal") || undefined)
- }, [])
-
return (
{isDiffAvailable && (
@@ -70,7 +60,7 @@ export const CheckpointMenu = ({ ts, commitHash, currentHash, checkpoint }: Chec
-
+
{!isCurrent && (
diff --git a/webview-ui/src/components/common/Alert.tsx b/webview-ui/src/components/common/Alert.tsx
new file mode 100644
index 0000000000..b16e799b91
--- /dev/null
+++ b/webview-ui/src/components/common/Alert.tsx
@@ -0,0 +1,15 @@
+import { cn } from "@/lib/utils"
+import { HTMLAttributes } from "react"
+
+type AlertProps = HTMLAttributes
+
+export const Alert = ({ className, children, ...props }: AlertProps) => (
+
+ {children}
+
+)
diff --git a/webview-ui/src/components/common/Tab.tsx b/webview-ui/src/components/common/Tab.tsx
new file mode 100644
index 0000000000..982fb7e103
--- /dev/null
+++ b/webview-ui/src/components/common/Tab.tsx
@@ -0,0 +1,23 @@
+import { HTMLAttributes } from "react"
+
+import { cn } from "@/lib/utils"
+
+type TabProps = HTMLAttributes
+
+export const Tab = ({ className, children, ...props }: TabProps) => (
+
+ {children}
+
+)
+
+export const TabHeader = ({ className, children, ...props }: TabProps) => (
+
+ {children}
+
+)
+
+export const TabContent = ({ className, children, ...props }: TabProps) => (
+
+ {children}
+
+)
diff --git a/webview-ui/src/components/history/HistoryView.tsx b/webview-ui/src/components/history/HistoryView.tsx
index e65a11a3ec..ec44f8eaca 100644
--- a/webview-ui/src/components/history/HistoryView.tsx
+++ b/webview-ui/src/components/history/HistoryView.tsx
@@ -9,6 +9,7 @@ import { formatLargeNumber, formatDate } from "@/utils/format"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui"
+import { Tab, TabContent, TabHeader } from "../common/Tab"
import { useTaskSearch } from "./useTaskSearch"
import { ExportButton } from "./ExportButton"
import { CopyButton } from "./CopyButton"
@@ -25,8 +26,8 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
const [deleteTaskId, setDeleteTaskId] = useState(null)
return (
-
-
+
+
History
Done
@@ -81,8 +82,9 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
-
-
+
+
+
{
)}
/>
-
+
+
{deleteTaskId && (
!open && setDeleteTaskId(null)} open />
)}
-
+
)
}
diff --git a/webview-ui/src/components/mcp/McpView.tsx b/webview-ui/src/components/mcp/McpView.tsx
index 7a24922d88..ce37a4c09d 100644
--- a/webview-ui/src/components/mcp/McpView.tsx
+++ b/webview-ui/src/components/mcp/McpView.tsx
@@ -1,3 +1,4 @@
+import { useState } from "react"
import {
VSCodeButton,
VSCodeCheckbox,
@@ -6,14 +7,17 @@ import {
VSCodePanelTab,
VSCodePanelView,
} from "@vscode/webview-ui-toolkit/react"
-import { useState } from "react"
-import { vscode } from "../../utils/vscode"
-import { useExtensionState } from "../../context/ExtensionStateContext"
+
import { McpServer } from "../../../../src/shared/mcp"
+
+import { vscode } from "@/utils/vscode"
+import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from "@/components/ui"
+
+import { useExtensionState } from "../../context/ExtensionStateContext"
+import { Tab, TabContent, TabHeader } from "../common/Tab"
import McpToolRow from "./McpToolRow"
import McpResourceRow from "./McpResourceRow"
import McpEnabledToggle from "./McpEnabledToggle"
-import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from "../ui/dialog"
type McpViewProps = {
onDone: () => void
@@ -29,12 +33,13 @@ const McpView = ({ onDone }: McpViewProps) => {
} = useExtensionState()
return (
-
-
+
+
MCP Servers
Done
-
-
-
+
+
)
}
-// Server Row Component
const ServerRow = ({ server, alwaysAllowMcp }: { server: McpServer; alwaysAllowMcp?: boolean }) => {
const [isExpanded, setIsExpanded] = useState(false)
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)
diff --git a/webview-ui/src/components/prompts/PromptsView.tsx b/webview-ui/src/components/prompts/PromptsView.tsx
index ccf1e6d700..d51d7d8909 100644
--- a/webview-ui/src/components/prompts/PromptsView.tsx
+++ b/webview-ui/src/components/prompts/PromptsView.tsx
@@ -42,6 +42,7 @@ import {
import { TOOL_GROUPS, GROUP_DISPLAY_NAMES, ToolGroup } from "../../../../src/shared/tool-groups"
import { vscode } from "../../utils/vscode"
+import { Tab, TabContent, TabHeader } from "../common/Tab"
// Get all available groups that should show in prompts view
const availableGroups = (Object.keys(TOOL_GROUPS) as ToolGroup[]).filter((group) => !TOOL_GROUPS[group].alwaysAvailable)
@@ -406,12 +407,13 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
}
return (
-
-
+
+
Prompts
Done
-
-
+
+
+
Preferred Language
@@ -934,6 +936,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
+
{
)}
-
+
+
{isCreateModeDialogOpen && (
{
)}
+
{isDialogOpen && (
{
)}
+
{isCustomLanguage && (
@@ -1497,7 +1503,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
)}
-
+
)
}
diff --git a/webview-ui/src/components/settings/SettingsView.tsx b/webview-ui/src/components/settings/SettingsView.tsx
index 7cafbd6663..df08a03971 100644
--- a/webview-ui/src/components/settings/SettingsView.tsx
+++ b/webview-ui/src/components/settings/SettingsView.tsx
@@ -22,6 +22,7 @@ import {
Button,
} from "@/components/ui"
+import { Tab, TabContent, TabHeader } from "../common/Tab"
import { SetCachedStateField, SetExperimentEnabled } from "./types"
import { SectionHeader } from "./SectionHeader"
import ApiConfigManager from "./ApiConfigManager"
@@ -263,53 +264,41 @@ const SettingsView = forwardRef(({ onDone },
const scrollToSection = (ref: React.RefObject) => ref.current?.scrollIntoView()
return (
-
-
-
-
-
-
Settings
-
- {sections.map(({ id, icon: Icon, ref }) => (
-
- ))}
-
-
-
-
- Save
-
- checkUnsaveChanges(onDone)}>
- Done
-
-
+
+
+
+
Settings
+
+ {sections.map(({ id, icon: Icon, ref }) => (
+
+ ))}
-
+
+
+ Save
+
+ checkUnsaveChanges(onDone)}>
+ Done
+
+
+
-
+
@@ -425,7 +414,7 @@ const SettingsView = forwardRef(({ onDone },
telemetrySetting={telemetrySetting}
setTelemetrySetting={setTelemetrySetting}
/>
-
+
@@ -442,7 +431,7 @@ const SettingsView = forwardRef(({ onDone },
-
+
)
})
diff --git a/webview-ui/src/components/ui/dropdown-menu.tsx b/webview-ui/src/components/ui/dropdown-menu.tsx
index fc5ad5b2b8..3193f497ca 100644
--- a/webview-ui/src/components/ui/dropdown-menu.tsx
+++ b/webview-ui/src/components/ui/dropdown-menu.tsx
@@ -53,23 +53,25 @@ DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayNam
const DropdownMenuContent = React.forwardRef<
React.ElementRef,
- React.ComponentPropsWithoutRef & {
- container?: HTMLElement
- }
->(({ className, sideOffset = 4, container, ...props }, ref) => (
-
-
-
-))
+ React.ComponentPropsWithoutRef
+>(({ className, sideOffset = 4, ...props }, ref) => {
+ const container = React.useMemo(() => document.getElementById("roo-portal"), [])
+
+ return (
+
+
+
+ )
+})
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
const DropdownMenuItem = React.forwardRef<
diff --git a/webview-ui/src/components/ui/popover.tsx b/webview-ui/src/components/ui/popover.tsx
index 3ab3344689..b6235853ca 100644
--- a/webview-ui/src/components/ui/popover.tsx
+++ b/webview-ui/src/components/ui/popover.tsx
@@ -11,23 +11,25 @@ const PopoverAnchor = PopoverPrimitive.Anchor
const PopoverContent = React.forwardRef<
React.ElementRef,
- React.ComponentPropsWithoutRef & {
- container?: HTMLElement
- }
->(({ className, align = "center", sideOffset = 4, container, ...props }, ref) => (
-
-
-
-))
+ React.ComponentPropsWithoutRef
+>(({ className, align = "center", sideOffset = 4, ...props }, ref) => {
+ const container = React.useMemo(() => document.getElementById("roo-portal"), [])
+
+ return (
+
+
+
+ )
+})
PopoverContent.displayName = PopoverPrimitive.Content.displayName
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
diff --git a/webview-ui/src/components/ui/select-dropdown.tsx b/webview-ui/src/components/ui/select-dropdown.tsx
index 775066732d..eef474cd02 100644
--- a/webview-ui/src/components/ui/select-dropdown.tsx
+++ b/webview-ui/src/components/ui/select-dropdown.tsx
@@ -7,7 +7,6 @@ import {
DropdownMenuSeparator,
} from "./dropdown-menu"
import { cn } from "@/lib/utils"
-import { useEffect, useState } from "react"
// Constants for option types
export enum DropdownOptionType {
@@ -39,6 +38,8 @@ export interface SelectDropdownProps {
shortcutText?: string
}
+// TODO: Get rid of this and use the native @shadcn/ui `Select` component.
+
export const SelectDropdown = React.forwardRef, SelectDropdownProps>(
(
{
@@ -60,16 +61,6 @@ export const SelectDropdown = React.forwardRef {
// Track open state
const [open, setOpen] = React.useState(false)
- const [portalContainer, setPortalContainer] = useState()
-
- useEffect(() => {
- // The dropdown menu uses a portal from @shadcn/ui which by default renders
- // at the document root. This causes the menu to remain visible even when
- // the parent ChatView component is hidden (during settings/history view).
- // By moving the portal inside ChatView, the menu will properly hide when
- // its parent is hidden.
- setPortalContainer(document.getElementById("chat-view-portal") || undefined)
- }, [])
// Find the selected option label
const selectedOption = options.find((option) => option.value === value)
@@ -130,7 +121,6 @@ export const SelectDropdown = React.forwardRef setOpen(false)}
onInteractOutside={() => setOpen(false)}
- container={portalContainer}
className={cn(
"bg-vscode-dropdown-background text-vscode-dropdown-foreground border border-vscode-dropdown-border z-50",
contentClassName,
diff --git a/webview-ui/src/components/ui/select.tsx b/webview-ui/src/components/ui/select.tsx
index 50f89b3760..5e673257cf 100644
--- a/webview-ui/src/components/ui/select.tsx
+++ b/webview-ui/src/components/ui/select.tsx
@@ -39,8 +39,10 @@ function SelectContent({
position = "popper",
...props
}: React.ComponentProps) {
+ const container = React.useMemo(() => document.getElementById("roo-portal"), [])
+
return (
-
+
{
const { apiConfiguration, currentApiConfigName, setApiConfiguration, uriScheme } = useExtensionState()
@@ -23,18 +26,16 @@ const WelcomeView = () => {
}, [apiConfiguration, currentApiConfigName])
return (
-
-
Hi, I'm Roo!
-
- I can do all kinds of tasks thanks to the latest breakthroughs in agentic coding capabilities and access
- to tools that let me create & edit files, explore complex projects, use the browser, and execute
- terminal commands (with your permission, of course). I can even use MCP to create new tools and extend
- my own capabilities.
-
-
-
To get started, this extension needs an API provider.
-
-
+
+
+ Hi, I'm Roo!
+
+ I can do all kinds of tasks thanks to the latest breakthroughs in agentic coding capabilities and
+ access to tools that let me create & edit files, explore complex projects, use the browser, and
+ execute terminal commands (with your permission, of course). I can even use MCP to create new tools
+ and extend my own capabilities.
+
+ To get started, this extension needs an API provider.
{
errorMessage={errorMessage}
setErrorMessage={setErrorMessage}
/>
-
-
-
-
+
+
+
Let's go!
- {errorMessage &&
{errorMessage}}
+ {errorMessage &&
{errorMessage}
}
-
+
)
}
diff --git a/webview-ui/src/index.css b/webview-ui/src/index.css
index 6cd405b93f..2144557a98 100644
--- a/webview-ui/src/index.css
+++ b/webview-ui/src/index.css
@@ -111,6 +111,10 @@
--color-vscode-charts-green: var(--vscode-charts-green);
--color-vscode-charts-yellow: var(--vscode-charts-yellow);
+
+ --color-vscode-inputValidation-infoForeground: var(--vscode-inputValidation-infoForeground);
+ --color-vscode-inputValidation-infoBackground: var(--vscode-inputValidation-infoBackground);
+ --color-vscode-inputValidation-infoBorder: var(--vscode-inputValidation-infoBorder);
}
@layer base {
From 5f20fbbf44126f097abb64cf06b52cc399a66086 Mon Sep 17 00:00:00 2001
From: cte
Date: Sun, 9 Mar 2025 15:08:36 -0700
Subject: [PATCH 04/10] Add changeset
---
.changeset/lemon-bulldogs-unite.md | 5 +++++
1 file changed, 5 insertions(+)
create mode 100644 .changeset/lemon-bulldogs-unite.md
diff --git a/.changeset/lemon-bulldogs-unite.md b/.changeset/lemon-bulldogs-unite.md
new file mode 100644
index 0000000000..b87abc1bd2
--- /dev/null
+++ b/.changeset/lemon-bulldogs-unite.md
@@ -0,0 +1,5 @@
+---
+"roo-cline": patch
+---
+
+App tab layout fixes
From 9c08d044b39afb766e012d311ef984be2dba0fdf Mon Sep 17 00:00:00 2001
From: Matt Rubens
Date: Sun, 9 Mar 2025 22:55:23 -0400
Subject: [PATCH 05/10] Add telemetry for checkpoint save/restore/diff
---
src/core/Cline.ts | 6 ++++++
src/services/telemetry/TelemetryService.ts | 15 +++++++++++++++
2 files changed, 21 insertions(+)
diff --git a/src/core/Cline.ts b/src/core/Cline.ts
index 3d1f980c7d..65e053c3d1 100644
--- a/src/core/Cline.ts
+++ b/src/core/Cline.ts
@@ -3798,6 +3798,8 @@ export class Cline {
return
}
+ telemetryService.captureCheckpointDiffed(this.taskId)
+
if (!previousCommitHash && mode === "checkpoint") {
const previousCheckpoint = this.clineMessages
.filter(({ say }) => say === "checkpoint_saved")
@@ -3849,6 +3851,8 @@ export class Cline {
return
}
+ telemetryService.captureCheckpointCreated(this.taskId)
+
// Start the checkpoint process in the background.
service.saveCheckpoint(`Task: ${this.taskId}, Time: ${Date.now()}`).catch((err) => {
console.error("[Cline#checkpointSave] caught unexpected error, disabling checkpoints", err)
@@ -3880,6 +3884,8 @@ export class Cline {
try {
await service.restoreCheckpoint(commitHash)
+ telemetryService.captureCheckpointRestored(this.taskId)
+
await this.providerRef.deref()?.postMessageToWebview({ type: "currentCheckpointUpdated", text: commitHash })
if (mode === "restore") {
diff --git a/src/services/telemetry/TelemetryService.ts b/src/services/telemetry/TelemetryService.ts
index 45a34bda4e..d3ea8bfb5f 100644
--- a/src/services/telemetry/TelemetryService.ts
+++ b/src/services/telemetry/TelemetryService.ts
@@ -22,6 +22,9 @@ class PostHogClient {
CONVERSATION_MESSAGE: "Conversation Message",
MODE_SWITCH: "Mode Switched",
TOOL_USED: "Tool Used",
+ CHECKPOINT_CREATED: "Checkpoint Created",
+ CHECKPOINT_RESTORED: "Checkpoint Restored",
+ CHECKPOINT_DIFFED: "Checkpoint Diffed",
},
}
@@ -246,6 +249,18 @@ class TelemetryService {
})
}
+ public captureCheckpointCreated(taskId: string): void {
+ this.captureEvent(PostHogClient.EVENTS.TASK.CHECKPOINT_CREATED, { taskId })
+ }
+
+ public captureCheckpointDiffed(taskId: string): void {
+ this.captureEvent(PostHogClient.EVENTS.TASK.CHECKPOINT_DIFFED, { taskId })
+ }
+
+ public captureCheckpointRestored(taskId: string): void {
+ this.captureEvent(PostHogClient.EVENTS.TASK.CHECKPOINT_RESTORED, { taskId })
+ }
+
/**
* Checks if telemetry is currently enabled
* @returns Whether telemetry is enabled
From 732eeddc6c87b9135900bb6770ec4409381ba6b7 Mon Sep 17 00:00:00 2001
From: Matt Rubens
Date: Sun, 9 Mar 2025 23:18:44 -0400
Subject: [PATCH 06/10] Add model telemetry too
---
src/core/webview/ClineProvider.ts | 9 +++
.../webview/__tests__/ClineProvider.test.ts | 59 +++++++++++++++++++
2 files changed, 68 insertions(+)
diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts
index b82a6a62e0..72cf56d35b 100644
--- a/src/core/webview/ClineProvider.ts
+++ b/src/core/webview/ClineProvider.ts
@@ -2567,6 +2567,15 @@ export class ClineProvider implements vscode.WebviewViewProvider {
properties.apiProvider = apiConfiguration.apiProvider
}
+ // Add model ID if available
+ const currentCline = this.getCurrentCline()
+ if (currentCline?.api) {
+ const { id: modelId } = currentCline.api.getModel()
+ if (modelId) {
+ properties.modelId = modelId
+ }
+ }
+
return properties
}
}
diff --git a/src/core/webview/__tests__/ClineProvider.test.ts b/src/core/webview/__tests__/ClineProvider.test.ts
index f9fc5d3ece..2e9fcdf336 100644
--- a/src/core/webview/__tests__/ClineProvider.test.ts
+++ b/src/core/webview/__tests__/ClineProvider.test.ts
@@ -1652,3 +1652,62 @@ describe("ContextProxy integration", () => {
expect(mockContextProxy.setValues).toBeDefined()
})
})
+
+describe("getTelemetryProperties", () => {
+ let provider: ClineProvider
+ let mockContext: vscode.ExtensionContext
+ let mockOutputChannel: vscode.OutputChannel
+ let mockCline: any
+
+ beforeEach(() => {
+ // Reset mocks
+ jest.clearAllMocks()
+
+ // Setup basic mocks
+ mockContext = {
+ globalState: {
+ get: jest.fn().mockImplementation((key: string) => {
+ if (key === "mode") return "code"
+ if (key === "apiProvider") return "anthropic"
+ return undefined
+ }),
+ update: jest.fn(),
+ keys: jest.fn().mockReturnValue([]),
+ },
+ secrets: { get: jest.fn(), store: jest.fn(), delete: jest.fn() },
+ extensionUri: {} as vscode.Uri,
+ globalStorageUri: { fsPath: "/test/path" },
+ extension: { packageJSON: { version: "1.0.0" } },
+ } as unknown as vscode.ExtensionContext
+
+ mockOutputChannel = { appendLine: jest.fn() } as unknown as vscode.OutputChannel
+ provider = new ClineProvider(mockContext, mockOutputChannel)
+
+ // Setup Cline instance with mocked getModel method
+ const { Cline } = require("../../Cline")
+ mockCline = new Cline()
+ mockCline.api = {
+ getModel: jest.fn().mockReturnValue({
+ id: "claude-3-7-sonnet-20250219",
+ info: { contextWindow: 200000 },
+ }),
+ }
+ })
+
+ test("includes basic properties in telemetry", async () => {
+ const properties = await provider.getTelemetryProperties()
+
+ expect(properties).toHaveProperty("vscodeVersion")
+ expect(properties).toHaveProperty("platform")
+ expect(properties).toHaveProperty("appVersion", "1.0.0")
+ })
+
+ test("includes model ID from current Cline instance if available", async () => {
+ // Add mock Cline to stack
+ await provider.addClineToStack(mockCline)
+
+ const properties = await provider.getTelemetryProperties()
+
+ expect(properties).toHaveProperty("modelId", "claude-3-7-sonnet-20250219")
+ })
+})
From 9fdc546f0189dbd21999b18227cea289cab9089f Mon Sep 17 00:00:00 2001
From: cte
Date: Sun, 9 Mar 2025 21:49:29 -0700
Subject: [PATCH 07/10] Fix portal stuff
---
webview-ui/src/App.tsx | 1 -
.../src/components/chat/ChatTextArea.tsx | 19 +++++-----
webview-ui/src/components/chat/ChatView.tsx | 2 +-
.../chat/checkpoints/CheckpointMenu.tsx | 4 ++-
.../src/components/ui/dropdown-menu.tsx | 35 +++++++++----------
webview-ui/src/components/ui/hooks/index.ts | 1 +
.../src/components/ui/hooks/useRooPortal.ts | 10 ++++++
webview-ui/src/components/ui/popover.tsx | 35 +++++++++----------
.../src/components/ui/select-dropdown.tsx | 27 ++++++--------
webview-ui/src/components/ui/select.tsx | 6 ++--
10 files changed, 71 insertions(+), 69 deletions(-)
create mode 100644 webview-ui/src/components/ui/hooks/useRooPortal.ts
diff --git a/webview-ui/src/App.tsx b/webview-ui/src/App.tsx
index 99dda495d0..389f5709fc 100644
--- a/webview-ui/src/App.tsx
+++ b/webview-ui/src/App.tsx
@@ -132,7 +132,6 @@ const App = () => {
const AppWithProviders = () => (
-
)
diff --git a/webview-ui/src/components/chat/ChatTextArea.tsx b/webview-ui/src/components/chat/ChatTextArea.tsx
index 3df3e87e9b..1002788dbc 100644
--- a/webview-ui/src/components/chat/ChatTextArea.tsx
+++ b/webview-ui/src/components/chat/ChatTextArea.tsx
@@ -1,22 +1,25 @@
import React, { forwardRef, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"
import DynamicTextArea from "react-textarea-autosize"
+
import { mentionRegex, mentionRegexGlobal } from "../../../../src/shared/context-mentions"
-import { useExtensionState } from "../../context/ExtensionStateContext"
+import { WebviewMessage } from "../../../../src/shared/WebviewMessage"
+import { Mode, getAllModes } from "../../../../src/shared/modes"
+
+import { vscode } from "@/utils/vscode"
import {
ContextMenuOptionType,
getContextMenuOptions,
insertMention,
removeMention,
shouldShowContextMenu,
-} from "../../utils/context-mentions"
+} from "@/utils/context-mentions"
+import { SelectDropdown, DropdownOptionType } from "@/components/ui"
+
+import { useExtensionState } from "../../context/ExtensionStateContext"
+import Thumbnails from "../common/Thumbnails"
+import { convertToMentionPath } from "../../utils/path-mentions"
import { MAX_IMAGES_PER_MESSAGE } from "./ChatView"
import ContextMenu from "./ContextMenu"
-import Thumbnails from "../common/Thumbnails"
-import { vscode } from "../../utils/vscode"
-import { WebviewMessage } from "../../../../src/shared/WebviewMessage"
-import { Mode, getAllModes } from "../../../../src/shared/modes"
-import { convertToMentionPath } from "../../utils/path-mentions"
-import { SelectDropdown, DropdownOptionType } from "../ui"
interface ChatTextAreaProps {
inputValue: string
diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx
index 5ac7f50559..09dcdd3ca1 100644
--- a/webview-ui/src/components/chat/ChatView.tsx
+++ b/webview-ui/src/components/chat/ChatView.tsx
@@ -1275,7 +1275,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
modeShortcutText={modeShortcutText}
/>
-
+
)
}
diff --git a/webview-ui/src/components/chat/checkpoints/CheckpointMenu.tsx b/webview-ui/src/components/chat/checkpoints/CheckpointMenu.tsx
index 63867c9858..b6aaebd518 100644
--- a/webview-ui/src/components/chat/checkpoints/CheckpointMenu.tsx
+++ b/webview-ui/src/components/chat/checkpoints/CheckpointMenu.tsx
@@ -2,6 +2,7 @@ import { useState, useCallback } from "react"
import { CheckIcon, Cross2Icon } from "@radix-ui/react-icons"
import { Button, Popover, PopoverContent, PopoverTrigger } from "@/components/ui"
+import { useRooPortal } from "@/components/ui/hooks"
import { vscode } from "../../../utils/vscode"
import { Checkpoint } from "./schema"
@@ -16,6 +17,7 @@ type CheckpointMenuProps = {
export const CheckpointMenu = ({ ts, commitHash, currentHash, checkpoint }: CheckpointMenuProps) => {
const [isOpen, setIsOpen] = useState(false)
const [isConfirming, setIsConfirming] = useState(false)
+ const portalContainer = useRooPortal("roo-portal")
const isCurrent = currentHash === commitHash
const isFirst = checkpoint.isFirst
@@ -60,7 +62,7 @@ export const CheckpointMenu = ({ ts, commitHash, currentHash, checkpoint }: Chec
-
+
{!isCurrent && (
diff --git a/webview-ui/src/components/ui/dropdown-menu.tsx b/webview-ui/src/components/ui/dropdown-menu.tsx
index 3193f497ca..c65f18b1b5 100644
--- a/webview-ui/src/components/ui/dropdown-menu.tsx
+++ b/webview-ui/src/components/ui/dropdown-menu.tsx
@@ -1,5 +1,6 @@
import * as React from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
+import { PortalProps } from "@radix-ui/react-portal"
import { CheckIcon, ChevronRightIcon, DotFilledIcon } from "@radix-ui/react-icons"
import { cn } from "@/lib/utils"
@@ -53,25 +54,21 @@ DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayNam
const DropdownMenuContent = React.forwardRef<
React.ElementRef
,
- React.ComponentPropsWithoutRef
->(({ className, sideOffset = 4, ...props }, ref) => {
- const container = React.useMemo(() => document.getElementById("roo-portal"), [])
-
- return (
-
-
-
- )
-})
+ React.ComponentPropsWithoutRef & Pick
+>(({ className, sideOffset = 4, container, ...props }, ref) => (
+
+
+
+))
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
const DropdownMenuItem = React.forwardRef<
diff --git a/webview-ui/src/components/ui/hooks/index.ts b/webview-ui/src/components/ui/hooks/index.ts
index 0ca9075f59..46aff4f28d 100644
--- a/webview-ui/src/components/ui/hooks/index.ts
+++ b/webview-ui/src/components/ui/hooks/index.ts
@@ -1 +1,2 @@
export * from "./useClipboard"
+export * from "./useRooPortal"
diff --git a/webview-ui/src/components/ui/hooks/useRooPortal.ts b/webview-ui/src/components/ui/hooks/useRooPortal.ts
new file mode 100644
index 0000000000..25ef139e64
--- /dev/null
+++ b/webview-ui/src/components/ui/hooks/useRooPortal.ts
@@ -0,0 +1,10 @@
+import { useState } from "react"
+import { useMount } from "react-use"
+
+export const useRooPortal = (id: string) => {
+ const [container, setContainer] = useState()
+
+ useMount(() => setContainer(document.getElementById(id) ?? undefined))
+
+ return container
+}
diff --git a/webview-ui/src/components/ui/popover.tsx b/webview-ui/src/components/ui/popover.tsx
index b6235853ca..9fc035ad2a 100644
--- a/webview-ui/src/components/ui/popover.tsx
+++ b/webview-ui/src/components/ui/popover.tsx
@@ -1,4 +1,5 @@
import * as React from "react"
+import { PortalProps } from "@radix-ui/react-portal"
import * as PopoverPrimitive from "@radix-ui/react-popover"
import { cn } from "@/lib/utils"
@@ -11,25 +12,21 @@ const PopoverAnchor = PopoverPrimitive.Anchor
const PopoverContent = React.forwardRef<
React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, align = "center", sideOffset = 4, ...props }, ref) => {
- const container = React.useMemo(() => document.getElementById("roo-portal"), [])
-
- return (
-
-
-
- )
-})
+ React.ComponentPropsWithoutRef & Pick
+>(({ className, align = "center", sideOffset = 4, container, ...props }, ref) => (
+
+
+
+))
PopoverContent.displayName = PopoverPrimitive.Content.displayName
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
diff --git a/webview-ui/src/components/ui/select-dropdown.tsx b/webview-ui/src/components/ui/select-dropdown.tsx
index eef474cd02..bec496ed50 100644
--- a/webview-ui/src/components/ui/select-dropdown.tsx
+++ b/webview-ui/src/components/ui/select-dropdown.tsx
@@ -1,4 +1,8 @@
import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+import { useRooPortal } from "./hooks/useRooPortal"
import {
DropdownMenu,
DropdownMenuContent,
@@ -6,9 +10,7 @@ import {
DropdownMenuTrigger,
DropdownMenuSeparator,
} from "./dropdown-menu"
-import { cn } from "@/lib/utils"
-// Constants for option types
export enum DropdownOptionType {
ITEM = "item",
SEPARATOR = "separator",
@@ -19,7 +21,7 @@ export interface DropdownOption {
value: string
label: string
disabled?: boolean
- type?: DropdownOptionType // Optional type to specify special behaviors
+ type?: DropdownOptionType
}
export interface SelectDropdownProps {
@@ -38,8 +40,6 @@ export interface SelectDropdownProps {
shortcutText?: string
}
-// TODO: Get rid of this and use the native @shadcn/ui `Select` component.
-
export const SelectDropdown = React.forwardRef, SelectDropdownProps>(
(
{
@@ -59,24 +59,19 @@ export const SelectDropdown = React.forwardRef {
- // Track open state
const [open, setOpen] = React.useState(false)
+ const portalContainer = useRooPortal("roo-portal")
- // Find the selected option label
const selectedOption = options.find((option) => option.value === value)
const displayText = selectedOption?.label || placeholder || ""
- // Handle menu item click
const handleSelect = (option: DropdownOption) => {
- // Check if this is an action option by its explicit type
if (option.type === DropdownOptionType.ACTION) {
- window.postMessage({
- type: "action",
- action: option.value,
- })
+ window.postMessage({ type: "action", action: option.value })
setOpen(false)
return
}
+
onChange(option.value)
setOpen(false)
}
@@ -94,7 +89,7 @@ export const SelectDropdown = React.forwardRef
@@ -121,17 +116,16 @@ export const SelectDropdown = React.forwardRef setOpen(false)}
onInteractOutside={() => setOpen(false)}
+ container={portalContainer}
className={cn(
"bg-vscode-dropdown-background text-vscode-dropdown-foreground border border-vscode-dropdown-border z-50",
contentClassName,
)}>
{options.map((option, index) => {
- // Handle separator type
if (option.type === DropdownOptionType.SEPARATOR) {
return
}
- // Handle shortcut text type (disabled label for keyboard shortcuts)
if (
option.type === DropdownOptionType.SHORTCUT ||
(option.disabled && shortcutText && option.label.includes(shortcutText))
@@ -143,7 +137,6 @@ export const SelectDropdown = React.forwardRef) {
- const container = React.useMemo(() => document.getElementById("roo-portal"), [])
-
+}: React.ComponentProps & Pick) {
return (
Date: Mon, 10 Mar 2025 11:51:47 +0100
Subject: [PATCH 08/10] ci: publish git tags to Github (resolves #444)
---
.changeset/automatic-tags-publish.md | 5 +++++
.github/workflows/marketplace-publish.yml | 9 +++++++++
2 files changed, 14 insertions(+)
create mode 100644 .changeset/automatic-tags-publish.md
diff --git a/.changeset/automatic-tags-publish.md b/.changeset/automatic-tags-publish.md
new file mode 100644
index 0000000000..a3ff07fb4c
--- /dev/null
+++ b/.changeset/automatic-tags-publish.md
@@ -0,0 +1,5 @@
+---
+"roo-cline": patch
+---
+
+Update GitHub Actions workflow to automatically create and push git tags during release
diff --git a/.github/workflows/marketplace-publish.yml b/.github/workflows/marketplace-publish.yml
index c6fd66b1b3..7d71b2f86f 100644
--- a/.github/workflows/marketplace-publish.yml
+++ b/.github/workflows/marketplace-publish.yml
@@ -10,6 +10,8 @@ env:
jobs:
publish-extension:
runs-on: ubuntu-latest
+ permissions:
+ contents: write # Required for pushing tags
if: >
( github.event_name == 'pull_request' &&
github.event.pull_request.base.ref == 'main' &&
@@ -51,3 +53,10 @@ jobs:
npm run publish:marketplace
echo "Successfully published version $current_package_version to VS Code Marketplace"
+
+ - name: Create and Push Git Tag
+ run: |
+ current_package_version=$(node -p "require('./package.json').version")
+ git tag -a "v${current_package_version}" -m "Release v${current_package_version}"
+ git push origin "v${current_package_version}"
+ echo "Successfully created and pushed git tag v${current_package_version}"
From 03045d8e752c4e693f7ba652cc09d97c11c5aea5 Mon Sep 17 00:00:00 2001
From: Matt Rubens
Date: Mon, 10 Mar 2025 09:54:45 -0400
Subject: [PATCH 09/10] Update src/core/Cline.ts
---
src/core/Cline.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/core/Cline.ts b/src/core/Cline.ts
index 4f27a89cc0..7f2b77ec1e 100644
--- a/src/core/Cline.ts
+++ b/src/core/Cline.ts
@@ -875,7 +875,7 @@ export class Cline {
// The way this agentic loop works is that cline will be given a task that he then calls tools to complete. unless there's an attempt_completion call, we keep responding back to him with his tool's responses until he either attempt_completion or does not use anymore tools. If he does not use anymore tools, we ask him to consider if he's completed the task and then call attempt_completion, otherwise proceed with completing the task.
// There is a MAX_REQUESTS_PER_TASK limit to prevent infinite requests, but Cline is prompted to finish the task as efficiently as he can.
- //const totalCost = this.calculateApiCostAntrhopic(totalInputTokens, totalOutputTokens)
+ //const totalCost = this.calculateApiCostAnthropic(totalInputTokens, totalOutputTokens)
if (didEndLoop) {
// For now a task never 'completes'. This will only happen if the user hits max requests and denies resetting the count.
//this.say("task_completed", `Task completed. Total API usage cost: ${totalCost}`)
From f99fc59460f1049f53ac82b809d9b11dc9d2663f Mon Sep 17 00:00:00 2001
From: Patrick Decat
Date: Mon, 10 Mar 2025 15:29:14 +0100
Subject: [PATCH 10/10] ci: tag after packaging but before publishing
---
.github/workflows/marketplace-publish.yml | 22 ++++++++++++++--------
1 file changed, 14 insertions(+), 8 deletions(-)
diff --git a/.github/workflows/marketplace-publish.yml b/.github/workflows/marketplace-publish.yml
index 7d71b2f86f..4ecd2af7a2 100644
--- a/.github/workflows/marketplace-publish.yml
+++ b/.github/workflows/marketplace-publish.yml
@@ -25,24 +25,24 @@ jobs:
- uses: actions/setup-node@v4
with:
node-version: 18
+
- run: |
git config user.name github-actions
git config user.email github-actions@github.com
+
- name: Install Dependencies
run: |
npm install -g vsce ovsx
npm run install:ci
+
- name: Create .env file
run: |
echo "# PostHog API Keys for telemetry" > .env
echo "POSTHOG_API_KEY=${{ secrets.POSTHOG_API_KEY }}" >> .env
- - name: Package and Publish Extension
- env:
- VSCE_PAT: ${{ secrets.VSCE_PAT }}
- OVSX_PAT: ${{ secrets.OVSX_PAT }}
+
+ - name: Package Extension
run: |
current_package_version=$(node -p "require('./package.json').version")
-
npm run vsix
package=$(unzip -l bin/roo-cline-${current_package_version}.vsix)
echo "$package"
@@ -51,12 +51,18 @@ jobs:
echo "$package" | grep -q "extension/node_modules/@vscode/codicons/dist/codicon.ttf" || exit 1
echo "$package" | grep -q ".env" || exit 1
- npm run publish:marketplace
- echo "Successfully published version $current_package_version to VS Code Marketplace"
-
- name: Create and Push Git Tag
run: |
current_package_version=$(node -p "require('./package.json').version")
git tag -a "v${current_package_version}" -m "Release v${current_package_version}"
git push origin "v${current_package_version}"
echo "Successfully created and pushed git tag v${current_package_version}"
+
+ - name: Publish Extension
+ env:
+ VSCE_PAT: ${{ secrets.VSCE_PAT }}
+ OVSX_PAT: ${{ secrets.OVSX_PAT }}
+ run: |
+ current_package_version=$(node -p "require('./package.json').version")
+ npm run publish:marketplace
+ echo "Successfully published version $current_package_version to VS Code Marketplace"