Compare commits

..

1 Commits

Author SHA1 Message Date
Andrei Edell 98b5285628 run prettier correctly on generated protos 2025-05-08 13:10:14 -10:00
157 changed files with 1757 additions and 4726 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Add the o4-mini model in the isOminiModel
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Allow option to collect events to send them in a bundle to avoid sending too many events
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
add open ai cache to ui
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
disable autocapture
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": minor
---
Refactor auto approval menu to use the existing modal architecture defined by the cline rules and mcp servers modals
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Update deployer team name
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Moved rule file conversions
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
suggested model exists again
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
getRelativePaths protobus migration
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
getOllamaModels protobus migration
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
Add FeatureFlagProvider service for the Node.js extension side
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
added copy buttons to task header and assistant messages
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Increasing file size that can be read by cline
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Add org to team affiliation check
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
fix-bedrock-api
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Add confirmation dialog to Delete All History button
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
breakpoint just in system prompt for gemini for OR and cline provider
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Fix for markdown copy excessive escape characters
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
fetch cache details from generation endpoint
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Bump ollama from 0.5.13 to 0.5.15
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
Add npm script for issue creation
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
Migrate more info section to new docs
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
Only initialize posthog in the webview if the user has opted into telemetry
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Extend ReasoningEffort to non-o3-mini reasoning models for all providers
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Re-enable tests in workflow
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
requestVsCodeLmModels protobus migration
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
Allow the user to form their next message while Cline is taking action
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Adding mistral medium-3 model
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
updated gemini caching for OR and cline provider
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
searchFiles protobus migration
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
Add ability to generate commit message via cline
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
updated OR/cline provider to automate gemini models caching
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
Add Task Favorites and several proto messages related to tasks
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
getLmStudioModels protobus migration
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
adding quote reply support
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
Add enterprise section to new docs
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Raise Errors when users try to upload images larger than 7500x7500 pixels
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Make Previous Updates in the Announcement a dropdown
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
Migrate prompting section to new docs
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
Migrate running models locally section to new docs
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
timestamp visualization
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
Allow the user to scroll when Cline is editing a file by disabling auto-scroll when the user scrolls up
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
Add image links to docs
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
Migrate MCP section to new docs
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
Migrate custom model config section to new docs
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Fixed an issue where loading never finished when using an application inference profile for the model ID
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Add support for Gemini Implicit Caching
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
add ui for windsurf and cursor rules
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Batch selection and deletion of tasks in history
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
Migrate getting-started section to new docs
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
Task Timeline
+1 -1
View File
@@ -209,7 +209,7 @@ class Task {
switch (chunk.type) {
case "text":
// Parse into content blocks
this.assistantMessageContent = parseAssistantMessageV2(chunk.text)
this.assistantMessageContent = parseAssistantMessage(chunk.text)
// Present blocks to user
await this.presentAssistantMessage()
break
+1 -1
View File
@@ -1,4 +1,4 @@
blank_issues_enabled: true
blank_issues_enabled: false
contact_links:
- name: ✨ Feature Request
url: https://github.com/cline/cline/discussions/categories/feature-requests?discussions_q=is%3Aopen+category%3A%22Feature+Requests%22+sort%3Atop
-1
View File
@@ -1,6 +1,5 @@
out
dist
dist-standalone
node_modules
tmp
.vscode-test/
-22
View File
@@ -16,28 +16,6 @@
"IS_DEV": "true",
"DEV_WORKSPACE_FOLDER": "${workspaceFolder}"
}
},
{
"name": "Run Extension (Fresh Install Mode)",
"type": "extensionHost",
"request": "launch",
"runtimeExecutable": "${execPath}",
"args": [
"--profile-temp",
"--sync",
"off",
"--disable-extensions",
"--extensionDevelopmentPath=${workspaceFolder}",
"${workspaceFolder}"
],
"outFiles": ["${workspaceFolder}/dist/**/*.js"],
"preLaunchTask": "clean-sandbox",
"internalConsoleOptions": "openOnSessionStart",
"postDebugTask": "stop",
"env": {
"IS_DEV": "true",
"DEV_WORKSPACE_FOLDER": "${workspaceFolder}"
}
}
]
}
-6
View File
@@ -185,12 +185,6 @@
"label": "stop",
"command": "echo ${input:terminate}",
"type": "shell"
},
{
"label": "clean-sandbox",
"type": "shell",
"dependsOn": ["watch"],
"command": "rm -rf .vscode-dev"
}
],
"inputs": [
-64
View File
@@ -1,69 +1,5 @@
# Changelog
## [3.15.4]
- Add gemini model back to vertex provider
- Add gemini telemetry
- Add filtering for tasks tied to the current workspace
## [3.15.3]
- Add Fireworks API Provider
- Fix minor visual issues with auto-approve menu
- Fix one instance of terminal not getting output
- Fix 'Chrome was launched but debug port is not responding' error
## [3.15.2]
- Added details to auto approve menu and more sensible default controls
- Add detailed configuration options for LiteLLM provider
- Add webview telemetry for users who have opted in to telemetry
- Update Gemini in OpenRouter/Cline providers to use implicit caching
- Fix freezing issues during rendering of large streaming text
- Fix grey screen webview crashes by releasing memory after every diff edit
- Fix breaking out of diff auto-scroll
- Fix IME composition Enter autosending edited message
## [3.15.1]
- Fix bug where PowerShell commands weren't given enough time before giving up and showing an error
## [3.15.0]
- Add Task Timeline visualization to tasks (Thanks eomcaleb!)
- Add cache to ui for OpenAi provider
- Add FeatureFlagProvider service for the Node.js extension side
- Add copy buttons to task header and assistant messages
- Add a more simplified home header was added
- Add ability to favorite a task, allowing it to be kept when clearing all tasks
- Add npm script for issue creation (Thanks DaveFres!)
- Add confirmation dialog to Delete All History button
- Add ability to allow the user to type their next message into the chat while Cline is taking action
- Add ability to generate commit message via cline (Thanks zapp88!)
- Add improvements to caching for gemini models on OpenRouter and Cline providers
- Add improvements to allow scrolling the file being edited.
- Add ui for windsurf and cursor rules
- Add mistral medium-3 model
- Add option to collect events to send them in a bundle to avoid sending too many events
- Add support to quote a previous message in chat
- Add support for Gemini Implicit Caching
- Add support for batch selection and deletion of tasks in history (Thanks danix800!)
- Update change suggested models
- Update fetch cache details from generation endpoint
- Update converted docs to Mintlify
- Update the isOminiModel to include o4-mini model (Thanks PeterDaveHello!)
- Update file size that can be read by Cline, allowing larger files
- Update defaults for bedrock API models (Thanks Watany!)
- Update to extend ReasoningEffort to non-o3-mini reasoning models for all providers (Thanks PeterDaveHello!)
- Update to give error when a user tries to upload an image larger than 7500x7500 pixels
- Update announcement so that previous updates are in a dropdown
- Update UI for auto approve with favorited settings
- Fix bug where certain terminal commands would lock you out of a task
- Fix only initialize posthog in the webview if the user has opted into telemetry
- Fix bug where autocapture was on for front-end telemetry
- Fix for markdown copy excessively escaping characters (Thanks weshoke!)
- Fix an issue where loading never finished when using an application inference profile for the model ID (Thanks WinterYukky!)
## [3.14.1]
- Disables autocaptures when initializing feature flags
+2 -2
View File
@@ -26,9 +26,9 @@ For complete transparency, you can inspect our [telemetry implementation](https:
### How to Opt Out
Telemetry in Cline is entirely optional:
Telemetry in Cline is entirely optional and requires your explicit consent:
- When you update or install our VS Code extension, you'll see a message about our anonymous telemetry
- When you update or install our VS Code extension, you'll see a simple prompt: "Help Improve Cline" with Allow or Deny options
- You can change your preference anytime in settings
Cline also respects VS Code's global telemetry settings. If you've disabled telemetry at the VS Code level, Cline's telemetry will automatically be disabled as well.
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "claude-dev",
"version": "3.15.4",
"version": "3.14.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "claude-dev",
"version": "3.15.4",
"version": "3.14.1",
"license": "Apache-2.0",
"dependencies": {
"@anthropic-ai/bedrock-sdk": "^0.12.4",
+35 -3
View File
@@ -2,7 +2,7 @@
"name": "claude-dev",
"displayName": "Cline",
"description": "Autonomous coding agent right in your IDE, capable of creating/editing files, running commands, using the browser, and more with your permission every step of the way.",
"version": "3.15.4",
"version": "3.14.1",
"icon": "assets/icons/icon.png",
"engines": {
"vscode": "^1.84.0"
@@ -40,8 +40,6 @@
"llama"
],
"activationEvents": [
"onLanguage",
"onStartupFinished",
"workspaceContains:evals.env"
],
"main": "./dist/extension.js",
@@ -237,11 +235,45 @@
"configuration": {
"title": "Cline",
"properties": {
"cline.vsCodeLmModelSelector": {
"type": "object",
"properties": {
"vendor": {
"type": "string",
"description": "The vendor of the language model (e.g. copilot)"
},
"family": {
"type": "string",
"description": "The family of the language model (e.g. gpt-4)"
}
},
"description": "Settings for VSCode Language Model API"
},
"cline.enableCheckpoints": {
"type": "boolean",
"default": true,
"description": "Enables extension to save checkpoints of workspace throughout the task. Uses git under the hood which may not work well with large workspaces."
},
"cline.disableBrowserTool": {
"type": "boolean",
"default": false,
"description": "Disables extension from spawning browser session."
},
"cline.modelSettings.o3Mini.reasoningEffort": {
"type": "string",
"enum": [
"low",
"medium",
"high"
],
"default": "medium",
"description": "Controls the reasoning effort when using an OpenAI reasoning model. Higher values may result in more thorough but slower responses."
},
"cline.chromeExecutablePath": {
"type": "string",
"default": null,
"description": "Path to Chrome executable for browser use functionality. If not set, the extension will attempt to find or download it automatically."
},
"cline.preferredLanguage": {
"type": "string",
"enum": [
-4
View File
@@ -40,8 +40,6 @@ message BrowserSettings {
Viewport viewport = 1;
optional string remote_browser_host = 2;
optional bool remote_browser_enabled = 3;
optional string chrome_executable_path = 4;
optional bool disable_tool_use = 5;
}
message UpdateBrowserSettingsRequest {
@@ -49,6 +47,4 @@ message UpdateBrowserSettingsRequest {
Viewport viewport = 2;
optional string remote_browser_host = 3;
optional bool remote_browser_enabled = 4;
optional string chrome_executable_path = 5;
optional bool disable_tool_use = 6;
}
+21 -34
View File
@@ -29,7 +29,6 @@ const serviceNameMap = {
task: "cline.TaskService",
web: "cline.WebService",
models: "cline.ModelsService",
slash: "cline.SlashService",
// Add new services here - no other code changes needed!
}
const serviceDirs = Object.keys(serviceNameMap).map((serviceKey) => path.join(ROOT_DIR, "src", "core", "controller", serviceKey))
@@ -55,42 +54,30 @@ async function main() {
// Process all proto files
console.log(chalk.cyan("Processing proto files from"), SCRIPT_DIR)
const protoFiles = await globby("*.proto", { cwd: SCRIPT_DIR, absolute: true })
const protoFiles = await globby("*.proto", { cwd: SCRIPT_DIR })
// Build the protoc command with proper path handling for cross-platform
const tsProtocCommand = [
protoc,
`--proto_path="${SCRIPT_DIR}"`,
`--plugin=protoc-gen-ts_proto="${tsProtoPlugin}"`,
`--ts_proto_out="${TS_OUT_DIR}"`,
"--ts_proto_opt=outputServices=generic-definitions,env=node,esModuleInterop=true,useDate=false,useOptionals=messages",
...protoFiles,
].join(" ")
try {
console.log(chalk.cyan(`Generating TypeScript code for:\n${protoFiles.join("\n")}...`))
execSync(tsProtocCommand, { stdio: "inherit" })
} catch (error) {
console.error(chalk.red("Error generating TypeScript for proto files:"), error)
process.exit(1)
}
for (const protoFile of protoFiles) {
console.log(chalk.cyan(`Generating TypeScript code for ${protoFile}...`))
const descriptorOutDir = path.join(ROOT_DIR, "dist-standalone", "proto")
await fs.mkdir(descriptorOutDir, { recursive: true })
// Build the protoc command with proper path handling for cross-platform
const protocCommand = [
protoc,
`--plugin=protoc-gen-ts_proto="${tsProtoPlugin}"`,
`--ts_proto_out="${TS_OUT_DIR}"`,
"--ts_proto_opt=outputServices=generic-definitions,env=node,esModuleInterop=true,useDate=false,useOptionals=messages",
`--proto_path="${SCRIPT_DIR}"`,
`"${path.join(SCRIPT_DIR, protoFile)}"`,
].join(" ")
const descriptorFile = path.join(descriptorOutDir, "descriptor_set.pb")
const descriptorProtocCommand = [
protoc,
`--proto_path="${SCRIPT_DIR}"`,
`--descriptor_set_out="${descriptorFile}"`,
"--include_imports",
...protoFiles,
].join(" ")
try {
console.log(chalk.cyan("Generating descriptor set..."))
execSync(descriptorProtocCommand, { stdio: "inherit" })
} catch (error) {
console.error(chalk.red("Error generating descriptor set for proto file:"), error)
process.exit(1)
try {
const execOptions = {
stdio: "inherit",
}
execSync(protocCommand, execOptions)
} catch (error) {
console.error(chalk.red(`Error generating TypeScript for ${protoFile}:`), error)
process.exit(1)
}
}
console.log(chalk.green("Protocol Buffer code generation completed successfully."))
-1
View File
@@ -10,7 +10,6 @@ service McpService {
rpc toggleMcpServer(ToggleMcpServerRequest) returns (McpServers);
rpc updateMcpTimeout(UpdateMcpTimeoutRequest) returns (McpServers);
rpc addRemoteMcpServer(AddRemoteMcpServerRequest) returns (McpServers);
rpc downloadMcp(StringRequest) returns (Empty);
}
message ToggleMcpServerRequest {
-31
View File
@@ -14,12 +14,6 @@ service ModelsService {
rpc getLmStudioModels(StringRequest) returns (StringArray);
// Fetches available models from VS Code LM API
rpc getVsCodeLmModels(EmptyRequest) returns (VsCodeLmModelsArray);
// Refreshes and returns OpenRouter models
rpc refreshOpenRouterModels(EmptyRequest) returns (OpenRouterCompatibleModelInfo);
// Refreshes and returns OpenAI models
rpc refreshOpenAiModels(OpenAiModelsRequest) returns (StringArray);
// Refreshes and returns Requesty models
rpc refreshRequestyModels(EmptyRequest) returns (OpenRouterCompatibleModelInfo);
}
// List of VS Code LM models
@@ -34,28 +28,3 @@ message VsCodeLmModel {
string version = 3;
string id = 4;
}
// For OpenRouterCompatibleModelInfo structure in OpenRouterModels
message OpenRouterModelInfo {
int32 max_tokens = 1;
int32 context_window = 2;
bool supports_images = 3;
bool supports_prompt_cache = 4;
double input_price = 5;
double output_price = 6;
double cache_writes_price = 7;
double cache_reads_price = 8;
string description = 9;
}
// Shared response message for model information
message OpenRouterCompatibleModelInfo {
map<string, OpenRouterModelInfo> models = 1;
}
// Request for fetching OpenAI models
message OpenAiModelsRequest {
Metadata metadata = 1;
string baseUrl = 2;
string apiKey = 3;
}
-14
View File
@@ -1,14 +0,0 @@
syntax = "proto3";
package cline;
option java_package = "bot.cline.proto";
option java_multiple_files = true;
import "common.proto";
// SlashService provides methods for managing slash
service SlashService {
// Sends button click message
rpc reportBug(StringRequest) returns (Empty);
rpc condense(StringRequest) returns (Empty);
}
-1
View File
@@ -6,7 +6,6 @@ import "common.proto";
service StateService {
rpc getLatestState(EmptyRequest) returns (State);
rpc subscribeToState(EmptyRequest) returns (stream State);
rpc toggleFavoriteModel(StringRequest) returns (Empty);
}
message State {
-1
View File
@@ -67,7 +67,6 @@ message GetTaskHistoryRequest {
bool favorites_only = 2;
string search_query = 3;
string sort_by = 4;
bool current_workspace_only = 5;
}
// Response for task history
-3
View File
@@ -19,7 +19,6 @@ import { DoubaoHandler } from "./providers/doubao"
import { VsCodeLmHandler } from "./providers/vscode-lm"
import { ClineHandler } from "./providers/cline"
import { LiteLlmHandler } from "./providers/litellm"
import { FireworksHandler } from "./providers/fireworks"
import { AskSageHandler } from "./providers/asksage"
import { XAIHandler } from "./providers/xai"
import { SambanovaHandler } from "./providers/sambanova"
@@ -59,8 +58,6 @@ export function buildApiHandler(configuration: ApiConfiguration): ApiHandler {
return new DeepSeekHandler(options)
case "requesty":
return new RequestyHandler(options)
case "fireworks":
return new FireworksHandler(options)
case "together":
return new TogetherHandler(options)
case "qwen":
-94
View File
@@ -1,94 +0,0 @@
import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI from "openai"
import { withRetry } from "../retry"
import { ApiHandler } from ".."
import {
ApiHandlerOptions,
DeepSeekModelId,
ModelInfo,
deepSeekDefaultModelId,
deepSeekModels,
openAiModelInfoSaneDefaults,
} from "../../shared/api"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { ApiStream } from "../transform/stream"
export class FireworksHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: OpenAI
constructor(options: ApiHandlerOptions) {
this.options = options
this.client = new OpenAI({
baseURL: "https://api.fireworks.ai/inference/v1",
apiKey: this.options.fireworksApiKey,
})
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const modelId = this.options.fireworksModelId ?? ""
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
{ role: "system", content: systemPrompt },
...convertToOpenAiMessages(messages),
]
const stream = await this.client.chat.completions.create({
model: modelId,
...(this.options.fireworksModelMaxCompletionTokens
? { max_completion_tokens: this.options.fireworksModelMaxCompletionTokens }
: {}),
...(this.options.fireworksModelMaxTokens ? { max_tokens: this.options.fireworksModelMaxTokens } : {}),
messages: openAiMessages,
stream: true,
stream_options: { include_usage: true },
temperature: 0,
})
let reasoning: string | null = null
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta
if (reasoning || delta?.content?.includes("<think>")) {
reasoning = (reasoning || "") + (delta.content ?? "")
}
if (delta?.content && !reasoning) {
yield {
type: "text",
text: delta.content,
}
}
if (reasoning || ("reasoning_content" in delta && delta.reasoning_content)) {
yield {
type: "reasoning",
reasoning: delta.content || ((delta as any).reasoning_content as string | undefined) || "",
}
if (reasoning?.includes("</think>")) {
// Reset so the next chunk is regular content
reasoning = null
}
}
if (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,
}
}
}
}
getModel(): { id: string; info: ModelInfo } {
return {
id: this.options.fireworksModelId ?? "",
info: openAiModelInfoSaneDefaults,
}
}
}
+43 -91
View File
@@ -6,7 +6,6 @@ import { ApiHandler } from "../"
import { ApiHandlerOptions, geminiDefaultModelId, GeminiModelId, geminiModels, ModelInfo } from "@shared/api"
import { convertAnthropicMessageToGemini } from "../transform/gemini-format"
import { ApiStream } from "../transform/stream"
import { telemetryService } from "@services/posthog/telemetry/TelemetryService"
// Define a default TTL for the cache (e.g., 15 minutes in seconds)
const DEFAULT_CACHE_TTL_SECONDS = 900
@@ -75,7 +74,7 @@ export class GeminiHandler implements ApiHandler {
*/
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const { id: modelId, info } = this.getModel()
const { id: model, info } = this.getModel()
const contents = messages.map(convertAnthropicMessageToGemini)
// Configure thinking budget if supported
@@ -99,99 +98,52 @@ export class GeminiHandler implements ApiHandler {
}
// Generate content using the configured parameters
const sdkCallStartTime = Date.now()
let sdkFirstChunkTime: number | undefined
let ttftSdkMs: number | undefined
let apiSuccess = false
let apiError: string | undefined
let promptTokens = 0
let outputTokens = 0
let cacheReadTokens = 0
const result = await this.client.models.generateContentStream({
model,
contents: contents,
config: {
...requestConfig,
},
})
// Track usage metadata
let lastUsageMetadata: GenerateContentResponseUsageMetadata | undefined
try {
const result = await this.client.models.generateContentStream({
model: modelId,
contents: contents,
config: {
...requestConfig,
},
// Process the stream
for await (const chunk of result) {
if (chunk.text) {
yield {
type: "text",
text: chunk.text,
}
}
if (chunk.usageMetadata) {
lastUsageMetadata = chunk.usageMetadata
}
}
// Yield usage information at the end
if (lastUsageMetadata) {
const inputTokens = lastUsageMetadata.promptTokenCount ?? 0
const outputTokens = lastUsageMetadata.candidatesTokenCount ?? 0
const cacheReadTokens = lastUsageMetadata.cachedContentTokenCount
// Calculate immediate costs
const totalCost = this.calculateCost({
info,
inputTokens,
outputTokens,
cacheReadTokens,
})
let isFirstSdkChunk = true
for await (const chunk of result) {
if (isFirstSdkChunk) {
sdkFirstChunkTime = Date.now()
ttftSdkMs = sdkFirstChunkTime - sdkCallStartTime
isFirstSdkChunk = false
}
if (chunk.text) {
yield {
type: "text",
text: chunk.text,
}
}
if (chunk.usageMetadata) {
lastUsageMetadata = chunk.usageMetadata
promptTokens = lastUsageMetadata.promptTokenCount ?? promptTokens
outputTokens = lastUsageMetadata.candidatesTokenCount ?? outputTokens
cacheReadTokens = lastUsageMetadata.cachedContentTokenCount ?? cacheReadTokens
}
}
apiSuccess = true
if (lastUsageMetadata) {
const totalCost = this.calculateCost({
info,
inputTokens: promptTokens,
outputTokens,
cacheReadTokens,
})
yield {
type: "usage",
inputTokens: promptTokens,
outputTokens,
cacheReadTokens,
cacheWriteTokens: 0,
totalCost,
}
}
} catch (error) {
apiSuccess = false
apiError = error instanceof Error ? error.message : String(error)
// Let the error propagate to be handled by withRetry or Task.ts
// Telemetry will be sent in the finally block.
throw error
} finally {
const sdkCallEndTime = Date.now()
const totalDurationSdkMs = sdkCallEndTime - sdkCallStartTime
const cacheHit = cacheReadTokens > 0
const cacheHitPercentage = promptTokens > 0 ? (cacheReadTokens / promptTokens) * 100 : undefined
const throughputTokensPerSecSdk =
totalDurationSdkMs > 0 && outputTokens > 0 ? outputTokens / (totalDurationSdkMs / 1000) : undefined
if (this.options.taskId) {
telemetryService.captureGeminiApiPerformance(
this.options.taskId,
modelId,
{
ttftSec: ttftSdkMs !== undefined ? ttftSdkMs / 1000 : undefined,
totalDurationSec: totalDurationSdkMs / 1000,
promptTokens,
outputTokens,
cacheReadTokens,
cacheHit,
cacheHitPercentage,
apiSuccess,
apiError,
throughputTokensPerSec: throughputTokensPerSecSdk,
},
true,
)
} else {
console.warn("GeminiHandler: taskId not available for telemetry in createMessage.")
yield {
type: "usage",
inputTokens,
outputTokens,
cacheReadTokens,
cacheWriteTokens: 0,
totalCost,
}
}
}
+2 -2
View File
@@ -65,7 +65,7 @@ export class LiteLlmHandler implements ApiHandler {
const reasoningOn = budgetTokens !== 0 ? true : false
const thinkingConfig = reasoningOn ? { type: "enabled", budget_tokens: budgetTokens } : undefined
let temperature: number | undefined = this.options.liteLlmModelInfo?.temperature ?? 0
let temperature: number | undefined = 0
if (isOminiModel && reasoningOn) {
temperature = undefined // Thinking mode doesn't support temperature
@@ -169,7 +169,7 @@ export class LiteLlmHandler implements ApiHandler {
getModel() {
return {
id: this.options.liteLlmModelId || liteLlmDefaultModelId,
info: this.options.liteLlmModelInfo || liteLlmModelInfoSaneDefaults,
info: liteLlmModelInfoSaneDefaults,
}
}
}
+68
View File
@@ -75,6 +75,74 @@ export async function createOpenRouterStream(
break
}
// handles gemini caching logic
if (model.id.startsWith("google/") && model.info.supportsPromptCache) {
// gemini only uses the last breakpoint for caching, so the others will be ignored
openAiMessages[0] = {
role: "system",
content: [
{
type: "text",
text: systemPrompt,
// @ts-ignore-next-line
cache_control: { type: "ephemeral" },
},
],
}
// for safety, but this should always be the case
if (openAiMessages.length >= 2) {
const msg = openAiMessages[1]
if (msg) {
if (typeof msg.content === "string") {
msg.content = [{ type: "text", text: msg.content }]
}
if (Array.isArray(msg.content)) {
// NOTE: this is fine since env details will always be added at the end. but if it weren't there, and the user added a image_url type message, it would pop a text part before it and then move it after to the end.
let lastTextPart = msg.content.filter((part) => part.type === "text").pop()
if (!lastTextPart) {
lastTextPart = { type: "text", text: "..." }
msg.content.push(lastTextPart)
}
// @ts-ignore-next-line
lastTextPart["cache_control"] = { type: "ephemeral" }
}
}
}
// it doesn't make sense to alter breakpoints at all with the gemini cache implementation at this time
/*const GEMINI_CACHE_USER_MESSAGE_INTERVAL = 4 // add new breakpoint every 4 turns
const userMessages = openAiMessages.filter((msg) => msg.role === "user")
const userMessageCount = userMessages.length
const targetUserMessageNumber =
Math.floor(userMessageCount / GEMINI_CACHE_USER_MESSAGE_INTERVAL) * GEMINI_CACHE_USER_MESSAGE_INTERVAL
if (targetUserMessageNumber > 0) {
// otherwise dont need to add a breakpoint
const msg = userMessages[targetUserMessageNumber - 1]
if (msg) {
if (typeof msg.content === "string") {
msg.content = [{ type: "text", text: msg.content }]
}
if (Array.isArray(msg.content)) {
// NOTE: this is fine since env details will always be added at the end. but if it weren't there, and the user added a image_url type message, it would pop a text part before it and then move it after to the end.
let lastTextPart = msg.content.filter((part) => part.type === "text").pop()
if (!lastTextPart) {
lastTextPart = { type: "text", text: "..." }
msg.content.push(lastTextPart)
}
// @ts-ignore-next-line
lastTextPart["cache_control"] = { type: "ephemeral" }
}
}
}*/
}
// Not sure how openrouter defaults max tokens when no value is provided, but the anthropic api requires this value and since they offer both 4096 and 8192 variants, we should ensure 8192.
// (models usually default to max tokens allowed)
let maxTokens: number | undefined
+1 -7
View File
@@ -1,6 +1,6 @@
export type AssistantMessageContent = TextContent | ToolUse
export { parseAssistantMessageV1, parseAssistantMessageV2 } from "./parse-assistant-message"
export { parseAssistantMessage } from "./parse-assistant-message"
export interface TextContent {
type: "text"
@@ -25,7 +25,6 @@ export const toolUseNames = [
"attempt_completion",
"new_task",
"condense",
"report_bug",
"new_rule",
] as const
@@ -54,11 +53,6 @@ export const toolParamNames = [
"response",
"result",
"context",
"title",
"what_happened",
"steps_to_reproduce",
"api_request_output",
"additional_context",
] as const
export type ToolParamName = (typeof toolParamNames)[number]
@@ -1,24 +1,6 @@
import { AssistantMessageContent, TextContent, ToolUse, ToolParamName, toolParamNames, toolUseNames, ToolUseName } from "." // Assuming types are defined in index.ts or a similar file
import { AssistantMessageContent, TextContent, ToolUse, ToolParamName, toolParamNames, toolUseNames, ToolUseName } from "."
/**
* @description **Version 1**
* Parses an assistant message string potentially containing mixed text and tool usage blocks
* marked with XML-like tags into an array of structured content objects.
*
* This version iterates through the message character by character, building an accumulator string.
* It maintains state to track whether it's currently parsing text, a tool use block, or a specific tool parameter.
* It detects the start and end of tool uses and parameters by checking if the accumulator ends with
* the corresponding opening or closing tags.
* Special handling is included for `write_to_file` and `new_rule` tool uses to correctly parse
* the `content` parameter, which might contain the closing tag itself, by looking for the *last*
* occurrence of the closing tag.
* If the input string ends mid-tag or mid-content, the last block (text or tool use) is marked as partial.
*
* @param assistantMessage The raw string output from the assistant.
* @returns An array of `AssistantMessageContent` objects, which can be `TextContent` or `ToolUse`.
* Blocks that were not fully closed by the end of the input string will have their `partial` flag set to `true`.
*/
export function parseAssistantMessageV1(assistantMessage: string): AssistantMessageContent[] {
export function parseAssistantMessage(assistantMessage: string) {
const contentBlocks: AssistantMessageContent[] = []
let currentTextContent: TextContent | undefined = undefined
let currentTextContentStartIndex = 0
@@ -32,56 +14,46 @@ export function parseAssistantMessageV1(assistantMessage: string): AssistantMess
const char = assistantMessage[i]
accumulator += char
// --- State: Parsing a Tool Parameter ---
// there should not be a param without a tool use
if (currentToolUse && currentParamName) {
const currentParamValue = accumulator.slice(currentParamValueStartIndex)
const paramClosingTag = `</${currentParamName}>`
if (currentParamValue.endsWith(paramClosingTag)) {
// End of param value found
// end of param value
currentToolUse.params[currentParamName] = currentParamValue.slice(0, -paramClosingTag.length).trim()
currentParamName = undefined // Go back to parsing tool content or looking for next param
continue // Move to next character
currentParamName = undefined
continue
} else {
// Partial param value is accumulating
continue // Move to next character
// partial param value is accumulating
continue
}
}
// --- State: Parsing a Tool Use (but not a specific parameter) ---
// no currentParamName
if (currentToolUse) {
const currentToolValue = accumulator.slice(currentToolUseStartIndex)
const toolUseClosingTag = `</${currentToolUse.name}>`
if (currentToolValue.endsWith(toolUseClosingTag)) {
// End of a tool use found
// end of a tool use
currentToolUse.partial = false
contentBlocks.push(currentToolUse)
currentToolUse = undefined // Go back to parsing text or looking for next tool
// Reset text start index in case text follows immediately
currentTextContentStartIndex = i + 1
continue // Move to next character
currentToolUse = undefined
continue
} else {
// Check if starting a new parameter within the current tool use
const possibleParamOpeningTags = toolParamNames.map((name) => `<${name}>`)
let foundParamStart = false
for (const paramOpeningTag of possibleParamOpeningTags) {
if (accumulator.endsWith(paramOpeningTag)) {
// Start of a new parameter found
// start of a new parameter
currentParamName = paramOpeningTag.slice(1, -1) as ToolParamName
currentParamValueStartIndex = accumulator.length
foundParamStart = true
break
}
}
if (foundParamStart) {
continue // Move to next character
}
// Special case for write_to_file/new_rule content param allowing nested tags
// Check if a </content> tag appears, potentially indicating the end of the content param
// even if the main tool closing tag hasn't been seen yet.
// there's no current param, and not starting a new param
// special case for write_to_file where file contents could contain the closing tag, in which case the param would have closed and we end up with the rest of the file contents here. To work around this, we get the string between the starting content tag and the LAST content tag.
const contentParamName: ToolParamName = "content"
if (
(currentToolUse.name === "write_to_file" || currentToolUse.name === "new_rule") &&
@@ -91,384 +63,73 @@ export function parseAssistantMessageV1(assistantMessage: string): AssistantMess
const contentStartTag = `<${contentParamName}>`
const contentEndTag = `</${contentParamName}>`
const contentStartIndex = toolContent.indexOf(contentStartTag) + contentStartTag.length
// Use lastIndexOf to handle cases where </content> might appear within the content itself
const contentEndIndex = toolContent.lastIndexOf(contentEndTag)
// Ensure we found valid start/end tags and end is after start
if (
contentStartIndex !== -1 &&
contentEndIndex !== -1 &&
contentEndIndex > contentStartIndex - contentStartTag.length // Ensure end tag is after start tag begins
) {
// Check if this content param was already being parsed. If so, update it.
// If not, and we just found the closing tag, assign it.
// This handles cases where the </content> detection might fire before
// the <content> tag detection logic, or if the content is very short.
if (currentParamName === contentParamName) {
// Already parsing content, now we found the end tag
currentToolUse.params[contentParamName] = toolContent.slice(contentStartIndex, contentEndIndex).trim()
currentParamName = undefined // Finished with this param
} else if (currentParamName === undefined) {
// Not parsing a param, but found </content>. Assume it closes the content block.
currentToolUse.params[contentParamName] = toolContent.slice(contentStartIndex, contentEndIndex).trim()
// We stay in the "parsing tool use" state, looking for more params or the tool end tag.
}
if (contentStartIndex !== -1 && contentEndIndex !== -1 && contentEndIndex > contentStartIndex) {
currentToolUse.params[contentParamName] = toolContent.slice(contentStartIndex, contentEndIndex).trim()
}
}
// If none of the above, partial tool value is accumulating
continue // Move to next character
// partial tool value is accumulating
continue
}
}
// --- State: Parsing Text (or looking for start of a tool use) ---
// no currentToolUse
let didStartToolUse = false
const possibleToolUseOpeningTags = toolUseNames.map((name) => `<${name}>`)
for (const toolUseOpeningTag of possibleToolUseOpeningTags) {
if (accumulator.endsWith(toolUseOpeningTag)) {
// Start of a new tool use found
const toolName = toolUseOpeningTag.slice(1, -1) as ToolUseName
// start of a new tool use
currentToolUse = {
type: "tool_use",
name: toolName,
name: toolUseOpeningTag.slice(1, -1) as ToolUseName,
params: {},
partial: true,
}
currentToolUseStartIndex = accumulator.length
// This also indicates the end of the current text content block (if any)
// this also indicates the end of the current text content
if (currentTextContent) {
currentTextContent.partial = false
// Extract text content, removing the part that formed the tool opening tag
const textEndIndex = accumulator.length - toolUseOpeningTag.length
currentTextContent.content = accumulator.slice(currentTextContentStartIndex, textEndIndex).trim()
// Only add if there's actual content
if (currentTextContent.content.length > 0) {
contentBlocks.push(currentTextContent)
}
// remove the partially accumulated tool use tag from the end of text (<tool)
currentTextContent.content = currentTextContent.content
.slice(0, -toolUseOpeningTag.slice(0, -1).length)
.trim()
contentBlocks.push(currentTextContent)
currentTextContent = undefined
} else {
// Check if there was text before this tool use started
const textEndIndex = accumulator.length - toolUseOpeningTag.length
const potentialText = accumulator.slice(currentTextContentStartIndex, textEndIndex).trim()
if (potentialText.length > 0) {
contentBlocks.push({
type: "text",
content: potentialText,
partial: false, // Ended because tool use started
})
}
}
didStartToolUse = true
break // Found tool start, stop checking for others
break
}
}
if (!didStartToolUse) {
// No tool use started, so it must be text content accumulating
// (or continuing after a closed tool use)
// no tool use, so it must be text either at the beginning or between tools
if (currentTextContent === undefined) {
// Start of a new text block
currentTextContentStartIndex = i - (accumulator.length - currentTextContentStartIndex - 1) // Adjust start index based on how much we've accumulated since the last block ended or the beginning
// If accumulator starts from 0, start index is i
if (contentBlocks.length === 0 && currentToolUse === undefined) {
currentTextContentStartIndex = accumulator.length - 1 // i
} else {
// Re-calculate based on the actual start of the current text segment
// Find the end of the last block
let lastBlockEndIndex = 0
if (contentBlocks.length > 0) {
const lastBlock = contentBlocks[contentBlocks.length - 1]
// Approximation: find where the accumulator matches the end of the message string representation of the last block. This is complex.
// Simpler: Assume text starts right after the last block ended implicitly at index i.
lastBlockEndIndex = i // Where the loop *was* when the last block finished processing
// Need a more robust way to track the end index of the *raw string* corresponding to the last block.
// Let's stick to the accumulator slice approach for simplicity in this version.
// The start index should be where the current *unmatched* text began.
let lastProcessedIndex = -1
if (contentBlocks.length > 0) {
// This requires knowing the raw string length of the previous block, which V1 doesn't explicitly track easily.
// We'll approximate based on the current accumulator and start index logic.
// The issue arises if a tool tag was just closed. accumulator contains everything up to i.
// lastBlockEndIndex should point to the character *after* the closing tag of the last block.
}
// Reset start index to the beginning of the *current* potential text block
currentTextContentStartIndex = accumulator.length - 1 // Start accumulating from the current character `i`
}
// If we just closed a tool, text starts *after* its closing tag
// The logic needs refinement here for accurate start index after a tool closure.
// Let's assume for now the start index logic inside the loop handles it via slicing.
}
currentTextContent = {
type: "text",
content: "", // Content will be filled by slicing accumulator
partial: true,
}
currentTextContentStartIndex = i
}
currentTextContent = {
type: "text",
content: accumulator.slice(currentTextContentStartIndex).trim(),
partial: true,
}
// Update text content based on the accumulator from its start index
currentTextContent.content = accumulator.slice(currentTextContentStartIndex).trimStart() // Trim start to avoid leading space if text follows tool
}
} // End of loop
}
// --- Finalization after loop ---
// If a tool use was open at the end
if (currentToolUse) {
// If a parameter was open within that tool use
// stream did not complete tool call, add it as partial
if (currentParamName) {
// The remaining accumulator content belongs to this partial parameter
// tool call has a parameter that was not completed
currentToolUse.params[currentParamName] = accumulator.slice(currentParamValueStartIndex).trim()
}
// Add the potentially partial tool use block
contentBlocks.push(currentToolUse)
}
// If text content was being accumulated at the end
// Note: Only one of currentToolUse or currentTextContent can be defined here,
// as starting a tool use finalizes the preceding text block.
else if (currentTextContent) {
// Update content one last time
currentTextContent.content = accumulator.slice(currentTextContentStartIndex).trim()
// Add the potentially partial text block only if it contains content
if (currentTextContent.content.length > 0) {
contentBlocks.push(currentTextContent)
}
}
return contentBlocks
}
/**
* @description **Version 2**
* Parses an assistant message string potentially containing mixed text and tool usage blocks
* marked with XML-like tags into an array of structured content objects.
*
* This version aims for efficiency by avoiding the character-by-character accumulator of V1.
* It iterates through the string using an index `i`. At each position, it checks if the substring
* *ending* at `i` matches any known opening or closing tags for tools or parameters using `startsWith`
* with an offset.
* It uses pre-computed Maps (`toolUseOpenTags`, `toolParamOpenTags`) for quick tag lookups.
* State is managed using indices (`currentTextContentStart`, `currentToolUseStart`, `currentParamValueStart`)
* pointing to the start of the current block within the original `assistantMessage` string.
* Slicing is used to extract content only when a block (text, parameter, or tool use) is completed.
* Special handling for `write_to_file` and `new_rule` content parameters is included, using `indexOf`
* and `lastIndexOf` on the relevant slice to handle potentially nested closing tags.
* If the input string ends mid-block, the last open block is added and marked as partial.
*
* @param assistantMessage The raw string output from the assistant.
* @returns An array of `AssistantMessageContent` objects, which can be `TextContent` or `ToolUse`.
* Blocks that were not fully closed by the end of the input string will have their `partial` flag set to `true`.
*/
export function parseAssistantMessageV2(assistantMessage: string): AssistantMessageContent[] {
const contentBlocks: AssistantMessageContent[] = []
let currentTextContentStart = 0 // Index where the current text block started
let currentTextContent: TextContent | undefined = undefined
let currentToolUseStart = 0 // Index *after* the opening tag of the current tool use
let currentToolUse: ToolUse | undefined = undefined
let currentParamValueStart = 0 // Index *after* the opening tag of the current param
let currentParamName: ToolParamName | undefined = undefined
// Precompute tags for faster lookups
const toolUseOpenTags = new Map<string, ToolUseName>()
const toolParamOpenTags = new Map<string, ToolParamName>()
for (const name of toolUseNames) {
toolUseOpenTags.set(`<${name}>`, name)
}
for (const name of toolParamNames) {
toolParamOpenTags.set(`<${name}>`, name)
}
const len = assistantMessage.length
for (let i = 0; i < len; i++) {
const currentCharIndex = i
// --- State: Parsing a Tool Parameter ---
if (currentToolUse && currentParamName) {
const closeTag = `</${currentParamName}>`
// Check if the string *ending* at index `i` matches the closing tag
if (
currentCharIndex >= closeTag.length - 1 &&
assistantMessage.startsWith(
closeTag,
currentCharIndex - closeTag.length + 1, // Start checking from potential start of tag
)
) {
// Found the closing tag for the parameter
const value = assistantMessage
.slice(
currentParamValueStart, // Start after the opening tag
currentCharIndex - closeTag.length + 1, // End before the closing tag
)
.trim()
currentToolUse.params[currentParamName] = value
currentParamName = undefined // Go back to parsing tool content
// We don't continue loop here, need to check for tool close or other params at index i
} else {
continue // Still inside param value, move to next char
}
}
// --- State: Parsing a Tool Use (but not a specific parameter) ---
if (currentToolUse && !currentParamName) {
// Ensure we are not inside a parameter already
// Check if starting a new parameter
let startedNewParam = false
for (const [tag, paramName] of toolParamOpenTags.entries()) {
if (currentCharIndex >= tag.length - 1 && assistantMessage.startsWith(tag, currentCharIndex - tag.length + 1)) {
currentParamName = paramName
currentParamValueStart = currentCharIndex + 1 // Value starts after the tag
startedNewParam = true
break
}
}
if (startedNewParam) {
continue // Handled start of param, move to next char
}
// Check if closing the current tool use
const toolCloseTag = `</${currentToolUse.name}>`
if (
currentCharIndex >= toolCloseTag.length - 1 &&
assistantMessage.startsWith(toolCloseTag, currentCharIndex - toolCloseTag.length + 1)
) {
// End of the tool use found
// Special handling for content params *before* finalizing the tool
const toolContentSlice = assistantMessage.slice(
currentToolUseStart, // From after the tool opening tag
currentCharIndex - toolCloseTag.length + 1, // To before the tool closing tag
)
// Check if content parameter needs special handling (write_to_file/new_rule)
// This check is important if the closing </content> tag was missed by the parameter parsing logic
// (e.g., if content is empty or parsing logic prioritizes tool close)
const contentParamName: ToolParamName = "content"
if (
currentToolUse.name === "write_to_file" /* || currentToolUse.name === "new_rule" */ &&
toolContentSlice.includes(`<${contentParamName}>`)
) {
const contentStartTag = `<${contentParamName}>`
const contentEndTag = `</${contentParamName}>`
const contentStart = toolContentSlice.indexOf(contentStartTag)
// Use lastIndexOf for robustness against nested tags
const contentEnd = toolContentSlice.lastIndexOf(contentEndTag)
if (contentStart !== -1 && contentEnd !== -1 && contentEnd > contentStart) {
const contentValue = toolContentSlice.slice(contentStart + contentStartTag.length, contentEnd).trim()
currentToolUse.params[contentParamName] = contentValue
}
}
currentToolUse.partial = false // Mark as complete
contentBlocks.push(currentToolUse)
currentToolUse = undefined // Reset state
currentTextContentStart = currentCharIndex + 1 // Potential text starts after this tag
continue // Move to next char
}
// If not starting a param and not closing the tool, continue accumulating tool content implicitly
continue
}
// --- State: Parsing Text / Looking for Tool Start ---
if (!currentToolUse) {
// Check if starting a new tool use
let startedNewTool = false
for (const [tag, toolName] of toolUseOpenTags.entries()) {
if (currentCharIndex >= tag.length - 1 && assistantMessage.startsWith(tag, currentCharIndex - tag.length + 1)) {
// End current text block if one was active
if (currentTextContent) {
currentTextContent.content = assistantMessage
.slice(
currentTextContentStart, // From where text started
currentCharIndex - tag.length + 1, // To before the tool tag starts
)
.trim()
currentTextContent.partial = false // Ended because tool started
if (currentTextContent.content.length > 0) {
contentBlocks.push(currentTextContent)
}
currentTextContent = undefined
} else {
// Check for any text between the last block and this tag
const potentialText = assistantMessage
.slice(
currentTextContentStart, // From where text *might* have started
currentCharIndex - tag.length + 1, // To before the tool tag starts
)
.trim()
if (potentialText.length > 0) {
contentBlocks.push({
type: "text",
content: potentialText,
partial: false,
})
}
}
// Start the new tool use
currentToolUse = {
type: "tool_use",
name: toolName,
params: {},
partial: true, // Assume partial until closing tag is found
}
currentToolUseStart = currentCharIndex + 1 // Tool content starts after the opening tag
startedNewTool = true
break
}
}
if (startedNewTool) {
continue // Handled start of tool, move to next char
}
// If not starting a tool, it must be text content
if (!currentTextContent) {
// Start a new text block if we aren't already in one
currentTextContentStart = currentCharIndex // Text starts at the current character
// Check if the current char is the start of potential text *immediately* after a tag
// This needs the previous state - simpler to let slicing handle it later.
// Resetting start index accurately is key.
// It should be the index *after* the last processed tag.
// The logic managing currentTextContentStart after closing tags handles this.
currentTextContent = {
type: "text",
content: "", // Will be determined by slicing at the end or when a tool starts
partial: true,
}
}
// Continue accumulating text implicitly; content is extracted later.
}
} // End of loop
// --- Finalization after loop ---
// Finalize any open parameter within an open tool use
if (currentToolUse && currentParamName) {
currentToolUse.params[currentParamName] = assistantMessage
.slice(currentParamValueStart) // From param start to end of string
.trim()
// Tool use remains partial
}
// Finalize any open tool use (which might contain the finalized partial param)
if (currentToolUse) {
// Tool use is partial because the loop finished before its closing tag
contentBlocks.push(currentToolUse)
}
// Finalize any trailing text content
// Only possible if a tool use wasn't open at the very end
else if (currentTextContent) {
currentTextContent.content = assistantMessage
.slice(currentTextContentStart) // From text start to end of string
.trim()
// Text is partial because the loop finished
if (currentTextContent.content.length > 0) {
contentBlocks.push(currentTextContent)
}
// Note: it doesn't matter if check for currentToolUse or currentTextContent, only one of them will be defined since only one can be partial at a time
if (currentTextContent) {
// stream did not complete text content, add it as partial
contentBlocks.push(currentTextContent)
}
return contentBlocks
@@ -2,7 +2,6 @@ import * as vscode from "vscode"
import crypto from "crypto"
import { Controller } from "../index"
import { storeSecret } from "../../storage/state"
import { EmptyRequest, String } from "../../../shared/proto/common"
/**
* Handles the user clicking the login link in the UI.
@@ -12,7 +11,7 @@ import { EmptyRequest, String } from "../../../shared/proto/common"
* @param controller The controller instance.
* @returns The login URL as a string.
*/
export async function accountLoginClicked(controller: Controller, unused: EmptyRequest): Promise<String> {
export async function accountLoginClicked(controller: Controller): Promise<String> {
// Generate nonce for state validation
const nonce = crypto.randomBytes(32).toString("hex")
await storeSecret(controller.context, "authNonce", nonce)
@@ -26,8 +25,6 @@ export async function accountLoginClicked(controller: Controller, unused: EmptyR
const authUrl = vscode.Uri.parse(
`https://app.cline.bot/auth?state=${encodeURIComponent(nonce)}&callback_url=${encodeURIComponent(`${uriScheme || "vscode"}://saoudrizwan.claude-dev/auth`)}`,
)
await vscode.env.openExternal(authUrl)
return {
value: authUrl.toString(),
}
vscode.env.openExternal(authUrl)
return authUrl.toString()
}
@@ -1,8 +1,8 @@
import { UpdateBrowserSettingsRequest } from "../../../shared/proto/browser"
import { Boolean } from "../../../shared/proto/common"
import { Controller } from "../index"
import { updateGlobalState, getGlobalState } from "../../storage/state"
import { BrowserSettings as SharedBrowserSettings, DEFAULT_BROWSER_SETTINGS } from "../../../shared/BrowserSettings"
import { updateGlobalState } from "../../storage/state"
import { BrowserSettings as SharedBrowserSettings } from "../../../shared/BrowserSettings"
/**
* Update browser settings
@@ -12,39 +12,23 @@ import { BrowserSettings as SharedBrowserSettings, DEFAULT_BROWSER_SETTINGS } fr
*/
export async function updateBrowserSettings(controller: Controller, request: UpdateBrowserSettingsRequest): Promise<Boolean> {
try {
// Get current browser settings to preserve fields not in the request
const currentSettings = (await getGlobalState(controller.context, "browserSettings")) as SharedBrowserSettings | undefined
const mergedWithDefaults = { ...DEFAULT_BROWSER_SETTINGS, ...currentSettings }
// Convert from protobuf format to shared format, merging with existing settings
const newBrowserSettings: SharedBrowserSettings = {
...mergedWithDefaults, // Start with existing settings (and defaults)
// Convert from protobuf format to shared format
const browserSettings: SharedBrowserSettings = {
viewport: {
// Apply updates from request
width: request.viewport?.width || mergedWithDefaults.viewport.width,
height: request.viewport?.height || mergedWithDefaults.viewport.height,
width: request.viewport?.width || 900,
height: request.viewport?.height || 600,
},
// Explicitly handle optional boolean and string fields from the request
remoteBrowserEnabled:
request.remoteBrowserEnabled === undefined
? mergedWithDefaults.remoteBrowserEnabled
: request.remoteBrowserEnabled,
remoteBrowserHost:
request.remoteBrowserHost === undefined ? mergedWithDefaults.remoteBrowserHost : request.remoteBrowserHost,
chromeExecutablePath:
// If chromeExecutablePath is explicitly in the request (even as ""), use it.
// Otherwise, fall back to mergedWithDefaults.
"chromeExecutablePath" in request ? request.chromeExecutablePath : mergedWithDefaults.chromeExecutablePath,
disableToolUse: request.disableToolUse === undefined ? mergedWithDefaults.disableToolUse : request.disableToolUse,
remoteBrowserEnabled: request.remoteBrowserEnabled || false,
remoteBrowserHost: request.remoteBrowserHost || undefined,
}
// Update global state with new settings
await updateGlobalState(controller.context, "browserSettings", newBrowserSettings)
await updateGlobalState(controller.context, "browserSettings", browserSettings)
// Update task browser settings if task exists
if (controller.task) {
controller.task.browserSettings = newBrowserSettings
controller.task.browserSession.browserSettings = newBrowserSettings
controller.task.browserSettings = browserSettings
controller.task.browserSession.browserSettings = browserSettings
}
// Post updated state to webview
@@ -12,7 +12,6 @@ import { handleStateServiceRequest, handleStateServiceStreamingRequest } from ".
import { handleTaskServiceRequest, handleTaskServiceStreamingRequest } from "./task/index"
import { handleWebServiceRequest, handleWebServiceStreamingRequest } from "./web/index"
import { handleModelsServiceRequest, handleModelsServiceStreamingRequest } from "./models/index"
import { handleSlashServiceRequest, handleSlashServiceStreamingRequest } from "./slash/index"
/**
* Configuration for a service handler
@@ -68,8 +67,4 @@ export const serviceHandlers: Record<string, ServiceHandlerConfig> = {
requestHandler: handleModelsServiceRequest,
streamingHandler: handleModelsServiceStreamingRequest,
},
"cline.SlashService": {
requestHandler: handleSlashServiceRequest,
streamingHandler: handleSlashServiceStreamingRequest,
},
}
+351 -13
View File
@@ -1,5 +1,6 @@
import { Anthropic } from "@anthropic-ai/sdk"
import axios from "axios"
import type { AxiosRequestConfig } from "axios"
import fs from "fs/promises"
import { setTimeout as setTimeoutPromise } from "node:timers/promises"
@@ -7,8 +8,6 @@ import pWaitFor from "p-wait-for"
import * as path from "path"
import * as vscode from "vscode"
import { handleGrpcRequest, handleGrpcRequestCancel } from "./grpc-handler"
import { handleModelsServiceRequest } from "./models"
import { EmptyRequest } from "@shared/proto/common"
import { buildApiHandler } from "@api/index"
import { cleanupLegacyCheckpoints } from "@integrations/checkpoints/CheckpointMigration"
import { downloadTask } from "@integrations/misc/export-markdown"
@@ -66,7 +65,7 @@ export class Controller {
workspaceTracker: WorkspaceTracker
mcpHub: McpHub
accountService: ClineAccountService
private latestAnnouncementId = "may-09-2025_17:11:00" // update to some unique identifier when we add a new announcement
private latestAnnouncementId = "may-02-2025_16:27:00" // update to some unique identifier when we add a new announcement
constructor(
readonly context: vscode.ExtensionContext,
@@ -231,15 +230,15 @@ export class Controller {
}
})
this.silentlyRefreshMcpMarketplace()
handleModelsServiceRequest(this, "refreshOpenRouterModels", EmptyRequest.create()).then(async (response) => {
if (response && response.models) {
this.refreshOpenRouterModels().then(async (openRouterModels) => {
if (openRouterModels) {
// 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 getAllExtensionState(this.context)
if (apiConfiguration.openRouterModelId && response.models[apiConfiguration.openRouterModelId]) {
if (apiConfiguration.openRouterModelId) {
await updateGlobalState(
this.context,
"openRouterModelInfo",
response.models[apiConfiguration.openRouterModelId],
openRouterModels[apiConfiguration.openRouterModelId],
)
await this.postStateToWebview()
}
@@ -249,7 +248,7 @@ export class Controller {
// If user already opted in to telemetry, enable telemetry service
this.getStateToPostToWebview().then((state) => {
const { telemetrySetting } = state
const isOptedIn = telemetrySetting !== "disabled"
const isOptedIn = telemetrySetting === "enabled"
telemetryService.updateTelemetryState(isOptedIn)
})
break
@@ -271,6 +270,9 @@ export class Controller {
// initializing new instance of Cline will make sure that any agentically running promises in old instance don't affect our new task. this essentially creates a fresh slate for the new task
await this.initTask(message.text, message.images)
break
case "condense":
this.task?.handleWebviewAskResponse("yesButtonClicked")
break
case "apiConfiguration":
if (message.apiConfiguration) {
await updateApiConfiguration(this.context, message.apiConfiguration)
@@ -328,6 +330,17 @@ export class Controller {
case "resetState":
await this.resetState()
break
case "refreshOpenRouterModels":
await this.refreshOpenRouterModels()
break
case "refreshRequestyModels":
await this.refreshRequestyModels()
break
case "refreshOpenAiModels":
const { apiConfiguration } = await getAllExtensionState(this.context)
const openAiModels = await this.getOpenAiModels(apiConfiguration.openAiBaseUrl, apiConfiguration.openAiApiKey)
this.postMessageToWebview({ type: "openAiModels", openAiModels })
break
case "refreshClineRules":
await refreshClineRulesToggles(this.context, cwd)
await refreshExternalRulesToggles(this.context, cwd)
@@ -373,6 +386,19 @@ export class Controller {
await this.fetchMcpMarketplace(message.bool)
break
}
case "downloadMcp": {
if (message.mcpId) {
// 1. Toggle to act mode if we are in plan mode
const { chatSettings } = await this.getStateToPostToWebview()
if (chatSettings.mode === "plan") {
await this.togglePlanActModeWithChatSettings({ mode: "act" })
}
// 2. download MCP
await this.downloadMcp(message.mcpId)
}
break
}
case "silentlyRefreshMcpMarketplace": {
await this.silentlyRefreshMcpMarketplace()
break
@@ -590,6 +616,27 @@ export class Controller {
this.postMessageToWebview({ type: "relinquishControl" })
break
}
case "toggleFavoriteModel": {
if (message.modelId) {
const { apiConfiguration } = await getAllExtensionState(this.context)
const favoritedModelIds = apiConfiguration.favoritedModelIds || []
// Toggle favorite status
const updatedFavorites = favoritedModelIds.includes(message.modelId)
? favoritedModelIds.filter((id) => id !== message.modelId)
: [...favoritedModelIds, message.modelId]
await updateGlobalState(this.context, "favoritedModelIds", updatedFavorites)
// Capture telemetry for model favorite toggle
const isFavorited = !favoritedModelIds.includes(message.modelId)
telemetryService.captureModelFavoritesUsage(message.modelId, isFavorited)
// Post state to webview without changing any other configuration
await this.postStateToWebview()
}
break
}
case "grpc_request": {
if (message.grpc_request) {
await handleGrpcRequest(this, message.grpc_request)
@@ -633,7 +680,7 @@ export class Controller {
async updateTelemetrySetting(telemetrySetting: TelemetrySetting) {
await updateGlobalState(this.context, "telemetrySetting", telemetrySetting)
const isOptedIn = telemetrySetting !== "disabled"
const isOptedIn = telemetrySetting === "enabled"
telemetryService.updateTelemetryState(isOptedIn)
}
@@ -713,7 +760,6 @@ export class Controller {
break
case "litellm":
await updateGlobalState(this.context, "previousModeModelId", apiConfiguration.liteLlmModelId)
await updateGlobalState(this.context, "previousModeModelInfo", apiConfiguration.liteLlmModelInfo)
break
case "requesty":
await updateGlobalState(this.context, "previousModeModelId", apiConfiguration.requestyModelId)
@@ -767,8 +813,7 @@ export class Controller {
await updateGlobalState(this.context, "lmStudioModelId", newModelId)
break
case "litellm":
await updateGlobalState(this.context, "previousModeModelId", apiConfiguration.liteLlmModelId)
await updateGlobalState(this.context, "previousModeModelInfo", apiConfiguration.liteLlmModelInfo)
await updateGlobalState(this.context, "liteLlmModelId", newModelId)
break
case "requesty":
await updateGlobalState(this.context, "requestyModelId", newModelId)
@@ -987,6 +1032,118 @@ export class Controller {
}
}
private async downloadMcp(mcpId: string) {
try {
// First check if we already have this MCP server installed
const servers = this.mcpHub?.getServers() || []
const isInstalled = servers.some((server: McpServer) => server.name === mcpId)
if (isInstalled) {
throw new Error("This MCP server is already installed")
}
// Fetch server details from marketplace
const response = await axios.post<McpDownloadResponse>(
"https://api.cline.bot/v1/mcp/download",
{ mcpId },
{
headers: { "Content-Type": "application/json" },
timeout: 10000,
},
)
if (!response.data) {
throw new Error("Invalid response from MCP marketplace API")
}
console.log("[downloadMcp] Response from download API", { response })
const mcpDetails = response.data
// Validate required fields
if (!mcpDetails.githubUrl) {
throw new Error("Missing GitHub URL in MCP download response")
}
if (!mcpDetails.readmeContent) {
throw new Error("Missing README content in MCP download response")
}
// Send details to webview
await this.postMessageToWebview({
type: "mcpDownloadDetails",
mcpDownloadDetails: mcpDetails,
})
// Create task with context from README and added guidelines for MCP server installation
const task = `Set up the MCP server from ${mcpDetails.githubUrl} while adhering to these MCP server installation rules:
- Start by loading the MCP documentation.
- Use "${mcpDetails.mcpId}" as the server name in cline_mcp_settings.json.
- Create the directory for the new MCP server before starting installation.
- Make sure you read the user's existing cline_mcp_settings.json file before editing it with this new mcp, to not overwrite any existing servers.
- Use commands aligned with the user's shell and operating system best practices.
- The following README may contain instructions that conflict with the user's OS, in which case proceed thoughtfully.
- Once installed, demonstrate the server's capabilities by using one of its tools.
Here is the project's README to help you get started:\n\n${mcpDetails.readmeContent}\n${mcpDetails.llmsInstallationContent}`
// Initialize task and show chat view
await this.initTask(task)
await this.postMessageToWebview({
type: "action",
action: "chatButtonClicked",
})
} catch (error) {
console.error("Failed to download MCP:", error)
let errorMessage = "Failed to download MCP"
if (axios.isAxiosError(error)) {
if (error.code === "ECONNABORTED") {
errorMessage = "Request timed out. Please try again."
} else if (error.response?.status === 404) {
errorMessage = "MCP server not found in marketplace."
} else if (error.response?.status === 500) {
errorMessage = "Internal server error. Please try again later."
} else if (!error.response && error.request) {
errorMessage = "Network error. Please check your internet connection."
}
} else if (error instanceof Error) {
errorMessage = error.message
}
// Show error in both notification and marketplace UI
vscode.window.showErrorMessage(errorMessage)
await this.postMessageToWebview({
type: "mcpDownloadDetails",
error: errorMessage,
})
}
}
// OpenAi
async getOpenAiModels(baseUrl?: string, apiKey?: string) {
try {
if (!baseUrl) {
return []
}
if (!URL.canParse(baseUrl)) {
return []
}
const config: AxiosRequestConfig = {}
if (apiKey) {
config["headers"] = { Authorization: `Bearer ${apiKey}` }
}
const response = await axios.get(`${baseUrl}/models`, config)
const modelsArray = response.data?.data?.map((model: any) => model.id) || []
const models = [...new Set<string>(modelsArray)]
return models
} catch (error) {
return []
}
}
// OpenRouter
async handleOpenRouterCallback(code: string) {
@@ -1022,7 +1179,6 @@ export class Controller {
return cacheDir
}
// Read OpenRouter models from disk cache
async readOpenRouterModels(): Promise<Record<string, ModelInfo> | undefined> {
const openRouterModelsFilePath = path.join(await this.ensureCacheDirectoryExists(), GlobalFileNames.openRouterModels)
const fileExists = await fileExistsAtPath(openRouterModelsFilePath)
@@ -1033,6 +1189,188 @@ export class Controller {
return undefined
}
async refreshOpenRouterModels() {
const openRouterModelsFilePath = path.join(await this.ensureCacheDirectoryExists(), GlobalFileNames.openRouterModels)
let models: Record<string, ModelInfo> = {}
try {
const response = await axios.get("https://openrouter.ai/api/v1/models")
/*
{
"id": "anthropic/claude-3.5-sonnet",
"name": "Anthropic: Claude 3.5 Sonnet",
"created": 1718841600,
"description": "Claude 3.5 Sonnet delivers better-than-Opus capabilities, faster-than-Sonnet speeds, at the same Sonnet prices. Sonnet is particularly good at:\n\n- Coding: Autonomously writes, edits, and runs code with reasoning and troubleshooting\n- Data science: Augments human data science expertise; navigates unstructured data while using multiple tools for insights\n- Visual processing: excelling at interpreting charts, graphs, and images, accurately transcribing text to derive insights beyond just the text alone\n- Agentic tasks: exceptional tool use, making it great at agentic tasks (i.e. complex, multi-step problem solving tasks that require engaging with other systems)\n\n#multimodal",
"context_length": 200000,
"architecture": {
"modality": "text+image-\u003Etext",
"tokenizer": "Claude",
"instruct_type": null
},
"pricing": {
"prompt": "0.000003",
"completion": "0.000015",
"image": "0.0048",
"request": "0"
},
"top_provider": {
"context_length": 200000,
"max_completion_tokens": 8192,
"is_moderated": true
},
"per_request_limits": null
},
*/
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: parsePrice(rawModel.pricing?.prompt),
outputPrice: parsePrice(rawModel.pricing?.completion),
description: rawModel.description,
}
switch (rawModel.id) {
case "anthropic/claude-3-7-sonnet":
case "anthropic/claude-3-7-sonnet:beta":
case "anthropic/claude-3.7-sonnet":
case "anthropic/claude-3.7-sonnet:beta":
case "anthropic/claude-3.7-sonnet:thinking":
case "anthropic/claude-3.5-sonnet":
case "anthropic/claude-3.5-sonnet:beta":
// NOTE: this needs to be synced with api.ts/openrouter default model info
modelInfo.supportsPromptCache = true
modelInfo.cacheWritesPrice = 3.75
modelInfo.cacheReadsPrice = 0.3
break
case "anthropic/claude-3.5-sonnet-20240620":
case "anthropic/claude-3.5-sonnet-20240620:beta":
modelInfo.supportsPromptCache = true
modelInfo.cacheWritesPrice = 3.75
modelInfo.cacheReadsPrice = 0.3
break
case "anthropic/claude-3-5-haiku":
case "anthropic/claude-3-5-haiku:beta":
case "anthropic/claude-3-5-haiku-20241022":
case "anthropic/claude-3-5-haiku-20241022:beta":
case "anthropic/claude-3.5-haiku":
case "anthropic/claude-3.5-haiku:beta":
case "anthropic/claude-3.5-haiku-20241022":
case "anthropic/claude-3.5-haiku-20241022:beta":
modelInfo.supportsPromptCache = true
modelInfo.cacheWritesPrice = 1.25
modelInfo.cacheReadsPrice = 0.1
break
case "anthropic/claude-3-opus":
case "anthropic/claude-3-opus:beta":
modelInfo.supportsPromptCache = true
modelInfo.cacheWritesPrice = 18.75
modelInfo.cacheReadsPrice = 1.5
break
case "anthropic/claude-3-haiku":
case "anthropic/claude-3-haiku:beta":
modelInfo.supportsPromptCache = true
modelInfo.cacheWritesPrice = 0.3
modelInfo.cacheReadsPrice = 0.03
break
case "deepseek/deepseek-chat":
modelInfo.supportsPromptCache = true
// see api.ts/deepSeekModels for more info
modelInfo.inputPrice = 0
modelInfo.cacheWritesPrice = 0.14
modelInfo.cacheReadsPrice = 0.014
break
default:
if (rawModel.id.startsWith("openai/")) {
modelInfo.cacheReadsPrice = parsePrice(rawModel.pricing?.input_cache_read)
if (modelInfo.cacheReadsPrice) {
modelInfo.supportsPromptCache = true
modelInfo.cacheWritesPrice = parsePrice(rawModel.pricing?.input_cache_write)
// openrouter charges no cache write pricing for openAI models
}
} else if (rawModel.id.startsWith("google/")) {
modelInfo.cacheReadsPrice = parsePrice(rawModel.pricing?.input_cache_read)
if (modelInfo.cacheReadsPrice) {
modelInfo.supportsPromptCache = true
modelInfo.cacheWritesPrice = parsePrice(rawModel.pricing?.input_cache_write)
}
}
break
}
models[rawModel.id] = modelInfo
}
} else {
console.error("Invalid response from OpenRouter API")
}
await fs.writeFile(openRouterModelsFilePath, JSON.stringify(models))
console.log("OpenRouter models fetched and saved", models)
} catch (error) {
console.error("Error fetching OpenRouter models:", error)
}
await this.postMessageToWebview({
type: "openRouterModels",
openRouterModels: models,
})
return models
}
async refreshRequestyModels() {
const parsePrice = (price: any) => {
if (price) {
return parseFloat(price) * 1_000_000
}
return undefined
}
let models: Record<string, ModelInfo> = {}
try {
const apiKey = await getSecret(this.context, "requestyApiKey")
const headers = {
Authorization: `Bearer ${apiKey}`,
}
const response = await axios.get("https://router.requesty.ai/v1/models", { headers })
if (response.data?.data) {
for (const model of response.data.data) {
const modelInfo: ModelInfo = {
maxTokens: model.max_output_tokens || undefined,
contextWindow: model.context_window,
supportsImages: model.supports_vision || undefined,
supportsPromptCache: model.supports_caching || undefined,
inputPrice: parsePrice(model.input_price),
outputPrice: parsePrice(model.output_price),
cacheWritesPrice: parsePrice(model.caching_price),
cacheReadsPrice: parsePrice(model.cached_price),
description: model.description,
}
models[model.id] = modelInfo
}
console.log("Requesty models fetched", 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
}
// Context menus and code actions
getFileMentionFromPath(filePath: string) {
-114
View File
@@ -1,114 +0,0 @@
import { Controller } from ".."
import { Empty, StringRequest } from "../../../shared/proto/common"
import { McpServer, McpDownloadResponse } from "@shared/mcp"
import axios from "axios"
import * as vscode from "vscode"
/**
* Download an MCP server from the marketplace
* @param controller The controller instance
* @param request The request containing the MCP ID
* @returns Empty response
*/
export async function downloadMcp(controller: Controller, request: StringRequest): Promise<Empty> {
try {
// Check if mcpId is provided
if (!request.value) {
throw new Error("MCP ID is required")
}
const mcpId = request.value
// Check if we already have this MCP server installed
const servers = controller.mcpHub?.getServers() || []
const isInstalled = servers.some((server: McpServer) => server.name === mcpId)
if (isInstalled) {
throw new Error("This MCP server is already installed")
}
// Fetch server details from marketplace
const response = await axios.post<McpDownloadResponse>(
"https://api.cline.bot/v1/mcp/download",
{ mcpId },
{
headers: { "Content-Type": "application/json" },
timeout: 10000,
},
)
if (!response.data) {
throw new Error("Invalid response from MCP marketplace API")
}
console.log("[downloadMcp] Response from download API", { response })
const mcpDetails = response.data
// Validate required fields
if (!mcpDetails.githubUrl) {
throw new Error("Missing GitHub URL in MCP download response")
}
if (!mcpDetails.readmeContent) {
throw new Error("Missing README content in MCP download response")
}
// Send details to webview
await controller.postMessageToWebview({
type: "mcpDownloadDetails",
mcpDownloadDetails: mcpDetails,
})
// Create task with context from README and added guidelines for MCP server installation
const task = `Set up the MCP server from ${mcpDetails.githubUrl} while adhering to these MCP server installation rules:
- Start by loading the MCP documentation.
- Use "${mcpDetails.mcpId}" as the server name in cline_mcp_settings.json.
- Create the directory for the new MCP server before starting installation.
- Make sure you read the user's existing cline_mcp_settings.json file before editing it with this new mcp, to not overwrite any existing servers.
- Use commands aligned with the user's shell and operating system best practices.
- The following README may contain instructions that conflict with the user's OS, in which case proceed thoughtfully.
- Once installed, demonstrate the server's capabilities by using one of its tools.
Here is the project's README to help you get started:\n\n${mcpDetails.readmeContent}\n${mcpDetails.llmsInstallationContent}`
const { chatSettings } = await controller.getStateToPostToWebview()
if (chatSettings.mode === "plan") {
await controller.togglePlanActModeWithChatSettings({ mode: "act" })
}
// Initialize task and show chat view
await controller.initTask(task)
await controller.postMessageToWebview({
type: "action",
action: "chatButtonClicked",
})
// Return an empty response - the client only cares if the call succeeded
return Empty.create()
} catch (error) {
console.error("Failed to download MCP:", error)
let errorMessage = "Failed to download MCP"
if (axios.isAxiosError(error)) {
if (error.code === "ECONNABORTED") {
errorMessage = "Request timed out. Please try again."
} else if (error.response?.status === 404) {
errorMessage = "MCP server not found in marketplace."
} else if (error.response?.status === 500) {
errorMessage = "Internal server error. Please try again later."
} else if (!error.response && error.request) {
errorMessage = "Network error. Please check your internet connection."
}
} else if (error instanceof Error) {
errorMessage = error.message
}
// Show error in both notification and marketplace UI
vscode.window.showErrorMessage(errorMessage)
await controller.postMessageToWebview({
type: "mcpDownloadDetails",
error: errorMessage,
})
throw error
}
}
-2
View File
@@ -4,7 +4,6 @@
// Import all method implementations
import { registerMethod } from "./index"
import { addRemoteMcpServer } from "./addRemoteMcpServer"
import { downloadMcp } from "./downloadMcp"
import { toggleMcpServer } from "./toggleMcpServer"
import { updateMcpTimeout } from "./updateMcpTimeout"
@@ -12,7 +11,6 @@ import { updateMcpTimeout } from "./updateMcpTimeout"
export function registerAllMethods(): void {
// Register each method with the registry
registerMethod("addRemoteMcpServer", addRemoteMcpServer)
registerMethod("downloadMcp", downloadMcp)
registerMethod("toggleMcpServer", toggleMcpServer)
registerMethod("updateMcpTimeout", updateMcpTimeout)
}
-6
View File
@@ -6,9 +6,6 @@ import { registerMethod } from "./index"
import { getLmStudioModels } from "./getLmStudioModels"
import { getOllamaModels } from "./getOllamaModels"
import { getVsCodeLmModels } from "./getVsCodeLmModels"
import { refreshOpenAiModels } from "./refreshOpenAiModels"
import { refreshOpenRouterModels } from "./refreshOpenRouterModels"
import { refreshRequestyModels } from "./refreshRequestyModels"
// Register all models service methods
export function registerAllMethods(): void {
@@ -16,7 +13,4 @@ export function registerAllMethods(): void {
registerMethod("getLmStudioModels", getLmStudioModels)
registerMethod("getOllamaModels", getOllamaModels)
registerMethod("getVsCodeLmModels", getVsCodeLmModels)
registerMethod("refreshOpenAiModels", refreshOpenAiModels)
registerMethod("refreshOpenRouterModels", refreshOpenRouterModels)
registerMethod("refreshRequestyModels", refreshRequestyModels)
}
@@ -1,37 +0,0 @@
import { Controller } from ".."
import { OpenAiModelsRequest } from "../../../shared/proto/models"
import { StringArray } from "../../../shared/proto/common"
import axios from "axios"
import type { AxiosRequestConfig } from "axios"
/**
* Fetches available models from the OpenAI API
* @param controller The controller instance
* @param request Request containing the base URL and API key
* @returns Array of model names
*/
export async function refreshOpenAiModels(controller: Controller, request: OpenAiModelsRequest): Promise<StringArray> {
try {
if (!request.baseUrl) {
return StringArray.create({ values: [] })
}
if (!URL.canParse(request.baseUrl)) {
return StringArray.create({ values: [] })
}
const config: AxiosRequestConfig = {}
if (request.apiKey) {
config["headers"] = { Authorization: `Bearer ${request.apiKey}` }
}
const response = await axios.get(`${request.baseUrl}/models`, config)
const modelsArray = response.data?.data?.map((model: any) => model.id) || []
const models = [...new Set<string>(modelsArray)]
return StringArray.create({ values: models })
} catch (error) {
console.error("Error fetching OpenAI models:", error)
return StringArray.create({ values: [] })
}
}
@@ -1,175 +0,0 @@
import { Controller } from ".."
import { EmptyRequest } from "../../../shared/proto/common"
import { OpenRouterCompatibleModelInfo, OpenRouterModelInfo } from "../../../shared/proto/models"
import axios from "axios"
import path from "path"
import fs from "fs/promises"
import { fileExistsAtPath } from "@utils/fs"
import { GlobalFileNames } from "@core/storage/disk"
/**
* Refreshes the OpenRouter models and returns the updated model list
* @param controller The controller instance
* @param request Empty request object
* @returns Response containing the OpenRouter models
*/
export async function refreshOpenRouterModels(
controller: Controller,
request: EmptyRequest,
): Promise<OpenRouterCompatibleModelInfo> {
const openRouterModelsFilePath = path.join(await ensureCacheDirectoryExists(controller), GlobalFileNames.openRouterModels)
let models: Record<string, Partial<OpenRouterModelInfo>> = {}
try {
const response = await axios.get("https://openrouter.ai/api/v1/models")
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: Partial<OpenRouterModelInfo> = {
maxTokens: rawModel.top_provider?.max_completion_tokens,
contextWindow: rawModel.context_length,
supportsImages: rawModel.architecture?.modality?.includes("image"),
supportsPromptCache: false,
inputPrice: parsePrice(rawModel.pricing?.prompt),
outputPrice: parsePrice(rawModel.pricing?.completion),
description: rawModel.description,
}
switch (rawModel.id) {
case "anthropic/claude-3-7-sonnet":
case "anthropic/claude-3-7-sonnet:beta":
case "anthropic/claude-3.7-sonnet":
case "anthropic/claude-3.7-sonnet:beta":
case "anthropic/claude-3.7-sonnet:thinking":
case "anthropic/claude-3.5-sonnet":
case "anthropic/claude-3.5-sonnet:beta":
// NOTE: this needs to be synced with api.ts/openrouter default model info
modelInfo.supportsPromptCache = true
modelInfo.cacheWritesPrice = 3.75
modelInfo.cacheReadsPrice = 0.3
break
case "anthropic/claude-3.5-sonnet-20240620":
case "anthropic/claude-3.5-sonnet-20240620:beta":
modelInfo.supportsPromptCache = true
modelInfo.cacheWritesPrice = 3.75
modelInfo.cacheReadsPrice = 0.3
break
case "anthropic/claude-3-5-haiku":
case "anthropic/claude-3-5-haiku:beta":
case "anthropic/claude-3-5-haiku-20241022":
case "anthropic/claude-3-5-haiku-20241022:beta":
case "anthropic/claude-3.5-haiku":
case "anthropic/claude-3.5-haiku:beta":
case "anthropic/claude-3.5-haiku-20241022":
case "anthropic/claude-3.5-haiku-20241022:beta":
modelInfo.supportsPromptCache = true
modelInfo.cacheWritesPrice = 1.25
modelInfo.cacheReadsPrice = 0.1
break
case "anthropic/claude-3-opus":
case "anthropic/claude-3-opus:beta":
modelInfo.supportsPromptCache = true
modelInfo.cacheWritesPrice = 18.75
modelInfo.cacheReadsPrice = 1.5
break
case "anthropic/claude-3-haiku":
case "anthropic/claude-3-haiku:beta":
modelInfo.supportsPromptCache = true
modelInfo.cacheWritesPrice = 0.3
modelInfo.cacheReadsPrice = 0.03
break
case "deepseek/deepseek-chat":
modelInfo.supportsPromptCache = true
// see api.ts/deepSeekModels for more info
modelInfo.inputPrice = 0
modelInfo.cacheWritesPrice = 0.14
modelInfo.cacheReadsPrice = 0.014
break
default:
if (rawModel.id.startsWith("openai/")) {
modelInfo.cacheReadsPrice = parsePrice(rawModel.pricing?.input_cache_read)
if (modelInfo.cacheReadsPrice) {
modelInfo.supportsPromptCache = true
modelInfo.cacheWritesPrice = parsePrice(rawModel.pricing?.input_cache_write)
// openrouter charges no cache write pricing for openAI models
}
} else if (rawModel.id.startsWith("google/")) {
modelInfo.cacheReadsPrice = parsePrice(rawModel.pricing?.input_cache_read)
if (modelInfo.cacheReadsPrice) {
modelInfo.supportsPromptCache = true
modelInfo.cacheWritesPrice = parsePrice(rawModel.pricing?.input_cache_write)
}
}
break
}
models[rawModel.id] = modelInfo
}
} else {
console.error("Invalid response from OpenRouter API")
}
await fs.writeFile(openRouterModelsFilePath, JSON.stringify(models))
console.log("OpenRouter models fetched and saved", models)
} catch (error) {
console.error("Error fetching OpenRouter models:", error)
// If we failed to fetch models, try to read cached models
const cachedModels = await readOpenRouterModels(controller)
if (cachedModels) {
models = cachedModels
}
}
// Convert the Record<string, Partial<OpenRouterModelInfo>> to Record<string, OpenRouterModelInfo>
// by filling in any missing required fields with defaults
const typedModels: Record<string, OpenRouterModelInfo> = {}
for (const [key, model] of Object.entries(models)) {
typedModels[key] = {
maxTokens: model.maxTokens ?? 0,
contextWindow: model.contextWindow ?? 0,
supportsImages: model.supportsImages ?? false,
supportsPromptCache: model.supportsPromptCache ?? false,
inputPrice: model.inputPrice ?? 0,
outputPrice: model.outputPrice ?? 0,
cacheWritesPrice: model.cacheWritesPrice ?? 0,
cacheReadsPrice: model.cacheReadsPrice ?? 0,
description: model.description ?? "",
}
}
return OpenRouterCompatibleModelInfo.create({ models: typedModels })
}
/**
* Reads cached OpenRouter models from disk
*/
async function readOpenRouterModels(controller: Controller): Promise<Record<string, Partial<OpenRouterModelInfo>> | undefined> {
const openRouterModelsFilePath = path.join(await ensureCacheDirectoryExists(controller), GlobalFileNames.openRouterModels)
const fileExists = await fileExistsAtPath(openRouterModelsFilePath)
if (fileExists) {
try {
const fileContents = await fs.readFile(openRouterModelsFilePath, "utf8")
return JSON.parse(fileContents)
} catch (error) {
console.error("Error reading cached OpenRouter models:", error)
return undefined
}
}
return undefined
}
/**
* Ensures the cache directory exists and returns its path
*/
async function ensureCacheDirectoryExists(controller: Controller): Promise<string> {
const cacheDir = path.join(controller.context.globalStorageUri.fsPath, "cache")
await fs.mkdir(cacheDir, { recursive: true })
return cacheDir
}
@@ -1,55 +0,0 @@
import { Controller } from ".."
import { EmptyRequest } from "../../../shared/proto/common"
import { OpenRouterCompatibleModelInfo, OpenRouterModelInfo } from "../../../shared/proto/models"
import axios from "axios"
import { getSecret } from "@core/storage/state"
/**
* Refreshes the Requesty models and returns the updated model list
* @param controller The controller instance
* @param request Empty request object
* @returns Response containing the Requesty models
*/
export async function refreshRequestyModels(
controller: Controller,
request: EmptyRequest,
): Promise<OpenRouterCompatibleModelInfo> {
const parsePrice = (price: any) => {
if (price) {
return parseFloat(price) * 1_000_000
}
return undefined
}
let models: Record<string, OpenRouterModelInfo> = {}
try {
const apiKey = await getSecret(controller.context, "requestyApiKey")
const headers = {
Authorization: `Bearer ${apiKey}`,
}
const response = await axios.get("https://router.requesty.ai/v1/models", { headers })
if (response.data?.data) {
for (const model of response.data.data) {
const modelInfo: OpenRouterModelInfo = {
maxTokens: model.max_output_tokens || undefined,
contextWindow: model.context_window,
supportsImages: model.supports_vision || undefined,
supportsPromptCache: model.supports_caching || undefined,
inputPrice: parsePrice(model.input_price) || 0,
outputPrice: parsePrice(model.output_price) || 0,
cacheWritesPrice: parsePrice(model.caching_price) || 0,
cacheReadsPrice: parsePrice(model.cached_price) || 0,
description: model.description,
}
models[model.id] = modelInfo
}
console.log("Requesty models fetched", models)
} else {
console.error("Invalid response from Requesty API")
}
} catch (error) {
console.error("Error fetching Requesty models:", error)
}
return OpenRouterCompatibleModelInfo.create({ models })
}
-10
View File
@@ -1,10 +0,0 @@
import { Controller } from ".."
import { StringRequest, Empty } from "../../../shared/proto/common"
/**
* Command slash command logic
*/
export async function condense(controller: Controller, request: StringRequest): Promise<Empty> {
await controller.task?.handleWebviewAskResponse("yesButtonClicked")
return Empty.create()
}
-22
View File
@@ -1,22 +0,0 @@
// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
// Generated by proto/build-proto.js
import { createServiceRegistry, ServiceMethodHandler, StreamingMethodHandler } from "../grpc-service"
import { StreamingResponseHandler } from "../grpc-handler"
import { registerAllMethods } from "./methods"
// Create slash service registry
const slashService = createServiceRegistry("slash")
// Export the method handler types and registration function
export type SlashMethodHandler = ServiceMethodHandler
export type SlashStreamingMethodHandler = StreamingMethodHandler
export const registerMethod = slashService.registerMethod
// Export the request handlers
export const handleSlashServiceRequest = slashService.handleRequest
export const handleSlashServiceStreamingRequest = slashService.handleStreamingRequest
export const isStreamingMethod = slashService.isStreamingMethod
// Register all slash methods
registerAllMethods()
-14
View File
@@ -1,14 +0,0 @@
// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
// Generated by proto/build-proto.js
// Import all method implementations
import { registerMethod } from "./index"
import { condense } from "./condense"
import { reportBug } from "./reportBug"
// Register all slash service methods
export function registerAllMethods(): void {
// Register each method with the registry
registerMethod("condense", condense)
registerMethod("reportBug", reportBug)
}
-10
View File
@@ -1,10 +0,0 @@
import { Controller } from ".."
import { StringRequest, Empty } from "../../../shared/proto/common"
/**
* Report bug slash command logic
*/
export async function reportBug(controller: Controller, request: StringRequest): Promise<Empty> {
await controller.task?.handleWebviewAskResponse("yesButtonClicked")
return Empty.create()
}
-2
View File
@@ -5,7 +5,6 @@
import { registerMethod } from "./index"
import { getLatestState } from "./getLatestState"
import { subscribeToState } from "./subscribeToState"
import { toggleFavoriteModel } from "./toggleFavoriteModel"
// Streaming methods for this service
export const streamingMethods = ["subscribeToState"]
@@ -15,5 +14,4 @@ export function registerAllMethods(): void {
// Register each method with the registry
registerMethod("getLatestState", getLatestState)
registerMethod("subscribeToState", subscribeToState, { isStreaming: true })
registerMethod("toggleFavoriteModel", toggleFavoriteModel)
}
@@ -1,46 +0,0 @@
import { telemetryService } from "@/services/posthog/telemetry/TelemetryService"
import { Controller } from ".."
import { Empty, StringRequest } from "../../../shared/proto/common"
import { updateGlobalState } from "@/core/storage/state"
/**
* Toggles a model's favorite status
* @param controller The controller instance
* @param request The request containing the model ID to toggle
* @returns An empty response
*/
export async function toggleFavoriteModel(controller: Controller, request: StringRequest): Promise<Empty> {
try {
if (!request.value) {
throw new Error("Model ID is required")
}
const modelId = request.value
const { apiConfiguration } = await controller.getStateToPostToWebview()
if (!apiConfiguration) {
throw new Error("API configuration not found")
}
const favoritedModelIds = apiConfiguration.favoritedModelIds || []
// Toggle favorite status
const updatedFavorites = favoritedModelIds.includes(modelId)
? favoritedModelIds.filter((id) => id !== modelId)
: [...favoritedModelIds, modelId]
await updateGlobalState(controller.context, "favoritedModelIds", updatedFavorites)
// Capture telemetry for model favorite toggle
const isFavorited = !favoritedModelIds.includes(modelId)
telemetryService.captureModelFavoritesUsage(modelId, isFavorited)
// Post state to webview without changing any other configuration
await controller.postStateToWebview()
return Empty.create()
} catch (error) {
console.error(`Failed to toggle favorite status for model ${request.value}:`, error)
throw error
}
}
+4 -32
View File
@@ -1,7 +1,6 @@
import { Controller } from ".."
import { GetTaskHistoryRequest, TaskHistoryArray } from "../../../shared/proto/task"
import { getGlobalState } from "../../storage/state"
import { getWorkspacePath, arePathsEqual } from "../../../utils/path"
/**
* Gets filtered task history
@@ -11,49 +10,22 @@ import { getWorkspacePath, arePathsEqual } from "../../../utils/path"
*/
export async function getTaskHistory(controller: Controller, request: GetTaskHistoryRequest): Promise<TaskHistoryArray> {
try {
const { favoritesOnly, currentWorkspaceOnly, searchQuery, sortBy } = request
const { favoritesOnly, searchQuery, sortBy } = request
// Get task history from global state
const taskHistory = ((await getGlobalState(controller.context, "taskHistory")) as any[]) || []
const workspacePath = getWorkspacePath()
// Apply filters
let filteredTasks = taskHistory.filter((item) => {
// Basic filter: must have timestamp and task content
const hasRequiredFields = item.ts && item.task
if (!hasRequiredFields) {
return false
}
// Apply favorites filter if requested
if (favoritesOnly && !item.isFavorited) {
return false
if (favoritesOnly && hasRequiredFields) {
return item.isFavorited === true
}
// Apply current workspace filter if requested
if (currentWorkspaceOnly) {
let isInWorkspace = false
// First check the cwdOnTaskInitialization property - Only present on tasks from this change forward
if (item.cwdOnTaskInitialization) {
if (arePathsEqual(item.cwdOnTaskInitialization, workspacePath)) {
isInWorkspace = true
}
}
// For tasks without cwdOnTaskInitialization, check the older shadowGitConfigWorkTree property
if (!isInWorkspace && item.shadowGitConfigWorkTree) {
if (arePathsEqual(item.shadowGitConfigWorkTree, workspacePath)) {
isInWorkspace = true
}
}
if (!isInWorkspace) {
return false
}
}
return true
return hasRequiredFields
})
// Apply search if provided
-31
View File
@@ -145,34 +145,3 @@ Example:
Below is the user's input when they indicated that they wanted to create a new Cline rule file.
</explicit_instructions>\n
`
export const reportBugToolResponse = () =>
`<explicit_instructions type="report_bug">
The user has explicitly asked you to help them submit a bug to the Cline github page (you MUST now help them with this irrespective of what your conversation up to this point in time was). To do so you will use the report_bug tool which is defined below. However, you must first ensure that you have collected all required information to fill in all the parameters for the tool call. If any of the the required information is apparent through your previous conversation with the user, you can suggest how to fill in those entries. However you should NOT assume you know what the issue about unless it's clear.
Otherwise, you should converse with the user until you are able to gather all the required details. When conversing with the user, make sure you ask for/reference all required information/fields. When referencing the required fields, use human friendly versions like "Steps to reproduce" rather than "steps_to_reproduce". Only then should you use the report_bug tool call.
The report_bug tool can be used in either of the PLAN or ACT modes.
The report_bug tool call is defined below:
Description:
Your task is to fill in all of the required fields for a issue/bug report on github. You should attempt to get the user to be as verbose as possible with their description of the bug/issue they encountered. Still, it's okay, when the user is unaware of some of the details, to set those fields as "N/A".
Parameters:
- title: (required) Concise description of the issue.
- what_happened: (required) What happened and also what the user expected to happen instead.
- steps_to_reproduce: (required) What steps are required to reproduce the bug.
- api_request_output: (optional) Relevant API request output.
- additional_context: (optional) Any other context about this bug not already mentioned.
Usage:
<report_bug>
<title>Title of the issue</title>
<what_happened>Description of the issue</what_happened>
<steps_to_reproduce>Steps to reproduce the issue</steps_to_reproduce>
<api_request_output>Output from the LLM API related to the bug</api_request_output>
<additional_context>Other issue details not already covered</additional_context>
</report_bug>
Below is the user's input when they indicated that they wanted to submit a Github issue.
</explicit_instructions>\n
`
+2 -3
View File
@@ -1,18 +1,17 @@
import { newTaskToolResponse, condenseToolResponse, newRuleToolResponse, reportBugToolResponse } from "../prompts/commands"
import { newTaskToolResponse, condenseToolResponse, newRuleToolResponse } from "../prompts/commands"
/**
* Processes text for slash commands and transforms them with appropriate instructions
* This is called after parseMentions() to process any slash commands in the user's message
*/
export function parseSlashCommands(text: string): { processedText: string; needsClinerulesFileCheck: boolean } {
const SUPPORTED_COMMANDS = ["newtask", "smol", "compact", "newrule", "reportbug"]
const SUPPORTED_COMMANDS = ["newtask", "smol", "compact", "newrule"]
const commandReplacements: Record<string, string> = {
newtask: newTaskToolResponse(),
smol: condenseToolResponse(),
compact: condenseToolResponse(),
newrule: newRuleToolResponse(),
reportbug: reportBugToolResponse(),
}
// this currently allows matching prepended whitespace prior to /slash-command
-5
View File
@@ -11,7 +11,6 @@ export type SecretKey =
| "deepSeekApiKey"
| "requestyApiKey"
| "togetherApiKey"
| "fireworksApiKey"
| "qwenApiKey"
| "doubaoApiKey"
| "mistralApiKey"
@@ -68,11 +67,7 @@ export type GlobalStateKey =
| "previousModeModelInfo"
| "liteLlmBaseUrl"
| "liteLlmModelId"
| "liteLlmModelInfo"
| "liteLlmUsePromptCache"
| "fireworksModelId"
| "fireworksModelMaxCompletionTokens"
| "fireworksModelMaxTokens"
| "qwenApiLine"
| "requestyModelId"
| "requestyModelInfo"
-18
View File
@@ -107,12 +107,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
vsCodeLmModelSelector,
liteLlmBaseUrl,
liteLlmModelId,
liteLlmModelInfo,
liteLlmUsePromptCache,
fireworksApiKey,
fireworksModelId,
fireworksModelMaxCompletionTokens,
fireworksModelMaxTokens,
userInfo,
previousModeApiProvider,
previousModeModelId,
@@ -191,12 +186,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
getGlobalState(context, "vsCodeLmModelSelector") as Promise<vscode.LanguageModelChatSelector | undefined>,
getGlobalState(context, "liteLlmBaseUrl") as Promise<string | undefined>,
getGlobalState(context, "liteLlmModelId") as Promise<string | undefined>,
getGlobalState(context, "liteLlmModelInfo") as Promise<ModelInfo | undefined>,
getGlobalState(context, "liteLlmUsePromptCache") as Promise<boolean | undefined>,
getSecret(context, "fireworksApiKey") as Promise<string | undefined>,
getGlobalState(context, "fireworksModelId") as Promise<string | undefined>,
getGlobalState(context, "fireworksModelMaxCompletionTokens") as Promise<number | undefined>,
getGlobalState(context, "fireworksModelMaxTokens") as Promise<number | undefined>,
getGlobalState(context, "userInfo") as Promise<UserInfo | undefined>,
getGlobalState(context, "previousModeApiProvider") as Promise<ApiProvider | undefined>,
getGlobalState(context, "previousModeModelId") as Promise<string | undefined>,
@@ -314,13 +304,8 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
reasoningEffort,
liteLlmBaseUrl,
liteLlmModelId,
liteLlmModelInfo,
liteLlmApiKey,
liteLlmUsePromptCache,
fireworksApiKey,
fireworksModelId,
fireworksModelMaxCompletionTokens,
fireworksModelMaxTokens,
asksageApiKey,
asksageApiUrl,
xaiApiKey,
@@ -401,7 +386,6 @@ export async function updateApiConfiguration(context: vscode.ExtensionContext, a
vsCodeLmModelSelector,
liteLlmBaseUrl,
liteLlmModelId,
liteLlmModelInfo,
liteLlmApiKey,
liteLlmUsePromptCache,
qwenApiLine,
@@ -460,7 +444,6 @@ export async function updateApiConfiguration(context: vscode.ExtensionContext, a
await updateGlobalState(context, "vsCodeLmModelSelector", vsCodeLmModelSelector)
await updateGlobalState(context, "liteLlmBaseUrl", liteLlmBaseUrl)
await updateGlobalState(context, "liteLlmModelId", liteLlmModelId)
await updateGlobalState(context, "liteLlmModelInfo", liteLlmModelInfo)
await updateGlobalState(context, "liteLlmUsePromptCache", liteLlmUsePromptCache)
await updateGlobalState(context, "qwenApiLine", qwenApiLine)
await updateGlobalState(context, "requestyModelId", requestyModelId)
@@ -497,7 +480,6 @@ export async function resetExtensionState(context: vscode.ExtensionContext) {
"mistralApiKey",
"clineApiKey",
"liteLlmApiKey",
"fireworksApiKey",
"asksageApiKey",
"xaiApiKey",
"sambanovaApiKey",
+6 -164
View File
@@ -57,10 +57,9 @@ import { DEFAULT_LANGUAGE_SETTINGS, getLanguageKey, LanguageDisplay } from "@sha
import { ClineAskResponse, ClineCheckpointRestore } from "@shared/WebviewMessage"
import { calculateApiCostAnthropic } from "@utils/cost"
import { fileExistsAtPath } from "@utils/fs"
import { createAndOpenGitHubIssue } from "@utils/github-url-utils"
import { arePathsEqual, getReadablePath, isLocatedInWorkspace } from "@utils/path"
import { fixModelHtmlEscaping, removeInvalidChars } from "@utils/string"
import { AssistantMessageContent, parseAssistantMessageV2, ToolParamName, ToolUseName } from "@core/assistant-message"
import { AssistantMessageContent, parseAssistantMessage, ToolParamName, ToolUseName } from "@core/assistant-message"
import { constructNewFileContent } from "@core/assistant-message/diff"
import { ClineIgnoreController } from "@core/ignore/ClineIgnoreController"
import { parseMentions } from "@core/mentions"
@@ -196,7 +195,9 @@ export class Task {
this.reinitExistingTaskFromId = reinitExistingTaskFromId
this.cancelTask = cancelTask
this.clineIgnoreController = new ClineIgnoreController(cwd)
// Initialization moved to startTask/resumeTaskFromHistory
this.clineIgnoreController.initialize().catch((error) => {
console.error("Failed to initialize ClineIgnoreController:", error)
})
this.terminalManager = new TerminalManager()
this.terminalManager.setShellIntegrationTimeout(shellIntegrationTimeout)
this.urlContentFetcher = new UrlContentFetcher(context)
@@ -314,7 +315,6 @@ export class Task {
totalCost: apiMetrics.totalCost,
size: taskDirSize,
shadowGitConfigWorkTree: await this.checkpointTracker?.getShadowGitConfigWorkTree(),
cwdOnTaskInitialization: cwd,
conversationHistoryDeletedRange: this.conversationHistoryDeletedRange,
isFavorited: this.taskIsFavorited,
})
@@ -856,12 +856,6 @@ export class Task {
// Task lifecycle
private async startTask(task?: string, images?: string[]): Promise<void> {
try {
await this.clineIgnoreController.initialize()
} catch (error) {
console.error("Failed to initialize ClineIgnoreController:", error)
// Optionally, inform the user or handle the error appropriately
}
// conversationHistory (for API) and clineMessages (for webview) need to be in sync
// if the extension process were killed, then on restart the clineMessages might not be empty, so we need to set it to [] when we create a new Cline client (otherwise webview would show stale messages from previous session)
this.clineMessages = []
@@ -884,12 +878,6 @@ export class Task {
}
private async resumeTaskFromHistory() {
try {
await this.clineIgnoreController.initialize()
} catch (error) {
console.error("Failed to initialize ClineIgnoreController:", error)
// Optionally, inform the user or handle the error appropriately
}
// UPDATE: we don't need this anymore since most tasks are now created with checkpoints enabled
// right now we let users init checkpoints for old tasks, assuming they're continuing them from the same workspace (which we never tied to tasks, so no way for us to know if it's opened in the right workspace)
// const doesShadowGitExist = await CheckpointTracker.doesShadowGitExist(this.taskId, this.controllerRef.deref())
@@ -1443,28 +1431,13 @@ export class Task {
return statusCode && !message.includes(statusCode.toString()) ? `${statusCode} - ${message}` : message
}
/**
* Migrates the disableBrowserTool setting from VSCode configuration to browserSettings
*/
private async migrateDisableBrowserToolSetting(): Promise<void> {
const config = vscode.workspace.getConfiguration("cline")
const disableBrowserTool = vscode.workspace.getConfiguration("cline").get<boolean>("disableBrowserTool")
if (disableBrowserTool !== undefined) {
this.browserSettings.disableToolUse = disableBrowserTool
// Remove from VSCode configuration
await config.update("disableBrowserTool", undefined, true)
}
}
async *attemptApiRequest(previousApiReqIndex: number): ApiStream {
// Wait for MCP servers to be connected before generating system prompt
await pWaitFor(() => this.mcpHub.isConnecting !== true, { timeout: 10_000 }).catch(() => {
console.error("MCP servers failed to connect in time")
})
await this.migrateDisableBrowserToolSetting()
const disableBrowserTool = this.browserSettings.disableToolUse ?? false
const disableBrowserTool = vscode.workspace.getConfiguration("cline").get<boolean>("disableBrowserTool") ?? false
// cline browser tool uses image recognition for navigation (requires model image support).
const modelSupportsBrowserUse = this.api.getModel().info.supportsImages ?? false
@@ -1741,8 +1714,6 @@ export class Task {
return `[${block.name} for creating a new task]`
case "condense":
return `[${block.name}]`
case "report_bug":
return `[${block.name}]`
case "new_rule":
return `[${block.name} for '${block.params.path}']`
}
@@ -3170,135 +3141,6 @@ export class Task {
break
}
}
case "report_bug": {
const title = block.params.title
const what_happened = block.params.what_happened
const steps_to_reproduce = block.params.steps_to_reproduce
const api_request_output = block.params.api_request_output
const additional_context = block.params.additional_context
try {
if (block.partial) {
await this.ask(
"report_bug",
JSON.stringify({
title: removeClosingTag("title", title),
what_happened: removeClosingTag("what_happened", what_happened),
steps_to_reproduce: removeClosingTag("steps_to_reproduce", steps_to_reproduce),
api_request_output: removeClosingTag("api_request_output", api_request_output),
additional_context: removeClosingTag("additional_context", additional_context),
}),
block.partial,
).catch(() => {})
break
} else {
if (!title) {
this.consecutiveMistakeCount++
pushToolResult(await this.sayAndCreateMissingParamError("report_bug", "title"))
await this.saveCheckpoint()
break
}
if (!what_happened) {
this.consecutiveMistakeCount++
pushToolResult(await this.sayAndCreateMissingParamError("report_bug", "what_happened"))
await this.saveCheckpoint()
break
}
if (!steps_to_reproduce) {
this.consecutiveMistakeCount++
pushToolResult(await this.sayAndCreateMissingParamError("report_bug", "steps_to_reproduce"))
await this.saveCheckpoint()
break
}
if (!api_request_output) {
this.consecutiveMistakeCount++
pushToolResult(await this.sayAndCreateMissingParamError("report_bug", "api_request_output"))
await this.saveCheckpoint()
break
}
if (!additional_context) {
this.consecutiveMistakeCount++
pushToolResult(await this.sayAndCreateMissingParamError("report_bug", "additional_context"))
await this.saveCheckpoint()
break
}
this.consecutiveMistakeCount = 0
if (this.autoApprovalSettings.enabled && this.autoApprovalSettings.enableNotifications) {
showSystemNotification({
subtitle: "Cline wants to create a github issue...",
message: `Cline is suggesting to create a github issue with the title: ${title}`,
})
}
// Derive system information values algorithmically
const operatingSystem = os.platform() + " " + os.release()
const clineVersion =
vscode.extensions.getExtension("saoudrizwan.claude-dev")?.packageJSON.version || "Unknown"
const systemInfo = `VSCode: ${vscode.version}, Node.js: ${process.version}, Architecture: ${os.arch()}`
const providerAndModel = `${(await getGlobalState(this.getContext(), "apiProvider")) as string} / ${this.api.getModel().id}`
// Ask user for confirmation
const bugReportData = JSON.stringify({
title,
what_happened,
steps_to_reproduce,
api_request_output,
additional_context,
// Include derived values in the JSON for display purposes
provider_and_model: providerAndModel,
operating_system: operatingSystem,
system_info: systemInfo,
cline_version: clineVersion,
})
const { text, images } = await this.ask("report_bug", bugReportData, false)
// If the user provided a response, treat it as feedback
if (text || images?.length) {
await this.say("user_feedback", text ?? "", images)
pushToolResult(
formatResponse.toolResult(
`The user did not submit the bug, and provided feedback on the Github issue generated instead:\n<feedback>\n${text}\n</feedback>`,
images,
),
)
} else {
// If no response, the user accepted the condensed version
pushToolResult(
formatResponse.toolResult(`The user accepted the creation of the Github issue.`),
)
try {
// Create a Map of parameters for the GitHub issue
const params = new Map<string, string>()
params.set("title", title)
params.set("operating-system", operatingSystem)
params.set("cline-version", clineVersion)
params.set("system-info", systemInfo)
params.set("additional-context", additional_context)
params.set("what-happened", what_happened)
params.set("steps", steps_to_reproduce)
params.set("provider-model", providerAndModel)
params.set("logs", api_request_output)
// Use our utility function to create and open the GitHub issue URL
// This bypasses VS Code's URI handling issues with special characters
await createAndOpenGitHubIssue("cline", "cline", "bug_report.yml", params)
} catch (error) {
console.error(`An error occurred while attempting to report the bug: ${error}`)
}
}
await this.saveCheckpoint()
break
}
} catch (error) {
await handleError("reporting bug", error)
await this.saveCheckpoint()
break
}
}
case "plan_mode_respond": {
const response: string | undefined = block.params.response
const optionsRaw: string | undefined = block.params.options
@@ -3860,7 +3702,7 @@ export class Task {
assistantMessage += chunk.text
// parse raw assistant message into content blocks
const prevLength = this.assistantMessageContent.length
this.assistantMessageContent = parseAssistantMessageV2(assistantMessage)
this.assistantMessageContent = parseAssistantMessage(assistantMessage)
if (this.assistantMessageContent.length > prevLength) {
this.userMessageContentReady = false // new content we need to present, reset to false in case previous content set this to true
}
+3 -9
View File
@@ -91,9 +91,9 @@ export class DiffViewProvider {
const currentFirstVisibleLine = e.visibleRanges[0]?.start.line || 0
// If the first visible line moved upward, user scrolled up
// if (currentFirstVisibleLine < this.lastFirstVisibleLine) {
// this.shouldAutoScroll = false
// }
if (currentFirstVisibleLine < this.lastFirstVisibleLine) {
this.shouldAutoScroll = false
}
// Always update our tracking variable
this.lastFirstVisibleLine = currentFirstVisibleLine
@@ -433,12 +433,6 @@ export class DiffViewProvider {
// close editor if open?
async reset() {
// releasing memory by clearing the diff editor
try {
await this.closeAllDiffViews()
} catch (error) {
console.error("Error closing diff views:", error)
}
this.editType = undefined
this.isEditing = false
this.originalContent = undefined
@@ -108,47 +108,6 @@ export class TerminalManager {
if (disposable) {
this.disposables.push(disposable)
}
// Add a listener for terminal state changes to detect CWD updates
try {
const stateChangeDisposable = vscode.window.onDidChangeTerminalState((terminal) => {
const terminalInfo = this.findTerminalInfoByTerminal(terminal)
if (terminalInfo && terminalInfo.pendingCwdChange && terminalInfo.cwdResolved) {
// Check if CWD has been updated to match the expected path
if (this.isCwdMatchingExpected(terminalInfo)) {
const resolver = terminalInfo.cwdResolved.resolve
terminalInfo.pendingCwdChange = undefined
terminalInfo.cwdResolved = undefined
resolver()
}
}
})
this.disposables.push(stateChangeDisposable)
} catch (error) {
console.error("Error setting up onDidChangeTerminalState", error)
}
}
//Find a TerminalInfo by its VSCode Terminal instance
private findTerminalInfoByTerminal(terminal: vscode.Terminal): TerminalInfo | undefined {
const terminals = TerminalRegistry.getAllTerminals()
return terminals.find((t) => t.terminal === terminal)
}
//Check if a terminal's CWD matches its expected pending change
private isCwdMatchingExpected(terminalInfo: TerminalInfo): boolean {
if (!terminalInfo.pendingCwdChange) {
return false
}
const currentCwd = terminalInfo.terminal.shellIntegration?.cwd?.fsPath
const targetCwd = vscode.Uri.file(terminalInfo.pendingCwdChange).fsPath
if (!currentCwd) {
return false
}
return arePathsEqual(currentCwd, targetCwd)
}
runCommand(terminalInfo: TerminalInfo, command: string): TerminalProcessResultPromise {
@@ -237,37 +196,8 @@ export class TerminalManager {
// If no matching terminal exists, try to find any non-busy terminal
const availableTerminal = terminals.find((t) => !t.busy)
if (availableTerminal) {
// Set up promise and tracking for CWD change
const cwdPromise = new Promise<void>((resolve, reject) => {
availableTerminal.pendingCwdChange = cwd
availableTerminal.cwdResolved = { resolve, reject }
})
// Navigate back to the desired directory
await this.runCommand(availableTerminal, `cd "${cwd}"`)
// Either resolve immediately if CWD already updated or wait for event/timeout
if (this.isCwdMatchingExpected(availableTerminal)) {
if (availableTerminal.cwdResolved) {
availableTerminal.cwdResolved.resolve()
}
availableTerminal.pendingCwdChange = undefined
availableTerminal.cwdResolved = undefined
} else {
try {
// Wait with a timeout for state change event to resolve
await Promise.race([
cwdPromise,
new Promise<void>((_, reject) =>
setTimeout(() => reject(new Error(`CWD timeout: Failed to update to ${cwd}`)), 1000),
),
])
} catch (err) {
// Clear pending state on timeout
availableTerminal.pendingCwdChange = undefined
availableTerminal.cwdResolved = undefined
}
}
this.terminalIds.add(availableTerminal.id)
return availableTerminal
}
@@ -1,7 +1,6 @@
import { EventEmitter } from "events"
import { stripAnsi } from "./ansiUtils"
import * as vscode from "vscode"
import { Logger } from "@services/logging/Logger"
export interface TerminalProcessEvents {
line: [line: string]
@@ -35,42 +34,7 @@ export class TerminalProcess extends EventEmitter<TerminalProcessEvents> {
let isFirstChunk = true
let didOutputNonCommand = false
let didEmitEmptyLine = false
let firstChunkTimeout: NodeJS.Timeout
const isWindows = process.platform === "win32"
const timeoutMs = isWindows ? 5000 : 500
const onTimeout = () => {
// In rare cases (e.g. running the same command twice like `npm run build`),
// the shell integration stream enters a broken state where no data is ever emitted.
// We never even get the first chunk, which bricks the UI and locks the user out.
// Interestingly, the stream still gets created, and future commands (like `ls`) will work,
// suggesting the stream itself isn't one-shot—but certain shell states break its behavior.
// To recover, we add a timeout waiting for the first chunk.
// If it doesnt arrive in time, we assume the terminal is broken, dispose it,
// and emit an error so the user can safely retry in a clean terminal.
Logger.debug(
`[TerminalProcess.run] First chunk timeout hit — terminal likely in bad state. Terminating terminal.`,
)
try {
terminal.dispose()
} catch (err) {
Logger.debug(`[TerminalProcess.run] Failed to dispose terminal: ${String(err)}`)
}
this.emit(
"error",
new Error("The command ran successfully, but we couldn't capture its output. Please proceed accordingly."),
)
this.emit("completed")
this.emit("continue")
}
firstChunkTimeout = setTimeout(onTimeout, timeoutMs)
for await (let data of stream) {
clearTimeout(firstChunkTimeout)
// 1. Process chunk and remove artifacts
if (isFirstChunk) {
/*
@@ -5,11 +5,6 @@ export interface TerminalInfo {
busy: boolean
lastCommand: string
id: number
pendingCwdChange?: string
cwdResolved?: {
resolve: () => void
reject: (error: Error) => void
}
}
// Although vscode.window.terminals provides a list of all open terminals, there's no way to know whether they're busy or not (exitStatus does not provide useful information for most commands). In order to prevent creating too many terminals, we need to keep track of terminals through the life of the extension, as well as session specific terminals for the life of a task (to get latest unretrieved output).

Some files were not shown because too many files have changed in this diff Show More