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
81 changed files with 1038 additions and 2111 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
@@ -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
-36
View File
@@ -1,41 +1,5 @@
# Changelog
## [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
@@ -1,12 +1,12 @@
{
"name": "claude-dev",
"version": "3.15.0",
"version": "3.14.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "claude-dev",
"version": "3.15.0",
"version": "3.14.1",
"license": "Apache-2.0",
"dependencies": {
"@anthropic-ai/bedrock-sdk": "^0.12.4",
+1 -1
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.0",
"version": "3.14.1",
"icon": "assets/icons/icon.png",
"engines": {
"vscode": "^1.84.0"
-30
View File
@@ -14,10 +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 (OpenRouterModels);
// Refreshes and returns OpenAI models
rpc refreshOpenAiModels(OpenAiModelsRequest) returns (StringArray);
}
// List of VS Code LM models
@@ -32,29 +28,3 @@ message VsCodeLmModel {
string version = 3;
string id = 4;
}
// For ModelInfo 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;
}
// Response message for OpenRouter models
message OpenRouterModels {
map<string, OpenRouterModelInfo> models = 1;
}
// Request for fetching OpenAI models
message OpenAiModelsRequest {
Metadata metadata = 1;
string baseUrl = 2;
string apiKey = 3;
}
-6
View File
@@ -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]
+177 -10
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()
}
@@ -274,9 +273,6 @@ export class Controller {
case "condense":
this.task?.handleWebviewAskResponse("yesButtonClicked")
break
case "reportBug":
this.task?.handleWebviewAskResponse("yesButtonClicked")
break
case "apiConfiguration":
if (message.apiConfiguration) {
await updateApiConfiguration(this.context, message.apiConfiguration)
@@ -334,9 +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)
@@ -1114,6 +1118,32 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
}
}
// 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) {
@@ -1159,6 +1189,143 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
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) {
-4
View File
@@ -6,8 +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"
// Register all models service methods
export function registerAllMethods(): void {
@@ -15,6 +13,4 @@ export function registerAllMethods(): void {
registerMethod("getLmStudioModels", getLmStudioModels)
registerMethod("getOllamaModels", getOllamaModels)
registerMethod("getVsCodeLmModels", getVsCodeLmModels)
registerMethod("refreshOpenAiModels", refreshOpenAiModels)
registerMethod("refreshOpenRouterModels", refreshOpenRouterModels)
}
@@ -1,40 +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)]
// Send models to webview
controller.postMessageToWebview({ type: "openAiModels", openAiModels: models })
return StringArray.create({ values: models })
} catch (error) {
console.error("Error fetching OpenAI models:", error)
return StringArray.create({ values: [] })
}
}
@@ -1,178 +0,0 @@
import { Controller } from ".."
import { EmptyRequest } from "../../../shared/proto/common"
import { OpenRouterModels, 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<OpenRouterModels> {
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 ?? "",
}
}
// Send models to webview
await controller.postMessageToWebview({
type: "openRouterModels",
openRouterModels: typedModels,
})
return OpenRouterModels.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
}
-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
+3 -145
View File
@@ -57,7 +57,6 @@ 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, parseAssistantMessage, ToolParamName, ToolUseName } from "@core/assistant-message"
@@ -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)
@@ -855,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 = []
@@ -883,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())
@@ -1725,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}']`
}
@@ -3154,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 provided feedback on the Github issue generated:\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
@@ -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,41 +34,7 @@ export class TerminalProcess extends EventEmitter<TerminalProcessEvents> {
let isFirstChunk = true
let didOutputNonCommand = false
let didEmitEmptyLine = false
let firstChunkTimeout: NodeJS.Timeout
const timeoutMs = 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) {
/*
-2
View File
@@ -17,7 +17,6 @@ export interface AutoApprovalSettings {
// Global settings
maxRequests: number // Maximum number of auto-approved requests
enableNotifications: boolean // Show notifications for approval and task completion
favorites: string[] // IDs of actions favorited by the user for quick access
}
export const DEFAULT_AUTO_APPROVAL_SETTINGS: AutoApprovalSettings = {
@@ -35,5 +34,4 @@ export const DEFAULT_AUTO_APPROVAL_SETTINGS: AutoApprovalSettings = {
},
maxRequests: 20,
enableNotifications: false,
favorites: [],
}
-1
View File
@@ -177,7 +177,6 @@ export type ClineAsk =
| "use_mcp_server"
| "new_task"
| "condense"
| "report_bug"
export type ClineSay =
| "task"
+2 -1
View File
@@ -13,7 +13,6 @@ export interface WebviewMessage {
| "webviewDidLaunch"
| "newTask"
| "condense"
| "reportBug"
| "askResponse"
| "didShowAnnouncement"
| "selectImages"
@@ -21,7 +20,9 @@ export interface WebviewMessage {
| "openInBrowser"
| "openMention"
| "showChatView"
| "refreshOpenRouterModels"
| "refreshRequestyModels"
| "refreshOpenAiModels"
| "refreshClineRules"
| "openMcpSettings"
| "restartMcpServer"
+1 -503
View File
@@ -6,7 +6,7 @@
/* eslint-disable */
import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire"
import { EmptyRequest, Metadata, StringArray, StringRequest } from "./common"
import { EmptyRequest, StringArray, StringRequest } from "./common"
export const protobufPackage = "cline"
@@ -23,36 +23,6 @@ export interface VsCodeLmModel {
id: string
}
/** For ModelInfo structure in OpenRouterModels */
export interface OpenRouterModelInfo {
maxTokens: number
contextWindow: number
supportsImages: boolean
supportsPromptCache: boolean
inputPrice: number
outputPrice: number
cacheWritesPrice: number
cacheReadsPrice: number
description: string
}
/** Response message for OpenRouter models */
export interface OpenRouterModels {
models: { [key: string]: OpenRouterModelInfo }
}
export interface OpenRouterModels_ModelsEntry {
key: string
value?: OpenRouterModelInfo | undefined
}
/** Request for fetching OpenAI models */
export interface OpenAiModelsRequest {
metadata?: Metadata | undefined
baseUrl: string
apiKey: string
}
function createBaseVsCodeLmModelsArray(): VsCodeLmModelsArray {
return { models: [] }
}
@@ -221,456 +191,6 @@ export const VsCodeLmModel: MessageFns<VsCodeLmModel> = {
},
}
function createBaseOpenRouterModelInfo(): OpenRouterModelInfo {
return {
maxTokens: 0,
contextWindow: 0,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
cacheWritesPrice: 0,
cacheReadsPrice: 0,
description: "",
}
}
export const OpenRouterModelInfo: MessageFns<OpenRouterModelInfo> = {
encode(message: OpenRouterModelInfo, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
if (message.maxTokens !== 0) {
writer.uint32(8).int32(message.maxTokens)
}
if (message.contextWindow !== 0) {
writer.uint32(16).int32(message.contextWindow)
}
if (message.supportsImages !== false) {
writer.uint32(24).bool(message.supportsImages)
}
if (message.supportsPromptCache !== false) {
writer.uint32(32).bool(message.supportsPromptCache)
}
if (message.inputPrice !== 0) {
writer.uint32(41).double(message.inputPrice)
}
if (message.outputPrice !== 0) {
writer.uint32(49).double(message.outputPrice)
}
if (message.cacheWritesPrice !== 0) {
writer.uint32(57).double(message.cacheWritesPrice)
}
if (message.cacheReadsPrice !== 0) {
writer.uint32(65).double(message.cacheReadsPrice)
}
if (message.description !== "") {
writer.uint32(74).string(message.description)
}
return writer
},
decode(input: BinaryReader | Uint8Array, length?: number): OpenRouterModelInfo {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input)
let end = length === undefined ? reader.len : reader.pos + length
const message = createBaseOpenRouterModelInfo()
while (reader.pos < end) {
const tag = reader.uint32()
switch (tag >>> 3) {
case 1: {
if (tag !== 8) {
break
}
message.maxTokens = reader.int32()
continue
}
case 2: {
if (tag !== 16) {
break
}
message.contextWindow = reader.int32()
continue
}
case 3: {
if (tag !== 24) {
break
}
message.supportsImages = reader.bool()
continue
}
case 4: {
if (tag !== 32) {
break
}
message.supportsPromptCache = reader.bool()
continue
}
case 5: {
if (tag !== 41) {
break
}
message.inputPrice = reader.double()
continue
}
case 6: {
if (tag !== 49) {
break
}
message.outputPrice = reader.double()
continue
}
case 7: {
if (tag !== 57) {
break
}
message.cacheWritesPrice = reader.double()
continue
}
case 8: {
if (tag !== 65) {
break
}
message.cacheReadsPrice = reader.double()
continue
}
case 9: {
if (tag !== 74) {
break
}
message.description = reader.string()
continue
}
}
if ((tag & 7) === 4 || tag === 0) {
break
}
reader.skip(tag & 7)
}
return message
},
fromJSON(object: any): OpenRouterModelInfo {
return {
maxTokens: isSet(object.maxTokens) ? globalThis.Number(object.maxTokens) : 0,
contextWindow: isSet(object.contextWindow) ? globalThis.Number(object.contextWindow) : 0,
supportsImages: isSet(object.supportsImages) ? globalThis.Boolean(object.supportsImages) : false,
supportsPromptCache: isSet(object.supportsPromptCache) ? globalThis.Boolean(object.supportsPromptCache) : false,
inputPrice: isSet(object.inputPrice) ? globalThis.Number(object.inputPrice) : 0,
outputPrice: isSet(object.outputPrice) ? globalThis.Number(object.outputPrice) : 0,
cacheWritesPrice: isSet(object.cacheWritesPrice) ? globalThis.Number(object.cacheWritesPrice) : 0,
cacheReadsPrice: isSet(object.cacheReadsPrice) ? globalThis.Number(object.cacheReadsPrice) : 0,
description: isSet(object.description) ? globalThis.String(object.description) : "",
}
},
toJSON(message: OpenRouterModelInfo): unknown {
const obj: any = {}
if (message.maxTokens !== 0) {
obj.maxTokens = Math.round(message.maxTokens)
}
if (message.contextWindow !== 0) {
obj.contextWindow = Math.round(message.contextWindow)
}
if (message.supportsImages !== false) {
obj.supportsImages = message.supportsImages
}
if (message.supportsPromptCache !== false) {
obj.supportsPromptCache = message.supportsPromptCache
}
if (message.inputPrice !== 0) {
obj.inputPrice = message.inputPrice
}
if (message.outputPrice !== 0) {
obj.outputPrice = message.outputPrice
}
if (message.cacheWritesPrice !== 0) {
obj.cacheWritesPrice = message.cacheWritesPrice
}
if (message.cacheReadsPrice !== 0) {
obj.cacheReadsPrice = message.cacheReadsPrice
}
if (message.description !== "") {
obj.description = message.description
}
return obj
},
create<I extends Exact<DeepPartial<OpenRouterModelInfo>, I>>(base?: I): OpenRouterModelInfo {
return OpenRouterModelInfo.fromPartial(base ?? ({} as any))
},
fromPartial<I extends Exact<DeepPartial<OpenRouterModelInfo>, I>>(object: I): OpenRouterModelInfo {
const message = createBaseOpenRouterModelInfo()
message.maxTokens = object.maxTokens ?? 0
message.contextWindow = object.contextWindow ?? 0
message.supportsImages = object.supportsImages ?? false
message.supportsPromptCache = object.supportsPromptCache ?? false
message.inputPrice = object.inputPrice ?? 0
message.outputPrice = object.outputPrice ?? 0
message.cacheWritesPrice = object.cacheWritesPrice ?? 0
message.cacheReadsPrice = object.cacheReadsPrice ?? 0
message.description = object.description ?? ""
return message
},
}
function createBaseOpenRouterModels(): OpenRouterModels {
return { models: {} }
}
export const OpenRouterModels: MessageFns<OpenRouterModels> = {
encode(message: OpenRouterModels, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
Object.entries(message.models).forEach(([key, value]) => {
OpenRouterModels_ModelsEntry.encode({ key: key as any, value }, writer.uint32(10).fork()).join()
})
return writer
},
decode(input: BinaryReader | Uint8Array, length?: number): OpenRouterModels {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input)
let end = length === undefined ? reader.len : reader.pos + length
const message = createBaseOpenRouterModels()
while (reader.pos < end) {
const tag = reader.uint32()
switch (tag >>> 3) {
case 1: {
if (tag !== 10) {
break
}
const entry1 = OpenRouterModels_ModelsEntry.decode(reader, reader.uint32())
if (entry1.value !== undefined) {
message.models[entry1.key] = entry1.value
}
continue
}
}
if ((tag & 7) === 4 || tag === 0) {
break
}
reader.skip(tag & 7)
}
return message
},
fromJSON(object: any): OpenRouterModels {
return {
models: isObject(object.models)
? Object.entries(object.models).reduce<{ [key: string]: OpenRouterModelInfo }>((acc, [key, value]) => {
acc[key] = OpenRouterModelInfo.fromJSON(value)
return acc
}, {})
: {},
}
},
toJSON(message: OpenRouterModels): unknown {
const obj: any = {}
if (message.models) {
const entries = Object.entries(message.models)
if (entries.length > 0) {
obj.models = {}
entries.forEach(([k, v]) => {
obj.models[k] = OpenRouterModelInfo.toJSON(v)
})
}
}
return obj
},
create<I extends Exact<DeepPartial<OpenRouterModels>, I>>(base?: I): OpenRouterModels {
return OpenRouterModels.fromPartial(base ?? ({} as any))
},
fromPartial<I extends Exact<DeepPartial<OpenRouterModels>, I>>(object: I): OpenRouterModels {
const message = createBaseOpenRouterModels()
message.models = Object.entries(object.models ?? {}).reduce<{ [key: string]: OpenRouterModelInfo }>(
(acc, [key, value]) => {
if (value !== undefined) {
acc[key] = OpenRouterModelInfo.fromPartial(value)
}
return acc
},
{},
)
return message
},
}
function createBaseOpenRouterModels_ModelsEntry(): OpenRouterModels_ModelsEntry {
return { key: "", value: undefined }
}
export const OpenRouterModels_ModelsEntry: MessageFns<OpenRouterModels_ModelsEntry> = {
encode(message: OpenRouterModels_ModelsEntry, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
if (message.key !== "") {
writer.uint32(10).string(message.key)
}
if (message.value !== undefined) {
OpenRouterModelInfo.encode(message.value, writer.uint32(18).fork()).join()
}
return writer
},
decode(input: BinaryReader | Uint8Array, length?: number): OpenRouterModels_ModelsEntry {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input)
let end = length === undefined ? reader.len : reader.pos + length
const message = createBaseOpenRouterModels_ModelsEntry()
while (reader.pos < end) {
const tag = reader.uint32()
switch (tag >>> 3) {
case 1: {
if (tag !== 10) {
break
}
message.key = reader.string()
continue
}
case 2: {
if (tag !== 18) {
break
}
message.value = OpenRouterModelInfo.decode(reader, reader.uint32())
continue
}
}
if ((tag & 7) === 4 || tag === 0) {
break
}
reader.skip(tag & 7)
}
return message
},
fromJSON(object: any): OpenRouterModels_ModelsEntry {
return {
key: isSet(object.key) ? globalThis.String(object.key) : "",
value: isSet(object.value) ? OpenRouterModelInfo.fromJSON(object.value) : undefined,
}
},
toJSON(message: OpenRouterModels_ModelsEntry): unknown {
const obj: any = {}
if (message.key !== "") {
obj.key = message.key
}
if (message.value !== undefined) {
obj.value = OpenRouterModelInfo.toJSON(message.value)
}
return obj
},
create<I extends Exact<DeepPartial<OpenRouterModels_ModelsEntry>, I>>(base?: I): OpenRouterModels_ModelsEntry {
return OpenRouterModels_ModelsEntry.fromPartial(base ?? ({} as any))
},
fromPartial<I extends Exact<DeepPartial<OpenRouterModels_ModelsEntry>, I>>(object: I): OpenRouterModels_ModelsEntry {
const message = createBaseOpenRouterModels_ModelsEntry()
message.key = object.key ?? ""
message.value =
object.value !== undefined && object.value !== null ? OpenRouterModelInfo.fromPartial(object.value) : undefined
return message
},
}
function createBaseOpenAiModelsRequest(): OpenAiModelsRequest {
return { metadata: undefined, baseUrl: "", apiKey: "" }
}
export const OpenAiModelsRequest: MessageFns<OpenAiModelsRequest> = {
encode(message: OpenAiModelsRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
if (message.metadata !== undefined) {
Metadata.encode(message.metadata, writer.uint32(10).fork()).join()
}
if (message.baseUrl !== "") {
writer.uint32(18).string(message.baseUrl)
}
if (message.apiKey !== "") {
writer.uint32(26).string(message.apiKey)
}
return writer
},
decode(input: BinaryReader | Uint8Array, length?: number): OpenAiModelsRequest {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input)
let end = length === undefined ? reader.len : reader.pos + length
const message = createBaseOpenAiModelsRequest()
while (reader.pos < end) {
const tag = reader.uint32()
switch (tag >>> 3) {
case 1: {
if (tag !== 10) {
break
}
message.metadata = Metadata.decode(reader, reader.uint32())
continue
}
case 2: {
if (tag !== 18) {
break
}
message.baseUrl = reader.string()
continue
}
case 3: {
if (tag !== 26) {
break
}
message.apiKey = reader.string()
continue
}
}
if ((tag & 7) === 4 || tag === 0) {
break
}
reader.skip(tag & 7)
}
return message
},
fromJSON(object: any): OpenAiModelsRequest {
return {
metadata: isSet(object.metadata) ? Metadata.fromJSON(object.metadata) : undefined,
baseUrl: isSet(object.baseUrl) ? globalThis.String(object.baseUrl) : "",
apiKey: isSet(object.apiKey) ? globalThis.String(object.apiKey) : "",
}
},
toJSON(message: OpenAiModelsRequest): unknown {
const obj: any = {}
if (message.metadata !== undefined) {
obj.metadata = Metadata.toJSON(message.metadata)
}
if (message.baseUrl !== "") {
obj.baseUrl = message.baseUrl
}
if (message.apiKey !== "") {
obj.apiKey = message.apiKey
}
return obj
},
create<I extends Exact<DeepPartial<OpenAiModelsRequest>, I>>(base?: I): OpenAiModelsRequest {
return OpenAiModelsRequest.fromPartial(base ?? ({} as any))
},
fromPartial<I extends Exact<DeepPartial<OpenAiModelsRequest>, I>>(object: I): OpenAiModelsRequest {
const message = createBaseOpenAiModelsRequest()
message.metadata =
object.metadata !== undefined && object.metadata !== null ? Metadata.fromPartial(object.metadata) : undefined
message.baseUrl = object.baseUrl ?? ""
message.apiKey = object.apiKey ?? ""
return message
},
}
/** Service for model-related operations */
export type ModelsServiceDefinition = typeof ModelsServiceDefinition
export const ModelsServiceDefinition = {
@@ -704,24 +224,6 @@ export const ModelsServiceDefinition = {
responseStream: false,
options: {},
},
/** Refreshes and returns OpenRouter models */
refreshOpenRouterModels: {
name: "refreshOpenRouterModels",
requestType: EmptyRequest,
requestStream: false,
responseType: OpenRouterModels,
responseStream: false,
options: {},
},
/** Refreshes and returns OpenAI models */
refreshOpenAiModels: {
name: "refreshOpenAiModels",
requestType: OpenAiModelsRequest,
requestStream: false,
responseType: StringArray,
responseStream: false,
options: {},
},
},
} as const
@@ -742,10 +244,6 @@ export type Exact<P, I extends P> = P extends Builtin
? P
: P & { [K in keyof P]: Exact<P[K], I[K]> } & { [K in Exclude<keyof I, KeysOfUnion<P>>]: never }
function isObject(value: any): boolean {
return typeof value === "object" && value !== null
}
function isSet(value: any): boolean {
return value !== null && value !== undefined
}
-208
View File
@@ -1,208 +0,0 @@
/**
* github-url-utils.ts
*
* Portable utility functions for creating and opening GitHub issue URLs
* with proper URL encoding that bypasses VS Code's URI handling issues.
*
* This utility addresses a longstanding issue in VS Code's URI handling:
* https://github.com/microsoft/vscode/issues/85930
*
* The issue causes URLs with special characters in query parameters to be incorrectly
* encoded when opened through VS Code's standard APIs (vscode.Uri.parse followed by
* vscode.env.openExternal). This particularly affects GitHub issue URLs with pre-filled
* fields containing special characters.
*/
import * as vscode from "vscode"
import * as cp from "child_process"
import * as os from "os"
import * as util from "util"
/**
* Creates a properly encoded GitHub issue URL.
*
* This function manually encodes each parameter value using encodeURIComponent()
* to ensure consistent and correct encoding of all special characters. This is
* necessary because VS Code's URI handling (vscode.Uri.parse) has issues with
* encoding/decoding URL parameters, as documented in:
* https://github.com/microsoft/vscode/issues/85930
*
* Specifically, VS Code's URI handling:
* - Double-encodes some characters like # (hash) becoming %2523 instead of %23
* - Inconsistently handles other characters like & (ampersand) and + (plus)
* - Can corrupt query parameters containing special characters
*
* @param baseUrl The base GitHub repository URL (e.g., 'https://github.com/owner/repo/issues/new')
* @param params Map of parameter names to values for the issue form
* @returns The properly encoded full URL
*/
export function createGitHubIssueUrl(baseUrl: string, params: Map<string, string>): string {
// Build query string manually with proper encoding
const queryParts: string[] = []
for (const [key, value] of params.entries()) {
const encodedKey = encodeURIComponent(key)
const encodedValue = encodeURIComponent(value)
queryParts.push(`${encodedKey}=${encodedValue}`)
}
// Determine the proper separator (? or &) based on whether baseUrl already has parameters
const separator = baseUrl.includes("?") ? "&" : "?"
// Join all parts to create the final URL
const queryString = queryParts.join("&")
return `${baseUrl}${separator}${queryString}`
}
/**
* Opens a URL using platform-specific commands to bypass VS Code's URI handling issues.
*
* IMPORTANT: This function intentionally avoids using VS Code's built-in URI handling
* (vscode.Uri.parse() and vscode.env.openExternal()) due to known encoding issues with URLs
* that contain special characters in query parameters. See:
* https://github.com/microsoft/vscode/issues/85930
*
* The specific issues with VS Code's URI handling include:
* 1. Double-encoding of certain characters (e.g., # becomes %23 then %2523)
* 2. Inconsistent handling where some characters are encoded and others are decoded
* 3. Issues with parameters in the query string being incorrectly processed
*
* Instead, this function:
* - Uses direct OS commands to open the browser with the URL
* - Preserves the exact encoding of the URL as provided
* - Provides multiple fallback approaches if the primary method fails
*
* @param url The URL to open
* @returns A promise that resolves when an attempt to open the URL has completed
*/
export async function openUrlInBrowser(url: string): Promise<void> {
// For debugging
console.log(`Opening URL: ${url}`)
// Always copy to clipboard as a fallback
try {
await vscode.env.clipboard.writeText(url)
console.log("URL copied to clipboard as backup")
} catch (error) {
console.error(`Failed to copy URL to clipboard: ${error}`)
}
// Try to open the URL using platform-specific commands
try {
const platform = os.platform()
console.log(`Detected platform: ${platform}`)
// Use promisify for better async error handling
const execPromise = util.promisify(cp.exec)
// Use platform-specific commands
if (platform === "win32") {
// Windows - try multiple approaches
try {
await execPromise(`start "" "${url}"`)
console.log("Opened URL with Windows 'start' command")
return
} catch (winError) {
console.error(`Error with Windows 'start' command: ${winError}`)
try {
await execPromise(`powershell.exe -Command "Start-Process '${url}'"`)
console.log("Opened URL with PowerShell command")
return
} catch (psError) {
console.error(`Error with PowerShell command: ${psError}`)
// Fall through to the fallbacks
}
}
} else if (platform === "darwin") {
// macOS
await execPromise(`open "${url}"`)
console.log("Opened URL with macOS 'open' command")
return
} else {
// Linux and others - try multiple commands
const linuxCommands = ["xdg-open", "gnome-open", "kde-open", "wslview"]
for (const cmd of linuxCommands) {
try {
await execPromise(`${cmd} "${url}"`)
console.log(`Opened URL with '${cmd}' command`)
return
} catch (cmdError) {
console.error(`Error with '${cmd}' command: ${cmdError}`)
// Try next command
}
}
}
// If we got here, none of the OS commands worked
throw new Error("All OS commands failed")
} catch (error) {
console.error(`OS commands failed: ${error}`)
// First fallback: Try VS Code's openExternal
// Note: This will likely have encoding issues per https://github.com/microsoft/vscode/issues/85930
// but we include it as a fallback in case OS commands completely fail
try {
// The 'true' parameter might help preserve some encodings, but this is not guaranteed
await vscode.env.openExternal(vscode.Uri.parse(url, true))
console.log("Opened URL with vscode.env.openExternal (note: URL encoding may be affected)")
return
} catch (vscodeError) {
console.error(`Error with vscode.env.openExternal: ${vscodeError}`)
// Last fallback: Show a message with instructions
vscode.window
.showInformationMessage(
"Couldn't open the URL automatically. It has been copied to your clipboard.",
"Copy URL Again",
)
.then((selection) => {
if (selection === "Copy URL Again") {
vscode.env.clipboard.writeText(url)
}
})
}
}
}
/**
* Utility function to create and open a GitHub issue with the specified parameters.
*
* This is a high-level function that combines URL creation and opening while
* working around VS Code's URI handling limitations (issue #85930). It provides
* a simple API for the common use case of opening GitHub issue templates with
* pre-filled fields.
*
* The function:
* 1. Constructs a correctly formatted GitHub issue URL
* 2. Properly encodes all special characters in parameters
* 3. Opens the URL directly using OS commands to avoid VS Code's problematic URI handling
* 4. Provides fallback options if opening fails
*
* Reference for the VS Code URI handling issue:
* https://github.com/microsoft/vscode/issues/85930
*
* @param repoOwner GitHub repository owner/organization
* @param repoName GitHub repository name
* @param issueTemplate Template name to use (e.g., 'bug_report.yml')
* @param params Map of parameter names to values for the issue form
*/
export async function createAndOpenGitHubIssue(
repoOwner: string,
repoName: string,
issueTemplate: string | null,
params: Map<string, string>,
): Promise<void> {
// Construct the base URL
let baseUrl = `https://github.com/${repoOwner}/${repoName}/issues/new`
// Add template parameter if provided
if (issueTemplate) {
params.set("template", issueTemplate)
}
// Create the URL and open it
const issueUrl = createGitHubIssueUrl(baseUrl, params)
await openUrlInBrowser(issueUrl)
}
-11
View File
@@ -1,11 +0,0 @@
import { SVGProps } from "react"
const ClineLogoBlack = (props: SVGProps<SVGSVGElement>) => (
<svg xmlns="http://www.w3.org/2000/svg" width="47" height="50" viewBox="0 0 47 50" fill="none" {...props}>
<path
d="M46.4075 28.1192L43.5011 22.3166V18.9747C43.5011 13.4354 39.0302 8.94931 33.5162 8.94931H28.5491C28.9086 8.21513 29.106 7.3898 29.106 6.5189C29.106 3.44039 26.6149 0.949219 23.5363 0.949219C20.4578 0.949219 17.9667 3.44039 17.9667 6.5189C17.9667 7.3898 18.1641 8.21513 18.5236 8.94931H13.5565C8.04249 8.94931 3.57155 13.4354 3.57155 18.9747V22.3166L0.604424 28.104C0.305687 28.6863 0.305687 29.3799 0.604424 29.9622L3.57155 35.6838V39.0256C3.57155 44.5649 8.04249 49.0511 13.5565 49.0511H33.5162C39.0302 49.0511 43.5011 44.5649 43.5011 39.0256V35.6838L46.4024 29.942C46.691 29.3698 46.691 28.6964 46.4075 28.1192ZM20.4983 32.8483C20.4983 35.3648 18.4578 37.4053 15.9413 37.4053C13.4248 37.4053 11.3843 35.3648 11.3843 32.8483V24.747C11.3843 22.2305 13.4248 20.19 15.9413 20.19C18.4578 20.19 20.4983 22.2305 20.4983 24.747V32.8483ZM35.182 32.8483C35.182 35.3648 33.1415 37.4053 30.625 37.4053C28.1085 37.4053 26.068 35.3648 26.068 32.8483V24.747C26.068 22.2305 28.1085 20.19 30.625 20.19C33.1415 20.19 35.182 22.2305 35.182 24.747V32.8483Z"
fill="black"
/>
</svg>
)
export default ClineLogoBlack
@@ -1,21 +0,0 @@
import { SVGProps } from "react"
/**
* ClineLogoVariable component renders the Cline logo with automatic theme adaptation.
*
* This component uses the VS Code theme variable `--vscode-icon-foreground` for the fill color,
* which automatically adjusts based on the active VS Code theme (light, dark, high contrast)
* to ensure optimal contrast with the background.
*
* @param {SVGProps<SVGSVGElement>} props - Standard SVG props including className, style, etc.
* @returns {JSX.Element} SVG Cline logo that adapts to VS Code themes
*/
const ClineLogoVariable = (props: SVGProps<SVGSVGElement>) => (
<svg xmlns="http://www.w3.org/2000/svg" width="47" height="50" viewBox="0 0 47 50" fill="none" {...props}>
<path
d="M46.4075 28.1192L43.5011 22.3166V18.9747C43.5011 13.4354 39.0302 8.94931 33.5162 8.94931H28.5491C28.9086 8.21513 29.106 7.3898 29.106 6.5189C29.106 3.44039 26.6149 0.949219 23.5363 0.949219C20.4578 0.949219 17.9667 3.44039 17.9667 6.5189C17.9667 7.3898 18.1641 8.21513 18.5236 8.94931H13.5565C8.04249 8.94931 3.57155 13.4354 3.57155 18.9747V22.3166L0.604424 28.104C0.305687 28.6863 0.305687 29.3799 0.604424 29.9622L3.57155 35.6838V39.0256C3.57155 44.5649 8.04249 49.0511 13.5565 49.0511H33.5162C39.0302 49.0511 43.5011 44.5649 43.5011 39.0256V35.6838L46.4024 29.942C46.691 29.3698 46.691 28.6964 46.4075 28.1192ZM20.4983 32.8483C20.4983 35.3648 18.4578 37.4053 15.9413 37.4053C13.4248 37.4053 11.3843 35.3648 11.3843 32.8483V24.747C11.3843 22.2305 13.4248 20.19 15.9413 20.19C18.4578 20.19 20.4983 22.2305 20.4983 24.747V32.8483ZM35.182 32.8483C35.182 35.3648 33.1415 37.4053 30.625 37.4053C28.1085 37.4053 26.068 35.3648 26.068 32.8483V24.747C26.068 22.2305 28.1085 20.19 30.625 20.19C33.1415 20.19 35.182 22.2305 35.182 24.747V32.8483Z"
fill="var(--vscode-icon-foreground)"
/>
</svg>
)
export default ClineLogoVariable
+69 -20
View File
@@ -44,23 +44,22 @@ const Announcement = ({ version, hideAnnouncement }: AnnouncementProps) => {
</h3>
<ul style={ulStyle}>
<li>
<b>Task Timeline:</b> See the history of your coding journey with a visual timeline of checkpoints, letting
you understand what Cline did at a glance.
<b>Gemini prompt caching:</b> Gemini and Vertex providers now support prompt caching and price tracking for
Gemini models.
</li>
<li>
<b>UX Improvements:</b> Type while Cline works, smarter auto-scrolling, new copy buttons for task headers and
messages, and a simplified home interface for a smoother experience.
<b>Copy Buttons:</b> Buttons were added to Markdown and Code blocks that allow you to copy their contents
easily.
</li>
<li>
<b>Commit Message Generation:</b> Let Cline help craft meaningful commit messages based on your changes.
<b>/newrule command:</b> New slash command to have cline write your .clinerules for you based on your
workflow.
</li>
<li>
<b>Quote Replies:</b> Easily reference previous messages with new quote reply support for clearer
conversations.
</li>
<li>
<b>Auto Caching for Gemini:</b> Native support for Gemini's recently released Implicit Caching.
<b>Drag and drop improvements:</b> Don't forget to hold shift while dragging files!
</li>
<li>Added more checkpoints across the task, allowing you to restore from more than just file changes.</li>
<li>Added support for rendering LaTeX in message responses. (Try asking Cline to show the quadratic formula)</li>
</ul>
<Accordion isCompact className="pl-0">
<AccordionItem
@@ -75,24 +74,74 @@ const Announcement = ({ version, hideAnnouncement }: AnnouncementProps) => {
}}>
<ul style={ulStyle}>
<li>
<b>Gemini prompt caching:</b> Gemini and Vertex providers now support prompt caching and price
tracking for Gemini models.
</li>
<li>
<b>Copy Buttons:</b> Buttons were added to Markdown and Code blocks that allow you to copy their
contents easily.
</li>
<li>
<b>Global Cline Rules:</b> Store multiple rules files in Documents/Cline/Rules to share between
<b>Global Cline Rules:</b> store multiple rules files in Documents/Cline/Rules to share between
projects.
</li>
<li>
<b>Cline Rules Popup:</b> New button in the chat area to view workspace and global cline rules files
to plug and play specific rules for the task
</li>
<li>
<b>Slash Commands:</b> Type <code>/</code> in chat to see the list of quick actions, like starting a
new task.
new task (more coming soon!)
</li>
<li>
<b>Edit Messages:</b> You can now edit a message you sent previously by clicking on it. Optionally
restore your project when the message was sent!
</li>
</ul>
</AccordionItem>
</Accordion>
{/*
// Leave this here for an example of how to structure the announcement
<ul style={{ margin: "0 0 8px", paddingLeft: "12px" }}>
<li>
OpenRouter now supports prompt caching! They also have much higher rate limits than other providers,
so I recommend trying them out.
<br />
{!apiConfiguration?.openRouterApiKey && (
<VSCodeButtonLink
href={getOpenRouterAuthUrl(vscodeUriScheme)}
style={{
transform: "scale(0.85)",
transformOrigin: "left center",
margin: "4px -30px 2px 0",
}}>
Get OpenRouter API Key
</VSCodeButtonLink>
)}
{apiConfiguration?.openRouterApiKey && apiConfiguration?.apiProvider !== "openrouter" && (
<VSCodeButton
onClick={() => {
vscode.postMessage({
type: "apiConfiguration",
apiConfiguration: { ...apiConfiguration, apiProvider: "openrouter" },
})
}}
style={{
transform: "scale(0.85)",
transformOrigin: "left center",
margin: "4px -30px 2px 0",
}}>
Switch to OpenRouter
</VSCodeButton>
)}
</li>
<li>
<b>Edit Cline's changes before accepting!</b> When he creates or edits a file, you can modify his
changes directly in the right side of the diff view (+ hover over the 'Revert Block' arrow button in
the center to undo "<code>{"// rest of code here"}</code>" shenanigans)
</li>
<li>
New <code>search_files</code> tool that lets Cline perform regex searches in your project, letting
him refactor code, address TODOs and FIXMEs, remove dead code, and more!
</li>
<li>
When Cline runs commands, you can now type directly in the terminal (+ support for Python
environments)
</li>
</ul>*/}
<div style={hrStyle} />
<p style={linkContainerStyle}>
Join us on{" "}
@@ -0,0 +1,488 @@
import { VSCodeCheckbox, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import { useCallback, useMemo, useRef, useState } from "react"
import styled from "styled-components"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { AutoApprovalSettings } from "@shared/AutoApprovalSettings"
import { CODE_BLOCK_BG_COLOR } from "@/components/common/CodeBlock"
import { vscode } from "@/utils/vscode"
import { getAsVar, VSC_FOREGROUND, VSC_TITLEBAR_INACTIVE_FOREGROUND, VSC_DESCRIPTION_FOREGROUND } from "@/utils/vscStyles"
import { useClickAway } from "react-use"
interface AutoApproveMenuProps {
style?: React.CSSProperties
}
const SubOptionAnimateIn = styled.div<{ show: boolean }>`
max-height: ${(props) => (props.show ? "100px" : "0")};
opacity: ${(props) => (props.show ? "1" : "0")};
overflow: hidden;
transition:
max-height 0.2s ease-in-out,
opacity 0.2s ease-in-out;
`
const ACTION_METADATA: {
id: keyof AutoApprovalSettings["actions"]
label: string
shortName: string
description: string
}[] = [
{
id: "readFiles",
label: "Read project files",
shortName: "Read Local",
description: "Allows Cline to read files within your workspace.",
},
{
id: "readFilesExternally",
label: "Read all files",
shortName: "Read (all)",
description: "Allows Cline to read any file on your computer.",
},
{
id: "editFiles",
label: "Edit project files",
shortName: "Edit",
description: "Allows Cline to modify files within your workspace.",
},
{
id: "editFilesExternally",
label: "Edit all files",
shortName: "Edit (all)",
description: "Allows Cline to modify any file on your computer.",
},
{
id: "executeSafeCommands",
label: "Execute safe commands",
shortName: "Safe Commands",
description:
"Allows Cline to execute of safe terminal commands. If the model determines a command is potentially destructive, it will still require approval.",
},
{
id: "executeAllCommands",
label: "Execute all commands",
shortName: "All Commands",
description: "Allows Cline to execute all terminal commands. Use at your own risk.",
},
{
id: "useBrowser",
label: "Use the browser",
shortName: "Browser",
description: "Allows Cline to launch and interact with any website in a browser.",
},
{
id: "useMcp",
label: "Use MCP servers",
shortName: "MCP",
description: "Allows Cline to use configured MCP servers which may modify filesystem or interact with APIs.",
},
]
const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => {
const { autoApprovalSettings } = useExtensionState()
const [isExpanded, setIsExpanded] = useState(false)
const [isHoveringCollapsibleSection, setIsHoveringCollapsibleSection] = useState(false)
const menuRef = useRef<HTMLDivElement>(null)
// Careful not to use partials to mutate since spread operator only does shallow copy
const enabledActions = ACTION_METADATA.filter((action) => autoApprovalSettings.actions[action.id])
const enabledActionsList = useMemo(() => {
// When nested auto-approve options are used, display the more permissive one (file reads, edits, and commands)
const readFilesEnabled = enabledActions.some((action) => action.id === "readFiles")
const readFilesExternallyEnabled = enabledActions.some((action) => action.id === "readFilesExternally")
const editFilesEnabled = enabledActions.some((action) => action.id === "editFiles")
const editFilesExternallyEnabled = enabledActions.some((action) => action.id === "editFilesExternally") ?? false
const safeCommandsEnabled = enabledActions.some((action) => action.id === "executeSafeCommands")
const allCommandsEnabled = enabledActions.some((action) => action.id === "executeAllCommands") ?? false
// Filter out the potentially nested options so we don't display them twice
const otherActions = enabledActions
.filter(
(action) =>
action.id !== "readFiles" &&
action.id !== "readFilesExternally" &&
action.id !== "editFiles" &&
action.id !== "editFilesExternally" &&
action.id !== "executeSafeCommands" &&
action.id !== "executeAllCommands",
)
.map((action) => action.shortName)
const labels = []
// Handle read editing labels
if ((readFilesExternallyEnabled ?? false) && readFilesEnabled) {
labels.push("Read (All)")
} else if (readFilesEnabled) {
labels.push("Read")
}
// Handle file editing labels
if ((editFilesExternallyEnabled ?? false) && editFilesEnabled) {
labels.push("Edit (All)")
} else if (editFilesEnabled) {
labels.push("Edit")
}
// Handle command execution labels
if ((allCommandsEnabled ?? false) && safeCommandsEnabled) {
labels.push("All Commands")
} else if (safeCommandsEnabled) {
labels.push("Safe Commands")
}
// Add remaining actions
return [...labels, ...otherActions].join(", ")
}, [enabledActions])
// This value is used to determine if the auto-approve menu should show 'Auto-approve: None'
// Note: we should use better logic to determine the state where no auto approve actions are in effect, regardless of the state of sub-auto-approve options
const hasEnabledActions = useMemo(() => {
let enabledActionsCount = enabledActions.length
if (!autoApprovalSettings.actions.readFiles && autoApprovalSettings.actions.readFilesExternally) {
enabledActionsCount--
}
if (!autoApprovalSettings.actions.editFiles && autoApprovalSettings.actions.editFilesExternally) {
enabledActionsCount--
}
if (!autoApprovalSettings.actions.executeSafeCommands && autoApprovalSettings.actions.executeAllCommands) {
enabledActionsCount--
}
return enabledActionsCount > 0
}, [enabledActions, autoApprovalSettings.actions])
// Get the full extension state to ensure we have the most up-to-date settings
const extensionState = useExtensionState()
const updateEnabled = useCallback(
(enabled: boolean) => {
const currentSettings = extensionState.autoApprovalSettings
vscode.postMessage({
type: "autoApprovalSettings",
autoApprovalSettings: {
...currentSettings,
version: (currentSettings.version ?? 1) + 1,
enabled,
},
})
},
[extensionState.autoApprovalSettings],
)
const updateAction = useCallback(
(actionId: keyof AutoApprovalSettings["actions"], value: boolean) => {
const currentSettings = extensionState.autoApprovalSettings
// Calculate what the new actions state will be
const newActions = {
...currentSettings.actions,
[actionId]: value,
}
// Check if this will result in any enabled actions
const willHaveEnabledActions = Object.values(newActions).some(Boolean)
vscode.postMessage({
type: "autoApprovalSettings",
autoApprovalSettings: {
...currentSettings,
version: (currentSettings.version ?? 1) + 1,
actions: newActions,
// If no actions will be enabled, ensure the main toggle is off
enabled: willHaveEnabledActions ? currentSettings.enabled : false,
},
})
},
[extensionState.autoApprovalSettings],
)
const updateMaxRequests = useCallback(
(maxRequests: number) => {
const currentSettings = extensionState.autoApprovalSettings
vscode.postMessage({
type: "autoApprovalSettings",
autoApprovalSettings: {
...currentSettings,
version: (currentSettings.version ?? 1) + 1,
maxRequests,
},
})
},
[extensionState.autoApprovalSettings],
)
const updateNotifications = useCallback(
(enableNotifications: boolean) => {
const currentSettings = extensionState.autoApprovalSettings
vscode.postMessage({
type: "autoApprovalSettings",
autoApprovalSettings: {
...currentSettings,
version: (currentSettings.version ?? 1) + 1,
enableNotifications,
},
})
},
[extensionState.autoApprovalSettings],
)
// Handle clicks outside the menu to close it
useClickAway(menuRef, () => {
if (isExpanded) {
setIsExpanded(false)
}
})
return (
<div
ref={menuRef}
style={{
padding: "0 15px",
userSelect: "none",
borderTop: isExpanded
? `0.5px solid color-mix(in srgb, ${getAsVar(VSC_TITLEBAR_INACTIVE_FOREGROUND)} 20%, transparent)`
: "none",
overflowY: "auto",
backgroundColor: isExpanded ? CODE_BLOCK_BG_COLOR : "transparent",
...style,
}}>
<div
style={{
display: "flex",
alignItems: "center",
gap: "8px",
padding: isExpanded ? "8px 0" : "8px 0 0 0",
cursor: !hasEnabledActions ? "pointer" : "default",
}}
onMouseEnter={() => {
if (!hasEnabledActions) {
setIsHoveringCollapsibleSection(true)
}
}}
onMouseLeave={() => {
if (!hasEnabledActions) {
setIsHoveringCollapsibleSection(false)
}
}}
onClick={() => {
if (!hasEnabledActions) {
setIsExpanded((prev) => !prev)
}
}}>
<VSCodeCheckbox
style={{
pointerEvents: hasEnabledActions ? "auto" : "none",
}}
checked={hasEnabledActions && autoApprovalSettings.enabled}
disabled={!hasEnabledActions}
// onChange={(e) => {
// const checked = (e.target as HTMLInputElement).checked
// updateEnabled(checked)
// }}
onClick={(e) => {
/*
vscode web toolkit bug: when changing the value of a vscodecheckbox programmatically, it will call its onChange with stale state. This led to updateEnabled being called with an old version of autoApprovalSettings, effectively undoing the state change that was triggered by the last action being unchecked. A simple workaround is to just not use onChange and instead use onClick. We are lucky this is a checkbox and the newvalue is simply opposite of current state.
*/
if (!hasEnabledActions) return
e.stopPropagation() // stops click from bubbling up to the parent, in this case stopping the expanding/collapsing
updateEnabled(!autoApprovalSettings.enabled)
}}
/>
<CollapsibleSection
isHovered={isHoveringCollapsibleSection}
style={{ cursor: "pointer" }}
onClick={() => {
// to prevent this from counteracting parent
if (hasEnabledActions) {
setIsExpanded((prev) => !prev)
}
}}>
<span
style={{
color: getAsVar(VSC_FOREGROUND),
whiteSpace: "nowrap",
}}>
Auto-approve:
</span>
<span
style={{
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
}}>
{!hasEnabledActions ? "None" : enabledActionsList}
</span>
<span
className={`codicon codicon-chevron-${isExpanded ? "down" : "right"}`}
style={{
flexShrink: 0,
marginLeft: isExpanded ? "2px" : "-2px",
}}
/>
</CollapsibleSection>
</div>
{isExpanded && (
<div style={{ padding: "0" }}>
<div
style={{
marginBottom: "10px",
color: getAsVar(VSC_DESCRIPTION_FOREGROUND),
fontSize: "12px",
}}>
Auto-approve allows Cline to perform the following actions without asking for permission. Please use with
caution and only enable if you understand the risks.
</div>
{ACTION_METADATA.map((action) => {
// Handle readFilesExternally, editFilesExternally, and executeAllCommands as animated sub-options
if (
action.id === "executeAllCommands" ||
action.id === "editFilesExternally" ||
action.id === "readFilesExternally"
) {
const parentAction =
action.id === "executeAllCommands"
? "executeSafeCommands"
: action.id === "readFilesExternally"
? "readFiles"
: "editFiles"
return (
<SubOptionAnimateIn key={action.id} show={autoApprovalSettings.actions[parentAction] ?? false}>
<div
style={{
margin: "3px 0",
marginLeft: "28px",
}}>
<VSCodeCheckbox
checked={autoApprovalSettings.actions[action.id]}
onChange={(e) => {
const checked = (e.target as HTMLInputElement).checked
updateAction(action.id, checked)
}}>
{action.label}
</VSCodeCheckbox>
<div
style={{
marginLeft: "28px",
color: getAsVar(VSC_DESCRIPTION_FOREGROUND),
fontSize: "12px",
}}>
{action.description}
</div>
</div>
</SubOptionAnimateIn>
)
}
return (
<div
key={action.id}
style={{
margin: "6px 0",
}}>
<VSCodeCheckbox
checked={autoApprovalSettings.actions[action.id]}
onChange={(e) => {
const checked = (e.target as HTMLInputElement).checked
updateAction(action.id, checked)
}}>
{action.label}
</VSCodeCheckbox>
<div
style={{
marginLeft: "28px",
color: getAsVar(VSC_DESCRIPTION_FOREGROUND),
fontSize: "12px",
}}>
{action.description}
</div>
</div>
)
})}
<div
style={{
height: "0.5px",
background: getAsVar(VSC_TITLEBAR_INACTIVE_FOREGROUND),
margin: "15px 0",
opacity: 0.2,
}}
/>
<div
style={{
display: "flex",
alignItems: "center",
gap: "8px",
marginTop: "10px",
marginBottom: "8px",
color: getAsVar(VSC_FOREGROUND),
}}>
<span style={{ flexShrink: 1, minWidth: 0 }}>Max Requests:</span>
<VSCodeTextField
// placeholder={DEFAULT_AUTO_APPROVAL_SETTINGS.maxRequests.toString()}
value={autoApprovalSettings.maxRequests.toString()}
onInput={(e) => {
const input = e.target as HTMLInputElement
// Remove any non-numeric characters
input.value = input.value.replace(/[^0-9]/g, "")
const value = parseInt(input.value)
if (!isNaN(value) && value > 0) {
updateMaxRequests(value)
}
}}
onKeyDown={(e) => {
// Prevent non-numeric keys (except for backspace, delete, arrows)
if (!/^\d$/.test(e.key) && !["Backspace", "Delete", "ArrowLeft", "ArrowRight"].includes(e.key)) {
e.preventDefault()
}
}}
style={{ flex: 1 }}
/>
</div>
<div
style={{
color: getAsVar(VSC_DESCRIPTION_FOREGROUND),
fontSize: "12px",
marginBottom: "10px",
}}>
Cline will automatically make this many API requests before asking for approval to proceed with the task.
</div>
<div style={{ margin: "6px 0" }}>
<VSCodeCheckbox
checked={autoApprovalSettings.enableNotifications}
onChange={(e) => {
const checked = (e.target as HTMLInputElement).checked
updateNotifications(checked)
}}>
Enable Notifications
</VSCodeCheckbox>
<div
style={{
marginLeft: "28px",
color: getAsVar(VSC_DESCRIPTION_FOREGROUND),
fontSize: "12px",
}}>
Receive system notifications when Cline requires approval to proceed or when a task is completed.
</div>
</div>
</div>
)}
</div>
)
}
const CollapsibleSection = styled.div<{ isHovered?: boolean }>`
display: flex;
align-items: center;
gap: 4px;
color: ${(props) => (props.isHovered ? getAsVar(VSC_FOREGROUND) : getAsVar(VSC_DESCRIPTION_FOREGROUND))};
flex: 1;
min-width: 0;
&:hover {
color: ${getAsVar(VSC_FOREGROUND)};
}
`
export default AutoApproveMenu
@@ -89,7 +89,6 @@ import { highlightText } from "./TaskHeader"
import SuccessButton from "@/components/common/SuccessButton"
import TaskFeedbackButtons from "@/components/chat/TaskFeedbackButtons"
import NewTaskPreview from "./NewTaskPreview"
import ReportBugPreview from "./ReportBugPreview"
import McpResourceRow from "@/components/mcp/configuration/tabs/installed/server-row/McpResourceRow"
import UserMessage from "./UserMessage"
import QuoteButton from "./QuoteButton"
@@ -1476,23 +1475,6 @@ export const ChatRowContent = ({
<NewTaskPreview context={message.text || ""} />
</>
)
case "report_bug":
return (
<>
<div style={headerStyle}>
<span
className="codicon codicon-new-file"
style={{
color: normalColor,
marginBottom: "-1.5px",
}}></span>
<span style={{ color: normalColor, fontWeight: "bold" }}>
Cline wants to create a Github issue:
</span>
</div>
<ReportBugPreview data={message.text || ""} />
</>
)
case "plan_mode_respond": {
let response: string | undefined
let options: string[] | undefined
+39 -35
View File
@@ -22,7 +22,7 @@ import { TaskServiceClient } from "@/services/grpc-client"
import HistoryPreview from "@/components/history/HistoryPreview"
import { normalizeApiConfiguration } from "@/components/settings/ApiOptions"
import Announcement from "@/components/chat/Announcement"
import AutoApproveMenu from "@/components/chat/auto-approve-menu/AutoApproveMenu"
import AutoApproveMenu from "@/components/chat/AutoApproveMenu"
import BrowserSessionRow from "@/components/chat/BrowserSessionRow"
import ChatRow from "@/components/chat/ChatRow"
import ChatTextArea from "@/components/chat/ChatTextArea"
@@ -33,7 +33,6 @@ import { unified } from "unified"
import remarkStringify from "remark-stringify"
import rehypeRemark from "rehype-remark"
import rehypeParse from "rehype-parse"
import HomeHeader from "../welcome/HomeHeader"
interface ChatViewProps {
isHidden: boolean
@@ -134,16 +133,6 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
useEffect(() => {
const handleCopy = async (e: ClipboardEvent) => {
const targetElement = e.target as HTMLElement | null
// If the copy event originated from an input or textarea,
// let the default browser behavior handle it.
if (
targetElement &&
(targetElement.tagName === "INPUT" || targetElement.tagName === "TEXTAREA" || targetElement.isContentEditable)
) {
return
}
if (window.getSelection) {
const selection = window.getSelection()
if (selection && selection.rangeCount > 0) {
@@ -299,13 +288,6 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
setPrimaryButtonText("Condense Conversation")
setSecondaryButtonText(undefined)
break
case "report_bug":
setSendingDisabled(isPartial)
setClineAsk("report_bug")
setEnableButtons(!isPartial)
setPrimaryButtonText("Report GitHub issue")
setSecondaryButtonText(undefined)
break
}
break
case "say":
@@ -434,14 +416,6 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
images,
})
break
case "report_bug":
vscode.postMessage({
type: "askResponse",
askResponse: "messageResponse",
text: messageToSend,
images,
})
break
// there is no other case that a textfield should be enabled
}
}
@@ -516,12 +490,6 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
text: lastMessage?.text,
})
break
case "report_bug":
vscode.postMessage({
type: "reportBug",
text: lastMessage?.text,
})
break
}
setSendingDisabled(true)
setClineAsk(undefined)
@@ -1003,12 +971,47 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
{showAnnouncement && <Announcement version={version} hideAnnouncement={hideAnnouncement} />}
<HomeHeader />
<div style={{ padding: "0 20px", flexShrink: 0 }}>
<h2>What can I do for you?</h2>
<p>
Thanks to{" "}
<VSCodeLink href="https://www.anthropic.com/claude/sonnet" style={{ display: "inline" }}>
Claude 3.7 Sonnet's
</VSCodeLink>
agentic coding capabilities, I can handle complex software development tasks step-by-step. With tools
that let me create & edit files, explore complex projects, use a browser, and execute terminal
commands (after you grant permission), I can assist you in ways that go beyond code completion or tech
support. I can even use MCP to create new tools and extend my own capabilities.
</p>
</div>
{taskHistory.length > 0 && <HistoryPreview showHistoryView={showHistoryView} />}
</div>
)}
{!task && <AutoApproveMenu />}
{/*
// Flex layout explanation:
// 1. Content div above uses flex: "1 1 0" to:
// - Grow to fill available space (flex-grow: 1)
// - Shrink when AutoApproveMenu needs space (flex-shrink: 1)
// - Start from zero size (flex-basis: 0) to ensure proper distribution
// minHeight: 0 allows it to shrink below its content height
//
// 2. AutoApproveMenu uses flex: "0 1 auto" to:
// - Not grow beyond its content (flex-grow: 0)
// - Shrink when viewport is small (flex-shrink: 1)
// - Use its content size as basis (flex-basis: auto)
// This ensures it takes its natural height when there's space
// but becomes scrollable when the viewport is too small
*/}
{!task && (
<AutoApproveMenu
style={{
marginBottom: -2,
flex: "0 1 auto", // flex-grow: 0, flex-shrink: 1, flex-basis: auto
minHeight: 0,
}}
/>
)}
{task && (
<>
@@ -1098,6 +1101,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
</>
)}
{(() => {
console.log("[ChatView] Rendering - activeQuote:", activeQuote) // Log here
return activeQuote ? (
<div style={{ marginBottom: "-12px", marginTop: "10px" }}>
<QuotedMessagePreview
@@ -1,84 +0,0 @@
import React from "react"
import MarkdownBlock from "../common/MarkdownBlock"
interface ReportBugPreviewProps {
data: string
}
const ReportBugPreview: React.FC<ReportBugPreviewProps> = ({ data }) => {
// Parse the JSON data from the context string
const bugData = React.useMemo(() => {
try {
return JSON.parse(data || "{}")
} catch (e) {
console.error("Failed to parse bug report data", e)
return {}
}
}, [data])
return (
<div className="bg-[var(--vscode-badge-background)] text-[var(--vscode-badge-foreground)] rounded-[3px] p-[14px]">
<h3 className="font-bold text-base mb-3 mt-0">{bugData.title || "Bug Report"}</h3>
<div className="space-y-3 text-sm">
{bugData.what_happened && (
<div>
<div className="font-semibold">What Happened?</div>
<MarkdownBlock markdown={bugData.what_happened} />
</div>
)}
{bugData.steps_to_reproduce && (
<div>
<div className="font-semibold">Steps to Reproduce</div>
<MarkdownBlock markdown={bugData.steps_to_reproduce} />
</div>
)}
{bugData.api_request_output && (
<div>
<div className="font-semibold">Relevant API Request Output</div>
<MarkdownBlock markdown={bugData.api_request_output} />
</div>
)}
{bugData.provider_and_model && (
<div>
<div className="font-semibold">Provider/Model</div>
<MarkdownBlock markdown={bugData.provider_and_model} />
</div>
)}
{bugData.operating_system && (
<div>
<div className="font-semibold">Operating System</div>
<MarkdownBlock markdown={bugData.operating_system} />
</div>
)}
{bugData.system_info && (
<div>
<div className="font-semibold">System Info</div>
<MarkdownBlock markdown={bugData.system_info} />
</div>
)}
{bugData.cline_version && (
<div>
<div className="font-semibold">Cline Version</div>
<MarkdownBlock markdown={bugData.cline_version} />
</div>
)}
{bugData.additional_context && (
<div>
<div className="font-semibold">Additional Context</div>
<MarkdownBlock markdown={bugData.additional_context} />
</div>
)}
</div>
</div>
)
}
export default ReportBugPreview
@@ -1,478 +0,0 @@
import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { AutoApprovalSettings } from "@shared/AutoApprovalSettings"
import { CODE_BLOCK_BG_COLOR } from "@/components/common/CodeBlock"
import AutoApproveMenuItem from "./AutoApproveMenuItem"
import { vscode } from "@/utils/vscode"
import { getAsVar, VSC_FOREGROUND, VSC_TITLEBAR_INACTIVE_FOREGROUND, VSC_DESCRIPTION_FOREGROUND } from "@/utils/vscStyles"
import { useClickAway } from "react-use"
import HeroTooltip from "@/components/common/HeroTooltip"
const breakpoint = 500
interface AutoApproveMenuProps {
style?: React.CSSProperties
}
export interface ActionMetadata {
id: keyof AutoApprovalSettings["actions"] | "enableNotifications" | "enableAll"
label: string
shortName: string
description: string
icon: string
subAction?: ActionMetadata
sub?: boolean
parentActionId?: string
}
const ACTION_METADATA: ActionMetadata[] = [
{
id: "readFiles",
label: "Read project files",
shortName: "Read",
description: "Allows Cline to read files within your workspace.",
icon: "codicon-search",
subAction: {
id: "readFilesExternally",
label: "Read all files",
shortName: "Read (all)",
description: "Allows Cline to read any file on your computer.",
icon: "codicon-folder-opened",
parentActionId: "readFiles",
},
},
{
id: "editFiles",
label: "Edit project files",
shortName: "Edit",
description: "Allows Cline to modify files within your workspace.",
icon: "codicon-edit",
subAction: {
id: "editFilesExternally",
label: "Edit all files",
shortName: "Edit (all)",
description: "Allows Cline to modify any file on your computer.",
icon: "codicon-files",
parentActionId: "editFiles",
},
},
{
id: "executeSafeCommands",
label: "Execute safe commands",
shortName: "Safe Commands",
description:
"Allows Cline to execute safe terminal commands. If the model determines a command is potentially destructive, it will still require approval.",
icon: "codicon-terminal",
subAction: {
id: "executeAllCommands",
label: "Execute all commands",
shortName: "All Commands",
description: "Allows Cline to execute all terminal commands. Use at your own risk.",
icon: "codicon-terminal-bash",
parentActionId: "executeSafeCommands",
},
},
{
id: "useBrowser",
label: "Use the browser",
shortName: "Browser",
description: "Allows Cline to launch and interact with any website in a browser.",
icon: "codicon-globe",
},
{
id: "useMcp",
label: "Use MCP servers",
shortName: "MCP",
description: "Allows Cline to use configured MCP servers which may modify filesystem or interact with APIs.",
icon: "codicon-server",
},
{
id: "enableAll",
label: "Enable all",
shortName: "All",
description: "Enable all actions.",
icon: "codicon-checklist",
},
{
id: "enableNotifications",
label: "Enable notifications",
shortName: "Notifications",
description: "Receive system notifications when Cline requires approval to proceed or when a task is completed.",
icon: "codicon-bell",
},
]
const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => {
const { autoApprovalSettings } = useExtensionState()
const [isExpanded, setIsExpanded] = useState(false)
const [containerWidth, setContainerWidth] = useState(0)
// Favorites are now derived from autoApprovalSettings
const favorites = useMemo(() => autoApprovalSettings.favorites || [], [autoApprovalSettings.favorites])
const menuRef = useRef<HTMLDivElement>(null)
const itemsContainerRef = useRef<HTMLDivElement>(null)
// Track container width for responsive layout
useEffect(() => {
if (!isExpanded) return
const updateWidth = () => {
if (itemsContainerRef.current) {
setContainerWidth(itemsContainerRef.current.offsetWidth)
}
}
// Initial measurement
updateWidth()
// Set up resize observer
const resizeObserver = new ResizeObserver(updateWidth)
if (itemsContainerRef.current) {
resizeObserver.observe(itemsContainerRef.current)
}
// Clean up
return () => {
resizeObserver.disconnect()
}
}, [isExpanded])
const toggleFavorite = useCallback(
(actionId: string) => {
const currentFavorites = autoApprovalSettings.favorites || []
let newFavorites: string[]
if (currentFavorites.includes(actionId)) {
newFavorites = currentFavorites.filter((id) => id !== actionId)
} else {
newFavorites = [...currentFavorites, actionId]
}
vscode.postMessage({
type: "autoApprovalSettings",
autoApprovalSettings: {
...autoApprovalSettings,
version: (autoApprovalSettings.version ?? 1) + 1,
favorites: newFavorites,
},
})
},
[autoApprovalSettings],
)
const updateAction = useCallback(
(action: ActionMetadata, value: boolean) => {
const actionId = action.id
const subActionId = action.subAction?.id
if (actionId === "enableAll" || subActionId === "enableAll") {
toggleAll(action, value)
return
}
if (actionId === "enableNotifications" || subActionId === "enableNotifications") {
updateNotifications(action, value)
return
}
let newActions = {
...autoApprovalSettings.actions,
[actionId]: value,
}
if (value === false && subActionId) {
newActions[subActionId] = false
}
if (value === true && action.parentActionId) {
newActions[action.parentActionId as keyof AutoApprovalSettings["actions"]] = true
}
// Check if this will result in any enabled actions
const willHaveEnabledActions = Object.values(newActions).some(Boolean)
vscode.postMessage({
type: "autoApprovalSettings",
autoApprovalSettings: {
...autoApprovalSettings,
version: (autoApprovalSettings.version ?? 1) + 1,
actions: newActions,
enabled: willHaveEnabledActions,
},
})
},
[autoApprovalSettings],
)
const updateMaxRequests = useCallback(
(maxRequests: number) => {
const currentSettings = autoApprovalSettings
vscode.postMessage({
type: "autoApprovalSettings",
autoApprovalSettings: {
...currentSettings,
version: (currentSettings.version ?? 1) + 1,
maxRequests,
},
})
},
[autoApprovalSettings],
)
const updateNotifications = useCallback(
(action: ActionMetadata, checked: boolean) => {
if (action.id === "enableNotifications") {
const currentSettings = autoApprovalSettings
vscode.postMessage({
type: "autoApprovalSettings",
autoApprovalSettings: {
...currentSettings,
version: (currentSettings.version ?? 1) + 1,
enableNotifications: checked,
},
})
}
},
[autoApprovalSettings],
)
const toggleAll = useCallback(
(action: ActionMetadata, checked: boolean) => {
let actions = { ...autoApprovalSettings.actions }
for (const action of Object.keys(actions)) {
actions[action as keyof AutoApprovalSettings["actions"]] = checked
}
vscode.postMessage({
type: "autoApprovalSettings",
autoApprovalSettings: {
...autoApprovalSettings,
version: (autoApprovalSettings.version ?? 1) + 1,
actions,
},
})
},
[autoApprovalSettings],
)
// Handle clicks outside the menu to close it
useClickAway(menuRef, () => {
if (isExpanded) {
setIsExpanded(false)
}
})
// Render a favorited item with a checkbox
const renderFavoritedItem = (favId: string) => {
// Regular action item
const action = ACTION_METADATA.flatMap((a) => [a, a.subAction]).find((a) => a?.id === favId)
if (!action) return null
return (
<AutoApproveMenuItem
action={action}
isChecked={isChecked}
isFavorited={isFavorited}
onToggle={updateAction}
condensed={true}
/>
)
}
const isChecked = (action: ActionMetadata): boolean => {
if (action.id === "enableNotifications") {
return autoApprovalSettings.enableNotifications
}
if (action.id === "enableAll") {
return Object.values(autoApprovalSettings.actions).every(Boolean)
}
return autoApprovalSettings.actions[action.id] ?? false
}
const isFavorited = (action: ActionMetadata): boolean => {
return favorites.includes(action.id)
}
return (
<div
ref={menuRef}
style={{
padding: "0 10px",
margin: "0 5px",
userSelect: "none",
borderTop: `0.5px solid color-mix(in srgb, ${getAsVar(VSC_TITLEBAR_INACTIVE_FOREGROUND)} 20%, transparent)`,
overflowY: "auto",
borderRadius: "10px 10px 0 0",
backgroundColor: isExpanded ? CODE_BLOCK_BG_COLOR : "transparent",
...style,
}}>
{/* Collapsed view with favorited items */}
{!isExpanded && (
<div
onClick={() => setIsExpanded(true)}
style={{
cursor: "pointer",
paddingTop: "6px",
display: "flex",
alignItems: "center",
justifyContent: "space-between",
gap: "8px",
}}>
{favorites.length > 0 ? (
<div
style={{
display: "flex",
flexWrap: "nowrap",
alignItems: "center",
overflowX: "auto",
msOverflowStyle: "none",
scrollbarWidth: "none",
WebkitOverflowScrolling: "touch",
gap: "4px",
whiteSpace: "nowrap", // Prevent text wrapping
}}>
{favorites.map((favId) => renderFavoritedItem(favId))}
</div>
) : (
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
cursor: "pointer",
}}>
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
<HeroTooltip
content="Auto-approve allows Cline to perform the following actions without asking for permission. Please use with caution and only enable if you understand the risks."
placement="top">
<span style={{ color: getAsVar(VSC_FOREGROUND), left: "0" }}>Auto-approve</span>
</HeroTooltip>
</div>
</div>
)}
<span className="codicon codicon-chevron-right" />
</div>
)}
{/* Expanded view */}
<div
style={{
maxHeight: isExpanded ? "1000px" : favorites.length > 0 ? "40px" : "22px", // Large enough to fit content
opacity: isExpanded ? 1 : 0,
overflow: "hidden",
transition: "max-height 0.3s ease-in-out, opacity 0.3s ease-in-out", // Removed padding to transition
}}>
{isExpanded && ( // Re-added conditional rendering for content
<>
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
padding: "8px 0",
cursor: "pointer",
position: "relative", // Added for positioning context
}}
onClick={() => setIsExpanded(false)}>
<HeroTooltip
content="Auto-approve allows Cline to perform the following actions without asking for permission. Please use with caution and only enable if you understand the risks."
placement="top">
<span style={{ color: getAsVar(VSC_FOREGROUND) }}>Auto-approve</span>
</HeroTooltip>
<span className="codicon codicon-chevron-down" />
</div>
<div
ref={itemsContainerRef}
style={{
display: containerWidth > breakpoint ? "grid" : "flex",
gridTemplateColumns: containerWidth > breakpoint ? "1fr 1fr" : "1fr",
gridAutoRows: "min-content",
flexDirection: "column",
gap: "4px",
margin: "8px 0",
position: "relative", // For absolute positioning of the separator
}}>
{/* Vertical separator line - only visible in two-column mode */}
{containerWidth > breakpoint && (
<div
style={{
position: "absolute",
left: "50%",
top: "0",
bottom: "0",
width: "0.5px",
background: getAsVar(VSC_TITLEBAR_INACTIVE_FOREGROUND),
opacity: 0.2,
transform: "translateX(-50%)", // Center the line
}}
/>
)}
{/* All items in a single list - CSS Grid will handle the column distribution */}
{ACTION_METADATA.map((action) => (
<div key={action.id} style={{ breakInside: "avoid" }}>
<AutoApproveMenuItem
action={action}
isChecked={isChecked}
isFavorited={isFavorited}
onToggle={updateAction}
onToggleFavorite={toggleFavorite}
/>
</div>
))}
</div>
<div
style={{
height: "0.5px",
background: getAsVar(VSC_TITLEBAR_INACTIVE_FOREGROUND),
margin: "10px 0",
opacity: 0.2,
}}
/>
<HeroTooltip
content="Cline will automatically make this many API requests before asking for approval to proceed with the task."
placement="top">
<div
style={{
display: "flex",
alignItems: "center",
gap: "8px",
width: "100%",
paddingBottom: "10px",
}}>
<span className="codicon codicon-settings" style={{ color: "#CCCCCC", fontSize: "14px" }} />
<span style={{ color: "#CCCCCC", fontSize: "12px", fontWeight: 500 }}>Max Requests:</span>
<VSCodeTextField
style={{ flex: "1", width: "100%" }}
value={autoApprovalSettings.maxRequests.toString()}
onInput={(e) => {
const input = e.target as HTMLInputElement
// Remove any non-numeric characters
input.value = input.value.replace(/[^0-9]/g, "")
const value = parseInt(input.value)
if (!isNaN(value) && value > 0) {
updateMaxRequests(value)
}
}}
onKeyDown={(e) => {
// Prevent non-numeric keys (except for backspace, delete, arrows)
if (
!/^\d$/.test(e.key) &&
!["Backspace", "Delete", "ArrowLeft", "ArrowRight"].includes(e.key)
) {
e.preventDefault()
}
}}
/>
</div>
</HeroTooltip>
</>
)}
</div>
</div>
)
}
export default AutoApproveMenu
@@ -1,131 +0,0 @@
import React, { type ChangeEvent, type ChangeEventHandler } from "react"
import styled from "styled-components"
import HeroTooltip from "@/components/common/HeroTooltip"
import { ActionMetadata } from "./AutoApproveMenu"
import { useState } from "react"
import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"
interface AutoApproveMenuItemProps {
action: ActionMetadata
isChecked: (action: ActionMetadata) => boolean
isFavorited?: (action: ActionMetadata) => boolean
onToggle: (action: ActionMetadata, checked: boolean) => void
onToggleFavorite?: (actionId: string) => void
condensed?: boolean
}
const CheckboxContainer = styled.div<{
isFavorited?: boolean
onClick?: (e: MouseEvent) => void
onMouseDown?: (e: React.MouseEvent) => void
}>`
display: flex;
align-items: center;
justify-content: space-between; /* Push content to edges */
padding: 0 4px;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s ease;
&:hover {
background-color: var(--vscode-textBlockQuote-background);
}
.left-content {
display: flex;
align-items: center;
gap: 8px;
}
.icon {
color: var(--vscode-foreground);
font-size: 14px;
}
.label {
color: var(--vscode-foreground);
font-size: 12px;
font-weight: 500;
}
.star {
color: ${(props) => (props.isFavorited ? "var(--vscode-terminal-ansiYellow)" : "var(--vscode-descriptionForeground)")};
opacity: ${(props) => (props.isFavorited ? 1 : 0.6)};
font-size: 12px;
}
`
const SubOptionAnimateIn = styled.div<{ show: boolean }>`
position: relative;
transform: ${(props) => (props.show ? "scaleY(1)" : "scaleY(0)")};
transform-origin: top;
padding-left: 24px;
opacity: ${(props) => (props.show ? "1" : "0")};
height: ${(props) => (props.show ? "auto" : "0")}; /* Manage height for layout */
overflow: visible; /* Allow tooltips to escape */
transition: transform 0.2s ease-in-out;
`
const ActionButtonContainer = styled.div`
margin: 4px;
`
const AutoApproveMenuItem = ({
action,
isChecked,
isFavorited,
onToggle,
onToggleFavorite,
condensed = false,
}: AutoApproveMenuItemProps) => {
const [isSubOptionOpen, setIsSubOptionOpen] = useState(isChecked(action))
const checked = isChecked(action)
const favorited = isFavorited?.(action)
const onChange = (e: Event) => {
e.stopPropagation()
const newChecked = !checked
setIsSubOptionOpen(newChecked)
onToggle(action, newChecked)
}
const content = (
<div>
<ActionButtonContainer>
<HeroTooltip content={action.description} delay={200}>
<CheckboxContainer isFavorited={favorited} onClick={onChange}>
<div className="left-content">
<VSCodeCheckbox checked={checked} />
<span className={`codicon ${action.icon} icon`}></span>
<span className="label">{condensed ? action.shortName : action.label}</span>
</div>
{onToggleFavorite && !condensed && (
<span
className={`codicon codicon-${favorited ? "star-full" : "star-empty"} star`}
onClick={(e) => {
e.stopPropagation()
onToggleFavorite?.(action.id)
}}
/>
)}
</CheckboxContainer>
</HeroTooltip>
</ActionButtonContainer>
{action.subAction && !condensed && (
<SubOptionAnimateIn show={isSubOptionOpen}>
<AutoApproveMenuItem
action={action.subAction}
isChecked={isChecked}
isFavorited={isFavorited}
onToggle={onToggle}
onToggleFavorite={onToggleFavorite}
/>
</SubOptionAnimateIn>
)}
</div>
)
return content
}
export default AutoApproveMenuItem
@@ -8,7 +8,7 @@ import {
VSCodeRadioGroup,
VSCodeTextField,
} from "@vscode/webview-ui-toolkit/react"
import { Fragment, memo, useCallback, useEffect, useMemo, useRef, useState } from "react"
import { Fragment, memo, useCallback, useEffect, useMemo, useState } from "react"
import ThinkingBudgetSlider from "./ThinkingBudgetSlider"
import { useEvent, useInterval } from "react-use"
import styled from "styled-components"
@@ -262,34 +262,6 @@ const ApiOptions = ({
)
}
// Debounced function to refresh OpenAI models (prevents excessive API calls while typing)
const debounceTimerRef = useRef<NodeJS.Timeout | null>(null)
useEffect(() => {
return () => {
if (debounceTimerRef.current) {
clearTimeout(debounceTimerRef.current)
}
}
}, [])
const debouncedRefreshOpenAiModels = useCallback((baseUrl?: string, apiKey?: string) => {
if (debounceTimerRef.current) {
clearTimeout(debounceTimerRef.current)
}
if (baseUrl && apiKey) {
debounceTimerRef.current = setTimeout(() => {
ModelsServiceClient.refreshOpenAiModels({
baseUrl,
apiKey,
}).catch((error) => {
console.error("Failed to refresh OpenAI models:", error)
})
}, 500)
}
}, [])
return (
<div style={{ display: "flex", flexDirection: "column", gap: 5, marginBottom: isPopup ? -10 : 0 }}>
<DropdownContainer className="dropdown-container">
@@ -1027,12 +999,7 @@ const ApiOptions = ({
value={apiConfiguration?.openAiBaseUrl || ""}
style={{ width: "100%", marginBottom: 10 }}
type="url"
onInput={(e: any) => {
const baseUrl = e.target.value
handleInputChange("openAiBaseUrl")({ target: { value: baseUrl } })
debouncedRefreshOpenAiModels(baseUrl, apiConfiguration?.openAiApiKey)
}}
onInput={handleInputChange("openAiBaseUrl")}
placeholder={"Enter base URL..."}>
<span style={{ fontWeight: 500 }}>Base URL</span>
</VSCodeTextField>
@@ -1040,12 +1007,7 @@ const ApiOptions = ({
value={apiConfiguration?.openAiApiKey || ""}
style={{ width: "100%", marginBottom: 10 }}
type="password"
onInput={(e: any) => {
const apiKey = e.target.value
handleInputChange("openAiApiKey")({ target: { value: apiKey } })
debouncedRefreshOpenAiModels(apiConfiguration?.openAiBaseUrl, apiKey)
}}
onInput={handleInputChange("openAiApiKey")}
placeholder="Enter API Key...">
<span style={{ fontWeight: 500 }}>API Key</span>
</VSCodeTextField>
@@ -6,7 +6,6 @@ import { useMount } from "react-use"
import styled from "styled-components"
import { openRouterDefaultModelId } from "@shared/api"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { ModelsServiceClient } from "@/services/grpc-client"
import { vscode } from "@/utils/vscode"
import { highlight } from "../history/HistoryView"
import { ModelInfoView, normalizeApiConfiguration } from "./ApiOptions"
@@ -85,9 +84,7 @@ const OpenRouterModelPicker: React.FC<OpenRouterModelPickerProps> = ({ isPopup }
}, [apiConfiguration])
useMount(() => {
ModelsServiceClient.refreshOpenRouterModels({}).catch((error: Error) =>
console.error("Failed to refresh OpenRouter models:", error),
)
vscode.postMessage({ type: "refreshOpenRouterModels" })
})
useEffect(() => {
@@ -1,28 +0,0 @@
import ClineLogoVariable from "@/assets/ClineLogoVariable"
import HeroTooltip from "@/components/common/HeroTooltip"
const HomeHeader = () => {
return (
<div className="flex flex-col items-center mb-5">
<div className="my-5">
<ClineLogoVariable className="size-16" />
</div>
<div className="text-center flex items-center justify-center">
<h2 className="m-0 text-[var(--vscode-font-size)]">{"What can I do for you?"}</h2>
<HeroTooltip
placement="bottom"
className="max-w-[300px]"
content={
"I can develop software step-by-step by editing files, exploring projects, running commands, and using browsers. I can even extend my capabilities with MCP tools to assist beyond basic code completion."
}>
<span
className="codicon codicon-info ml-2 cursor-pointer"
style={{ fontSize: "14px", color: "var(--vscode-textLink-foreground)" }}
/>
</HeroTooltip>
</div>
</div>
)
}
export default HomeHeader
-4
View File
@@ -16,10 +16,6 @@ export const SUPPORTED_SLASH_COMMANDS: SlashCommand[] = [
name: "newrule",
description: "Create a new Cline rule based on your conversation",
},
{
name: "reportbug",
description: "Create a Github issue with Cline",
},
]
// Regex for detecting slash commands in text