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"