Compare commits

..
Author SHA1 Message Date
Toshii b1e129f8e8 Stateless modification of context when truncating previous conversation (#2374)
* stateless adjustment of context window on truncation

* changeset

* support old interface
2025-03-24 16:57:34 -07:00
Toshii d54146623f Merge branch 'main' into ft/move-context-logic-out 2025-03-23 14:28:04 +09:00
Saoud Rizwan f9c46f898f Fixes 2025-03-22 19:28:36 -07:00
Saoud Rizwan 2d2eb5dd36 Add Announcement 2025-03-22 19:25:46 -07:00
Saoud Rizwan 9562671bcc Remove gallery banner color on marketplace 2025-03-22 19:04:29 -07:00
Saoud Rizwan 6a2aef27ea FIx z-index for dropdowns 2025-03-22 18:58:05 -07:00
Saoud Rizwan c7d6173e20 Fixes 2025-03-22 18:40:12 -07:00
Saoud Rizwan 62da7fe720 Fixes 2025-03-22 18:32:11 -07:00
Saoud Rizwan f97f3e47cc Move sort routing option 2025-03-22 17:43:24 -07:00
Saoud Rizwan f47be1a7bb Add OpenRouter usage feature with original generation endpoint as fallback 2025-03-22 17:21:31 -07:00
Saoud Rizwan 0cd2e69551 Revert "Revert "Fix bug where OpenRouter/Cline providers generation endpoint failed (#2262)""
This reverts commit 2ca4738b59.
2025-03-22 17:15:36 -07:00
Saoud Rizwan 4b19010d56 Revert "Use OpenRouter's new usage flag for more reliable pricing responses"
This reverts commit a29689dfd3.
2025-03-22 17:15:26 -07:00
Saoud Rizwan 49803bf958 Fixes 2025-03-22 17:13:58 -07:00
Saoud Rizwan 33704dcdc1 Copy 2025-03-22 14:53:46 -07:00
Saoud Rizwan 78552287ae Update CHANGELOG 2025-03-22 14:51:56 -07:00
Saoud Rizwan a29689dfd3 Use OpenRouter's new usage flag for more reliable pricing responses 2025-03-22 14:40:23 -07:00
Saoud Rizwan 2ca4738b59 Revert "Fix bug where OpenRouter/Cline providers generation endpoint failed (#2262)"
This reverts commit 8cc7759584.
2025-03-22 14:39:30 -07:00
Saoud Rizwan b4a2d5a06d Add 'Add to Cline' and 'Fix with Cline' menu options 2025-03-22 14:18:50 -07:00
Saoud Rizwan 7f2de3956c Add docs button 2025-03-22 11:47:03 -07:00
Saoud Rizwan 059b14e494 Fix #2167 where openRouterModelId was returning a non-string value 2025-03-22 11:37:55 -07:00
Saoud Rizwan 4d95817eec Move tracer initialization to capture 2025-03-22 00:48:52 -07:00
Saoud Rizwan d8c13db683 Make conversation telemetry only available in dev builds for testing 2025-03-22 00:44:12 -07:00
Saoud Rizwan a35e1ff8df Add delete mcp server button for failed server 2025-03-21 23:48:36 -07:00
Saoud Rizwan a0c5276fb1 Fixes 2025-03-21 23:43:35 -07:00
Saoud Rizwan 696a2c4e48 Prepare for release 2025-03-21 23:34:43 -07:00
Saoud Rizwan e983358dde Clean up new accounts billing view styles 2025-03-21 23:22:57 -07:00
Saoud Rizwan 9ae90c99ad Allow anyone to opt in to conversation telemetry 2025-03-21 21:29:41 -07:00
0xtoshii 652034232e changeset 2025-03-21 20:47:00 -07:00
0xtoshii 8d57cc6bfa updated move context logic out of Cline 2025-03-21 20:45:29 -07:00
Saoud Rizwan 4cd914970e Update llm obs description 2025-03-21 20:11:05 -07:00
Evan 4919424faf Handle Openrouter Input Too Large (#2375)
* correct calculation of quarter truncation

* changeset

* truncate and retry if we encounter context window exceeded error

* optional chaining for error message check

* changeset
2025-03-21 20:05:41 -07:00
e65fa89566 [ENG-251] Add Account View (#2324)
* Accounts Modal mocked

* fix dividers

* Fix flex styling

* Fix account card in other views

* Move AccountInfoCard to tailwind and fix flex styling

* Get credits data from endpoint

* refactor creditshistorytable

* prettier

* changeset

* Properly connect and format data from endpoint

* Overflow scroll box

* linter

* Update webview-ui/src/utils/format.ts

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

* Make svg asset a component

* fix svg type

* Use clineprovider weakref

* Move shared types to shared folder

* Make account view buttons full width

* Make history table take full height

* Update webview-ui/src/components/account/AccountView.tsx

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-03-21 20:03:41 -07:00
0xtoshii 57a1deea22 move context management out 2025-03-20 17:37:19 -07:00
42 changed files with 1132 additions and 359 deletions
@@ -2,4 +2,4 @@
"claude-dev": minor
---
opt-in llm observability
update context on truncation
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Fix truncation logic for small message arrays
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Handle openrouter input too large error
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
updated move context management out of cline
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
fixes an issue with Azure API version detection in the OpenAI provider
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": minor
---
Add size calculation to "Delete all Tasks" button
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Start using ErrorBoundaries for chat widgets to handle crashes
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
SuccessButton to Tailwind
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Harden MCP Response Display with network throttling, ErrorBoundaries, URL checking, HTTPs security, limit number of URLs parsed per response. Adds support for svg, webp, and gifs
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Add saito-sv as CodeOwner
+14
View File
@@ -1,5 +1,19 @@
# Changelog
## [3.8.0]
- Add 'Add to Cline' as an option when you right-click in a file or the terminal, making it easier to add context to your current task
- Add 'Fix with Cline' code action - when you see a lightbulb icon in your editor, you can now select 'Fix with Cline' to send the code and associated errors for Cline to fix. (Cursor users can also use the 'Quick Fix (CMD + .)' menu to see this option)
- Add Account view to display billing and usage history for Cline account users. You can now keep track of credits used and transaction history right in the extension!
- Add 'Sort underling provider routing' setting to Cline/OpenRouter allowing you to sort provider used by throughput, price, latency, or the default (combination of price and uptime)
- Improve rich MCP display with dynamic image loading and support for GIFs
- Add 'Documentation' menu item to easily access Cline's docs
- Add OpenRouter's new usage_details feature for more reliable cost reporting
- Display total space Cline takes on disk next to 'Delete all Tasks' button in History view
- Fix 'Context Window Exceeded' error for OpenRouter/Cline Accounts (additional support coming soon)
- Fix bug where OpenRouter model ID would be set to invalid value
- Add button to delete MCP servers in a failure state
## [3.7.1]
- Fix issue with 'See more' button in task header not showing when starting new tasks
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "claude-dev",
"version": "3.7.1",
"version": "3.8.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "claude-dev",
"version": "3.7.1",
"version": "3.8.0",
"license": "Apache-2.0",
"dependencies": {
"@anthropic-ai/bedrock-sdk": "^0.12.4",
+51 -12
View File
@@ -2,12 +2,8 @@
"name": "claude-dev",
"displayName": "Cline",
"description": "Autonomous coding agent right in your IDE, capable of creating/editing files, running commands, using the browser, and more with your permission every step of the way.",
"version": "3.7.1",
"version": "3.8.0",
"icon": "assets/icons/icon.png",
"galleryBanner": {
"color": "#617A91",
"theme": "dark"
},
"engines": {
"vscode": "^1.84.0"
},
@@ -73,7 +69,7 @@
{
"command": "cline.mcpButtonClicked",
"title": "MCP Servers",
"icon": "$(extensions)"
"icon": "$(server)"
},
{
"command": "cline.historyButtonClicked",
@@ -85,6 +81,11 @@
"title": "Open in Editor",
"icon": "$(link-external)"
},
{
"command": "cline.accountButtonClicked",
"title": "Account",
"icon": "$(account)"
},
{
"command": "cline.settingsButtonClicked",
"title": "Settings",
@@ -94,6 +95,26 @@
"command": "cline.openInNewTab",
"title": "Open In New Tab",
"category": "Cline"
},
{
"command": "cline.openDocumentation",
"title": "Documentation",
"icon": "$(book)"
},
{
"command": "cline.addToChat",
"title": "Add to Cline",
"category": "Cline"
},
{
"command": "cline.addTerminalOutputToChat",
"title": "Add to Cline",
"category": "Cline"
},
{
"command": "cline.fixWithCline",
"title": "Fix with Cline",
"category": "Cline"
}
],
"menus": {
@@ -119,9 +140,32 @@
"when": "view == claude-dev.SidebarProvider"
},
{
"command": "cline.settingsButtonClicked",
"command": "cline.openDocumentation",
"group": "navigation@5",
"when": "view == claude-dev.SidebarProvider"
},
{
"command": "cline.accountButtonClicked",
"group": "navigation@6",
"when": "view == claude-dev.SidebarProvider"
},
{
"command": "cline.settingsButtonClicked",
"group": "navigation@7",
"when": "view == claude-dev.SidebarProvider"
}
],
"editor/context": [
{
"command": "cline.addToChat",
"group": "navigation",
"when": "editorHasSelection"
}
],
"terminal/context": [
{
"command": "cline.addTerminalOutputToChat",
"group": "navigation"
}
]
},
@@ -211,11 +255,6 @@
"type": "boolean",
"default": true,
"description": "Controls whether the MCP Marketplace is enabled."
},
"cline.conversationTelemetry": {
"type": "boolean",
"default": false,
"description": "Share message data, code, and more extensive telemetry. This data may be used to improve prompts used in Cline, train models, and understand failure states more accurately."
}
}
}
+19 -3
View File
@@ -33,6 +33,8 @@ export class ClineHandler implements ApiHandler {
this.options.openRouterProviderSorting,
)
let didOutputUsage: boolean = false
for await (const chunk of stream) {
// openrouter returns an error object instead of the openai sdk throwing an error
if ("error" in chunk) {
@@ -63,11 +65,25 @@ export class ClineHandler implements ApiHandler {
reasoning: delta.reasoning,
}
}
if (!didOutputUsage && chunk.usage) {
yield {
type: "usage",
inputTokens: chunk.usage.prompt_tokens || 0,
outputTokens: chunk.usage.completion_tokens || 0,
// @ts-ignore-next-line
totalCost: chunk.usage.cost || 0,
}
didOutputUsage = true
}
}
const apiStreamUsage = await this.getApiStreamUsage()
if (apiStreamUsage) {
yield apiStreamUsage
// Fallback to generation endpoint if usage chunk not returned
if (!didOutputUsage) {
const apiStreamUsage = await this.getApiStreamUsage()
if (apiStreamUsage) {
yield apiStreamUsage
}
}
}
+19 -3
View File
@@ -40,6 +40,8 @@ export class OpenRouterHandler implements ApiHandler {
this.options.openRouterProviderSorting,
)
let didOutputUsage: boolean = false
for await (const chunk of stream) {
// openrouter returns an error object instead of the openai sdk throwing an error
if ("error" in chunk) {
@@ -70,11 +72,25 @@ export class OpenRouterHandler implements ApiHandler {
reasoning: delta.reasoning,
}
}
if (!didOutputUsage && chunk.usage) {
yield {
type: "usage",
inputTokens: chunk.usage.prompt_tokens || 0,
outputTokens: chunk.usage.completion_tokens || 0,
// @ts-ignore-next-line
totalCost: chunk.usage.cost || 0,
}
didOutputUsage = true
}
}
const apiStreamUsage = await this.getApiStreamUsage()
if (apiStreamUsage) {
yield apiStreamUsage
// Fallback to generation endpoint if usage chunk not returned
if (!didOutputUsage) {
const apiStreamUsage = await this.getApiStreamUsage()
if (apiStreamUsage) {
yield apiStreamUsage
}
}
}
+1
View File
@@ -142,6 +142,7 @@ export async function createOpenRouterStream(
top_p: topP,
messages: openAiMessages,
stream: true,
stream_options: { include_usage: true },
transforms: shouldApplyMiddleOutTransform ? ["middle-out"] : undefined,
include_reasoning: true,
...(model.id === "openai/o3-mini" ? { reasoning_effort: o3MiniReasoningEffort || "medium" } : {}),
+10 -48
View File
@@ -1369,58 +1369,20 @@ export class Cline {
})
}
// If the previous API request's total token usage is close to the context window, truncate the conversation history to free up space for the new request
if (previousApiReqIndex >= 0) {
const previousRequest = this.clineMessages[previousApiReqIndex]
if (previousRequest && previousRequest.text) {
const { tokensIn, tokensOut, cacheWrites, cacheReads }: ClineApiReqInfo = JSON.parse(previousRequest.text)
const totalTokens = (tokensIn || 0) + (tokensOut || 0) + (cacheWrites || 0) + (cacheReads || 0)
let contextWindow = this.api.getModel().info.contextWindow || 128_000
// FIXME: hack to get anyone using openai compatible with deepseek to have the proper context window instead of the default 128k. We need a way for the user to specify the context window for models they input through openai compatible
if (this.api instanceof OpenAiHandler && this.api.getModel().id.toLowerCase().includes("deepseek")) {
contextWindow = 64_000
}
let maxAllowedSize: number
switch (contextWindow) {
case 64_000: // deepseek models
maxAllowedSize = contextWindow - 27_000
break
case 128_000: // most models
maxAllowedSize = contextWindow - 30_000
break
case 200_000: // claude models
maxAllowedSize = contextWindow - 40_000
break
default:
maxAllowedSize = Math.max(contextWindow - 40_000, contextWindow * 0.8) // for deepseek, 80% of 64k meant only ~10k buffer which was too small and resulted in users getting context window errors.
}
// This is the most reliable way to know when we're close to hitting the context window.
if (totalTokens >= maxAllowedSize) {
// Since the user may switch between models with different context windows, truncating half may not be enough (ie if switching from claude 200k to deepseek 64k, half truncation will only remove 100k tokens, but we need to remove much more)
// So if totalTokens/2 is greater than maxAllowedSize, we truncate 3/4 instead of 1/2
// FIXME: truncating the conversation in a way that is optimal for prompt caching AND takes into account multi-context window complexity is something we need to improve
const keep = totalTokens / 2 > maxAllowedSize ? "quarter" : "half"
// NOTE: it's okay that we overwriteConversationHistory in resume task since we're only ever removing the last user message and not anything in the middle which would affect this range
this.conversationHistoryDeletedRange = this.contextManager.getNextTruncationRange(
this.apiConversationHistory,
this.conversationHistoryDeletedRange,
keep,
)
await this.saveClineMessages() // saves task history item which we use to keep track of conversation history deleted range
// await this.overwriteApiConversationHistory(truncatedMessages)
}
}
}
// conversationHistoryDeletedRange is updated only when we're close to hitting the context window, so we don't continuously break the prompt cache
const truncatedConversationHistory = this.contextManager.getTruncatedMessages(
const contextManagementMetadata = this.contextManager.getNewContextMessagesAndMetadata(
this.apiConversationHistory,
this.clineMessages,
this.api,
this.conversationHistoryDeletedRange,
previousApiReqIndex,
)
let stream = this.api.createMessage(systemPrompt, truncatedConversationHistory)
if (contextManagementMetadata.updatedConversationHistoryDeletedRange) {
this.conversationHistoryDeletedRange = contextManagementMetadata.conversationHistoryDeletedRange
await this.saveClineMessages() // saves task history item which we use to keep track of conversation history deleted range
}
let stream = this.api.createMessage(systemPrompt, contextManagementMetadata.truncatedConversationHistory)
const iterator = stream[Symbol.asyncIterator]()
+101 -15
View File
@@ -1,14 +1,85 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { ClineApiReqInfo, ClineMessage } from "../../shared/ExtensionMessage"
import { ApiHandler } from "../../api"
import { OpenAiHandler } from "../../api/providers/openai"
import { formatResponse } from "../prompts/responses"
export class ContextManager {
getNextTruncationRange(
messages: Anthropic.Messages.MessageParam[],
currentDeletedRange: [number, number] | undefined = undefined,
keep: "half" | "quarter" = "half",
getNewContextMessagesAndMetadata(
apiConversationHistory: Anthropic.Messages.MessageParam[],
clineMessages: ClineMessage[],
api: ApiHandler,
conversationHistoryDeletedRange: [number, number] | undefined,
previousApiReqIndex: number,
) {
let updatedConversationHistoryDeletedRange = false
// If the previous API request's total token usage is close to the context window, truncate the conversation history to free up space for the new request
if (previousApiReqIndex >= 0) {
const previousRequest = clineMessages[previousApiReqIndex]
if (previousRequest && previousRequest.text) {
const { tokensIn, tokensOut, cacheWrites, cacheReads }: ClineApiReqInfo = JSON.parse(previousRequest.text)
const totalTokens = (tokensIn || 0) + (tokensOut || 0) + (cacheWrites || 0) + (cacheReads || 0)
let contextWindow = api.getModel().info.contextWindow || 128_000
// FIXME: hack to get anyone using openai compatible with deepseek to have the proper context window instead of the default 128k. We need a way for the user to specify the context window for models they input through openai compatible
if (api instanceof OpenAiHandler && api.getModel().id.toLowerCase().includes("deepseek")) {
contextWindow = 64_000
}
let maxAllowedSize: number
switch (contextWindow) {
case 64_000: // deepseek models
maxAllowedSize = contextWindow - 27_000
break
case 128_000: // most models
maxAllowedSize = contextWindow - 30_000
break
case 200_000: // claude models
maxAllowedSize = contextWindow - 40_000
break
default:
maxAllowedSize = Math.max(contextWindow - 40_000, contextWindow * 0.8) // for deepseek, 80% of 64k meant only ~10k buffer which was too small and resulted in users getting context window errors.
}
// This is the most reliable way to know when we're close to hitting the context window.
if (totalTokens >= maxAllowedSize) {
// Since the user may switch between models with different context windows, truncating half may not be enough (ie if switching from claude 200k to deepseek 64k, half truncation will only remove 100k tokens, but we need to remove much more)
// So if totalTokens/2 is greater than maxAllowedSize, we truncate 3/4 instead of 1/2
// FIXME: truncating the conversation in a way that is optimal for prompt caching AND takes into account multi-context window complexity is something we need to improve
const keep = totalTokens / 2 > maxAllowedSize ? "quarter" : "half"
// NOTE: it's okay that we overwriteConversationHistory in resume task since we're only ever removing the last user message and not anything in the middle which would affect this range
conversationHistoryDeletedRange = this.getNextTruncationRange(
apiConversationHistory,
conversationHistoryDeletedRange,
keep,
)
updatedConversationHistoryDeletedRange = true
}
}
}
// conversationHistoryDeletedRange is updated only when we're close to hitting the context window, so we don't continuously break the prompt cache
const truncatedConversationHistory = this.getAndAlterTruncatedMessages(
apiConversationHistory,
conversationHistoryDeletedRange,
)
return {
conversationHistoryDeletedRange: conversationHistoryDeletedRange,
updatedConversationHistoryDeletedRange: updatedConversationHistoryDeletedRange,
truncatedConversationHistory: truncatedConversationHistory,
}
}
public getNextTruncationRange(
apiMessages: Anthropic.Messages.MessageParam[],
currentDeletedRange: [number, number] | undefined,
keep: "half" | "quarter",
): [number, number] {
// Since we always keep the first message, currentDeletedRange[0] will always be 1 (for now until we have a smarter truncation algorithm)
const rangeStartIndex = 1
const startOfRest = currentDeletedRange ? currentDeletedRange[1] + 1 : 1
// We always keep the first user-assistant pairing, and truncate an even number of messages from there
const rangeStartIndex = 2 // index 0 and 1 are kept
const startOfRest = currentDeletedRange ? currentDeletedRange[1] + 1 : 2 // inclusive starting index
let messagesToRemove: number
if (keep === "half") {
@@ -16,20 +87,20 @@ export class ContextManager {
// We first calculate half of the messages then divide by 2 to get the number of pairs.
// After flooring, we multiply by 2 to get the number of messages.
// Note that this will also always be an even number.
messagesToRemove = Math.floor((messages.length - startOfRest) / 4) * 2 // Keep even number
messagesToRemove = Math.floor((apiMessages.length - startOfRest) / 4) * 2 // Keep even number
} else {
// Remove 3/4 of remaining user-assistant pairs
// We calculate 3/4ths of the messages then divide by 2 to get the number of pairs.
// After flooring, we multiply by 2 to get the number of messages.
// Note that this will also always be an even number.
messagesToRemove = Math.floor(((messages.length - startOfRest) * 3) / 4 / 2) * 2
messagesToRemove = Math.floor(((apiMessages.length - startOfRest) * 3) / 4 / 2) * 2
}
let rangeEndIndex = startOfRest + messagesToRemove - 1
let rangeEndIndex = startOfRest + messagesToRemove - 1 // inclusive ending index
// Make sure the last message being removed is a user message, so that the next message after the initial task message is an assistant message. This preservers the user-assistant-user-assistant structure.
// Make sure that the last message being removed is a assistant message, so the next message after the initial user-assistant pair is an assistant message. This preservers the user-assistant-user-assistant structure.
// NOTE: anthropic format messages are always user-assistant-user-assistant, while openai format messages can have multiple user messages in a row (we use anthropic format throughout cline)
if (messages[rangeEndIndex].role !== "user") {
if (apiMessages[rangeEndIndex].role !== "assistant") {
rangeEndIndex -= 1
}
@@ -37,7 +108,14 @@ export class ContextManager {
return [rangeStartIndex, rangeEndIndex]
}
getTruncatedMessages(
public getTruncatedMessages(
messages: Anthropic.Messages.MessageParam[],
deletedRange: [number, number] | undefined,
): Anthropic.Messages.MessageParam[] {
return this.getAndAlterTruncatedMessages(messages, deletedRange)
}
private getAndAlterTruncatedMessages(
messages: Anthropic.Messages.MessageParam[],
deletedRange: [number, number] | undefined,
): Anthropic.Messages.MessageParam[] {
@@ -45,9 +123,17 @@ export class ContextManager {
return messages
}
const [start, end] = deletedRange
const [start, end] = deletedRange // inclusive range to ignore
// need a deep copy
const firstMessageChunk = JSON.parse(JSON.stringify(messages.slice(0, start)))
if (Array.isArray(firstMessageChunk[1].content)) {
// should always be the case
firstMessageChunk[1].content[0].text = formatResponse.contextTruncationNotice()
}
// the range is inclusive - both start and end indices and everything in between will be removed from the final result.
// NOTE: if you try to console log these, don't forget that logging a reference to an array may not provide the same result as logging a slice() snapshot of that array at that exact moment. The following DOES in fact include the latest assistant message.
return [...messages.slice(0, start), ...messages.slice(end + 1)]
return [...firstMessageChunk, ...messages.slice(end + 1)]
}
}
+4 -1
View File
@@ -72,7 +72,10 @@ export async function parseMentions(text: string, cwd: string, urlContentFetcher
}
}
for (const mention of mentions) {
// Filter out duplicate mentions while preserving order
const uniqueMentions = Array.from(new Set(mentions))
for (const mention of uniqueMentions) {
if (mention.startsWith("http")) {
let result: string
if (launchBrowserError) {
+3
View File
@@ -4,6 +4,9 @@ import * as path from "path"
import { ClineIgnoreController, LOCK_TEXT_SYMBOL } from "../ignore/ClineIgnoreController"
export const formatResponse = {
contextTruncationNotice: () =>
`[NOTE] Some previous conversation history with the user has been removed to maintain optimal context window length. The initial user task and the most recent exchanges have been retained for continuity, while intermediate conversation history has been removed. Please keep this in mind as you continue assisting the user.`,
toolDenied: () => `The user denied this operation.`,
toolError: (error?: string) => `The tool execution failed with the following error:\n<error>\n${error}\n</error>`,
+130 -4
View File
@@ -14,6 +14,7 @@ import { fetchOpenGraphData, isImageUrl } from "../../integrations/misc/link-pre
import { selectImages } from "../../integrations/misc/process-images"
import { getTheme } from "../../integrations/theme/getTheme"
import WorkspaceTracker from "../../integrations/workspace/WorkspaceTracker"
import { ClineAccountService } from "../../services/account/ClineAccountService"
import { McpHub } from "../../services/mcp/McpHub"
import { UserInfo } from "../../shared/UserInfo"
import { ApiConfiguration, ApiProvider, ModelInfo } from "../../shared/api"
@@ -39,6 +40,7 @@ import CheckpointTracker from "../../integrations/checkpoints/CheckpointTracker"
import { getTotalTasksSize } from "../../utils/storage"
import { ConversationTelemetryService } from "../../services/telemetry/ConversationTelemetryService"
import { GlobalFileNames } from "../../global-constants"
import delay from "delay"
/*
https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
@@ -122,7 +124,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
private cline?: Cline
workspaceTracker?: WorkspaceTracker
mcpHub?: McpHub
private latestAnnouncementId = "feb-19-2025" // update to some unique identifier when we add a new announcement
accountService?: ClineAccountService
private latestAnnouncementId = "march-22-2025" // update to some unique identifier when we add a new announcement
conversationTelemetryService: ConversationTelemetryService
constructor(
@@ -133,6 +136,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
ClineProvider.activeInstances.add(this)
this.workspaceTracker = new WorkspaceTracker(this)
this.mcpHub = new McpHub(this)
this.accountService = new ClineAccountService(this)
this.conversationTelemetryService = new ConversationTelemetryService(this)
// Clean up legacy checkpoints
@@ -164,6 +168,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
this.workspaceTracker = undefined
this.mcpHub?.dispose()
this.mcpHub = undefined
this.accountService = undefined
this.conversationTelemetryService.shutdown()
this.outputChannel.appendLine("Disposed all disposables")
ClineProvider.activeInstances.delete(this)
@@ -725,6 +730,14 @@ export class ClineProvider implements vscode.WebviewViewProvider {
await this.handleSignOut()
break
}
case "showAccountViewClicked": {
await this.postMessageToWebview({ type: "action", action: "accountButtonClicked" })
break
}
case "fetchUserCreditsData": {
await this.fetchUserCreditsData()
break
}
case "showMcpView": {
await this.postMessageToWebview({ type: "action", action: "mcpButtonClicked" })
break
@@ -1314,6 +1327,20 @@ export class ClineProvider implements vscode.WebviewViewProvider {
}
}
// Account
async fetchUserCreditsData() {
try {
await Promise.all([
this.accountService?.fetchBalance(),
this.accountService?.fetchUsageTransactions(),
this.accountService?.fetchPaymentTransactions(),
])
} catch (error) {
console.error("Failed to fetch user credits data:", error)
}
}
// Auth
public async validateAuthState(state: string | null): Promise<boolean> {
@@ -1352,7 +1379,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
}
await this.postStateToWebview()
vscode.window.showInformationMessage("Successfully logged in to Cline")
// vscode.window.showInformationMessage("Successfully logged in to Cline")
} catch (error) {
console.error("Failed to handle auth callback:", error)
vscode.window.showErrorMessage("Failed to log in to Cline")
@@ -1722,6 +1749,104 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
return models
}
// Context menus and code actions
getFileMentionFromPath(filePath: string) {
const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0)
if (!cwd) {
return "@/" + filePath
}
const relativePath = path.relative(cwd, filePath)
return "@/" + relativePath
}
// 'Add to Cline' context menu in editor and code action
async addSelectedCodeToChat(code: string, filePath: string, languageId: string, diagnostics?: vscode.Diagnostic[]) {
// Ensure the sidebar view is visible
await vscode.commands.executeCommand("claude-dev.SidebarProvider.focus")
await delay(100)
// Post message to webview with the selected code
const fileMention = this.getFileMentionFromPath(filePath)
let input = `${fileMention}\n\`\`\`\n${code}\n\`\`\``
if (diagnostics) {
const problemsString = this.convertDiagnosticsToProblemsString(diagnostics)
input += `\nProblems:\n${problemsString}`
}
await this.postMessageToWebview({
type: "addToInput",
text: input,
})
console.log("addSelectedCodeToChat", code, filePath, languageId)
}
// 'Add to Cline' context menu in Terminal
async addSelectedTerminalOutputToChat(output: string, terminalName: string) {
// Ensure the sidebar view is visible
await vscode.commands.executeCommand("claude-dev.SidebarProvider.focus")
await delay(100)
// Post message to webview with the selected terminal output
// await this.postMessageToWebview({
// type: "addSelectedTerminalOutput",
// output,
// terminalName
// })
await this.postMessageToWebview({
type: "addToInput",
text: `Terminal output:\n\`\`\`\n${output}\n\`\`\``,
})
console.log("addSelectedTerminalOutputToChat", output, terminalName)
}
// 'Fix with Cline' in code actions
async fixWithCline(code: string, filePath: string, languageId: string, diagnostics: vscode.Diagnostic[]) {
// Ensure the sidebar view is visible
await vscode.commands.executeCommand("claude-dev.SidebarProvider.focus")
await delay(100)
const fileMention = this.getFileMentionFromPath(filePath)
const problemsString = this.convertDiagnosticsToProblemsString(diagnostics)
await this.initClineWithTask(
`Fix the following code in ${fileMention}\n\`\`\`\n${code}\n\`\`\`\n\nProblems:\n${problemsString}`,
)
console.log("fixWithCline", code, filePath, languageId, diagnostics, problemsString)
}
convertDiagnosticsToProblemsString(diagnostics: vscode.Diagnostic[]) {
let problemsString = ""
for (const diagnostic of diagnostics) {
let label: string
switch (diagnostic.severity) {
case vscode.DiagnosticSeverity.Error:
label = "Error"
break
case vscode.DiagnosticSeverity.Warning:
label = "Warning"
break
case vscode.DiagnosticSeverity.Information:
label = "Information"
break
case vscode.DiagnosticSeverity.Hint:
label = "Hint"
break
default:
label = "Diagnostic"
}
const line = diagnostic.range.start.line + 1 // VSCode lines are 0-indexed
const source = diagnostic.source ? `${diagnostic.source} ` : ""
problemsString += `\n- [${source}${label}] Line ${line}: ${diagnostic.message}`
}
problemsString = problemsString.trim()
return problemsString
}
// Task history
async getTaskWithId(id: string): Promise<{
@@ -2160,7 +2285,8 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
qwenApiLine,
mistralApiKey,
azureApiVersion,
openRouterModelId,
// Fixes bug where switching to plan/act would result in setting this model id to previousModeModelId which may have been a non-string value by default, causing a type error in the webview when calling .toLowerCase() on it.
openRouterModelId: openRouterModelId ? String(openRouterModelId) : undefined,
openRouterModelInfo,
openRouterProviderSorting,
vsCodeLmModelSelector,
@@ -2182,7 +2308,7 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
chatSettings: chatSettings || DEFAULT_CHAT_SETTINGS,
userInfo,
previousModeApiProvider,
previousModeModelId,
previousModeModelId: previousModeModelId ? String(previousModeModelId) : undefined,
previousModeModelInfo,
previousModeThinkingBudgetTokens,
mcpMarketplaceEnabled,
+153 -2
View File
@@ -115,14 +115,20 @@ export function activate(context: vscode.ExtensionContext) {
)
context.subscriptions.push(
vscode.commands.registerCommand("cline.accountLoginClicked", () => {
vscode.commands.registerCommand("cline.accountButtonClicked", () => {
sidebarProvider.postMessageToWebview({
type: "action",
action: "accountLoginClicked",
action: "accountButtonClicked",
})
}),
)
context.subscriptions.push(
vscode.commands.registerCommand("cline.openDocumentation", () => {
vscode.env.openExternal(vscode.Uri.parse("https://docs.cline.bot/"))
}),
)
/*
We use the text document content provider API to show the left side for diff view by creating a virtual document for the original content. This makes it readonly so users know to edit the right side if they want to keep their changes.
@@ -187,6 +193,151 @@ export function activate(context: vscode.ExtensionContext) {
}
context.subscriptions.push(vscode.window.registerUriHandler({ handleUri }))
context.subscriptions.push(
vscode.commands.registerCommand("cline.addToChat", async (range?: vscode.Range, diagnostics?: vscode.Diagnostic[]) => {
const editor = vscode.window.activeTextEditor
if (!editor) {
return
}
// Use provided range if available, otherwise use current selection
// (vscode command passes an argument in the first param by default, so we need to ensure it's a Range object)
const textRange = range instanceof vscode.Range ? range : editor.selection
const selectedText = editor.document.getText(textRange)
if (!selectedText) {
return
}
// Get the file path and language ID
const filePath = editor.document.uri.fsPath
const languageId = editor.document.languageId
// Send to sidebar provider
await sidebarProvider.addSelectedCodeToChat(
selectedText,
filePath,
languageId,
Array.isArray(diagnostics) ? diagnostics : undefined,
)
}),
)
context.subscriptions.push(
vscode.commands.registerCommand("cline.addTerminalOutputToChat", async () => {
const terminal = vscode.window.activeTerminal
if (!terminal) {
return
}
// Save current clipboard content
const tempCopyBuffer = await vscode.env.clipboard.readText()
try {
// Copy the *existing* terminal selection (without selecting all)
await vscode.commands.executeCommand("workbench.action.terminal.copySelection")
// Get copied content
let terminalContents = (await vscode.env.clipboard.readText()).trim()
// Restore original clipboard content
await vscode.env.clipboard.writeText(tempCopyBuffer)
if (!terminalContents) {
// No terminal content was copied (either nothing selected or some error)
return
}
// [Optional] Any additional logic to process multi-line content can remain here
// For example:
/*
const lines = terminalContents.split("\n")
const lastLine = lines.pop()?.trim()
if (lastLine) {
let i = lines.length - 1
while (i >= 0 && !lines[i].trim().startsWith(lastLine)) {
i--
}
terminalContents = lines.slice(Math.max(i, 0)).join("\n")
}
*/
// Send to sidebar provider
await sidebarProvider.addSelectedTerminalOutputToChat(terminalContents, terminal.name)
} catch (error) {
// Ensure clipboard is restored even if an error occurs
await vscode.env.clipboard.writeText(tempCopyBuffer)
console.error("Error getting terminal contents:", error)
vscode.window.showErrorMessage("Failed to get terminal contents")
}
}),
)
// Register code action provider
context.subscriptions.push(
vscode.languages.registerCodeActionsProvider(
"*",
new (class implements vscode.CodeActionProvider {
public static readonly providedCodeActionKinds = [vscode.CodeActionKind.QuickFix]
provideCodeActions(
document: vscode.TextDocument,
range: vscode.Range,
context: vscode.CodeActionContext,
): vscode.CodeAction[] {
// Expand range to include surrounding 3 lines
const expandedRange = new vscode.Range(
Math.max(0, range.start.line - 3),
0,
Math.min(document.lineCount - 1, range.end.line + 3),
document.lineAt(Math.min(document.lineCount - 1, range.end.line + 3)).text.length,
)
const addAction = new vscode.CodeAction("Add to Cline", vscode.CodeActionKind.QuickFix)
addAction.command = {
command: "cline.addToChat",
title: "Add to Cline",
arguments: [expandedRange, context.diagnostics],
}
const fixAction = new vscode.CodeAction("Fix with Cline", vscode.CodeActionKind.QuickFix)
fixAction.command = {
command: "cline.fixWithCline",
title: "Fix with Cline",
arguments: [expandedRange, context.diagnostics],
}
// Only show actions when there are errors
if (context.diagnostics.length > 0) {
return [addAction, fixAction]
} else {
return []
}
}
})(),
{
providedCodeActionKinds: [vscode.CodeActionKind.QuickFix],
},
),
)
// Register the command handler
context.subscriptions.push(
vscode.commands.registerCommand("cline.fixWithCline", async (range: vscode.Range, diagnostics: any[]) => {
const editor = vscode.window.activeTextEditor
if (!editor) {
return
}
const selectedText = editor.document.getText(range)
const filePath = editor.document.uri.fsPath
const languageId = editor.document.languageId
// Send to sidebar provider with diagnostics
await sidebarProvider.fixWithCline(selectedText, filePath, languageId, diagnostics)
}),
)
return createClineAPI(outputChannel, sidebarProvider)
}
+118
View File
@@ -0,0 +1,118 @@
import axios, { AxiosRequestConfig, AxiosResponse } from "axios"
import { ClineProvider } from "../../core/webview/ClineProvider"
import type { BalanceResponse, PaymentTransaction, UsageTransaction } from "../../shared/ClineAccount"
export class ClineAccountService {
private readonly baseUrl = "https://api.cline.bot/v1"
private providerRef: WeakRef<ClineProvider>
constructor(provider: ClineProvider) {
this.providerRef = new WeakRef(provider)
}
/**
* Get the user's Cline Account key from the apiConfiguration
*/
private async getClineApiKey(): Promise<string | undefined> {
const provider = this.providerRef.deref()
if (!provider) {
return undefined
}
const { apiConfiguration } = await provider.getStateToPostToWebview()
return apiConfiguration?.clineApiKey
}
/**
* Helper function to make authenticated requests to the Cline API
* @param endpoint The API endpoint to call (without the base URL)
* @param config Additional axios request configuration
* @returns The API response data
* @throws Error if the API key is not found or the request fails
*/
private async authenticatedRequest<T>(endpoint: string, config: AxiosRequestConfig = {}): Promise<T> {
const clineApiKey = await this.getClineApiKey()
if (!clineApiKey) {
throw new Error("Cline API key not found")
}
const url = `${this.baseUrl}${endpoint}`
const requestConfig: AxiosRequestConfig = {
...config,
headers: {
Authorization: `Bearer ${clineApiKey}`,
"Content-Type": "application/json",
...config.headers,
},
}
const response: AxiosResponse<T> = await axios.get(url, requestConfig)
if (!response.data) {
throw new Error(`Invalid response from ${endpoint} API`)
}
return response.data
}
/**
* Fetches the user's current credit balance
*/
async fetchBalance(): Promise<BalanceResponse | undefined> {
try {
const data = await this.authenticatedRequest<BalanceResponse>("/user/credits/balance")
// Post to webview
await this.providerRef.deref()?.postMessageToWebview({
type: "userCreditsBalance",
userCreditsBalance: data,
})
return data
} catch (error) {
console.error("Failed to fetch balance:", error)
return undefined
}
}
/**
* Fetches the user's usage transactions
*/
async fetchUsageTransactions(): Promise<UsageTransaction[] | undefined> {
try {
const data = await this.authenticatedRequest<UsageTransaction[]>("/user/credits/usage")
// Post to webview
await this.providerRef.deref()?.postMessageToWebview({
type: "userCreditsUsage",
userCreditsUsage: data,
})
return data
} catch (error) {
console.error("Failed to fetch usage transactions:", error)
return undefined
}
}
/**
* Fetches the user's payment transactions
*/
async fetchPaymentTransactions(): Promise<PaymentTransaction[] | undefined> {
try {
const data = await this.authenticatedRequest<PaymentTransaction[]>("/user/credits/payments")
// Post to webview
await this.providerRef.deref()?.postMessageToWebview({
type: "userCreditsPayments",
userCreditsPayments: data,
})
return data
} catch (error) {
console.error("Failed to fetch payment transactions:", error)
return undefined
}
}
}
@@ -14,8 +14,6 @@ export type TelemetryChatMessage = {
content: Anthropic.Messages.MessageParam["content"]
}
const { IS_DEV } = process.env
interface ConversationMetadata {
apiProvider?: string
model?: string
@@ -23,9 +21,20 @@ interface ConversationMetadata {
tokensOut: number
}
const { IS_DEV } = process.env
/**
* Service for collecting conversation data using OpenTelemetry
Cline Telemetry (currently only available in DEV builds)
Advanced Setting to opt-in to LLM observability, allowing you to share message data, code, and more extensive telemetry to help improve prompts used in Cline, train our models, and understand failure states more accurately.
"cline.conversationTelemetry": {
"type": "boolean",
"default": false,
"markdownDescription": "Share message data, code, and more extensive telemetry. This data may be used to improve prompts used in Cline, train models, and understand failure states more accurately. [Learn more](https://docs.cline.bot/more-info/llm-observability)"
}
*/
export class ConversationTelemetryService {
private providerRef: WeakRef<ClineProvider>
private distinctId: string = vscode.env.machineId
@@ -36,7 +45,6 @@ export class ConversationTelemetryService {
constructor(provider: ClineProvider) {
this.providerRef = new WeakRef(provider)
this.initializeTracer()
}
private async getClineApiKey(): Promise<string | undefined> {
@@ -122,7 +130,7 @@ export class ConversationTelemetryService {
}
if (!this.tracer) {
return
await this.initializeTracer()
}
try {
+18
View File
@@ -0,0 +1,18 @@
export interface BalanceResponse {
currentBalance: number
}
export interface UsageTransaction {
spentAt: string
credits: string
modelProvider: string
model: string
promptTokens: string
completionTokens: string
}
export interface PaymentTransaction {
paidAt: string
amountCents: string
credits: string
}
+9
View File
@@ -8,6 +8,7 @@ import { ChatSettings } from "./ChatSettings"
import { HistoryItem } from "./HistoryItem"
import { McpServer, McpMarketplaceCatalog, McpMarketplaceItem, McpDownloadResponse } from "./mcp"
import { TelemetrySetting } from "./TelemetrySetting"
import type { BalanceResponse, UsageTransaction, PaymentTransaction } from "../shared/ClineAccount"
// webview will hold state
export interface ExtensionMessage {
@@ -34,7 +35,11 @@ export interface ExtensionMessage {
| "openGraphData"
| "isImageUrlResult"
| "didUpdateSettings"
| "userCreditsBalance"
| "userCreditsUsage"
| "userCreditsPayments"
| "totalTasksSize"
| "addToInput"
text?: string
action?:
| "chatButtonClicked"
@@ -44,6 +49,7 @@ export interface ExtensionMessage {
| "didBecomeVisible"
| "accountLoginClicked"
| "accountLogoutClicked"
| "accountButtonClicked"
invoke?: Invoke
state?: ExtensionState
images?: string[]
@@ -70,6 +76,9 @@ export interface ExtensionMessage {
}
url?: string
isImage?: boolean
userCreditsBalance?: BalanceResponse
userCreditsUsage?: UsageTransaction[]
userCreditsPayments?: PaymentTransaction[]
totalTasksSize?: number | null
}
+2
View File
@@ -45,6 +45,7 @@ export interface WebviewMessage {
| "getLatestState"
| "accountLoginClicked"
| "accountLogoutClicked"
| "showAccountViewClicked"
| "authStateChanged"
| "authCallback"
| "fetchMcpMarketplace"
@@ -61,6 +62,7 @@ export interface WebviewMessage {
| "invoke"
| "updateSettings"
| "clearAllTaskHistory"
| "fetchUserCreditsData"
| "optionsResponse"
| "requestTotalTasksSize"
// | "relaunchChromeDebugMode"
+23 -4
View File
@@ -21,6 +21,7 @@
"posthog-js": "^1.224.0",
"pretty-bytes": "^6.1.1",
"react": "^18.3.1",
"react-countup": "^6.5.3",
"react-dom": "^18.3.1",
"react-remark": "^2.1.0",
"react-textarea-autosize": "^8.5.7",
@@ -49,7 +50,7 @@
"tailwindcss": "^4.0.12",
"typescript": "^5.7.3",
"typescript-eslint": "^8.18.2",
"vite": "^6.1.1",
"vite": "^6.2.1",
"vitest": "^3.0.5"
}
},
@@ -4177,6 +4178,12 @@
"layout-base": "^1.0.0"
}
},
"node_modules/countup.js": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/countup.js/-/countup.js-2.8.0.tgz",
"integrity": "sha512-f7xEhX0awl4NOElHulrl4XRfKoNH3rB+qfNSZZyjSZhaAoUk6elvhH+MNxMmlmuUJ2/QNTWPSA7U4mNtIAKljQ==",
"license": "MIT"
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -7095,6 +7102,18 @@
"node": ">=0.10.0"
}
},
"node_modules/react-countup": {
"version": "6.5.3",
"resolved": "https://registry.npmjs.org/react-countup/-/react-countup-6.5.3.tgz",
"integrity": "sha512-udnqVQitxC7QWADSPDOxVWULkLvKUWrDapn5i53HE4DPRVgs+Y5rr4bo25qEl8jSh+0l2cToJgGMx+clxPM3+w==",
"license": "MIT",
"dependencies": {
"countup.js": "^2.8.0"
},
"peerDependencies": {
"react": ">= 16.3.0"
}
},
"node_modules/react-dom": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
@@ -8359,9 +8378,9 @@
}
},
"node_modules/vite": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.2.0.tgz",
"integrity": "sha512-7dPxoo+WsT/64rDcwoOjk76XHj+TqNTIvHKcuMQ1k4/SeHDaQt5GFAeLYzrimZrMpn/O6DtdI03WUjdxuPM0oQ==",
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.2.1.tgz",
"integrity": "sha512-n2GnqDb6XPhlt9B8olZPrgMD/es/Nd1RdChF6CBD/fHW6pUyUTt2sQW2fPRX5GiD9XEa6+8A6A4f2vT6pSsE7Q==",
"dev": true,
"license": "MIT",
"dependencies": {
+2 -1
View File
@@ -25,6 +25,7 @@
"posthog-js": "^1.224.0",
"pretty-bytes": "^6.1.1",
"react": "^18.3.1",
"react-countup": "^6.5.3",
"react-dom": "^18.3.1",
"react-remark": "^2.1.0",
"react-textarea-autosize": "^8.5.7",
@@ -53,7 +54,7 @@
"tailwindcss": "^4.0.12",
"typescript": "^5.7.3",
"typescript-eslint": "^8.18.2",
"vite": "^6.1.1",
"vite": "^6.2.1",
"vitest": "^3.0.5"
}
}
+2 -1
View File
@@ -42,7 +42,7 @@ const AppContent = () => {
setShowMcp(true)
setShowAccount(false)
break
case "accountLoginClicked":
case "accountButtonClicked":
setShowSettings(false)
setShowHistory(false)
setShowMcp(false)
@@ -96,6 +96,7 @@ const AppContent = () => {
showHistoryView={() => {
setShowSettings(false)
setShowMcp(false)
setShowAccount(false)
setShowHistory(true)
}}
isHidden={showSettings || showHistory || showMcp || showAccount}
+11
View File
@@ -0,0 +1,11 @@
import { SVGProps } from "react"
const ClineLogoWhite = (props: SVGProps<SVGSVGElement>) => (
<svg xmlns="http://www.w3.org/2000/svg" width="47" height="50" viewBox="0 0 47 50" fill="none" {...props}>
<path
d="M46.4075 28.1192L43.5011 22.3166V18.9747C43.5011 13.4354 39.0302 8.94931 33.5162 8.94931H28.5491C28.9086 8.21513 29.106 7.3898 29.106 6.5189C29.106 3.44039 26.6149 0.949219 23.5363 0.949219C20.4578 0.949219 17.9667 3.44039 17.9667 6.5189C17.9667 7.3898 18.1641 8.21513 18.5236 8.94931H13.5565C8.04249 8.94931 3.57155 13.4354 3.57155 18.9747V22.3166L0.604424 28.104C0.305687 28.6863 0.305687 29.3799 0.604424 29.9622L3.57155 35.6838V39.0256C3.57155 44.5649 8.04249 49.0511 13.5565 49.0511H33.5162C39.0302 49.0511 43.5011 44.5649 43.5011 39.0256V35.6838L46.4024 29.942C46.691 29.3698 46.691 28.6964 46.4075 28.1192ZM20.4983 32.8483C20.4983 35.3648 18.4578 37.4053 15.9413 37.4053C13.4248 37.4053 11.3843 35.3648 11.3843 32.8483V24.747C11.3843 22.2305 13.4248 20.19 15.9413 20.19C18.4578 20.19 20.4983 22.2305 20.4983 24.747V32.8483ZM35.182 32.8483C35.182 35.3648 33.1415 37.4053 30.625 37.4053C28.1085 37.4053 26.068 35.3648 26.068 32.8483V24.747C26.068 22.2305 28.1085 20.19 30.625 20.19C33.1415 20.19 35.182 22.2305 35.182 24.747V32.8483Z"
fill="white"
/>
</svg>
)
export default ClineLogoWhite
+123 -125
View File
@@ -1,8 +1,12 @@
import { VSCodeButton, VSCodeDivider } from "@vscode/webview-ui-toolkit/react"
import { memo } from "react"
import { VSCodeButton, VSCodeDivider, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
import { memo, useEffect, useState } from "react"
import { useFirebaseAuth } from "../../context/FirebaseAuthContext"
import { vscode } from "../../utils/vscode"
import VSCodeButtonLink from "../common/VSCodeButtonLink"
import ClineLogoWhite from "../../assets/ClineLogoWhite"
import CountUp from "react-countup"
import CreditsHistoryTable from "./CreditsHistoryTable"
import { UsageTransaction, PaymentTransaction } from "../../../../src/shared/ClineAccount"
type AccountViewProps = {
onDone: () => void
@@ -10,38 +14,13 @@ type AccountViewProps = {
const AccountView = ({ onDone }: AccountViewProps) => {
return (
<div
style={{
position: "fixed",
top: 0,
left: 0,
right: 0,
bottom: 0,
padding: "10px 0px 0px 20px",
display: "flex",
flexDirection: "column",
overflow: "hidden",
}}>
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
marginBottom: "17px",
paddingRight: 17,
}}>
<h3 style={{ color: "var(--vscode-foreground)", margin: 0 }}>Cline Account</h3>
<div className="fixed inset-0 flex flex-col overflow-hidden pt-[10px] pl-[20px]">
<div className="flex justify-between items-center mb-[17px] pr-[17px]">
<h3 className="text-[var(--vscode-foreground)] m-0">Account</h3>
<VSCodeButton onClick={onDone}>Done</VSCodeButton>
</div>
<div
style={{
flexGrow: 1,
overflowY: "scroll",
paddingRight: 8,
display: "flex",
flexDirection: "column",
}}>
<div style={{ marginBottom: 5 }}>
<div className="flex-grow overflow-hidden pr-[8px] flex flex-col">
<div className="h-full mb-[5px]">
<ClineAccountView />
</div>
</div>
@@ -51,6 +30,37 @@ const AccountView = ({ onDone }: AccountViewProps) => {
export const ClineAccountView = () => {
const { user, handleSignOut } = useFirebaseAuth()
const [balance, setBalance] = useState(0)
const [isLoading, setIsLoading] = useState(true)
const [usageData, setUsageData] = useState<UsageTransaction[]>([])
const [paymentsData, setPaymentsData] = useState<PaymentTransaction[]>([])
// Listen for balance and transaction data updates from the extension
useEffect(() => {
const handleMessage = (event: MessageEvent) => {
const message = event.data
if (message.type === "userCreditsBalance" && message.userCreditsBalance) {
setBalance(message.userCreditsBalance.currentBalance)
} else if (message.type === "userCreditsUsage" && message.userCreditsUsage) {
setUsageData(message.userCreditsUsage.usageTransactions)
} else if (message.type === "userCreditsPayments" && message.userCreditsPayments) {
setPaymentsData(message.userCreditsPayments.paymentTransactions)
}
setIsLoading(false)
}
window.addEventListener("message", handleMessage)
// Fetch all account data when component mounts
if (user) {
setIsLoading(true)
vscode.postMessage({ type: "fetchUserCreditsData" })
}
return () => {
window.removeEventListener("message", handleMessage)
}
}, [user])
const handleLogin = () => {
vscode.postMessage({ type: "accountLoginClicked" })
@@ -63,108 +73,96 @@ export const ClineAccountView = () => {
handleSignOut()
}
return (
<div style={{ maxWidth: "600px" }}>
<div className="h-full flex flex-col">
{user ? (
<div
style={{
padding: "8px 10px",
border: "1px solid var(--vscode-input-border)",
borderRadius: "2px",
backgroundColor: "var(--vscode-dropdown-background)",
}}>
<div
style={{
display: "flex",
alignItems: "center",
gap: "8px",
}}>
{user.photoURL ? (
<img
src={user.photoURL}
alt="Profile"
style={{
width: 38,
height: 38,
borderRadius: "50%",
}}
/>
) : (
<div
style={{
width: 38,
height: 38,
borderRadius: "50%",
backgroundColor: "var(--vscode-button-background)",
display: "flex",
alignItems: "center",
justifyContent: "center",
fontSize: "20px",
color: "var(--vscode-button-foreground)",
}}>
{user.displayName?.[0] || user.email?.[0] || "?"}
</div>
)}
<div
style={{
display: "flex",
flexDirection: "column",
gap: "4px",
}}>
{user.displayName && (
<div
style={{
fontSize: "13px",
fontWeight: "bold",
color: "var(--vscode-foreground)",
}}>
{user.displayName}
<div className="flex flex-col pr-3 h-full">
<div className="flex flex-col w-full">
<div className="flex items-center mb-6 flex-wrap gap-y-4">
{user.photoURL ? (
<img src={user.photoURL} alt="Profile" className="size-16 rounded-full mr-4" />
) : (
<div className="size-16 rounded-full bg-[var(--vscode-button-background)] flex items-center justify-center text-2xl text-[var(--vscode-button-foreground)] mr-4">
{user.displayName?.[0] || user.email?.[0] || "?"}
</div>
)}
{user.email && (
<div
style={{
fontSize: "13px",
color: "var(--vscode-descriptionForeground)",
}}>
{user.email}
</div>
)}
<div style={{ display: "flex", gap: "8px", flexWrap: "wrap" }}>
<VSCodeButtonLink
href="https://app.cline.bot/credits"
appearance="primary"
style={{
transform: "scale(0.85)",
transformOrigin: "left center",
width: "fit-content",
marginTop: 2,
marginBottom: 0,
marginRight: -12,
}}>
Account
</VSCodeButtonLink>
<VSCodeButton
appearance="secondary"
onClick={handleLogout}
style={{
transform: "scale(0.85)",
transformOrigin: "left center",
width: "fit-content",
marginTop: 2,
marginBottom: 0,
marginRight: -12,
}}>
Log out
</VSCodeButton>
<div className="flex flex-col">
{user.displayName && (
<h2 className="text-[var(--vscode-foreground)] m-0 mb-1 text-lg font-medium">
{user.displayName}
</h2>
)}
{user.email && (
<div className="text-sm text-[var(--vscode-descriptionForeground)]">{user.email}</div>
)}
</div>
</div>
</div>
<div className="w-full flex gap-2 flex-col min-[225px]:flex-row">
<div className="w-full min-[225px]:w-1/2">
<VSCodeButtonLink href="https://app.cline.bot/credits" appearance="primary" className="w-full">
Dashboard
</VSCodeButtonLink>
</div>
<VSCodeButton appearance="secondary" onClick={handleLogout} className="w-full min-[225px]:w-1/2">
Log out
</VSCodeButton>
</div>
<VSCodeDivider className="w-full my-6" />
<div className="w-full flex flex-col items-center">
<div className="text-sm text-[var(--vscode-descriptionForeground)] mb-3">CURRENT BALANCE</div>
<div className="text-4xl font-bold text-[var(--vscode-foreground)] mb-6 flex items-center gap-2">
{isLoading ? (
<div className="text-[var(--vscode-descriptionForeground)]">Loading...</div>
) : (
<>
<span>$</span>
<CountUp end={balance} duration={0.66} decimals={2} />
<VSCodeButton
appearance="icon"
className="mt-1"
onClick={() => vscode.postMessage({ type: "fetchUserCreditsData" })}>
<span className="codicon codicon-refresh"></span>
</VSCodeButton>
</>
)}
</div>
<div className="w-full">
<VSCodeButtonLink href="https://app.cline.bot/credits/#buy" className="w-full">
Add Credits
</VSCodeButtonLink>
</div>
</div>
<VSCodeDivider className="mt-6 mb-3 w-full" />
<div className="flex-grow flex flex-col min-h-0 pb-[0px]">
<CreditsHistoryTable isLoading={isLoading} usageData={usageData} paymentsData={paymentsData} />
</div>
</div>
) : (
<div style={{}}>
<VSCodeButton onClick={handleLogin} style={{ marginTop: 0 }}>
Sign Up with Cline
<div className="flex flex-col items-center pr-3 max-w-[400px]">
<ClineLogoWhite className="size-16 mb-4" />
<p style={{}}>
Sign up for an account to get access to the latest models, billing dashboard to view usage and credits,
and more upcoming features.
</p>
<VSCodeButton onClick={handleLogin} className="w-full mb-4">
Sign up with Cline
</VSCodeButton>
<p className="text-[var(--vscode-descriptionForeground)] text-xs text-center m-0">
By continuing, you agree to the <VSCodeLink href="https://cline.bot/tos">Terms of Service</VSCodeLink> and{" "}
<VSCodeLink href="https://cline.bot/privacy">Privacy Policy.</VSCodeLink>
</p>
</div>
)}
</div>
@@ -0,0 +1,114 @@
import { VSCodeDataGrid, VSCodeDataGridRow, VSCodeDataGridCell } from "@vscode/webview-ui-toolkit/react"
import { useState } from "react"
import { TabButton } from "../mcp/McpView"
import { UsageTransaction, PaymentTransaction } from "../../../../src/shared/ClineAccount"
import { formatDollars, formatTimestamp } from "../../utils/format"
interface CreditsHistoryTableProps {
isLoading: boolean
usageData: UsageTransaction[]
paymentsData: PaymentTransaction[]
}
const CreditsHistoryTable = ({ isLoading, usageData, paymentsData }: CreditsHistoryTableProps) => {
const [activeTab, setActiveTab] = useState<"usage" | "payments">("usage")
return (
<div className="flex flex-col flex-grow h-full">
{/* Tabs container */}
<div className="flex border-b border-[var(--vscode-panel-border)]">
<TabButton isActive={activeTab === "usage"} onClick={() => setActiveTab("usage")}>
USAGE HISTORY
</TabButton>
<TabButton isActive={activeTab === "payments"} onClick={() => setActiveTab("payments")}>
PAYMENTS HISTORY
</TabButton>
</div>
{/* Content container */}
<div className="mt-[15px] mb-[0px] rounded-md overflow-auto flex-grow">
{isLoading ? (
<div className="flex justify-center items-center p-4">
<div className="text-[var(--vscode-descriptionForeground)]">Loading...</div>
</div>
) : (
<>
{activeTab === "usage" && (
<>
{usageData.length > 0 ? (
<VSCodeDataGrid>
<VSCodeDataGridRow row-type="header">
<VSCodeDataGridCell cell-type="columnheader" grid-column="1">
Date
</VSCodeDataGridCell>
<VSCodeDataGridCell cell-type="columnheader" grid-column="2">
Model
</VSCodeDataGridCell>
{/* <VSCodeDataGridCell cell-type="columnheader" grid-column="3">
Tokens Used
</VSCodeDataGridCell> */}
<VSCodeDataGridCell cell-type="columnheader" grid-column="3">
Credits Used
</VSCodeDataGridCell>
</VSCodeDataGridRow>
{usageData.map((row, index) => (
<VSCodeDataGridRow key={index}>
<VSCodeDataGridCell grid-column="1">
{formatTimestamp(row.spentAt)}
</VSCodeDataGridCell>
<VSCodeDataGridCell grid-column="2">{`${row.modelProvider}/${row.model}`}</VSCodeDataGridCell>
{/* <VSCodeDataGridCell grid-column="3">{`${row.promptTokens} → ${row.completionTokens}`}</VSCodeDataGridCell> */}
<VSCodeDataGridCell grid-column="3">{`$${Number(row.credits).toFixed(7)}`}</VSCodeDataGridCell>
</VSCodeDataGridRow>
))}
</VSCodeDataGrid>
) : (
<div className="flex justify-center items-center p-4">
<div className="text-[var(--vscode-descriptionForeground)]">No usage history</div>
</div>
)}
</>
)}
{activeTab === "payments" && (
<>
{paymentsData.length > 0 ? (
<VSCodeDataGrid>
<VSCodeDataGridRow row-type="header">
<VSCodeDataGridCell cell-type="columnheader" grid-column="1">
Date
</VSCodeDataGridCell>
<VSCodeDataGridCell cell-type="columnheader" grid-column="2">
Total Cost
</VSCodeDataGridCell>
<VSCodeDataGridCell cell-type="columnheader" grid-column="3">
Credits
</VSCodeDataGridCell>
</VSCodeDataGridRow>
{paymentsData.map((row, index) => (
<VSCodeDataGridRow key={index}>
<VSCodeDataGridCell grid-column="1">
{formatTimestamp(row.paidAt)}
</VSCodeDataGridCell>
<VSCodeDataGridCell grid-column="2">{`$${formatDollars(parseInt(row.amountCents))}`}</VSCodeDataGridCell>
<VSCodeDataGridCell grid-column="3">{`${row.credits}`}</VSCodeDataGridCell>
</VSCodeDataGridRow>
))}
</VSCodeDataGrid>
) : (
<div className="flex justify-center items-center p-4">
<div className="text-[var(--vscode-descriptionForeground)]">No payment history</div>
</div>
)}
</>
)}
</>
)}
</div>
</div>
)
}
export default CreditsHistoryTable
@@ -31,34 +31,22 @@ const Announcement = ({ version, hideAnnouncement }: AnnouncementProps) => {
</h3>
<ul style={{ margin: "0 0 8px", paddingLeft: "12px" }}>
<li>
<b>Introducing MCP Marketplace:</b> Discover and install the best MCP servers right from the extension, with
new servers added regularly! Get started by going to the{" "}
<span className="codicon codicon-extensions" style={{ marginRight: "4px", fontSize: 10 }}></span>
<VSCodeLink
onClick={() => {
vscode.postMessage({ type: "showMcpView" })
}}>
MCP Servers tab
</VSCodeLink>
.
<b>Add to Cline:</b> Right-click selected text in any file or terminal to quickly add context to your current
task! Plus, when you see a lightbulb icon, select 'Fix with Cline' to have Cline fix errors in your code.
</li>
<li>
<b>Mermaid diagrams in Plan mode!</b> Cline can now visualize his plans using flowcharts, sequences,
entity-relationships, and more. When he explains his approach using mermaid, you'll see a diagram right in
chat that you can click to expand.
<b>Billing Dashboard:</b> Track your remaining credits and transaction history right in the extension with a{" "}
<span className="codicon codicon-account" style={{ fontSize: 11 }}></span> Cline account!
</li>
<li>
Use <code>@terminal</code> to reference terminal contents, and <code>@git</code> to reference working changes
and commits!
<b>Faster Inference:</b> Cline/OpenRouter users can sort underlying providers used by throughput, price, and
latency. Sorting by throughput will output faster generations (at a higher cost).
</li>
<li>
New visual indicator for checkpoints after edits & commands, and automatic checkpoint at the start of each
task.
<b>Enhanced MCP Support:</b> Dynamic image loading with GIF support, and a new delete button to clean up
failed servers.
</li>
</ul>
<VSCodeLink href="https://x.com/sdrzn/status/1892262424881090721" style={{ display: "inline" }}>
See a demo of the changes here!
</VSCodeLink>
{/*<ul style={{ margin: "0 0 8px", paddingLeft: "12px" }}>
<li>
OpenRouter now supports prompt caching! They also have much higher rate limits than other providers,
@@ -453,6 +453,18 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
setSelectedImages((prevImages) => [...prevImages, ...newImages].slice(0, MAX_IMAGES_PER_MESSAGE))
}
break
case "addToInput":
setInputValue((prevValue) => {
const newText = message.text ?? ""
return prevValue ? `${prevValue}\n${newText}` : newText
})
// Add scroll to bottom after state update
setTimeout(() => {
if (textAreaRef.current) {
textAreaRef.current.scrollTop = textAreaRef.current.scrollHeight
}
}, 0)
break
case "invoke":
switch (message.invoke!) {
case "sendMessage":
@@ -30,7 +30,7 @@ const CreditLimitError: React.FC<CreditLimitErrorProps> = ({ currentBalance, tot
</div>
<VSCodeButtonLink
href="https://app.cline.bot/credits"
href="https://app.cline.bot/credits/#buy"
style={{
width: "100%",
marginBottom: "8px",
@@ -387,6 +387,15 @@ const ServerRow = ({ server }: { server: McpServer }) => {
}}>
{server.status === "connecting" ? "Retrying..." : "Retry Connection"}
</VSCodeButton>
<DangerButton
onClick={handleDelete}
disabled={isDeleting}
style={{
width: "calc(100% - 20px)",
margin: "0 10px 10px 10px",
}}>
{isDeleting ? "Deleting..." : "Delete Server"}
</DangerButton>
</div>
) : (
isExpanded && (
@@ -51,8 +51,8 @@ import { useExtensionState } from "../../context/ExtensionStateContext"
import { vscode } from "../../utils/vscode"
import { getAsVar, VSC_DESCRIPTION_FOREGROUND } from "../../utils/vscStyles"
import VSCodeButtonLink from "../common/VSCodeButtonLink"
import OpenRouterModelPicker, { ModelDescriptionMarkdown } from "./OpenRouterModelPicker"
import AccountView, { ClineAccountView } from "../account/AccountView"
import OpenRouterModelPicker, { ModelDescriptionMarkdown, OPENROUTER_MODEL_PICKER_Z_INDEX } from "./OpenRouterModelPicker"
import { ClineAccountInfoCard } from "./ClineAccountInfoCard"
interface ApiOptionsProps {
showModelOptions: boolean
@@ -62,7 +62,7 @@ interface ApiOptionsProps {
}
// This is necessary to ensure dropdown opens downward, important for when this is used in popup
const DROPDOWN_Z_INDEX = 1001 // Higher than the OpenRouterModelPicker's and ModelSelectorTooltip's z-index
const DROPDOWN_Z_INDEX = OPENROUTER_MODEL_PICKER_Z_INDEX + 2 // Higher than the OpenRouterModelPicker's and ModelSelectorTooltip's z-index
export const DropdownContainer = styled.div<{ zIndex?: number }>`
position: relative;
@@ -95,6 +95,7 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
const [awsEndpointSelected, setAwsEndpointSelected] = useState(!!apiConfiguration?.awsBedrockEndpoint)
const [modelConfigurationSelected, setModelConfigurationSelected] = useState(false)
const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false)
const [providerSortingSelected, setProviderSortingSelected] = useState(!!apiConfiguration?.openRouterProviderSorting)
const handleInputChange = (field: keyof ApiConfiguration) => (event: any) => {
setApiConfiguration({
@@ -215,8 +216,8 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
</DropdownContainer>
{selectedProvider === "cline" && (
<div style={{ marginBottom: 8, marginTop: 4 }}>
<ClineAccountView />
<div style={{ marginBottom: 14, marginTop: 4 }}>
<ClineAccountInfoCard />
</div>
)}
@@ -675,7 +676,7 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
placeholder="Enter Project ID...">
<span style={{ fontWeight: 500 }}>Google Cloud Project ID</span>
</VSCodeTextField>
<DropdownContainer zIndex={DROPDOWN_Z_INDEX - 2} className="dropdown-container">
<DropdownContainer zIndex={DROPDOWN_Z_INDEX - 1} className="dropdown-container">
<label htmlFor="vertex-region-dropdown">
<span style={{ fontWeight: 500 }}>Google Cloud Region</span>
</label>
@@ -1353,6 +1354,57 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
</p>
)}
{(selectedProvider === "openrouter" || selectedProvider === "cline") && showModelOptions && (
<>
<VSCodeCheckbox
style={{ marginTop: -10 }}
checked={providerSortingSelected}
onChange={(e: any) => {
const isChecked = e.target.checked === true
setProviderSortingSelected(isChecked)
if (!isChecked) {
setApiConfiguration({
...apiConfiguration,
openRouterProviderSorting: "",
})
}
}}>
Sort underlying provider routing
</VSCodeCheckbox>
{providerSortingSelected && (
<div style={{ marginBottom: -6 }}>
<DropdownContainer className="dropdown-container" zIndex={OPENROUTER_MODEL_PICKER_Z_INDEX + 1}>
<VSCodeDropdown
style={{ width: "100%", marginTop: 3 }}
value={apiConfiguration?.openRouterProviderSorting}
onChange={(e: any) => {
setApiConfiguration({
...apiConfiguration,
openRouterProviderSorting: e.target.value,
})
}}>
<VSCodeOption value="">Default</VSCodeOption>
<VSCodeOption value="price">Price</VSCodeOption>
<VSCodeOption value="throughput">Throughput</VSCodeOption>
<VSCodeOption value="latency">Latency</VSCodeOption>
</VSCodeDropdown>
</DropdownContainer>
<p style={{ fontSize: "12px", marginTop: 3, color: "var(--vscode-descriptionForeground)" }}>
{!apiConfiguration?.openRouterProviderSorting &&
"Default behavior is to load balance requests across providers (like AWS, Google Vertex, Anthropic), prioritizing price while considering provider uptime"}
{apiConfiguration?.openRouterProviderSorting === "price" &&
"Sort providers by price, prioritizing the lowest cost provider"}
{apiConfiguration?.openRouterProviderSorting === "throughput" &&
"Sort providers by throughput, prioritizing the provider with the highest throughput (may increase cost)"}
{apiConfiguration?.openRouterProviderSorting === "latency" &&
"Sort providers by response time, prioritizing the provider with the lowest latency"}
</p>
</div>
)}
</>
)}
{selectedProvider !== "openrouter" &&
selectedProvider !== "cline" &&
selectedProvider !== "openai" &&
@@ -0,0 +1,70 @@
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
import { useFirebaseAuth } from "../../context/FirebaseAuthContext"
import { vscode } from "../../utils/vscode"
export const ClineAccountInfoCard = () => {
const { user, handleSignOut } = useFirebaseAuth()
const handleLogin = () => {
vscode.postMessage({ type: "accountLoginClicked" })
}
const handleLogout = () => {
// First notify extension to clear API keys and state
vscode.postMessage({ type: "accountLogoutClicked" })
// Then sign out of Firebase
handleSignOut()
}
const handleShowAccount = () => {
vscode.postMessage({ type: "showAccountViewClicked" })
}
return (
<div className="max-w-[600px]">
{user ? (
<VSCodeButton appearance="secondary" onClick={handleShowAccount}>
View Billing & Usage
</VSCodeButton>
) : (
// <div className="p-2 rounded-[2px] bg-[var(--vscode-dropdown-background)]">
// <div className="flex items-center gap-3">
// {user.photoURL ? (
// <img src={user.photoURL} alt="Profile" className="w-[38px] h-[38px] rounded-full flex-shrink-0" />
// ) : (
// <div className="w-[38px] h-[38px] rounded-full bg-[var(--vscode-button-background)] flex items-center justify-center text-xl text-[var(--vscode-button-foreground)] flex-shrink-0">
// {user.displayName?.[0] || user.email?.[0] || "?"}
// </div>
// )}
// <div className="flex flex-col gap-1 flex-1 overflow-hidden">
// {user.displayName && (
// <div className="text-[13px] font-bold text-[var(--vscode-foreground)] break-words">
// {user.displayName}
// </div>
// )}
// {user.email && (
// <div className="text-[13px] text-[var(--vscode-descriptionForeground)] break-words overflow-hidden text-ellipsis">
// {user.email}
// </div>
// )}
// <div className="flex gap-2 flex-wrap mt-1">
// <VSCodeButton
// appearance="secondary"
// onClick={handleLogout}
// className="scale-[0.85] origin-left w-fit mt-0.5 mb-0 -mr-3">
// Log out
// </VSCodeButton>
// </div>
// </div>
// </div>
// </div>
<div>
<VSCodeButton onClick={handleLogin} className="mt-0">
Sign Up with Cline
</VSCodeButton>
</div>
)}
</div>
)
}
@@ -25,7 +25,6 @@ const OpenRouterModelPicker: React.FC<OpenRouterModelPickerProps> = ({ isPopup }
const itemRefs = useRef<(HTMLDivElement | null)[]>([])
const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false)
const dropdownListRef = useRef<HTMLDivElement>(null)
const [providerSortingSelected, setProviderSortingSelected] = useState(!!apiConfiguration?.openRouterProviderSorting)
const handleModelChange = (newModelId: string) => {
// could be setting invalid model id/undefined info but validation will catch it
@@ -223,52 +222,7 @@ const OpenRouterModelPicker: React.FC<OpenRouterModelPickerProps> = ({ isPopup }
{showBudgetSlider && (
<ThinkingBudgetSlider apiConfiguration={apiConfiguration} setApiConfiguration={setApiConfiguration} />
)}
<VSCodeCheckbox
style={{ marginTop: 3 }}
checked={providerSortingSelected}
onChange={(e: any) => {
const isChecked = e.target.checked === true
setProviderSortingSelected(isChecked)
if (!isChecked) {
setApiConfiguration({
...apiConfiguration,
openRouterProviderSorting: "",
})
}
}}>
Sort underlying provider routing
</VSCodeCheckbox>
{providerSortingSelected && (
<div style={{ marginBottom: -6 }}>
<DropdownContainer className="dropdown-container" zIndex={OPENROUTER_MODEL_PICKER_Z_INDEX - 3}>
<VSCodeDropdown
style={{ width: "100%", marginTop: 3 }}
value={apiConfiguration?.openRouterProviderSorting}
onChange={(e: any) => {
setApiConfiguration({
...apiConfiguration,
openRouterProviderSorting: e.target.value,
})
}}>
<VSCodeOption value="">Default</VSCodeOption>
<VSCodeOption value="price">Price</VSCodeOption>
<VSCodeOption value="throughput">Throughput</VSCodeOption>
<VSCodeOption value="latency">Latency</VSCodeOption>
</VSCodeDropdown>
</DropdownContainer>
<p style={{ fontSize: "12px", marginTop: 3, color: "var(--vscode-descriptionForeground)" }}>
{!apiConfiguration?.openRouterProviderSorting &&
"Default behavior is to load balance requests across providers (like AWS, Google Vertex, Anthropic), prioritizing price while considering provider uptime"}
{apiConfiguration?.openRouterProviderSorting === "price" &&
"Sort providers by price, prioritizing the lowest cost provider"}
{apiConfiguration?.openRouterProviderSorting === "throughput" &&
"Sort providers by throughput, prioritizing the provider with the highest throughput (may increase cost)"}
{apiConfiguration?.openRouterProviderSorting === "latency" &&
"Sort providers by response time, prioritizing the provider with the lowest latency"}
</p>
</div>
)}
<ModelInfoView
selectedModelId={selectedModelId}
modelInfo={selectedModelInfo}
@@ -6,6 +6,7 @@ import { useExtensionState } from "../../context/ExtensionStateContext"
import { validateApiConfiguration } from "../../utils/validate"
import { vscode } from "../../utils/vscode"
import ApiOptions from "../settings/ApiOptions"
import ClineLogoWhite from "../../assets/ClineLogoWhite"
const WelcomeView = () => {
const { apiConfiguration } = useExtensionState()
@@ -46,7 +47,7 @@ const WelcomeView = () => {
}}>
<h2>Hi, I'm Cline</h2>
<div style={{ display: "flex", justifyContent: "center", margin: "20px 0" }}>
<ClineLogo />
<ClineLogoWhite className="size-16" />
</div>
<p>
I can do all kinds of tasks thanks to breakthroughs in{" "}
@@ -91,22 +92,4 @@ const WelcomeView = () => {
)
}
const ClineLogo: React.FC<{ style?: React.CSSProperties }> = ({ style }) => {
// (can't use svgs in vsc extensions)
const logoBase64 =
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADoAAAA8CAYAAAA34qk1AAAAAXNSR0IArs4c6QAAAERlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAOqADAAQAAAABAAAAPAAAAAAs615UAAAGuElEQVRoBd1aW2hcRRj+5yRp4242m0tbSqMitSLaaFGLUXvDB0HUmlYRFXxQQRG0iFXxwctDfWgDKlgREYRWkaJPGsULUjX6IFRbkwZaaL1AUWI1JnvJ7ibpJmf8ZrNnc86ZmXPJZtM9DhzOzH+b/zszZ+af/xxGS1A4543pPD3OON1LjK5Al8txnWFEXzYQ9bW0sLO1dgN91baMTfD1DYzeRS/XaXoawwN4IplgH2j4i0KuKdDUJL+EzdIwPE34ecuJHmpvYQf95BbKrxlQTFeWydPXcOzmgM5lZg3q7oyxPwPKhxIzQkmHEE7naCfEg4IUlpMNJu0J0UUo0ZoBZYzuCOXJnPD2BegEUqkZUPS+NpAHTqEVo6Pc9312qgRr1RLobDAXnFLFFbQgPacVuVU7oJx+kbvzpmBlHFnDWMFbamHcqoFidTVwNbkvZtAnYV3ijD522ym3EVdUV0JtL+jUSE3QpgaDdmDf24qrCwZWwYWqHQkAIyVG3CQaQoDRf26Svli1iuUC6JVEAgMdz/Pt6GAfFK4MarzGcmnY35uM037G2JRfX75Az3Ieby7Qe8TpLj9j54XP6Ffsv72JBDvp1b8n0FyOr55B4A0DG7yM1AEvCx/ubmthh3W+aIHifVyeydEAThs36JTrjJ5t4HSjbmS1qy5Avh4hkOKZtyJW7v+D8wtUA6AEOp7jVwHkIyqFuqZxWpfI05MqH5VTNzPB+7Gn3alSiAAtMztNF3d2MvHeVoo0oqkUbwPI2yoS0askG5fJ/ktAjSa6Hdgao4dv3mMMVO98a64mAQX5WrdQBNtS2kYCirBuTQSBuV2WMEhAoSFi16iX+AjnMTsIFdClCNDtPtSk3vSv86ChAhq24wLSJi+ZJm3Bsp5EdLIe0/9B7FsnwhqC/CET21oTozVFRl2iDtqipEGlfTSd4wMwvg1XkDJIM3RPWxv7zS0sQshsnvoAWrmBO+QZ5SD3QHuc9Tvo5UaqwHcyk95H0zEdVbIWrThJrStXsgmrrRrRZovpc8/rQAo9HJ2mW+P0FKoi5elXntWBFIrtMfYRjojP+Rmx85ubyYHDATSV59dA+Hq7gq6O6bpPNZJ2eYDl2JEftdMU9WPJGL2toDtIrS30JghDDqJHA3GvI4R1AMWZcw90pemssocM/LcqupvW1sx+B+2Mm25rD5QeiI2gqgoZOPadiqekcdptzyhWgGYKvAeGAudicwkaVHagInK9LPr8WaWioiGNErxPos6mGO2y7FSAcpNetogB7jxMtg5Doc3tYGXV8iQ/OFUWF4mnInB62hrVEtDxLN8CuVtUshGndVijWgJqGKFGM1rYy6NqlA7ZwffNaIGc81aM6v0GIpmgwUEUQZZ85py2GlgM/hexrdcoYGVHzj3M3uRlrZ55nL438Dl9CMv/D/XsZ5W+pc0iHZrbR016vkpjdauOUPW1jg6WKQFtT7ABeBok+F4oILwm2jL3sLXseQaMBJYta6VmppCfRqko4jNfmFFl6Sm+dt4F7xocXKeV4HSpludmsBCyQpfRq1baswI0GWNH4NCnbtvadpE2ank2Bs6lIqO4wUZyVNFnIDslJRZClmisWKD9VmcVoIKAoPlF3HAG9i+YAbvE91I/yWyOHoOM42zo0tkeZHZkMvwyeBYm3/yK9uCNFfg4jB1xOaJsYhPejP+InlEyy8Rsll+OFb3PSwa8OA7wB/DQmnRy4C0zG+kg+IEzDIZJ79jtySPCaNou4FPvy+T4gfFxnnTL4RD/sGnQj6AHcW4rHtpPqRyXprhIBoB3FEf4m9x9eLWnp5048Io4S8ickaWcxzI+iDk/jNzOhVgENqIu5VYtYY873h46CaeOlWWEHfGTpDwgHkYEy50zWqxPD3ExlWF/s0gDVFEEoG6A667ChlJV9aRmlZIRI7r/V1IB/TtimGR3kbVwZ0AkoJh5f8maEaNwGnF7LAGFwFG3UATb1mJWcV0COnuOPge3WJGIYgU/XLndloCKSB9Bw2duwQi1U8Wp0mA5XJaACi6W3RdwE3taFMtee+hnAVAC7UywE9gP37KEInQ/jV/m3lD5q93eRXyZLtA3CL02qRTrkJZBfNvT2spOqXxTjqgQxLeOc41m6f8/aQVTGTrPtDQisx06kMI3LVDBxO9m/0zEaQuqH4p2nZZTYiTLWRKti55AhdZFjE3iZ8L7MMdvRXNYa2npGeOIiXfjnbwaI3nar3vtO6pSxHvLspPUgw9SvdiCtuGU0gW51biWqeQXkSZ2gFFcI3D4OHLR/ZMx+sod5nn19x8Bu+YF5eP/fAAAAABJRU5ErkJggg=="
return (
<img
src={logoBase64}
style={{
width: "57px",
height: "60px",
...style,
}}
alt="Cline Logo"
/>
)
}
export default WelcomeView
+24
View File
@@ -10,3 +10,27 @@ export function formatLargeNumber(num: number): string {
}
return num.toString()
}
// Helper to format cents as dollars with 2 decimal places
export function formatDollars(cents?: number): string {
if (cents === undefined) {
return ""
}
return (cents / 100).toFixed(2)
}
export function formatTimestamp(timestamp: string): string {
const date = new Date(timestamp)
const dateFormatter = new Intl.DateTimeFormat("en-US", {
month: "2-digit",
day: "2-digit",
year: "2-digit",
hour: "numeric",
minute: "2-digit",
hour12: true,
})
return dateFormatter.format(date)
}