mirror of
https://github.com/cline/cline.git
synced 2026-09-14 02:29:17 +08:00
Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f3661d9b9a | ||
|
|
21d4ec13e1 | ||
|
|
38e51bcf73 | ||
|
|
f40ff25d1e | ||
|
|
29c01eb7d3 | ||
|
|
682fce458e | ||
|
|
34e87af5f1 | ||
|
|
eaa76512fc | ||
|
|
3b5556ae6c | ||
|
|
df7b458229 | ||
|
|
560cefecca |
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Add correct cost and tokens info to Native OpenAI and DeepSeek providers
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Update Claude Sonnet 35. -> 3.7 in README(s)
|
||||
@@ -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.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
fix: make AWS Bedrock authentication predictable
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Add translation to CODE_OF_CONDUCT, CONTRIBUTING and README to Arabic ar-sa.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
A new MCP Marketplace display setting has been added to VSCode settings. (It is defaulted to "true")
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Added a .clinerules file with details about our most common features
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Make tags section in marketplace scrollable
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
opt: Enhance OpenAiHandler to diagnose request issues and handle empty streams
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
.clineignore should be included in .gitignore
|
||||
@@ -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.
|
||||
|
||||
---
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Migrate webview from CRA to Vite
|
||||
@@ -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.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Add rich MCP responses with images and link previews
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Add preferred language option to settings
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
GPT 4.5 Preview added
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Fix checking "Support Images" setting had no effect on first click
|
||||
@@ -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.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
feat(vertex): Add prompt caching support for Claude on Vertex AI
|
||||
@@ -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.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Update anthropic SDK to the latest version
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
re-added prompt caching to vertex
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
fix: Anthropic's default model is 3.7
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Adjustment to MCP server installation prompt
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
ADD IS_DEV and Hot Reloading to debug
|
||||
@@ -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
|
||||
|
||||
Generated
+9
-8
@@ -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
@@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 isn’t 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":
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface CheckpointSettings {
|
||||
/** Whether checkpoints are enabled */
|
||||
enableCheckpoints: boolean
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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",
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
Generated
+903
-1120
File diff suppressed because it is too large
Load Diff
@@ -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,
|
||||
|
||||
@@ -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>,
|
||||
)
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user