mirror of
https://github.com/cline/cline.git
synced 2026-09-09 15:02:23 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b1e129f8e8 | ||
|
|
d54146623f | ||
|
|
f9c46f898f | ||
|
|
2d2eb5dd36 | ||
|
|
9562671bcc | ||
|
|
6a2aef27ea | ||
|
|
c7d6173e20 | ||
|
|
62da7fe720 | ||
|
|
f97f3e47cc | ||
|
|
f47be1a7bb | ||
|
|
0cd2e69551 | ||
|
|
4b19010d56 | ||
|
|
49803bf958 | ||
|
|
33704dcdc1 | ||
|
|
78552287ae | ||
|
|
a29689dfd3 | ||
|
|
2ca4738b59 | ||
|
|
b4a2d5a06d | ||
|
|
7f2de3956c | ||
|
|
059b14e494 | ||
|
|
4d95817eec | ||
|
|
d8c13db683 | ||
|
|
a35e1ff8df | ||
|
|
a0c5276fb1 | ||
|
|
696a2c4e48 | ||
|
|
e983358dde | ||
|
|
9ae90c99ad | ||
|
|
652034232e | ||
|
|
8d57cc6bfa | ||
|
|
4cd914970e | ||
|
|
4919424faf | ||
|
|
e65fa89566 | ||
|
|
57a1deea22 |
@@ -2,4 +2,4 @@
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
opt-in llm observability
|
||||
update context on truncation
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Fix truncation logic for small message arrays
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Handle openrouter input too large error
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
updated move context management out of cline
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
fixes an issue with Azure API version detection in the OpenAI provider
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Add size calculation to "Delete all Tasks" button
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Start using ErrorBoundaries for chat widgets to handle crashes
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
SuccessButton to Tailwind
|
||||
@@ -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
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Add saito-sv as CodeOwner
|
||||
@@ -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
|
||||
|
||||
Generated
+2
-2
@@ -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
@@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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]()
|
||||
|
||||
|
||||
@@ -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)]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>`,
|
||||
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
Generated
+23
-4
@@ -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": {
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user