Compare commits

...
Author SHA1 Message Date
frostbournesb f3661d9b9a Merge remote-tracking branch 'origin/main' into frostbourne/eng-130 2025-02-28 23:06:06 -08:00
Saoud Rizwan 21d4ec13e1 Fix CHANGELOG 2025-02-28 22:52:03 -08:00
Saoud Rizwan 38e51bcf73 Update changelog 2025-02-28 22:51:08 -08:00
Saoud Rizwan f40ff25d1e Update package-locks 2025-02-28 22:45:13 -08:00
Evan 29c01eb7d3 Sonnet: Make Separate Thinking Model (#2031)
* make thinking model separate and remove UI slider

* use constant for min budget tokens

* changeset
2025-02-28 22:36:02 -08:00
Saoud Rizwan 682fce458e Prepare for release 2025-02-28 22:31:44 -08:00
Saoud Rizwan 34e87af5f1 Remove webview telemetry 2025-02-28 22:24:34 -08:00
Saoud Rizwan eaa76512fc Remove requesty polling; fix deepseek cost calculation changes; fix preferred language parsing 2025-02-28 22:23:24 -08:00
frostbournesb 3b5556ae6c Checkpoint Settings and UI 2025-02-28 21:44:39 -08:00
Evan df7b458229 Sonnet, Give Me a Reason (#1961)
* wip

* added slider for setting reasoning budget tokens

* refactor out generic slider component; improve styling; add debounce

* added setting validation

* styling and adding reasoning level

* changeset

* make change to trigger test rerun

* revert useless comment
2025-02-28 18:16:56 -08:00
pashpashpash 560cefecca Only requesting requesty models if requesty api key is set (#2028)
* changeset

* changeset
2025-02-28 17:59:21 -08:00
59 changed files with 2187 additions and 1990 deletions
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Add correct cost and tokens info to Native OpenAI and DeepSeek providers
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Update Claude Sonnet 35. -> 3.7 in README(s)
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": minor
---
Added X AI as a new provider with support for all current models including Grok-2 and Grok Vision. This integration enables users to connect to X AI's API using their API key and access models with context windows up to 131K tokens. The implementation includes proper handling for vision models and accurate pricing information.
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
fix: make AWS Bedrock authentication predictable
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Add translation to CODE_OF_CONDUCT, CONTRIBUTING and README to Arabic ar-sa.
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
A new MCP Marketplace display setting has been added to VSCode settings. (It is defaulted to "true")
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Added a .clinerules file with details about our most common features
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Make tags section in marketplace scrollable
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
opt: Enhance OpenAiHandler to diagnose request issues and handle empty streams
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
.clineignore should be included in .gitignore
+18
View File
@@ -0,0 +1,18 @@
---
"claude-dev": minor
---
## Checkpoints 2.0: **User-Configurable Checkpoints** Settings
### New Features
- **Global File Exclusions List:** Comprehensive list of files to exclude (build outputs, media files, logs, etc.)
- **Checkpoint Cleanup:** New command to delete all checkpoints and reclaim storage space
### Quality of Life
- **Settings Panel:** Manage all checkpoint settings in one convenient location, providing space for easily adding future checkpoint settings
- **Space Management:** More control over what gets saved in your checkpoints
- **Smart Defaults:** Out-of-the-box checkpoint settings that work for most projects
Settings are preserved when updating, your existing configuration will work as expected.
---
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Migrate webview from CRA to Vite
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Change how Cline finds the path to the user's Documents folder by querying xdg-user-dir on Linux systems.
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": minor
---
Add rich MCP responses with images and link previews
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": minor
---
Add preferred language option to settings
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": minor
---
GPT 4.5 Preview added
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Fix checking "Support Images" setting had no effect on first click
-11
View File
@@ -1,11 +0,0 @@
---
"claude-dev": patch
---
Add dynamic model fetching for the Requesty provider.
Instead of manually typing the model name, the extension dynamically fetches
all the supported model names from Requesty's /v1/models API.
This allows users to use a fuzzy search logic when choosing the models and
also guarantees the information for each model is up to date.
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": minor
---
feat(vertex): Add prompt caching support for Claude on Vertex AI
-11
View File
@@ -1,11 +0,0 @@
---
"claude-dev": minor
---
- **Branch-Per-Task:** Each repo now has a single Shadow Git repo, with separate branches per task (instead of one Shadow Git repo per task).
- **Legacy Support:** Existing Checkpoints remain functional, while all new Checkpoints use branch-per-task.
- **Commits:** Legacy tasks commit to legacy Checkpoints; new tasks commit using branch-per-task.
- **Diffing & Deletions:** Both legacy and branch-per-task Checkpoints support diffing and deletion.
No migration needed—existing tasks stay as-is, and new tasks adopt **branch-per-task** automatically.
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": minor
---
Update anthropic SDK to the latest version
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": minor
---
re-added prompt caching to vertex
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
fix: Anthropic's default model is 3.7
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Adjustment to MCP server installation prompt
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": minor
---
ADD IS_DEV and Hot Reloading to debug
+9
View File
@@ -1,5 +1,14 @@
# Changelog
## [3.5.0]
- Add Sonnet 3.7 thinking model option to Anthropic provider (thanks @celestial-vault!)
- Update checkpoints with improved storage optimization & faster performance (thanks @tiki_brc!)
- New Rich MCP Responses with automatic image previews, website thumbnails, and WolframAlpha visualizations right in your conversation (thanks @.lung!)
- Added xAI Provider Integration with support for all Grok models, including the massive 131K token context window for large codebases (thanks @ocasta181!)
- Language preferences feature to set your preferred language in settings (thanks @brownrw8!)
- Added Linux XDG Support for user's document folder settings (thanks @jonatkinson!)
## [3.4.10]
- Add support for GPT-4.5 preview model
+9 -8
View File
@@ -1,12 +1,12 @@
{
"name": "claude-dev",
"version": "3.4.10",
"version": "3.5.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "claude-dev",
"version": "3.4.10",
"version": "3.5.0",
"license": "Apache-2.0",
"dependencies": {
"@anthropic-ai/bedrock-sdk": "^0.12.4",
@@ -112,9 +112,9 @@
}
},
"node_modules/@anthropic-ai/sdk/node_modules/@types/node": {
"version": "18.19.76",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.76.tgz",
"integrity": "sha512-yvR7Q9LdPz2vGpmpJX5LolrgRdWvB67MJKDPSgIIzpFbaf9a1j/f5DnLp5VDyHGMR0QZHlTr1afsD87QCXFHKw==",
"version": "18.19.78",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.78.tgz",
"integrity": "sha512-m1ilZCTwKLkk9rruBJXFeYN0Bc5SbjirwYX/Td3MqPfioYbgun3IvK/m8dQxMCnrPGZPg1kvXjp3SIekCN/ynw==",
"license": "MIT",
"dependencies": {
"undici-types": "~5.26.4"
@@ -3791,6 +3791,7 @@
"version": "0.18.0",
"resolved": "https://registry.npmjs.org/@google/generative-ai/-/generative-ai-0.18.0.tgz",
"integrity": "sha512-AhaIWSpk2tuhYHrBhUqC0xrWWznmYEja1/TRDIb+5kruBU5kUzMlFsXCQNO9PzyTZ4clUJ3CX/Rvy+Xm9x+w3g==",
"license": "Apache-2.0",
"engines": {
"node": ">=18.0.0"
}
@@ -11091,9 +11092,9 @@
}
},
"node_modules/open-graph-scraper/node_modules/chardet": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/chardet/-/chardet-2.0.0.tgz",
"integrity": "sha512-xVgPpulCooDjY6zH4m9YW3jbkaBe3FKIAvF5sj5t7aBNsVl2ljIE+xwJ4iNgiDZHFQvNIpjdKdVOQvvk5ZfxbQ==",
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.0.tgz",
"integrity": "sha512-bNFETTG/pM5ryzQ9Ad0lJOTa6HWD/YsScAR3EnCPZRPlQh77JocYktSHOUHelyhm8IARL+o4c4F1bP5KVOjiRA==",
"license": "MIT"
},
"node_modules/openai": {
+6 -6
View File
@@ -2,7 +2,7 @@
"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.4.10",
"version": "3.5.0",
"icon": "assets/icons/icon.png",
"galleryBanner": {
"color": "#617A91",
@@ -157,11 +157,6 @@
"default": "full",
"description": "Controls MCP inclusion in prompts, reduces token usage if you only need access to certain functionality."
},
"cline.enableCheckpoints": {
"type": "boolean",
"default": true,
"description": "Enables extension to save checkpoints of workspace throughout the task."
},
"cline.disableBrowserTool": {
"type": "boolean",
"default": false,
@@ -211,6 +206,11 @@
"type": "boolean",
"default": true,
"description": "Controls whether the MCP Marketplace is enabled."
},
"cline.modelSettings.anthropic.thinkingBudgetTokens": {
"type": "number",
"default": 0,
"description": "Controls the token budget for Claude's thinking capability. Set to 0 to disable thinking. When enabled, must be ≥ 1024 and less than the model's max token output."
}
}
}
+45 -3
View File
@@ -1,7 +1,14 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { Stream as AnthropicStream } from "@anthropic-ai/sdk/streaming"
import { withRetry } from "../retry"
import { anthropicDefaultModelId, AnthropicModelId, anthropicModels, ApiHandlerOptions, ModelInfo } from "../../shared/api"
import {
ANTHROPIC_THINKING_BUDGET_TOKENS_MIN,
anthropicDefaultModelId,
AnthropicModelId,
anthropicModels,
ApiHandlerOptions,
ModelInfo,
} from "../../shared/api"
import { ApiHandler } from "../index"
import { ApiStream } from "../transform/stream"
@@ -25,6 +32,7 @@ export class AnthropicHandler implements ApiHandler {
switch (modelId) {
// 'latest' alias does not support cache_control
case "claude-3-7-sonnet-20250219":
case "claude-3-7-sonnet-20250219:thinking":
case "claude-3-5-sonnet-20241022":
case "claude-3-5-haiku-20241022":
case "claude-3-opus-20240229":
@@ -40,9 +48,18 @@ export class AnthropicHandler implements ApiHandler {
const secondLastMsgUserIndex = userMsgIndices[userMsgIndices.length - 2] ?? -1
stream = await this.client.messages.create(
{
model: modelId,
model: modelId === "claude-3-7-sonnet-20250219:thinking" ? "claude-3-7-sonnet-20250219" : modelId,
thinking:
modelId === "claude-3-7-sonnet-20250219:thinking"
? {
type: "enabled",
budget_tokens: this.options.thinkingBudgetTokens || ANTHROPIC_THINKING_BUDGET_TOKENS_MIN,
}
: undefined,
max_tokens: model.info.maxTokens || 8192,
temperature: 0,
// "Thinking isnt compatible with temperature, top_p, or top_k modifications as well as forced tool use."
// (https://docs.anthropic.com/en/docs/build-with-claude/extended-thinking#important-considerations-when-using-extended-thinking)
temperature: modelId === "claude-3-7-sonnet-20250219:thinking" ? 1 : 0,
system: [
{
text: systemPrompt,
@@ -90,6 +107,7 @@ export class AnthropicHandler implements ApiHandler {
// https://github.com/anthropics/anthropic-sdk-typescript/commit/c920b77fc67bd839bfeb6716ceab9d7c9bbe7393
switch (modelId) {
case "claude-3-7-sonnet-20250219":
case "claude-3-7-sonnet-20250219:thinking":
case "claude-3-5-sonnet-20241022":
case "claude-3-5-haiku-20241022":
case "claude-3-opus-20240229":
@@ -148,6 +166,20 @@ export class AnthropicHandler implements ApiHandler {
break
case "content_block_start":
switch (chunk.content_block.type) {
case "thinking":
yield {
type: "reasoning",
reasoning: chunk.content_block.thinking || "",
}
break
case "redacted_thinking":
// Handle redacted thinking blocks - we still mark it as reasoning
// but note that the content is encrypted
yield {
type: "reasoning",
reasoning: "[Redacted thinking block]",
}
break
case "text":
// we may receive multiple text blocks, in which case just insert a line break between them
if (chunk.index > 0) {
@@ -165,12 +197,22 @@ export class AnthropicHandler implements ApiHandler {
break
case "content_block_delta":
switch (chunk.delta.type) {
case "thinking_delta":
yield {
type: "reasoning",
reasoning: chunk.delta.thinking,
}
break
case "text_delta":
yield {
type: "text",
text: chunk.delta.text,
}
break
case "signature_delta":
// We don't need to do anything with the signature in the client
// It's used when sending the thinking block back to the API
break
}
break
case "content_block_stop":
+9 -33
View File
@@ -3,7 +3,6 @@ import OpenAI from "openai"
import { withRetry } from "../retry"
import { ApiHandler } from "../"
import { ApiHandlerOptions, DeepSeekModelId, ModelInfo, deepSeekDefaultModelId, deepSeekModels } from "../../shared/api"
import { calculateApiCostOpenAI } from "../../utils/cost"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { ApiStream } from "../transform/stream"
import { convertToR1Format } from "../transform/r1-format"
@@ -20,37 +19,6 @@ export class DeepSeekHandler implements ApiHandler {
})
}
private async *yieldUsage(info: ModelInfo, usage: OpenAI.Completions.CompletionUsage | undefined): ApiStream {
// Deepseek reports total input AND cache reads/writes,
// see context caching: https://api-docs.deepseek.com/guides/kv_cache)
// where the input tokens is the sum of the cache hits/misses, just like OpenAI.
// This affects:
// 1) context management truncation algorithm, and
// 2) cost calculation
// Deepseek usage includes extra fields.
// Safely cast the prompt token details section to the appropriate structure.
interface DeepSeekUsage extends OpenAI.CompletionUsage {
prompt_cache_hit_tokens?: number
prompt_cache_miss_tokens?: number
}
const deepUsage = usage as DeepSeekUsage
const inputTokens = deepUsage?.prompt_tokens || 0
const outputTokens = deepUsage?.completion_tokens || 0
const cacheReadTokens = deepUsage?.prompt_cache_hit_tokens || 0
const cacheWriteTokens = deepUsage?.prompt_cache_miss_tokens || 0
const totalCost = calculateApiCostOpenAI(info, inputTokens, outputTokens, cacheWriteTokens, cacheReadTokens)
yield {
type: "usage",
inputTokens: inputTokens,
outputTokens: outputTokens,
cacheWriteTokens: cacheWriteTokens,
cacheReadTokens: cacheReadTokens,
totalCost: totalCost,
}
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const model = this.getModel()
@@ -93,7 +61,15 @@ export class DeepSeekHandler implements ApiHandler {
}
if (chunk.usage) {
yield* this.yieldUsage(model.info, chunk.usage)
yield {
type: "usage",
inputTokens: chunk.usage.prompt_tokens || 0, // (deepseek reports total input AND cache reads/writes, see context caching: https://api-docs.deepseek.com/guides/kv_cache) where the input tokens is the sum of the cache hits/misses, while anthropic reports them as separate tokens. This is important to know for 1) context management truncation algorithm, and 2) cost calculation (NOTE: we report both input and cache stats but for now set input price to 0 since all the cost calculation will be done using cache hits/misses)
outputTokens: chunk.usage.completion_tokens || 0,
// @ts-ignore-next-line
cacheReadTokens: chunk.usage.prompt_cache_hit_tokens || 0,
// @ts-ignore-next-line
cacheWriteTokens: chunk.usage.prompt_cache_miss_tokens || 0,
}
}
}
}
+21 -30
View File
@@ -10,7 +10,6 @@ import {
openAiNativeModels,
} from "../../shared/api"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { calculateApiCostOpenAI } from "../../utils/cost"
import { ApiStream } from "../transform/stream"
import { ChatCompletionReasoningEffort } from "openai/resources/chat/completions.mjs"
@@ -25,47 +24,31 @@ export class OpenAiNativeHandler implements ApiHandler {
})
}
private async *yieldUsage(info: ModelInfo, usage: OpenAI.Completions.CompletionUsage | undefined): ApiStream {
const inputTokens = usage?.prompt_tokens || 0
const outputTokens = usage?.completion_tokens || 0
const cacheReadTokens = usage?.prompt_tokens_details?.cached_tokens || 0
const cacheWriteTokens = 0
const totalCost = calculateApiCostOpenAI(info, inputTokens, outputTokens, cacheWriteTokens, cacheReadTokens)
yield {
type: "usage",
inputTokens: inputTokens,
outputTokens: outputTokens,
cacheWriteTokens: cacheWriteTokens,
cacheReadTokens: cacheReadTokens,
totalCost: totalCost,
}
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const model = this.getModel()
switch (model.id) {
switch (this.getModel().id) {
case "o1":
case "o1-preview":
case "o1-mini": {
// o1 doesnt support streaming, non-1 temp, or system prompt
const response = await this.client.chat.completions.create({
model: model.id,
model: this.getModel().id,
messages: [{ role: "user", content: systemPrompt }, ...convertToOpenAiMessages(messages)],
})
yield {
type: "text",
text: response.choices[0]?.message.content || "",
}
yield* this.yieldUsage(model.info, response.usage)
yield {
type: "usage",
inputTokens: response.usage?.prompt_tokens || 0,
outputTokens: response.usage?.completion_tokens || 0,
}
break
}
case "o3-mini": {
const stream = await this.client.chat.completions.create({
model: model.id,
model: this.getModel().id,
messages: [{ role: "developer", content: systemPrompt }, ...convertToOpenAiMessages(messages)],
stream: true,
stream_options: { include_usage: true },
@@ -80,15 +63,18 @@ export class OpenAiNativeHandler implements ApiHandler {
}
}
if (chunk.usage) {
// Only last chunk contains usage
yield* this.yieldUsage(model.info, chunk.usage)
yield {
type: "usage",
inputTokens: chunk.usage.prompt_tokens || 0,
outputTokens: chunk.usage.completion_tokens || 0,
}
}
}
break
}
default: {
const stream = await this.client.chat.completions.create({
model: model.id,
model: this.getModel().id,
// max_completion_tokens: this.getModel().info.maxTokens,
temperature: 0,
messages: [{ role: "system", content: systemPrompt }, ...convertToOpenAiMessages(messages)],
@@ -104,9 +90,14 @@ export class OpenAiNativeHandler implements ApiHandler {
text: delta.content,
}
}
// contains a null value except for the last chunk which contains the token usage statistics for the entire request
if (chunk.usage) {
// Only last chunk contains usage
yield* this.yieldUsage(model.info, chunk.usage)
yield {
type: "usage",
inputTokens: chunk.usage.prompt_tokens || 0,
outputTokens: chunk.usage.completion_tokens || 0,
}
}
}
}
+21 -81
View File
@@ -28,65 +28,6 @@ export class OpenAiHandler implements ApiHandler {
}
}
private async diagnoseRequestProblem(
modelId: string,
messages: OpenAI.Chat.ChatCompletionMessageParam[],
apiKey: string,
baseURL: string,
) {
const url = `${baseURL}/chat/completions`
try {
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${apiKey}`,
},
body: JSON.stringify({
model: modelId,
messages: messages,
temperature: 0,
stream: true,
}),
})
if (!response.ok) {
return `HTTP error! status: ${response.status}, statusText: ${response.statusText}`
}
const responseData = await response.json()
return responseData
} catch (error) {
return error instanceof Error ? error.message : String(error)
}
}
private async *handleChunk(chunk: OpenAI.Chat.Completions.ChatCompletionChunk): ApiStream {
const delta = chunk.choices[0]?.delta
if (delta?.content) {
yield {
type: "text",
text: delta.content,
}
}
if (delta && "reasoning_content" in delta && delta.reasoning_content) {
yield {
type: "reasoning",
reasoning: (delta.reasoning_content as string | undefined) || "",
}
}
if (chunk.usage) {
yield {
type: "usage",
inputTokens: chunk.usage.prompt_tokens || 0,
outputTokens: chunk.usage.completion_tokens || 0,
}
}
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const modelId = this.options.openAiModelId ?? ""
@@ -108,30 +49,29 @@ export class OpenAiHandler implements ApiHandler {
stream: true,
stream_options: { include_usage: true },
})
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta
if (delta?.content) {
yield {
type: "text",
text: delta.content,
}
}
const [validationStream, contentStream] = stream.tee()
if (delta && "reasoning_content" in delta && delta.reasoning_content) {
yield {
type: "reasoning",
reasoning: (delta.reasoning_content as string | undefined) || "",
}
}
// Check the first chunk to detect potential stream issues early
// This helps to provide better error messages for cases like:
// https://github.com/cline/cline/issues/1662
// where the stream appears valid but contains no actual data
const firstChunk = await validationStream[Symbol.asyncIterator]().next()
if (firstChunk.done || !firstChunk.value) {
// Make an additional request to get detailed error information
// This gives us more context about what went wrong with the API call
const errorResponse = await this.diagnoseRequestProblem(
modelId,
openAiMessages,
this.client.apiKey,
this.client.baseURL,
)
throw new Error(`Stream empty. Error details: ${JSON.stringify(errorResponse)}`)
}
yield* this.handleChunk(firstChunk.value)
for await (const chunk of contentStream) {
yield* this.handleChunk(chunk)
if (chunk.usage) {
yield {
type: "usage",
inputTokens: chunk.usage.prompt_tokens || 0,
outputTokens: chunk.usage.completion_tokens || 0,
}
}
}
}
+8 -18
View File
@@ -1,15 +1,8 @@
import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI from "openai"
import { withRetry } from "../retry"
import { calculateApiCostOpenAI } from "../../utils/cost"
import {
ApiHandlerOptions,
ModelInfo,
openAiModelInfoSaneDefaults,
requestyDefaultModelId,
requestyDefaultModelInfo,
} from "../../shared/api"
import { ApiHandlerOptions, ModelInfo, openAiModelInfoSaneDefaults } from "../../shared/api"
import { ApiHandler } from "../index"
import { withRetry } from "../retry"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { ApiStream } from "../transform/stream"
@@ -31,7 +24,7 @@ export class RequestyHandler implements ApiHandler {
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const model = this.getModel()
const modelId = this.options.requestyModelId ?? ""
let openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
{ role: "system", content: systemPrompt },
@@ -40,13 +33,12 @@ export class RequestyHandler implements ApiHandler {
// @ts-ignore-next-line
const stream = await this.client.chat.completions.create({
model: model.id,
max_tokens: model.info.maxTokens || undefined,
model: modelId,
messages: openAiMessages,
temperature: 0,
stream: true,
stream_options: { include_usage: true },
...(model.id === "openai/o3-mini" ? { reasoning_effort: this.options.o3MiniReasoningEffort || "medium" } : {}),
...(modelId === "openai/o3-mini" ? { reasoning_effort: this.options.o3MiniReasoningEffort || "medium" } : {}),
})
for await (const chunk of stream) {
@@ -96,11 +88,9 @@ export class RequestyHandler implements ApiHandler {
}
getModel(): { id: string; info: ModelInfo } {
const modelId = this.options.requestyModelId
const modelInfo = this.options.requestyModelInfo
if (modelId && modelInfo) {
return { id: modelId, info: modelInfo }
return {
id: this.options.requestyModelId ?? "",
info: openAiModelInfoSaneDefaults,
}
return { id: requestyDefaultModelId, info: requestyDefaultModelInfo }
}
}
+7 -26
View File
@@ -75,7 +75,6 @@ export class Cline {
browserSession: BrowserSession
private didEditFile: boolean = false
customInstructions?: string
preferredLanguage?: LanguageKey
autoApprovalSettings: AutoApprovalSettings
private browserSettings: BrowserSettings
private chatSettings: ChatSettings
@@ -136,9 +135,6 @@ export class Cline {
this.browserSession = new BrowserSession(provider.context, browserSettings)
this.diffViewProvider = new DiffViewProvider(cwd)
this.customInstructions = customInstructions
this.preferredLanguage = getLanguageKey(
vscode.workspace.getConfiguration("cline").get<LanguageDisplay>("preferredLanguage"),
)
this.autoApprovalSettings = autoApprovalSettings
this.browserSettings = browserSettings
this.chatSettings = chatSettings
@@ -152,16 +148,6 @@ export class Cline {
} else {
throw new Error("Either historyItem or task/images must be provided")
}
// capture start of thread with the state at the beginning
telemetryService.capture({
event: "cline created",
properties: {
taskId: this.taskId,
isHistory: !!historyItem,
chatMode: this.chatSettings.mode,
hasImages: !!images,
},
})
}
updateBrowserSettings(browserSettings: BrowserSettings) {
@@ -1292,9 +1278,12 @@ export class Cline {
let systemPrompt = await SYSTEM_PROMPT(cwd, supportsComputerUse, mcpHub, this.browserSettings)
let settingsCustomInstructions = this.customInstructions?.trim()
const preferredLanguage = getLanguageKey(
vscode.workspace.getConfiguration("cline").get<LanguageDisplay>("preferredLanguage"),
)
const preferredLanguageInstructions =
this.preferredLanguage && this.preferredLanguage !== DEFAULT_LANGUAGE_SETTINGS
? `# Preferred Language\n\nSpeak in ${this.preferredLanguage}.`
preferredLanguage && preferredLanguage !== DEFAULT_LANGUAGE_SETTINGS
? `# Preferred Language\n\nSpeak in ${preferredLanguage}.`
: ""
const clineRulesFilePath = path.resolve(cwd, GlobalFileNames.clineRules)
let clineRulesFileInstructions: string | undefined
@@ -1318,8 +1307,8 @@ export class Cline {
if (
settingsCustomInstructions ||
clineRulesFileInstructions ||
preferredLanguageInstructions ||
clineIgnoreInstructions
clineIgnoreInstructions ||
preferredLanguageInstructions
) {
// altering the system prompt mid-task will break the prompt cache, but in the grand scheme this will not change often so it's better to not pollute user messages with it the way we have to with <potentially relevant details>
systemPrompt += addUserInstructions(
@@ -3311,14 +3300,6 @@ export class Cline {
this.consecutiveMistakeCount++
}
telemetryService.capture({
event: "message sent",
properties: {
taskId: this.taskId,
chatMode: this.chatSettings.mode,
},
})
const recDidEndLoop = await this.recursivelyMakeClineRequests(this.userMessageContent)
didEndLoop = recDidEndLoop
} else {
+91 -97
View File
@@ -9,6 +9,8 @@ import * as path from "path"
import * as vscode from "vscode"
import { buildApiHandler } from "../../api"
import CheckpointTracker from "../../integrations/checkpoints/CheckpointTracker"
import { CheckpointSettingsManager } from "../../integrations/checkpoints/CheckpointSettings"
import { deleteAllCheckpoints } from "../../integrations/checkpoints/CheckpointUtils"
import { downloadTask } from "../../integrations/misc/export-markdown"
import { openFile, openImage } from "../../integrations/misc/open-file"
import { fetchOpenGraphData, isImageUrl } from "../../integrations/misc/link-preview"
@@ -35,6 +37,7 @@ import { getNonce } from "./getNonce"
import { getUri } from "./getUri"
import { telemetryService } from "../../services/telemetry/TelemetryService"
import { TelemetrySetting } from "../../shared/TelemetrySetting"
import { validateThinkingBudget } from "../../utils/validation"
/*
https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
@@ -95,7 +98,6 @@ type GlobalStateKey =
| "liteLlmModelId"
| "qwenApiLine"
| "requestyModelId"
| "requestyModelInfo"
| "togetherModelId"
| "mcpMarketplaceCatalog"
| "telemetrySetting"
@@ -104,7 +106,6 @@ export const GlobalFileNames = {
apiConversationHistory: "api_conversation_history.json",
uiMessages: "ui_messages.json",
openRouterModels: "openrouter_models.json",
requestyModels: "requesty_models.json",
mcpSettings: "cline_mcp_settings.json",
clineRules: ".clinerules",
}
@@ -261,6 +262,19 @@ export class ClineProvider implements vscode.WebviewViewProvider {
// Update state when marketplace tab setting changes
await this.postStateToWebview()
}
if (e && e.affectsConfiguration("cline.modelSettings.anthropic.thinkingBudgetTokens")) {
const config = vscode.workspace.getConfiguration("cline.modelSettings.anthropic")
const thinkingBudget = config.get<number>("thinkingBudgetTokens", 0)
const validatedValue = validateThinkingBudget(thinkingBudget)
// Only update if the value changed
if (validatedValue !== thinkingBudget) {
await config.update("thinkingBudgetTokens", validatedValue, true)
}
await this.postStateToWebview()
}
},
null,
this.disposables,
@@ -499,7 +513,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
}),
)
// post last cached models in case the call to endpoint fails
this.readDynamicProviderModels(GlobalFileNames.openRouterModels).then((openRouterModels) => {
this.readOpenRouterModels().then((openRouterModels) => {
if (openRouterModels) {
this.postMessageToWebview({
type: "openRouterModels",
@@ -542,33 +556,6 @@ export class ClineProvider implements vscode.WebviewViewProvider {
telemetryService.updateTelemetryState(isOptedIn)
})
// post last cached models in case the call to endpoint fails
this.readDynamicProviderModels(GlobalFileNames.requestyModels).then((requestyModels) => {
if (requestyModels) {
this.postMessageToWebview({
type: "requestyModels",
requestyModels,
})
}
})
// gui relies on model info to be up-to-date to provide the most accurate pricing, so we need to fetch the latest details on launch.
// we do this for all users since many users switch between api providers and if they were to switch back to openrouter it would be showing outdated model info if we hadn't retrieved the latest at this point
// (see normalizeApiConfiguration > openrouter)
this.refreshRequestyModels().then(async (requestyModels) => {
if (requestyModels) {
// update model info in state (this needs to be done here since we don't want to update state while settings is open, and we may refresh models there)
const { apiConfiguration } = await this.getState()
if (apiConfiguration.requestyModelId) {
await this.updateGlobalState(
"requestyModelInfo",
requestyModels[apiConfiguration.requestyModelId],
)
await this.postStateToWebview()
}
}
})
break
case "newTask":
// Code that should run in response to the hello message command
@@ -611,7 +598,6 @@ export class ClineProvider implements vscode.WebviewViewProvider {
deepSeekApiKey,
requestyApiKey,
requestyModelId,
requestyModelInfo,
togetherApiKey,
togetherModelId,
qwenApiKey,
@@ -665,7 +651,6 @@ export class ClineProvider implements vscode.WebviewViewProvider {
await this.updateGlobalState("liteLlmModelId", liteLlmModelId)
await this.updateGlobalState("qwenApiLine", qwenApiLine)
await this.updateGlobalState("requestyModelId", requestyModelId)
await this.updateGlobalState("requestyModelInfo", requestyModelInfo)
await this.updateGlobalState("togetherModelId", togetherModelId)
if (this.cline) {
this.cline.api = buildApiHandler(message.apiConfiguration)
@@ -762,9 +747,6 @@ export class ClineProvider implements vscode.WebviewViewProvider {
case "refreshOpenRouterModels":
await this.refreshOpenRouterModels()
break
case "refreshRequestyModels":
await this.refreshRequestyModels()
break
case "refreshOpenAiModels":
const { apiConfiguration } = await this.getState()
const openAiModels = await this.getOpenAiModels(
@@ -814,6 +796,48 @@ export class ClineProvider implements vscode.WebviewViewProvider {
}
break
}
case "getCheckpointSettings": {
const settings = await CheckpointSettingsManager.getInstance().getSettings()
await this.postMessageToWebview({
type: "setCheckpointSettings",
checkpointSettings: settings,
})
break
}
case "updateCheckpointSettings": {
if (message.checkpointSettings) {
await CheckpointSettingsManager.getInstance().saveSettings(message.checkpointSettings)
await this.postMessageToWebview({
type: "setCheckpointSettings",
checkpointSettings: message.checkpointSettings,
})
}
break
}
case "openCheckpointsIgnore": {
await vscode.commands.executeCommand("cline.openCheckpointsIgnore")
break
}
case "confirmDeleteAllCheckpoints": {
vscode.window
.showWarningMessage(
"This action will delete all checkpoints that have been created by Cline! Deleting all checkpoints will remove the ability to use these features for all historical tasks.",
"Delete All",
"Cancel",
)
.then(async (selection) => {
if (selection === "Delete All") {
try {
await deleteAllCheckpoints(this.context.globalStoragePath)
vscode.window.showInformationMessage("All checkpoints have been deleted")
} catch (error) {
vscode.window.showErrorMessage("Failed to delete checkpoints")
console.error("Failed to delete all checkpoints:", error)
}
}
})
break
}
case "taskCompletionViewChanges": {
if (message.number) {
await this.cline?.presentMultifileDiff(message.number, true)
@@ -973,6 +997,15 @@ export class ClineProvider implements vscode.WebviewViewProvider {
}
break
}
case "updateThinkingBudgetTokens": {
if (message.number !== undefined) {
const validatedValue = validateThinkingBudget(message.number)
const config = vscode.workspace.getConfiguration("cline.modelSettings.anthropic")
await config.update("thinkingBudgetTokens", validatedValue, true)
}
break
}
case "openExtensionSettings": {
const settingsFilter = message.text || ""
await vscode.commands.executeCommand(
@@ -1030,10 +1063,6 @@ export class ClineProvider implements vscode.WebviewViewProvider {
await this.updateGlobalState("previousModeModelId", apiConfiguration.openRouterModelId)
await this.updateGlobalState("previousModeModelInfo", apiConfiguration.openRouterModelInfo)
break
case "requesty":
await this.updateGlobalState("previousModeModelId", apiConfiguration.requestyModelId)
await this.updateGlobalState("previousModeModelInfo", apiConfiguration.requestyModelInfo)
break
case "vscode-lm":
await this.updateGlobalState("previousModeModelId", apiConfiguration.vsCodeLmModelSelector)
break
@@ -1050,6 +1079,9 @@ export class ClineProvider implements vscode.WebviewViewProvider {
case "litellm":
await this.updateGlobalState("previousModeModelId", apiConfiguration.liteLlmModelId)
break
case "requesty":
await this.updateGlobalState("previousModeModelId", apiConfiguration.requestyModelId)
break
}
// Restore the model used in previous mode
@@ -1066,10 +1098,6 @@ export class ClineProvider implements vscode.WebviewViewProvider {
await this.updateGlobalState("openRouterModelId", newModelId)
await this.updateGlobalState("openRouterModelInfo", newModelInfo)
break
case "requesty":
await this.updateGlobalState("requestyModelId", newModelId)
await this.updateGlobalState("requestyModelInfo", newModelInfo)
break
case "vscode-lm":
await this.updateGlobalState("vsCodeLmModelSelector", newModelId)
break
@@ -1086,6 +1114,9 @@ export class ClineProvider implements vscode.WebviewViewProvider {
case "litellm":
await this.updateGlobalState("liteLlmModelId", newModelId)
break
case "requesty":
await this.updateGlobalState("requestyModelId", newModelId)
break
}
if (this.cline) {
@@ -1542,61 +1573,16 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
return cacheDir
}
async readDynamicProviderModels(filename: string): Promise<Record<string, ModelInfo> | undefined> {
const filePath = path.join(await this.ensureCacheDirectoryExists(), filename)
const fileExists = await fileExistsAtPath(filePath)
async readOpenRouterModels(): Promise<Record<string, ModelInfo> | undefined> {
const openRouterModelsFilePath = path.join(await this.ensureCacheDirectoryExists(), GlobalFileNames.openRouterModels)
const fileExists = await fileExistsAtPath(openRouterModelsFilePath)
if (fileExists) {
const fileContents = await fs.readFile(filePath, "utf8")
const fileContents = await fs.readFile(openRouterModelsFilePath, "utf8")
return JSON.parse(fileContents)
}
return undefined
}
adjustPriceToMillionTokens(price: any) {
if (price) {
return parseFloat(price) * 1_000_000
}
return undefined
}
async refreshRequestyModels() {
const requestyModelsFilePath = path.join(await this.ensureCacheDirectoryExists(), GlobalFileNames.requestyModels)
let models: Record<string, ModelInfo> = {}
try {
const response = await axios.get("https://router.requesty.ai/v1/models")
if (response.data?.data) {
for (const model of response.data.data) {
const modelInfo: ModelInfo = {
maxTokens: model.max_output_tokens,
contextWindow: model.context_window,
supportsImages: model.supports_images || undefined,
supportsComputerUse: model.supports_computer_use || undefined,
supportsPromptCache: model.supports_caching || undefined,
inputPrice: this.adjustPriceToMillionTokens(model.input_price),
outputPrice: this.adjustPriceToMillionTokens(model.output_price),
cacheWritesPrice: this.adjustPriceToMillionTokens(model.caching_price),
cacheReadsPrice: this.adjustPriceToMillionTokens(model.cached_price),
description: model.description,
}
models[model.id] = modelInfo
}
await fs.writeFile(requestyModelsFilePath, JSON.stringify(models))
console.log("Requesty models fetched and saved", models)
} else {
console.error("Invalid response from Requesty API")
}
} catch (error) {
console.error("Error fetching Requesty models:", error)
}
await this.postMessageToWebview({
type: "requestyModels",
requestyModels: models,
})
return models
}
async refreshOpenRouterModels() {
const openRouterModelsFilePath = path.join(await this.ensureCacheDirectoryExists(), GlobalFileNames.openRouterModels)
@@ -1631,14 +1617,20 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
*/
if (response.data?.data) {
const rawModels = response.data.data
const parsePrice = (price: any) => {
if (price) {
return parseFloat(price) * 1_000_000
}
return undefined
}
for (const rawModel of rawModels) {
const modelInfo: ModelInfo = {
maxTokens: rawModel.top_provider?.max_completion_tokens,
contextWindow: rawModel.context_length,
supportsImages: rawModel.architecture?.modality?.includes("image"),
supportsPromptCache: false,
inputPrice: this.adjustPriceToMillionTokens(rawModel.pricing?.prompt),
outputPrice: this.adjustPriceToMillionTokens(rawModel.pricing?.completion),
inputPrice: parsePrice(rawModel.pricing?.prompt),
outputPrice: parsePrice(rawModel.pricing?.completion),
description: rawModel.description,
}
@@ -1939,7 +1931,6 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
deepSeekApiKey,
requestyApiKey,
requestyModelId,
requestyModelInfo,
togetherApiKey,
togetherModelId,
qwenApiKey,
@@ -1993,7 +1984,6 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
this.getSecret("deepSeekApiKey") as Promise<string | undefined>,
this.getSecret("requestyApiKey") as Promise<string | undefined>,
this.getGlobalState("requestyModelId") as Promise<string | undefined>,
this.getGlobalState("requestyModelInfo") as Promise<ModelInfo | undefined>,
this.getSecret("togetherApiKey") as Promise<string | undefined>,
this.getGlobalState("togetherModelId") as Promise<string | undefined>,
this.getSecret("qwenApiKey") as Promise<string | undefined>,
@@ -2039,6 +2029,10 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
.getConfiguration("cline.modelSettings.o3Mini")
.get("reasoningEffort", "medium")
const thinkingBudgetTokens = vscode.workspace
.getConfiguration("cline.modelSettings.anthropic")
.get("thinkingBudgetTokens", 0)
const mcpMarketplaceEnabled = vscode.workspace.getConfiguration("cline").get<boolean>("mcpMarketplace.enabled", true)
return {
@@ -2070,7 +2064,6 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
deepSeekApiKey,
requestyApiKey,
requestyModelId,
requestyModelInfo,
togetherApiKey,
togetherModelId,
qwenApiKey,
@@ -2081,6 +2074,7 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
openRouterModelInfo,
vsCodeLmModelSelector,
o3MiniReasoningEffort,
thinkingBudgetTokens,
liteLlmBaseUrl,
liteLlmModelId,
liteLlmApiKey,
+12
View File
@@ -9,6 +9,7 @@ import "./utils/path" // necessary to have access to String.prototype.toPosix
import { DIFF_VIEW_URI_SCHEME } from "./integrations/editor/DiffViewProvider"
import assert from "node:assert"
import { telemetryService } from "./services/telemetry/TelemetryService"
import { CheckpointSettingsManager } from "./integrations/checkpoints/CheckpointSettings"
/*
Built using https://github.com/microsoft/vscode-webview-ui-toolkit
@@ -32,6 +33,9 @@ export function activate(context: vscode.ExtensionContext) {
const sidebarProvider = new ClineProvider(context, outputChannel)
// Initialize CheckpointSettingsManager
CheckpointSettingsManager.initialize(context.globalStorageUri.fsPath)
context.subscriptions.push(
vscode.window.registerWebviewViewProvider(ClineProvider.sideBarId, sidebarProvider, {
webviewOptions: { retainContextWhenHidden: true },
@@ -123,6 +127,14 @@ export function activate(context: vscode.ExtensionContext) {
}),
)
context.subscriptions.push(
vscode.commands.registerCommand("cline.openCheckpointsIgnore", async () => {
const settingsManager = CheckpointSettingsManager.getInstance()
const doc = await vscode.workspace.openTextDocument(vscode.Uri.file(settingsManager.checkpointsIgnorePath))
await vscode.window.showTextDocument(doc)
}),
)
/*
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.
@@ -2,6 +2,7 @@ import fs from "fs/promises"
import { join } from "path"
import { fileExistsAtPath } from "../../utils/fs"
import { GIT_DISABLED_SUFFIX } from "./CheckpointGitOperations"
import { CheckpointSettingsManager } from "./CheckpointSettings"
/**
* CheckpointExclusions Module
@@ -42,13 +43,9 @@ interface ExclusionResult {
/**
* Returns the default list of file and directory patterns to exclude from checkpoints.
* Combines built-in patterns with workspace-specific LFS patterns.
*
* @param lfsPatterns - Optional array of Git LFS patterns from workspace
* @returns Array of glob patterns to exclude
* @todo Make this configurable by the user
* These patterns will be written to .checkpointsignore when it's created.
*/
export const getDefaultExclusions = (lfsPatterns: string[] = []): string[] => [
export const getDefaultExclusions = (): string[] => [
// Build and Development Artifacts
".git/",
`.git${GIT_DISABLED_SUFFIX}/`,
@@ -74,10 +71,51 @@ export const getDefaultExclusions = (lfsPatterns: string[] = []): string[] => [
// Log Files
...getLogFilePatterns(),
...lfsPatterns,
]
/**
* Writes the combined exclusion patterns to Git's exclude file.
* Creates the info directory if it doesn't exist.
*
* @param gitPath - Path to the .git directory
* @param lfsPatterns - Optional array of Git LFS patterns to include
*/
export const writeExcludesFile = async (gitPath: string, lfsPatterns: string[] = []): Promise<void> => {
const excludesPath = join(gitPath, "info", "exclude")
await fs.mkdir(join(gitPath, "info"), { recursive: true })
const settingsManager = CheckpointSettingsManager.getInstance()
// Ensure .checkpointsignore exists and load its patterns
const ignorePatterns = await settingsManager.getIgnorePatterns()
// Combine patterns and write to git exclude file
const patterns = [...ignorePatterns, ...lfsPatterns]
await fs.writeFile(excludesPath, patterns.join("\n"))
}
/**
* Retrieves Git LFS patterns from the workspace's .gitattributes file.
* Returns an empty array if no patterns found or file doesn't exist.
*
* @param workspacePath - Path to the workspace root
* @returns Array of Git LFS patterns found in .gitattributes
*/
export const getLfsPatterns = async (workspacePath: string): Promise<string[]> => {
try {
const attributesPath = join(workspacePath, ".gitattributes")
if (await fileExistsAtPath(attributesPath)) {
const attributesContent = await fs.readFile(attributesPath, "utf8")
return attributesContent
.split("\n")
.filter((line) => line.includes("filter=lfs"))
.map((line) => line.split(" ")[0].trim())
}
} catch (error) {
console.warn("Failed to read .gitattributes:", error)
}
return []
}
/**
* Returns patterns for common build and development artifact directories
* @returns Array of glob patterns for build artifacts
@@ -295,41 +333,3 @@ function getGeospatialPatterns(): string[] {
function getLogFilePatterns(): string[] {
return ["*.error", "*.log", "*.logs", "*.npm-debug.log*", "*.out", "*.stdout", "yarn-debug.log*", "yarn-error.log*"]
}
/**
* Writes the combined exclusion patterns to Git's exclude file.
* Creates the info directory if it doesn't exist.
*
* @param gitPath - Path to the .git directory
* @param lfsPatterns - Optional array of Git LFS patterns to include
*/
export const writeExcludesFile = async (gitPath: string, lfsPatterns: string[] = []): Promise<void> => {
const excludesPath = join(gitPath, "info", "exclude")
await fs.mkdir(join(gitPath, "info"), { recursive: true })
const patterns = getDefaultExclusions(lfsPatterns)
await fs.writeFile(excludesPath, patterns.join("\n"))
}
/**
* Retrieves Git LFS patterns from the workspace's .gitattributes file.
* Returns an empty array if no patterns found or file doesn't exist.
*
* @param workspacePath - Path to the workspace root
* @returns Array of Git LFS patterns found in .gitattributes
*/
export const getLfsPatterns = async (workspacePath: string): Promise<string[]> => {
try {
const attributesPath = join(workspacePath, ".gitattributes")
if (await fileExistsAtPath(attributesPath)) {
const attributesContent = await fs.readFile(attributesPath, "utf8")
return attributesContent
.split("\n")
.filter((line) => line.includes("filter=lfs"))
.map((line) => line.split(" ")[0].trim())
}
} catch (error) {
console.warn("Failed to read .gitattributes:", error)
}
return []
}
@@ -0,0 +1,203 @@
import fs from "fs/promises"
import * as path from "path"
import * as vscode from "vscode"
import { fileExistsAtPath } from "../../utils/fs"
import { getDefaultExclusions } from "./CheckpointExclusions"
import { CheckpointSettings } from "../../shared/Checkpoints"
/**
* CheckpointSettings Module
*
* Manages user-configurable settings for the Checkpoints system. Key features:
*
* Settings Management:
* - Enable/disable checkpoints functionality
*
* File Exclusions Management:
* - .checkpointsignore file for exclusion patterns
* - Default patterns for common file types
* - User-customizable pattern list
*
* Storage Structure:
* - Settings stored in globalStorage/settings/cline_checkpoints_settings.json
* - Ignore patterns stored in globalStorage/settings/.checkpointsignore
*
* Integration Points:
* - Used by CheckpointTracker for file filtering
* - Consumed by CheckpointExclusions for pattern management
*/
/**
* Default settings values.
* These are used when no settings file exists or when reading fails.
*/
const DEFAULT_SETTINGS: CheckpointSettings = {
enableCheckpoints: true, // Enabled by default
}
/**
* CheckpointSettingsManager Class
*
* Handles all checkpoint settings operations including:
* - Reading and writing settings to disk
* - Managing .checkpointsignore patterns
* - Providing default values when needed
*
* File Structure:
* globalStorage/
* settings/
* cline_checkpoints_settings.json - Contains enable flag
* .checkpointsignore - Contains file exclusion patterns
*/
export class CheckpointSettingsManager {
public readonly settingsDir: string
public readonly checkpointSettingsPath: string
public readonly checkpointsIgnorePath: string
private settings: CheckpointSettings = DEFAULT_SETTINGS
private static instance: CheckpointSettingsManager | null = null
/**
* Creates a new CheckpointSettingsManager instance.
* Initializes paths for settings and ignore files.
*
* @param globalStoragePath - VS Code's global storage path for the extension
*/
private constructor(globalStoragePath: string) {
this.settingsDir = path.join(globalStoragePath, "settings")
this.checkpointSettingsPath = path.join(this.settingsDir, "cline_checkpoints_settings.json")
this.checkpointsIgnorePath = path.join(this.settingsDir, ".checkpointsignore")
this.readSettings().then((settings) => {
this.settings = settings
this.migrateEnableCheckpointsSetting()
})
this.ensureIgnoreFileExists()
}
/**
* Initialize the singleton instance
*/
public static initialize(globalStoragePath: string): void {
if (!CheckpointSettingsManager.instance) {
CheckpointSettingsManager.instance = new CheckpointSettingsManager(globalStoragePath)
}
}
/**
* Get the singleton instance
*/
public static getInstance(): CheckpointSettingsManager {
if (!CheckpointSettingsManager.instance) {
throw new Error("CheckpointSettingsManager not initialized")
}
return CheckpointSettingsManager.instance
}
/**
* Retrieves current checkpoint settings from memory.
*
* @returns CheckpointSettings Current settings
*/
getSettings(): CheckpointSettings {
return this.settings
}
/**
* Reads checkpoint settings from disk.
* Merges stored settings with defaults to ensure all fields are present.
*
* @returns Promise<CheckpointSettings> Settings read from disk, with defaults for any missing values
*/
private async readSettings(): Promise<CheckpointSettings> {
try {
if (await fileExistsAtPath(this.checkpointSettingsPath)) {
const settingsContent = await fs.readFile(this.checkpointSettingsPath, "utf8")
return { ...DEFAULT_SETTINGS, ...JSON.parse(settingsContent) }
}
// If file doesn't exist, create it with default settings
await this.saveSettings(DEFAULT_SETTINGS)
} catch (error) {
console.error("Error reading checkpoint settings:", error)
}
return DEFAULT_SETTINGS
}
/**
* Saves checkpoint settings to disk and updates in-memory settings.
* Creates settings directory if it doesn't exist.
* Merges new settings with existing ones.
*
* @param settings - Partial settings to update
*/
async saveSettings(settings: Partial<CheckpointSettings>): Promise<void> {
// Ensure settings directory exists
await fs.mkdir(this.settingsDir, { recursive: true })
// Merge with current settings
const updatedSettings = { ...this.settings, ...settings }
this.settings = updatedSettings
// Save to disk
await fs.writeFile(this.checkpointSettingsPath, JSON.stringify(updatedSettings, null, 2))
}
/**
* Retrieves patterns from .checkpointsignore file.
* Filters out empty lines and comments.
*
* @returns Promise<string[]> Array of active ignore patterns
*/
async getIgnorePatterns(): Promise<string[]> {
try {
if (await fileExistsAtPath(this.checkpointsIgnorePath)) {
const content = await fs.readFile(this.checkpointsIgnorePath, "utf8")
return content.split("\n").filter((line) => line.trim() && !line.startsWith("#"))
}
} catch (error) {
console.error("Error loading .checkpointsignore:", error)
}
return []
}
/**
* Ensures .checkpointsignore file exists.
* Creates it with default patterns if it doesn't exist.
*/
private async ensureIgnoreFileExists(): Promise<void> {
try {
await fs.mkdir(this.settingsDir, { recursive: true })
if (!(await fileExistsAtPath(this.checkpointsIgnorePath))) {
await fs.writeFile(this.checkpointsIgnorePath, getDefaultExclusions().join("\n"))
}
} catch (error) {
console.error("Error creating .checkpointsignore:", error)
}
}
/**
* Migrates the enableCheckpoints setting from VSCode configuration to settings file
* All checkpoints settings will be kept in the CheckpointSettingsView from now on
*/
private async migrateEnableCheckpointsSetting(): Promise<void> {
const config = vscode.workspace.getConfiguration("cline")
const enableCheckpoints = config.get<boolean>("enableCheckpoints")
if (enableCheckpoints !== undefined) {
// Save to settings file
await this.saveSettings({
enableCheckpoints,
})
// Remove from VSCode configuration
await config.update("enableCheckpoints", undefined, true)
}
}
/**
* Reinitializes the settings manager by reading settings from disk.
* This should be called when a new CheckpointTracker is created.
*/
async reinitialize(): Promise<void> {
this.settings = await this.readSettings()
}
}
@@ -1,10 +1,11 @@
import fs from "fs/promises"
import * as path from "path"
import simpleGit, { SimpleGit } from "simple-git"
import simpleGit, { type SimpleGit } from "simple-git"
import * as vscode from "vscode"
import { HistoryItem } from "../../shared/HistoryItem"
import { GitOperations } from "./CheckpointGitOperations"
import { getShadowGitPath, hashWorkingDir, getWorkingDirectory, detectLegacyCheckpoint } from "./CheckpointUtils"
import { CheckpointSettingsManager } from "./CheckpointSettings"
/**
* CheckpointTracker Module
@@ -85,7 +86,7 @@ class CheckpointTracker {
* - Sets up task-specific branch for new checkpoints
*
* Configuration:
* - Respects 'cline.enableCheckpoints' VS Code setting
* - Uses settings from CheckpointSettingsManager
* - Uses branch-per-task architecture for new checkpoints
* - Maintains backwards compatibility with legacy structure
*/
@@ -96,9 +97,13 @@ class CheckpointTracker {
try {
console.info(`Creating new CheckpointTracker for task ${taskId}`)
// Check if checkpoints are disabled in VS Code settings
const enableCheckpoints = vscode.workspace.getConfiguration("cline").get<boolean>("enableCheckpoints") ?? true
if (!enableCheckpoints) {
// Get settings manager instance and reinitialize
const settingsManager = CheckpointSettingsManager.getInstance()
await settingsManager.reinitialize()
// Check if checkpoints are enabled in settings
const settings = settingsManager.getSettings()
if (!settings.enableCheckpoints) {
return undefined // Don't create tracker when disabled
}
@@ -291,7 +296,6 @@ class CheckpointTracker {
* - the current working directory (including uncommitted changes).
*
* If `rhsHash` is omitted, compares `lhsHash` to the working directory.
* If you want truly untracked files to appear, `git add` them first.
*
* @param lhsHash - The commit to compare from (older commit)
* @param rhsHash - The commit to compare to (newer commit).
@@ -339,7 +343,7 @@ class CheckpointTracker {
const batchSize = 50
// Get list of files that exist in base commit
const existingFiles = await this.getExistingFiles(git, baseHash, files)
const existingFiles = await this.getExistingFiles(git, baseHash)
// Process files in batches
for (let i = 0; i < files.length; i += batchSize) {
@@ -373,7 +377,7 @@ class CheckpointTracker {
let afterContents: string[] = []
if (rhsHash) {
// Split after files into existing and new in target commit
const afterExistingFiles = await this.getExistingFiles(git, rhsHash, batch)
const afterExistingFiles = await this.getExistingFiles(git, rhsHash)
const afterExistingBatch = batch.filter((file) => afterExistingFiles.has(file))
if (afterExistingBatch.length > 0) {
@@ -438,7 +442,7 @@ class CheckpointTracker {
/**
* Helper function to get a set of files that exist in a given commit
*/
private async getExistingFiles(git: SimpleGit, commitHash: string, files: string[]): Promise<Set<string>> {
private async getExistingFiles(git: SimpleGit, commitHash: string): Promise<Set<string>> {
try {
const result = await git.raw(["ls-tree", "-r", "--name-only", commitHash])
const existingFiles = new Set<string>(result.split("\n"))
@@ -1,6 +1,6 @@
import { mkdir } from "fs/promises"
import * as path from "path"
import * as vscode from "vscode"
import fs from "fs/promises"
import os from "os"
import { fileExistsAtPath } from "../../utils/fs"
@@ -26,9 +26,10 @@ export async function getLegacyShadowGitPath(globalStoragePath: string, taskId:
throw new Error("Global storage uri is invalid")
}
const checkpointsDir = path.join(globalStoragePath, "tasks", taskId, "checkpoints")
await mkdir(checkpointsDir, { recursive: true })
await fs.mkdir(checkpointsDir, { recursive: true })
const gitPath = path.join(checkpointsDir, ".git")
console.info(`Legacy shadow git path: ${gitPath}`)
console.log(`Legacy shadow git path: ${gitPath}`)
return gitPath
}
@@ -63,7 +64,8 @@ export async function getShadowGitPath(
throw new Error("Global storage uri is invalid")
}
const checkpointsDir = path.join(globalStoragePath, "checkpoints", cwdHash)
await mkdir(checkpointsDir, { recursive: true })
await fs.mkdir(checkpointsDir, { recursive: true })
const gitPath = path.join(checkpointsDir, ".git")
return gitPath
}
@@ -154,6 +156,40 @@ export async function detectLegacyCheckpoint(globalStoragePath: string | undefin
}
const legacyGitPath = path.join(globalStoragePath, "tasks", taskId, "checkpoints", ".git")
const isLegacy = await fileExistsAtPath(legacyGitPath)
console.info(`Legacy checkpoint detection result: ${isLegacy}`)
return isLegacy
}
/**
* Deletes all checkpoint data across all tasks.
* This is a destructive operation that removes all checkpoint history.
* Handles both legacy checkpoints (under tasks/{taskId}/checkpoints/.git/)
* and branch-per-task checkpoints (under checkpoints/{workspaceHash}/.git/).
*
* @param globalStoragePath - The VS Code global storage path
* @throws Error if deletion fails or if global storage path is invalid
*/
export async function deleteAllCheckpoints(globalStoragePath: string): Promise<void> {
if (!globalStoragePath) {
throw new Error("Global storage path is invalid")
}
// Delete legacy checkpoints
const tasksDir = path.join(globalStoragePath, "tasks")
if (await fileExistsAtPath(tasksDir)) {
const taskDirs = await fs.readdir(tasksDir)
for (const taskId of taskDirs) {
const checkpointsDir = path.join(tasksDir, taskId, "checkpoints")
if (await fileExistsAtPath(checkpointsDir)) {
await fs.rm(checkpointsDir, { recursive: true, force: true })
}
}
}
// Delete branch-per-task checkpoints
const checkpointsDir = path.join(globalStoragePath, "checkpoints")
if (await fileExistsAtPath(checkpointsDir)) {
await fs.rm(checkpointsDir, { recursive: true, force: true })
}
}
+4
View File
@@ -0,0 +1,4 @@
export interface CheckpointSettings {
/** Whether checkpoints are enabled */
enableCheckpoints: boolean
}
+3 -2
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 { CheckpointSettings } from "./Checkpoints"
// webview will hold state
export interface ExtensionMessage {
@@ -22,7 +23,6 @@ export interface ExtensionMessage {
| "invoke"
| "partialMessage"
| "openRouterModels"
| "requestyModels"
| "openAiModels"
| "mcpServers"
| "relinquishControl"
@@ -34,6 +34,7 @@ export interface ExtensionMessage {
| "commitSearchResults"
| "openGraphData"
| "isImageUrlResult"
| "setCheckpointSettings"
text?: string
action?:
| "chatButtonClicked"
@@ -52,12 +53,12 @@ export interface ExtensionMessage {
filePaths?: string[]
partialMessage?: ClineMessage
openRouterModels?: Record<string, ModelInfo>
requestyModels?: Record<string, ModelInfo>
openAiModels?: string[]
mcpServers?: McpServer[]
mcpMarketplaceCatalog?: McpMarketplaceCatalog
error?: string
mcpDownloadDetails?: McpDownloadResponse
checkpointSettings?: CheckpointSettings
commits?: GitCommit[]
openGraphData?: {
title?: string
+7 -1
View File
@@ -3,6 +3,7 @@ import { AutoApprovalSettings } from "./AutoApprovalSettings"
import { BrowserSettings } from "./BrowserSettings"
import { ChatSettings } from "./ChatSettings"
import { ChatContent } from "./ChatContent"
import { CheckpointSettings } from "./Checkpoints"
export interface WebviewMessage {
type:
@@ -27,7 +28,6 @@ export interface WebviewMessage {
| "openMention"
| "cancelTask"
| "refreshOpenRouterModels"
| "refreshRequestyModels"
| "refreshOpenAiModels"
| "openMcpSettings"
| "restartMcpServer"
@@ -37,6 +37,10 @@ export interface WebviewMessage {
| "togglePlanActMode"
| "checkpointDiff"
| "checkpointRestore"
| "getCheckpointSettings"
| "updateCheckpointSettings"
| "openCheckpointsIgnore"
| "confirmDeleteAllCheckpoints"
| "taskCompletionViewChanges"
| "openExtensionSettings"
| "requestVsCodeLmModels"
@@ -57,6 +61,7 @@ export interface WebviewMessage {
| "updateMcpTimeout"
| "fetchOpenGraphData"
| "checkIsImageUrl"
| "updateThinkingBudgetTokens"
// | "relaunchChromeDebugMode"
text?: string
disabled?: boolean
@@ -67,6 +72,7 @@ export interface WebviewMessage {
number?: number
autoApprovalSettings?: AutoApprovalSettings
browserSettings?: BrowserSettings
checkpointSettings?: CheckpointSettings
chatSettings?: ChatSettings
chatContent?: ChatContent
mcpId?: string
+13 -17
View File
@@ -49,7 +49,6 @@ export interface ApiHandlerOptions {
deepSeekApiKey?: string
requestyApiKey?: string
requestyModelId?: string
requestyModelInfo?: ModelInfo
togetherApiKey?: string
togetherModelId?: string
qwenApiKey?: string
@@ -59,6 +58,7 @@ export interface ApiHandlerOptions {
o3MiniReasoningEffort?: string
qwenApiLine?: string
xaiApiKey?: string
thinkingBudgetTokens?: number
}
export type ApiConfiguration = ApiHandlerOptions & {
@@ -84,6 +84,7 @@ export interface ModelInfo {
// https://docs.anthropic.com/en/docs/about-claude/models // prices updated 2025-01-02
export type AnthropicModelId = keyof typeof anthropicModels
export const anthropicDefaultModelId: AnthropicModelId = "claude-3-7-sonnet-20250219"
export const ANTHROPIC_THINKING_BUDGET_TOKENS_MIN = 1024
export const anthropicModels = {
"claude-3-7-sonnet-20250219": {
maxTokens: 8192,
@@ -96,6 +97,17 @@ export const anthropicModels = {
cacheWritesPrice: 3.75,
cacheReadsPrice: 0.3,
},
"claude-3-7-sonnet-20250219:thinking": {
maxTokens: 8192,
contextWindow: 200_000,
supportsImages: true,
supportsComputerUse: true,
supportsPromptCache: true,
inputPrice: 3.0,
outputPrice: 15.0,
cacheWritesPrice: 3.75,
cacheReadsPrice: 0.3,
},
"claude-3-5-sonnet-20241022": {
maxTokens: 8192,
contextWindow: 200_000,
@@ -803,22 +815,6 @@ export const liteLlmModelInfoSaneDefaults: ModelInfo = {
outputPrice: 0,
}
// Requesty
// https://requesty.ai/models
export const requestyDefaultModelId = "anthropic/claude-3-5-sonnet-latest"
export const requestyDefaultModelInfo: ModelInfo = {
maxTokens: 8192,
contextWindow: 200_000,
supportsImages: true,
supportsComputerUse: false,
supportsPromptCache: true,
inputPrice: 3.0,
outputPrice: 15.0,
cacheWritesPrice: 3.75,
cacheReadsPrice: 0.3,
description: "Anthropic's most intelligent model. Highest level of intelligence and capability.",
}
// X AI
// https://docs.x.ai/docs/api-reference
export type XAIModelId = keyof typeof xaiModels
+36
View File
@@ -0,0 +1,36 @@
import { ANTHROPIC_THINKING_BUDGET_TOKENS_MIN, anthropicModels } from "../shared/api"
/**
* Validates the thinking budget token value according to the specified rules:
* - If disabled (0), return as is
* - If enabled but less than minimum (1024), set to minimum
* - If greater than or equal to max tokens, set to max tokens - 1
* - Otherwise, return the original value
*
* @param value The thinking budget token value to validate
* @param maxTokens The maximum tokens for the current model
* @returns The validated thinking budget token value
*/
export function validateThinkingBudget(
value: number,
maxTokens: number = anthropicModels["claude-3-7-sonnet-20250219"].maxTokens,
): number {
// If disabled (0), return as is
if (value === 0) {
return 0
}
// If enabled but less than minimum, set to minimum
if (value > 0 && value < ANTHROPIC_THINKING_BUDGET_TOKENS_MIN) {
return ANTHROPIC_THINKING_BUDGET_TOKENS_MIN
}
// If greater than or equal to max allowed tokens (80% of max tokens), cap at that value
const maxAllowedTokens = Math.floor(maxTokens * 0.8)
if (value >= maxAllowedTokens) {
return maxAllowedTokens
}
// Otherwise, return the original value
return value
}
+6 -1
View File
@@ -19,8 +19,13 @@ export default tseslint.config(
},
rules: {
...reactHooks.configs.recommended.rules,
"react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
// "react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
"@typescript-eslint/no-unused-vars": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-empty-object-type": "off",
"no-case-declarations": "off",
"react-hooks/exhaustive-deps": "off",
"prefer-const": "off",
},
},
)
+903 -1120
View File
File diff suppressed because it is too large Load Diff
+8 -9
View File
@@ -9,7 +9,6 @@ import AccountView from "./components/account/AccountView"
import { ExtensionStateContextProvider, useExtensionState } from "./context/ExtensionStateContext"
import { vscode } from "./utils/vscode"
import McpView from "./components/mcp/McpView"
import posthog from "posthog-js"
const AppContent = () => {
const { didHydrateState, showWelcome, shouldShowAnnouncement, telemetrySetting, vscMachineId } = useExtensionState()
@@ -61,14 +60,14 @@ const AppContent = () => {
useEvent("message", handleMessage)
useEffect(() => {
if (telemetrySetting === "enabled") {
posthog.identify(vscMachineId)
posthog.opt_in_capturing()
} else {
posthog.opt_out_capturing()
}
}, [telemetrySetting, vscMachineId])
// useEffect(() => {
// if (telemetrySetting === "enabled") {
// posthog.identify(vscMachineId)
// posthog.opt_in_capturing()
// } else {
// posthog.opt_out_capturing()
// }
// }, [telemetrySetting, vscMachineId])
useEffect(() => {
if (shouldShowAnnouncement) {
@@ -214,7 +214,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
},
ref,
) => {
const { filePaths, chatSettings, apiConfiguration, openRouterModels, requestyModels, platform } = useExtensionState()
const { filePaths, chatSettings, apiConfiguration, openRouterModels, platform } = useExtensionState()
const [isTextAreaFocused, setIsTextAreaFocused] = useState(false)
const [gitCommits, setGitCommits] = useState<any[]>([])
@@ -635,14 +635,14 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
// Separate the API config submission logic
const submitApiConfig = useCallback(() => {
const apiValidationResult = validateApiConfiguration(apiConfiguration)
const modelIdValidationResult = validateModelId(apiConfiguration, openRouterModels, requestyModels)
const modelIdValidationResult = validateModelId(apiConfiguration, openRouterModels)
if (!apiValidationResult && !modelIdValidationResult) {
vscode.postMessage({ type: "apiConfiguration", apiConfiguration })
} else {
vscode.postMessage({ type: "getLatestState" })
}
}, [apiConfiguration, openRouterModels, requestyModels])
}, [apiConfiguration, openRouterModels])
const onModeToggle = useCallback(() => {
// if (textAreaDisabled) return
@@ -0,0 +1,203 @@
import { memo, useEffect, useState, useMemo } from "react"
import debounce from "debounce"
interface ClineSliderProps {
id: string
label: string
value: number
min: number
max: number
step: number
onChange: (value: number) => void
onChangeEnd?: (value: number) => void
description?: string
validateValue?: (value: number) => number
dynamicColor?: boolean
secondaryLabel?: string
getSecondaryLabel?: (value: number, min: number, max: number) => string | JSX.Element
}
// Constants
const THUMB_SIZE = 24
const DEBOUNCE_DELAY = 300
const ClineSlider = ({
id,
label,
value,
min,
max,
step,
onChange,
onChangeEnd,
description,
validateValue,
dynamicColor = false,
secondaryLabel,
getSecondaryLabel,
}: ClineSliderProps) => {
// State
const [localValue, setLocalValue] = useState(value)
const [isDragging, setIsDragging] = useState(false)
// Update local value when prop value changes (if not dragging)
useEffect(() => {
if (!isDragging) {
setLocalValue(value)
}
}, [value, isDragging])
// Create debounced onChange handler
const debouncedOnChange = useMemo(() => debounce((val: number) => onChange(val), DEBOUNCE_DELAY), [onChange])
// Clear debounce on unmount
useEffect(() => () => debouncedOnChange.clear(), [debouncedOnChange])
// Event handlers
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newValue = parseInt(e.target.value, 10)
setLocalValue(newValue)
setIsDragging(true)
debouncedOnChange(newValue)
}
const handleSlideEnd = () => {
if (onChangeEnd) {
const finalValue = validateValue ? validateValue(localValue) : localValue
setLocalValue(finalValue)
onChangeEnd(finalValue)
}
setTimeout(() => setIsDragging(false), 50)
}
// Calculate percentage for dynamic color
const percentage = (localValue - min) / (max - min)
const intensity = dynamicColor ? percentage : 0
// Initialize styles once
useEffect(() => {
if (document.getElementById("cline-slider-styles")) return
const styleElement = document.createElement("style")
styleElement.id = "cline-slider-styles"
styleElement.innerHTML = `
.cline-slider {
width: 100%;
height: 16px;
appearance: none;
background-color: var(--track-color, var(--vscode-scrollbarSlider-background));
border-radius: 8px;
outline: none;
cursor: pointer;
margin: 0;
padding: 0;
box-sizing: border-box;
}
.cline-slider::-webkit-slider-thumb {
appearance: none;
width: var(--thumb-size, 24px);
height: var(--thumb-size, 24px);
border-radius: 50%;
background: white;
cursor: pointer;
border: 2px solid var(--thumb-color, var(--vscode-button-background));
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
margin: 0;
}
.cline-slider:focus { outline: none; }
.cline-slider:focus::-webkit-slider-thumb,
.cline-slider:hover::-webkit-slider-thumb {
background: white;
border-color: var(--thumb-color, var(--vscode-button-hoverBackground));
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
}
.cline-slider:active::-webkit-slider-thumb {
background: white;
border-color: var(--thumb-color, var(--vscode-button-hoverBackground));
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);
}
`
document.head.appendChild(styleElement)
}, [])
// Common styles
const containerStyle: React.CSSProperties = { marginTop: "10px" }
const labelContainerStyle: React.CSSProperties = {
display: "flex",
justifyContent: "space-between",
marginBottom: "10px",
flexWrap: "wrap",
gap: "12px",
}
const labelStyle: React.CSSProperties = {
fontWeight: 500,
display: "block",
marginRight: "auto",
}
const valueStyle: React.CSSProperties = {
color: "var(--vscode-button-foreground)",
backgroundColor: "var(--vscode-button-background)",
padding: "2px 6px",
borderRadius: "4px",
}
const secondaryLabelStyle: React.CSSProperties = {
fontWeight: 500,
textAlign: "right",
whiteSpace: "nowrap",
flexShrink: 0,
}
const descriptionStyle: React.CSSProperties = {
fontSize: "12px",
marginTop: "5px",
color: "var(--vscode-descriptionForeground)",
}
// Input style with dynamic color if enabled
const inputStyle = {
marginTop: "5px",
"--thumb-size": `${THUMB_SIZE}px`,
...(dynamicColor
? {
"--thumb-color": `var(--vscode-button-background)`,
"--track-color": `var(--vscode-button-background)`,
opacity: 0.5 + intensity * 0.5,
}
: {}),
} as React.CSSProperties
return (
<div style={containerStyle}>
<div style={labelContainerStyle}>
<label htmlFor={id} style={labelStyle}>
<span style={{ color: "var(--vscode-editor-foreground)" }}>{label}:</span>{" "}
<span style={valueStyle}>{localValue}</span>
</label>
{(secondaryLabel || getSecondaryLabel) && (
<div style={secondaryLabelStyle}>
{secondaryLabel || (getSecondaryLabel && getSecondaryLabel(localValue, min, max))}
</div>
)}
</div>
<input
id={id}
type="range"
min={min}
max={max}
step={step}
value={localValue}
onChange={handleChange}
onMouseUp={handleSlideEnd}
onTouchEnd={handleSlideEnd}
className="cline-slider"
style={inputStyle}
/>
{description && <p style={descriptionStyle}>{description}</p>}
</div>
)
}
export default memo(ClineSlider)
@@ -9,30 +9,30 @@ import {
} from "@vscode/webview-ui-toolkit/react"
import { Fragment, memo, useCallback, useEffect, useMemo, useState } from "react"
import { useEvent, useInterval } from "react-use"
import styled from "styled-components"
import * as vscodemodels from "vscode"
import {
ApiConfiguration,
ApiProvider,
ModelInfo,
anthropicDefaultModelId,
anthropicModels,
ApiConfiguration,
ApiProvider,
azureOpenAiDefaultApiVersion,
bedrockDefaultModelId,
bedrockModels,
deepSeekDefaultModelId,
deepSeekModels,
qwenDefaultModelId,
qwenModels,
geminiDefaultModelId,
geminiModels,
mistralDefaultModelId,
mistralModels,
ModelInfo,
openAiModelInfoSaneDefaults,
openAiNativeDefaultModelId,
openAiNativeModels,
openRouterDefaultModelId,
openRouterDefaultModelInfo,
requestyDefaultModelId,
requestyDefaultModelInfo,
qwenDefaultModelId,
qwenModels,
vertexDefaultModelId,
vertexModels,
xaiDefaultModelId,
@@ -41,13 +41,9 @@ import {
import { ExtensionMessage } from "../../../../src/shared/ExtensionMessage"
import { useExtensionState } from "../../context/ExtensionStateContext"
import { vscode } from "../../utils/vscode"
import VSCodeButtonLink from "../common/VSCodeButtonLink"
import OpenRouterModelPicker from "./OpenRouterModelPicker"
import RequestyModelPicker from "./RequestyModelPicker"
import ModelDescriptionMarkdown from "./ModelDescriptionMarkdown"
import styled from "styled-components"
import * as vscodemodels from "vscode"
import { getAsVar, VSC_DESCRIPTION_FOREGROUND } from "../../utils/vscStyles"
import VSCodeButtonLink from "../common/VSCodeButtonLink"
import OpenRouterModelPicker, { ModelDescriptionMarkdown } from "./OpenRouterModelPicker"
interface ApiOptionsProps {
showModelOptions: boolean
@@ -57,7 +53,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 Requesty/OpenRouterModelPicker's and ModelSelectorTooltip's z-index
const DROPDOWN_Z_INDEX = 1001 // Higher than the OpenRouterModelPicker's and ModelSelectorTooltip's z-index
const DropdownContainer = styled.div<{ zIndex?: number }>`
position: relative;
@@ -848,7 +844,24 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
placeholder="Enter API Key...">
<span style={{ fontWeight: 500 }}>API Key</span>
</VSCodeTextField>
{!apiConfiguration?.requestyApiKey && <a href="https://app.requesty.ai/manage-api">Get API Key</a>}
<VSCodeTextField
value={apiConfiguration?.requestyModelId || ""}
style={{ width: "100%" }}
onInput={handleInputChange("requestyModelId")}
placeholder={"Enter Model ID..."}>
<span style={{ fontWeight: 500 }}>Model ID</span>
</VSCodeTextField>
<p
style={{
fontSize: "12px",
marginTop: 3,
color: "var(--vscode-descriptionForeground)",
}}>
<span style={{ color: "var(--vscode-errorForeground)" }}>
(<span style={{ fontWeight: 500 }}>Note:</span> Cline uses complex prompts and works best with Claude
models. Less capable models may not work as expected.)
</span>
</p>
</div>
)}
@@ -1164,11 +1177,12 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
)}
{selectedProvider !== "openrouter" &&
selectedProvider !== "requesty" &&
selectedProvider !== "openai" &&
selectedProvider !== "ollama" &&
selectedProvider !== "lmstudio" &&
selectedProvider !== "vscode-lm" &&
selectedProvider !== "litellm" &&
selectedProvider !== "requesty" &&
showModelOptions && (
<>
<DropdownContainer zIndex={DROPDOWN_Z_INDEX - 2} className="dropdown-container">
@@ -1197,7 +1211,6 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
)}
{selectedProvider === "openrouter" && showModelOptions && <OpenRouterModelPicker isPopup={isPopup} />}
{selectedProvider === "requesty" && showModelOptions && <RequestyModelPicker isPopup={isPopup} />}
{modelIdErrorMessage && (
<p
@@ -1401,12 +1414,6 @@ export function normalizeApiConfiguration(apiConfiguration?: ApiConfiguration):
selectedModelId: apiConfiguration?.openRouterModelId || openRouterDefaultModelId,
selectedModelInfo: apiConfiguration?.openRouterModelInfo || openRouterDefaultModelInfo,
}
case "requesty":
return {
selectedProvider: provider,
selectedModelId: apiConfiguration?.requestyModelId || requestyDefaultModelId,
selectedModelInfo: apiConfiguration?.requestyModelInfo || requestyDefaultModelInfo,
}
case "openai":
return {
selectedProvider: provider,
@@ -0,0 +1,102 @@
import { VSCodeButton, VSCodeLink, VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"
import { memo, useEffect, useState } from "react"
import { vscode } from "../../utils/vscode"
import { CheckpointSettings } from "../../../../src/shared/Checkpoints"
const CheckpointsSettingsView = () => {
const [settings, setSettings] = useState<CheckpointSettings>({
enableCheckpoints: true,
})
useEffect(() => {
// Message extension to get initial settings
vscode.postMessage({
type: "getCheckpointSettings",
})
}, [])
useEffect(() => {
const messageHandler = (event: MessageEvent) => {
const message = event.data
switch (message.type) {
case "setCheckpointSettings": {
const newSettings = message.checkpointSettings
setSettings(newSettings)
}
}
}
window.addEventListener("message", messageHandler)
return () => window.removeEventListener("message", messageHandler)
}, [])
const handleEnableChange = (e: any) => {
const checkbox = e.target as HTMLInputElement
const newSettings = {
...settings,
enableCheckpoints: checkbox.checked,
}
setSettings(newSettings)
vscode.postMessage({
type: "updateCheckpointSettings",
checkpointSettings: newSettings,
})
}
const handleDeleteCheckpoints = () => {
vscode.postMessage({
type: "confirmDeleteAllCheckpoints",
})
}
return (
<div style={{ display: "flex", flexDirection: "column", gap: "16px" }}>
<div>
<div style={{ fontWeight: "500", marginBottom: "8px" }}>Enable Checkpoints</div>
<VSCodeCheckbox checked={settings.enableCheckpoints} onChange={handleEnableChange}>
Enable checkpoints
</VSCodeCheckbox>
<p
style={{
fontSize: "12px",
marginTop: "5px",
color: "var(--vscode-descriptionForeground)",
}}>
Automatically create checkpoints when Cline makes changes to your files
</p>
</div>
<div>
<div style={{ fontWeight: "500", marginBottom: "8px" }}>Checkpoints File Exclusions</div>
<VSCodeLink onClick={() => vscode.postMessage({ type: "openCheckpointsIgnore" })}>
Edit .checkpointsignore
</VSCodeLink>
<p
style={{
fontSize: "12px",
marginTop: "5px",
color: "var(--vscode-descriptionForeground)",
}}>
Configure which files and directories to exclude from checkpoint creation globally
</p>
</div>
<div>
<div style={{ fontWeight: "500", marginBottom: "8px" }}>Delete All Checkpoints</div>
<VSCodeButton onClick={handleDeleteCheckpoints}>
<i className="codicon codicon-trash" style={{ marginRight: "6px" }} />
Delete All
</VSCodeButton>
<p
style={{
fontSize: "12px",
marginTop: "5px",
color: "var(--vscode-descriptionForeground)",
}}>
Permanently remove all saved checkpoints from your system
</p>
</div>
</div>
)
}
export default memo(CheckpointsSettingsView)
@@ -1,6 +1,7 @@
import { VSCodeLink, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import Fuse from "fuse.js"
import React, { KeyboardEvent, useEffect, useMemo, useRef, useState } from "react"
import React, { KeyboardEvent, memo, useEffect, useMemo, useRef, useState } from "react"
import { useRemark } from "react-remark"
import { useMount } from "react-use"
import styled from "styled-components"
import { openRouterDefaultModelId } from "../../../../src/shared/api"
@@ -8,6 +9,7 @@ import { useExtensionState } from "../../context/ExtensionStateContext"
import { vscode } from "../../utils/vscode"
import { highlight } from "../history/HistoryView"
import { ModelInfoView, normalizeApiConfiguration } from "./ApiOptions"
import { CODE_BLOCK_BG_COLOR } from "../common/CodeBlock"
export interface OpenRouterModelPickerProps {
isPopup?: boolean
@@ -274,3 +276,158 @@ const DropdownItem = styled.div<{ isSelected: boolean }>`
background-color: var(--vscode-list-activeSelectionBackground);
}
`
// Markdown
const StyledMarkdown = styled.div`
font-family:
var(--vscode-font-family),
system-ui,
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
Roboto,
Oxygen,
Ubuntu,
Cantarell,
"Open Sans",
"Helvetica Neue",
sans-serif;
font-size: 12px;
color: var(--vscode-descriptionForeground);
p,
li,
ol,
ul {
line-height: 1.25;
margin: 0;
}
ol,
ul {
padding-left: 1.5em;
margin-left: 0;
}
p {
white-space: pre-wrap;
}
a {
text-decoration: none;
}
a {
&:hover {
text-decoration: underline;
}
}
`
export const ModelDescriptionMarkdown = memo(
({
markdown,
key,
isExpanded,
setIsExpanded,
isPopup,
}: {
markdown?: string
key: string
isExpanded: boolean
setIsExpanded: (isExpanded: boolean) => void
isPopup?: boolean
}) => {
const [reactContent, setMarkdown] = useRemark()
// const [isExpanded, setIsExpanded] = useState(false)
const [showSeeMore, setShowSeeMore] = useState(false)
const textContainerRef = useRef<HTMLDivElement>(null)
const textRef = useRef<HTMLDivElement>(null)
useEffect(() => {
setMarkdown(markdown || "")
}, [markdown, setMarkdown])
useEffect(() => {
if (textRef.current && textContainerRef.current) {
const { scrollHeight } = textRef.current
const { clientHeight } = textContainerRef.current
const isOverflowing = scrollHeight > clientHeight
setShowSeeMore(isOverflowing)
// if (!isOverflowing) {
// setIsExpanded(false)
// }
}
}, [reactContent, setIsExpanded])
return (
<StyledMarkdown key={key} style={{ display: "inline-block", marginBottom: 0 }}>
<div
ref={textContainerRef}
style={{
overflowY: isExpanded ? "auto" : "hidden",
position: "relative",
wordBreak: "break-word",
overflowWrap: "anywhere",
}}>
<div
ref={textRef}
style={{
display: "-webkit-box",
WebkitLineClamp: isExpanded ? "unset" : 3,
WebkitBoxOrient: "vertical",
overflow: "hidden",
// whiteSpace: "pre-wrap",
// wordBreak: "break-word",
// overflowWrap: "anywhere",
}}>
{reactContent}
</div>
{!isExpanded && showSeeMore && (
<div
style={{
position: "absolute",
right: 0,
bottom: 0,
display: "flex",
alignItems: "center",
}}>
<div
style={{
width: 30,
height: "1.2em",
background: "linear-gradient(to right, transparent, var(--vscode-sideBar-background))",
}}
/>
<VSCodeLink
style={{
// cursor: "pointer",
// color: "var(--vscode-textLink-foreground)",
fontSize: "inherit",
paddingRight: 0,
paddingLeft: 3,
backgroundColor: isPopup ? CODE_BLOCK_BG_COLOR : "var(--vscode-sideBar-background)",
}}
onClick={() => setIsExpanded(true)}>
See more
</VSCodeLink>
</div>
)}
</div>
{/* {isExpanded && showSeeMore && (
<div
style={{
cursor: "pointer",
color: "var(--vscode-textLink-foreground)",
marginLeft: "auto",
textAlign: "right",
paddingRight: 2,
}}
onClick={() => setIsExpanded(false)}>
See less
</div>
)} */}
</StyledMarkdown>
)
},
)
@@ -1,274 +0,0 @@
import { VSCodeLink, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import Fuse from "fuse.js"
import React, { KeyboardEvent, useEffect, useMemo, useRef, useState } from "react"
import { useMount } from "react-use"
import styled from "styled-components"
import { requestyDefaultModelId } from "../../../../src/shared/api"
import { useExtensionState } from "../../context/ExtensionStateContext"
import { vscode } from "../../utils/vscode"
import { highlight } from "../history/HistoryView"
import { ModelInfoView, normalizeApiConfiguration } from "./ApiOptions"
export interface RequestyModelPickerProps {
isPopup?: boolean
}
const RequestyModelPicker: React.FC<RequestyModelPickerProps> = ({ isPopup }) => {
const { apiConfiguration, setApiConfiguration, requestyModels } = useExtensionState()
const [searchTerm, setSearchTerm] = useState(apiConfiguration?.requestyModelId || requestyDefaultModelId)
const [isDropdownVisible, setIsDropdownVisible] = useState(false)
const [selectedIndex, setSelectedIndex] = useState(-1)
const dropdownRef = useRef<HTMLDivElement>(null)
const itemRefs = useRef<(HTMLDivElement | null)[]>([])
const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false)
const dropdownListRef = useRef<HTMLDivElement>(null)
const handleModelChange = (newModelId: string) => {
// could be setting invalid model id/undefined info but validation will catch it
setApiConfiguration({
...apiConfiguration,
...{
requestyModelId: newModelId,
requestyModelInfo: requestyModels[newModelId],
},
})
setSearchTerm(newModelId)
}
const { selectedModelId, selectedModelInfo } = useMemo(() => {
return normalizeApiConfiguration(apiConfiguration)
}, [apiConfiguration])
useMount(() => {
vscode.postMessage({ type: "refreshRequestyModels" })
})
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
setIsDropdownVisible(false)
}
}
document.addEventListener("mousedown", handleClickOutside)
return () => {
document.removeEventListener("mousedown", handleClickOutside)
}
}, [])
const modelIds = useMemo(() => {
return Object.keys(requestyModels).sort((a, b) => a.localeCompare(b))
}, [requestyModels])
const searchableItems = useMemo(() => {
return modelIds.map((id) => ({
id,
html: id,
}))
}, [modelIds])
const fuse = useMemo(() => {
return new Fuse(searchableItems, {
keys: ["html"], // highlight function will update this
threshold: 0.6,
shouldSort: true,
isCaseSensitive: false,
ignoreLocation: false,
includeMatches: true,
minMatchCharLength: 1,
})
}, [searchableItems])
const modelSearchResults = useMemo(() => {
let results: { id: string; html: string }[] = searchTerm
? highlight(fuse.search(searchTerm), "model-item-highlight")
: searchableItems
return results
}, [searchableItems, searchTerm, fuse])
const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
if (!isDropdownVisible) return
switch (event.key) {
case "ArrowDown":
event.preventDefault()
setSelectedIndex((prev) => (prev < modelSearchResults.length - 1 ? prev + 1 : prev))
break
case "ArrowUp":
event.preventDefault()
setSelectedIndex((prev) => (prev > 0 ? prev - 1 : prev))
break
case "Enter":
event.preventDefault()
if (selectedIndex >= 0 && selectedIndex < modelSearchResults.length) {
handleModelChange(modelSearchResults[selectedIndex].id)
setIsDropdownVisible(false)
}
break
case "Escape":
setIsDropdownVisible(false)
setSelectedIndex(-1)
break
}
}
const hasInfo = useMemo(() => {
return modelIds.some((id) => id.toLowerCase() === searchTerm.toLowerCase())
}, [modelIds, searchTerm])
useEffect(() => {
setSelectedIndex(-1)
if (dropdownListRef.current) {
dropdownListRef.current.scrollTop = 0
}
}, [searchTerm])
useEffect(() => {
if (selectedIndex >= 0 && itemRefs.current[selectedIndex]) {
itemRefs.current[selectedIndex]?.scrollIntoView({
block: "nearest",
behavior: "smooth",
})
}
}, [selectedIndex])
return (
<div style={{ width: "100%" }}>
<style>
{`
.model-item-highlight {
background-color: var(--vscode-editor-findMatchHighlightBackground);
color: inherit;
}
`}
</style>
<div style={{ display: "flex", flexDirection: "column" }}>
<label htmlFor="model-search">
<span style={{ fontWeight: 500 }}>Model</span>
</label>
<DropdownWrapper ref={dropdownRef}>
<VSCodeTextField
id="model-search"
placeholder="Search and select a model..."
value={searchTerm}
onInput={(e) => {
handleModelChange((e.target as HTMLInputElement)?.value?.toLowerCase())
setIsDropdownVisible(true)
}}
onFocus={() => setIsDropdownVisible(true)}
onKeyDown={handleKeyDown}
style={{
width: "100%",
zIndex: REQUESTY_MODEL_PICKER_Z_INDEX,
position: "relative",
}}>
{searchTerm && (
<div
className="input-icon-button codicon codicon-close"
aria-label="Clear search"
onClick={() => {
handleModelChange("")
setIsDropdownVisible(true)
}}
slot="end"
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
height: "100%",
}}
/>
)}
</VSCodeTextField>
{isDropdownVisible && (
<DropdownList ref={dropdownListRef}>
{modelSearchResults.map((item, index) => (
<DropdownItem
key={item.id}
ref={(el) => (itemRefs.current[index] = el)}
isSelected={index === selectedIndex}
onMouseEnter={() => setSelectedIndex(index)}
onClick={() => {
handleModelChange(item.id)
setIsDropdownVisible(false)
}}
dangerouslySetInnerHTML={{
__html: item.html,
}}
/>
))}
</DropdownList>
)}
</DropdownWrapper>
</div>
{hasInfo ? (
<ModelInfoView
selectedModelId={selectedModelId}
modelInfo={selectedModelInfo}
isDescriptionExpanded={isDescriptionExpanded}
setIsDescriptionExpanded={setIsDescriptionExpanded}
isPopup={isPopup}
/>
) : (
<p
style={{
fontSize: "12px",
marginTop: 0,
color: "var(--vscode-descriptionForeground)",
}}>
<>
The extension automatically fetches the latest list of models available on{" "}
<VSCodeLink style={{ display: "inline", fontSize: "inherit" }} href="https://app.requesty.ai/router/list">
Requesty.
</VSCodeLink>
If you're unsure which model to choose, Cline works best with{" "}
<VSCodeLink
style={{ display: "inline", fontSize: "inherit" }}
onClick={() => handleModelChange("anthropic/claude-3-5-sonnet-latest")}>
anthropic/claude-3-5-sonnet-latest.
</VSCodeLink>
</>
</p>
)}
</div>
)
}
export default RequestyModelPicker
// Dropdown
const DropdownWrapper = styled.div`
position: relative;
width: 100%;
`
export const REQUESTY_MODEL_PICKER_Z_INDEX = 1_000
const DropdownList = styled.div`
position: absolute;
top: calc(100% - 3px);
left: 0;
width: calc(100% - 2px);
max-height: 200px;
overflow-y: auto;
background-color: var(--vscode-dropdown-background);
border: 1px solid var(--vscode-list-activeSelectionBackground);
z-index: ${REQUESTY_MODEL_PICKER_Z_INDEX - 1};
border-bottom-left-radius: 3px;
border-bottom-right-radius: 3px;
`
const DropdownItem = styled.div<{ isSelected: boolean }>`
padding: 5px 10px;
cursor: pointer;
word-break: break-all;
white-space: normal;
background-color: ${({ isSelected }) => (isSelected ? "var(--vscode-list-activeSelectionBackground)" : "inherit")};
&:hover {
background-color: var(--vscode-list-activeSelectionBackground);
}
`
@@ -3,14 +3,18 @@ import { memo, useEffect, useState } from "react"
import { useExtensionState } from "../../context/ExtensionStateContext"
import { validateApiConfiguration, validateModelId } from "../../utils/validate"
import { vscode } from "../../utils/vscode"
import SettingsButton from "../common/SettingsButton"
import ApiOptions from "./ApiOptions"
import SettingsButton from "../common/SettingsButton"
import CheckpointsSettingsView from "./CheckpointSettingsView"
const { IS_DEV } = process.env
type SettingsViewProps = {
onDone: () => void
}
type View = "main" | "checkpoints"
const SettingsView = ({ onDone }: SettingsViewProps) => {
const {
apiConfiguration,
@@ -18,16 +22,16 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
customInstructions,
setCustomInstructions,
openRouterModels,
requestyModels,
telemetrySetting,
setTelemetrySetting,
} = useExtensionState()
const [apiErrorMessage, setApiErrorMessage] = useState<string | undefined>(undefined)
const [modelIdErrorMessage, setModelIdErrorMessage] = useState<string | undefined>(undefined)
const [currentView, setCurrentView] = useState<View>("main")
const handleSubmit = () => {
const apiValidationResult = validateApiConfiguration(apiConfiguration)
const modelIdValidationResult = validateModelId(apiConfiguration, openRouterModels, requestyModels)
const modelIdValidationResult = validateModelId(apiConfiguration, openRouterModels)
setApiErrorMessage(apiValidationResult)
setModelIdErrorMessage(modelIdValidationResult)
@@ -67,6 +71,45 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
vscode.postMessage({ type: "resetState" })
}
if (currentView === "checkpoints") {
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 }}>Checkpoint Settings</h3>
<VSCodeButton onClick={() => setCurrentView("main")}>Done</VSCodeButton>
</div>
<div
style={{
flexGrow: 1,
overflowY: "scroll",
paddingRight: 8,
display: "flex",
flexDirection: "column",
}}>
<CheckpointsSettingsView />
</div>
</div>
)
}
return (
<div
style={{
@@ -127,6 +170,14 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
</p>
</div>
<div style={{ marginBottom: 5 }}>
<div style={{ fontWeight: "500", marginBottom: "8px" }}>Checkpoints</div>
<SettingsButton onClick={() => setCurrentView("checkpoints")}>
<i className="codicon codicon-bookmark" />
Configure Checkpoints
</SettingsButton>
</div>
<div style={{ marginBottom: 5 }}>
<VSCodeCheckbox
style={{ marginBottom: "5px" }}
@@ -0,0 +1,93 @@
import { memo } from "react"
import { ANTHROPIC_THINKING_BUDGET_TOKENS_MIN, anthropicModels, ApiConfiguration } from "../../../../src/shared/api"
import { vscode } from "../../utils/vscode"
import ClineSlider from "../common/cline-ui/ClineSlider"
interface ThinkingBudgetSliderProps {
apiConfiguration: ApiConfiguration | undefined
setApiConfiguration: (apiConfiguration: ApiConfiguration) => void
}
// Constants
const MIN_VALID_TOKENS = ANTHROPIC_THINKING_BUDGET_TOKENS_MIN
const MAX_PERCENTAGE = 0.8
const ThinkingBudgetSlider = ({ apiConfiguration, setApiConfiguration }: ThinkingBudgetSliderProps) => {
// Calculate max tokens based on model
const maxTokens = anthropicModels["claude-3-7-sonnet-20250219"].maxTokens
const maxSliderValue = Math.floor(maxTokens * MAX_PERCENTAGE)
const currentValue = apiConfiguration?.thinkingBudgetTokens || 0
// Style constants for reasoning level display
const labelStyle: React.CSSProperties = { color: "var(--vscode-editor-foreground)" }
const valueStyle: React.CSSProperties = {
color: "white",
backgroundColor: "var(--vscode-button-background)",
padding: "2px 6px",
borderRadius: "4px",
fontWeight: "bold",
}
// Handlers
const handleChange = (value: number) => {
if (!apiConfiguration) return
setApiConfiguration({
...apiConfiguration,
thinkingBudgetTokens: value,
})
}
const handleChangeEnd = (value: number) => {
if (!apiConfiguration) return
const validValue = getValidValue(value)
setApiConfiguration({
...apiConfiguration,
thinkingBudgetTokens: validValue,
})
vscode.postMessage({
type: "updateThinkingBudgetTokens",
number: validValue,
})
}
// Utility functions
const getValidValue = (value: number): number => (value === 0 ? 0 : Math.max(MIN_VALID_TOKENS, value))
const getReasoningLevel = (value: number, min: number, max: number): JSX.Element => {
let levelText: string
if (value === 0) {
levelText = "Off"
} else {
const percentage = (value - min) / (max - min)
levelText = percentage <= 1 / 3 ? "Low" : percentage <= 2 / 3 ? "Medium" : "High"
}
return (
<span>
<span style={labelStyle}>Reasoning:</span> <span style={valueStyle}>{levelText}</span>
</span>
)
}
return (
<ClineSlider
id="thinking-budget-slider"
label="Thinking tokens"
value={currentValue}
min={0}
max={maxSliderValue}
step={100}
onChange={handleChange}
onChangeEnd={handleChangeEnd}
validateValue={getValidValue}
dynamicColor={true}
getSecondaryLabel={getReasoningLevel}
description="Set to 0 to disable extended thinking. Higher values allow Claude to think more deeply before responding."
/>
)
}
export default memo(ThinkingBudgetSlider)
@@ -2,14 +2,7 @@ import React, { createContext, useCallback, useContext, useEffect, useState } fr
import { useEvent } from "react-use"
import { DEFAULT_AUTO_APPROVAL_SETTINGS } from "../../../src/shared/AutoApprovalSettings"
import { ExtensionMessage, ExtensionState, DEFAULT_PLATFORM } from "../../../src/shared/ExtensionMessage"
import {
ApiConfiguration,
ModelInfo,
openRouterDefaultModelId,
openRouterDefaultModelInfo,
requestyDefaultModelId,
requestyDefaultModelInfo,
} from "../../../src/shared/api"
import { ApiConfiguration, ModelInfo, openRouterDefaultModelId, openRouterDefaultModelInfo } from "../../../src/shared/api"
import { findLastIndex } from "../../../src/shared/array"
import { McpMarketplaceCatalog, McpServer } from "../../../src/shared/mcp"
import { convertTextMateToHljs } from "../utils/textMateToHljs"
@@ -23,7 +16,6 @@ interface ExtensionStateContextType extends ExtensionState {
showWelcome: boolean
theme: any
openRouterModels: Record<string, ModelInfo>
requestyModels: Record<string, ModelInfo>
openAiModels: string[]
mcpServers: McpServer[]
mcpMarketplaceCatalog: McpMarketplaceCatalog
@@ -59,9 +51,6 @@ export const ExtensionStateContextProvider: React.FC<{
const [openRouterModels, setOpenRouterModels] = useState<Record<string, ModelInfo>>({
[openRouterDefaultModelId]: openRouterDefaultModelInfo,
})
const [requestyModels, setRequestyModels] = useState<Record<string, ModelInfo>>({
[requestyDefaultModelId]: requestyDefaultModelInfo,
})
const [openAiModels, setOpenAiModels] = useState<string[]>([])
const [mcpServers, setMcpServers] = useState<McpServer[]>([])
@@ -76,7 +65,6 @@ export const ExtensionStateContextProvider: React.FC<{
? [
config.apiKey,
config.openRouterApiKey,
config.requestyApiKey,
config.awsRegion,
config.vertexProjectId,
config.openAiApiKey,
@@ -122,14 +110,6 @@ export const ExtensionStateContextProvider: React.FC<{
})
break
}
case "requestyModels": {
const updatedModels = message.requestyModels ?? {}
setRequestyModels({
[requestyDefaultModelId]: requestyDefaultModelInfo, // in case the extension sent a model list without the default model
...updatedModels,
})
break
}
case "openRouterModels": {
const updatedModels = message.openRouterModels ?? {}
setOpenRouterModels({
@@ -168,7 +148,6 @@ export const ExtensionStateContextProvider: React.FC<{
showWelcome,
theme,
openRouterModels,
requestyModels,
openAiModels,
mcpServers,
mcpMarketplaceCatalog,
+1 -6
View File
@@ -5,13 +5,8 @@ import "./index.css"
import App from "./App.tsx"
import "../../node_modules/@vscode/codicons/dist/codicon.css"
const apiKey = "phc_5WnLHpYyC30Bsb7VSJ6DzcPXZ34JSF08DJLyM7svZ15"
const apiHost = "https://us.i.posthog.com"
createRoot(document.getElementById("root")!).render(
<StrictMode>
<PostHogProvider apiKey={apiKey} options={{ api_host: apiHost }}>
<App />
</PostHogProvider>
<App />
</StrictMode>,
)
+4 -15
View File
@@ -1,4 +1,4 @@
import { ApiConfiguration, openRouterDefaultModelId, requestyDefaultModelId } from "../../../src/shared/api"
import { ApiConfiguration, openRouterDefaultModelId } from "../../../src/shared/api"
import { ModelInfo } from "../../../src/shared/api"
export function validateApiConfiguration(apiConfiguration?: ApiConfiguration): string | undefined {
if (apiConfiguration) {
@@ -91,26 +91,15 @@ export function validateApiConfiguration(apiConfiguration?: ApiConfiguration): s
export function validateModelId(
apiConfiguration?: ApiConfiguration,
openRouterModels?: Record<string, ModelInfo>,
requestyModels?: Record<string, ModelInfo>,
): string | undefined {
if (apiConfiguration) {
switch (apiConfiguration.apiProvider) {
case "openrouter":
const openRouterModelId = apiConfiguration.openRouterModelId || openRouterDefaultModelId // in case the user hasn't changed the model id, it will be undefined by default
if (!openRouterModelId) {
const modelId = apiConfiguration.openRouterModelId || openRouterDefaultModelId // in case the user hasn't changed the model id, it will be undefined by default
if (!modelId) {
return "You must provide a model ID."
}
if (openRouterModels && !Object.keys(openRouterModels).includes(openRouterModelId)) {
// even if the model list endpoint failed, extensionstatecontext will always have the default model info
return "The model ID you provided is not available. Please choose a different model."
}
break
case "requesty":
const requestyModelId = apiConfiguration.requestyModelId || requestyDefaultModelId // in case the user hasn't changed the model id, it will be undefined by default
if (!requestyModelId) {
return "You must provide a model ID."
}
if (requestyModels && !Object.keys(requestyModels).includes(requestyModelId)) {
if (openRouterModels && !Object.keys(openRouterModels).includes(modelId)) {
// even if the model list endpoint failed, extensionstatecontext will always have the default model info
return "The model ID you provided is not available. Please choose a different model."
}