Compare commits

..

2 Commits

65 changed files with 546 additions and 1513 deletions
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
prevent IME composition Enter from autosending edited message
+1 -1
View File
@@ -209,7 +209,7 @@ class Task {
switch (chunk.type) {
case "text":
// Parse into content blocks
this.assistantMessageContent = parseAssistantMessageV2(chunk.text)
this.assistantMessageContent = parseAssistantMessage(chunk.text)
// Present blocks to user
await this.presentAssistantMessage()
break
-22
View File
@@ -16,28 +16,6 @@
"IS_DEV": "true",
"DEV_WORKSPACE_FOLDER": "${workspaceFolder}"
}
},
{
"name": "Run Extension (Fresh Install Mode)",
"type": "extensionHost",
"request": "launch",
"runtimeExecutable": "${execPath}",
"args": [
"--profile-temp",
"--sync",
"off",
"--disable-extensions",
"--extensionDevelopmentPath=${workspaceFolder}",
"${workspaceFolder}"
],
"outFiles": ["${workspaceFolder}/dist/**/*.js"],
"preLaunchTask": "clean-sandbox",
"internalConsoleOptions": "openOnSessionStart",
"postDebugTask": "stop",
"env": {
"IS_DEV": "true",
"DEV_WORKSPACE_FOLDER": "${workspaceFolder}"
}
}
]
}
-6
View File
@@ -185,12 +185,6 @@
"label": "stop",
"command": "echo ${input:terminate}",
"type": "shell"
},
{
"label": "clean-sandbox",
"type": "shell",
"dependsOn": ["watch"],
"command": "rm -rf .vscode-dev"
}
],
"inputs": [
-11
View File
@@ -1,16 +1,5 @@
# Changelog
## [3.15.2]
- Added details to auto approve menu and more sensible default controls
- Add detailed configuration options for LiteLLM provider
- Add webview telemetry for users who have opted in to telemetry
- Update Gemini in OpenRouter/Cline providers to use implicit caching
- Fix freezing issues during rendering of large streaming text
- Fix grey screen webview crashes by releasing memory after every diff edit
- Fix breaking out of diff auto-scroll
- Fix IME composition Enter autosending edited message
## [3.15.1]
- Fix bug where PowerShell commands weren't given enough time before giving up and showing an error
+2 -2
View File
@@ -26,9 +26,9 @@ For complete transparency, you can inspect our [telemetry implementation](https:
### How to Opt Out
Telemetry in Cline is entirely optional:
Telemetry in Cline is entirely optional and requires your explicit consent:
- When you update or install our VS Code extension, you'll see a message about our anonymous telemetry
- When you update or install our VS Code extension, you'll see a simple prompt: "Help Improve Cline" with Allow or Deny options
- You can change your preference anytime in settings
Cline also respects VS Code's global telemetry settings. If you've disabled telemetry at the VS Code level, Cline's telemetry will automatically be disabled as well.
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "claude-dev",
"version": "3.15.2",
"version": "3.15.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "claude-dev",
"version": "3.15.2",
"version": "3.15.1",
"license": "Apache-2.0",
"dependencies": {
"@anthropic-ai/bedrock-sdk": "^0.12.4",
+35 -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.2",
"version": "3.15.1",
"icon": "assets/icons/icon.png",
"engines": {
"vscode": "^1.84.0"
@@ -235,11 +235,45 @@
"configuration": {
"title": "Cline",
"properties": {
"cline.vsCodeLmModelSelector": {
"type": "object",
"properties": {
"vendor": {
"type": "string",
"description": "The vendor of the language model (e.g. copilot)"
},
"family": {
"type": "string",
"description": "The family of the language model (e.g. gpt-4)"
}
},
"description": "Settings for VSCode Language Model API"
},
"cline.enableCheckpoints": {
"type": "boolean",
"default": true,
"description": "Enables extension to save checkpoints of workspace throughout the task. Uses git under the hood which may not work well with large workspaces."
},
"cline.disableBrowserTool": {
"type": "boolean",
"default": false,
"description": "Disables extension from spawning browser session."
},
"cline.modelSettings.o3Mini.reasoningEffort": {
"type": "string",
"enum": [
"low",
"medium",
"high"
],
"default": "medium",
"description": "Controls the reasoning effort when using an OpenAI reasoning model. Higher values may result in more thorough but slower responses."
},
"cline.chromeExecutablePath": {
"type": "string",
"default": null,
"description": "Path to Chrome executable for browser use functionality. If not set, the extension will attempt to find or download it automatically."
},
"cline.preferredLanguage": {
"type": "string",
"enum": [
-4
View File
@@ -40,8 +40,6 @@ message BrowserSettings {
Viewport viewport = 1;
optional string remote_browser_host = 2;
optional bool remote_browser_enabled = 3;
optional string chrome_executable_path = 4;
optional bool disable_tool_use = 5;
}
message UpdateBrowserSettingsRequest {
@@ -49,6 +47,4 @@ message UpdateBrowserSettingsRequest {
Viewport viewport = 2;
optional string remote_browser_host = 3;
optional bool remote_browser_enabled = 4;
optional string chrome_executable_path = 5;
optional bool disable_tool_use = 6;
}
-1
View File
@@ -29,7 +29,6 @@ const serviceNameMap = {
task: "cline.TaskService",
web: "cline.WebService",
models: "cline.ModelsService",
slash: "cline.SlashService",
// Add new services here - no other code changes needed!
}
const serviceDirs = Object.keys(serviceNameMap).map((serviceKey) => path.join(ROOT_DIR, "src", "core", "controller", serviceKey))
+5 -6
View File
@@ -15,11 +15,9 @@ service ModelsService {
// Fetches available models from VS Code LM API
rpc getVsCodeLmModels(EmptyRequest) returns (VsCodeLmModelsArray);
// Refreshes and returns OpenRouter models
rpc refreshOpenRouterModels(EmptyRequest) returns (OpenRouterCompatibleModelInfo);
rpc refreshOpenRouterModels(EmptyRequest) returns (OpenRouterModels);
// Refreshes and returns OpenAI models
rpc refreshOpenAiModels(OpenAiModelsRequest) returns (StringArray);
// Refreshes and returns Requesty models
rpc refreshRequestyModels(EmptyRequest) returns (OpenRouterCompatibleModelInfo);
}
// List of VS Code LM models
@@ -35,7 +33,7 @@ message VsCodeLmModel {
string id = 4;
}
// For OpenRouterCompatibleModelInfo structure in OpenRouterModels
// For ModelInfo structure in OpenRouterModels
message OpenRouterModelInfo {
int32 max_tokens = 1;
int32 context_window = 2;
@@ -48,8 +46,8 @@ message OpenRouterModelInfo {
string description = 9;
}
// Shared response message for model information
message OpenRouterCompatibleModelInfo {
// Response message for OpenRouter models
message OpenRouterModels {
map<string, OpenRouterModelInfo> models = 1;
}
@@ -59,3 +57,4 @@ message OpenAiModelsRequest {
string baseUrl = 2;
string apiKey = 3;
}
-13
View File
@@ -1,13 +0,0 @@
syntax = "proto3";
package cline;
option java_package = "bot.cline.proto";
option java_multiple_files = true;
import "common.proto";
// SlashService provides methods for managing slash
service SlashService {
// Sends button click message
rpc reportBug(StringRequest) returns (Empty);
}
+2 -2
View File
@@ -65,7 +65,7 @@ export class LiteLlmHandler implements ApiHandler {
const reasoningOn = budgetTokens !== 0 ? true : false
const thinkingConfig = reasoningOn ? { type: "enabled", budget_tokens: budgetTokens } : undefined
let temperature: number | undefined = this.options.liteLlmModelInfo?.temperature ?? 0
let temperature: number | undefined = 0
if (isOminiModel && reasoningOn) {
temperature = undefined // Thinking mode doesn't support temperature
@@ -169,7 +169,7 @@ export class LiteLlmHandler implements ApiHandler {
getModel() {
return {
id: this.options.liteLlmModelId || liteLlmDefaultModelId,
info: this.options.liteLlmModelInfo || liteLlmModelInfoSaneDefaults,
info: liteLlmModelInfoSaneDefaults,
}
}
}
+68
View File
@@ -75,6 +75,74 @@ export async function createOpenRouterStream(
break
}
// handles gemini caching logic
if (model.id.startsWith("google/") && model.info.supportsPromptCache) {
// gemini only uses the last breakpoint for caching, so the others will be ignored
openAiMessages[0] = {
role: "system",
content: [
{
type: "text",
text: systemPrompt,
// @ts-ignore-next-line
cache_control: { type: "ephemeral" },
},
],
}
// for safety, but this should always be the case
if (openAiMessages.length >= 2) {
const msg = openAiMessages[1]
if (msg) {
if (typeof msg.content === "string") {
msg.content = [{ type: "text", text: msg.content }]
}
if (Array.isArray(msg.content)) {
// NOTE: this is fine since env details will always be added at the end. but if it weren't there, and the user added a image_url type message, it would pop a text part before it and then move it after to the end.
let lastTextPart = msg.content.filter((part) => part.type === "text").pop()
if (!lastTextPart) {
lastTextPart = { type: "text", text: "..." }
msg.content.push(lastTextPart)
}
// @ts-ignore-next-line
lastTextPart["cache_control"] = { type: "ephemeral" }
}
}
}
// it doesn't make sense to alter breakpoints at all with the gemini cache implementation at this time
/*const GEMINI_CACHE_USER_MESSAGE_INTERVAL = 4 // add new breakpoint every 4 turns
const userMessages = openAiMessages.filter((msg) => msg.role === "user")
const userMessageCount = userMessages.length
const targetUserMessageNumber =
Math.floor(userMessageCount / GEMINI_CACHE_USER_MESSAGE_INTERVAL) * GEMINI_CACHE_USER_MESSAGE_INTERVAL
if (targetUserMessageNumber > 0) {
// otherwise dont need to add a breakpoint
const msg = userMessages[targetUserMessageNumber - 1]
if (msg) {
if (typeof msg.content === "string") {
msg.content = [{ type: "text", text: msg.content }]
}
if (Array.isArray(msg.content)) {
// NOTE: this is fine since env details will always be added at the end. but if it weren't there, and the user added a image_url type message, it would pop a text part before it and then move it after to the end.
let lastTextPart = msg.content.filter((part) => part.type === "text").pop()
if (!lastTextPart) {
lastTextPart = { type: "text", text: "..." }
msg.content.push(lastTextPart)
}
// @ts-ignore-next-line
lastTextPart["cache_control"] = { type: "ephemeral" }
}
}
}*/
}
// Not sure how openrouter defaults max tokens when no value is provided, but the anthropic api requires this value and since they offer both 4096 and 8192 variants, we should ensure 8192.
// (models usually default to max tokens allowed)
let maxTokens: number | undefined
+1 -1
View File
@@ -1,6 +1,6 @@
export type AssistantMessageContent = TextContent | ToolUse
export { parseAssistantMessageV1, parseAssistantMessageV2 } from "./parse-assistant-message"
export { parseAssistantMessage } from "./parse-assistant-message"
export interface TextContent {
type: "text"
@@ -1,24 +1,6 @@
import { AssistantMessageContent, TextContent, ToolUse, ToolParamName, toolParamNames, toolUseNames, ToolUseName } from "." // Assuming types are defined in index.ts or a similar file
import { AssistantMessageContent, TextContent, ToolUse, ToolParamName, toolParamNames, toolUseNames, ToolUseName } from "."
/**
* @description **Version 1**
* Parses an assistant message string potentially containing mixed text and tool usage blocks
* marked with XML-like tags into an array of structured content objects.
*
* This version iterates through the message character by character, building an accumulator string.
* It maintains state to track whether it's currently parsing text, a tool use block, or a specific tool parameter.
* It detects the start and end of tool uses and parameters by checking if the accumulator ends with
* the corresponding opening or closing tags.
* Special handling is included for `write_to_file` and `new_rule` tool uses to correctly parse
* the `content` parameter, which might contain the closing tag itself, by looking for the *last*
* occurrence of the closing tag.
* If the input string ends mid-tag or mid-content, the last block (text or tool use) is marked as partial.
*
* @param assistantMessage The raw string output from the assistant.
* @returns An array of `AssistantMessageContent` objects, which can be `TextContent` or `ToolUse`.
* Blocks that were not fully closed by the end of the input string will have their `partial` flag set to `true`.
*/
export function parseAssistantMessageV1(assistantMessage: string): AssistantMessageContent[] {
export function parseAssistantMessage(assistantMessage: string) {
const contentBlocks: AssistantMessageContent[] = []
let currentTextContent: TextContent | undefined = undefined
let currentTextContentStartIndex = 0
@@ -32,56 +14,46 @@ export function parseAssistantMessageV1(assistantMessage: string): AssistantMess
const char = assistantMessage[i]
accumulator += char
// --- State: Parsing a Tool Parameter ---
// there should not be a param without a tool use
if (currentToolUse && currentParamName) {
const currentParamValue = accumulator.slice(currentParamValueStartIndex)
const paramClosingTag = `</${currentParamName}>`
if (currentParamValue.endsWith(paramClosingTag)) {
// End of param value found
// end of param value
currentToolUse.params[currentParamName] = currentParamValue.slice(0, -paramClosingTag.length).trim()
currentParamName = undefined // Go back to parsing tool content or looking for next param
continue // Move to next character
currentParamName = undefined
continue
} else {
// Partial param value is accumulating
continue // Move to next character
// partial param value is accumulating
continue
}
}
// --- State: Parsing a Tool Use (but not a specific parameter) ---
// no currentParamName
if (currentToolUse) {
const currentToolValue = accumulator.slice(currentToolUseStartIndex)
const toolUseClosingTag = `</${currentToolUse.name}>`
if (currentToolValue.endsWith(toolUseClosingTag)) {
// End of a tool use found
// end of a tool use
currentToolUse.partial = false
contentBlocks.push(currentToolUse)
currentToolUse = undefined // Go back to parsing text or looking for next tool
// Reset text start index in case text follows immediately
currentTextContentStartIndex = i + 1
continue // Move to next character
currentToolUse = undefined
continue
} else {
// Check if starting a new parameter within the current tool use
const possibleParamOpeningTags = toolParamNames.map((name) => `<${name}>`)
let foundParamStart = false
for (const paramOpeningTag of possibleParamOpeningTags) {
if (accumulator.endsWith(paramOpeningTag)) {
// Start of a new parameter found
// start of a new parameter
currentParamName = paramOpeningTag.slice(1, -1) as ToolParamName
currentParamValueStartIndex = accumulator.length
foundParamStart = true
break
}
}
if (foundParamStart) {
continue // Move to next character
}
// Special case for write_to_file/new_rule content param allowing nested tags
// Check if a </content> tag appears, potentially indicating the end of the content param
// even if the main tool closing tag hasn't been seen yet.
// there's no current param, and not starting a new param
// special case for write_to_file where file contents could contain the closing tag, in which case the param would have closed and we end up with the rest of the file contents here. To work around this, we get the string between the starting content tag and the LAST content tag.
const contentParamName: ToolParamName = "content"
if (
(currentToolUse.name === "write_to_file" || currentToolUse.name === "new_rule") &&
@@ -91,385 +63,73 @@ export function parseAssistantMessageV1(assistantMessage: string): AssistantMess
const contentStartTag = `<${contentParamName}>`
const contentEndTag = `</${contentParamName}>`
const contentStartIndex = toolContent.indexOf(contentStartTag) + contentStartTag.length
// Use lastIndexOf to handle cases where </content> might appear within the content itself
const contentEndIndex = toolContent.lastIndexOf(contentEndTag)
// Ensure we found valid start/end tags and end is after start
if (
contentStartIndex !== -1 &&
contentEndIndex !== -1 &&
contentEndIndex > contentStartIndex - contentStartTag.length // Ensure end tag is after start tag begins
) {
// Check if this content param was already being parsed. If so, update it.
// If not, and we just found the closing tag, assign it.
// This handles cases where the </content> detection might fire before
// the <content> tag detection logic, or if the content is very short.
if (currentParamName === contentParamName) {
// Already parsing content, now we found the end tag
currentToolUse.params[contentParamName] = toolContent.slice(contentStartIndex, contentEndIndex).trim()
currentParamName = undefined // Finished with this param
} else if (currentParamName === undefined) {
// Not parsing a param, but found </content>. Assume it closes the content block.
currentToolUse.params[contentParamName] = toolContent.slice(contentStartIndex, contentEndIndex).trim()
// We stay in the "parsing tool use" state, looking for more params or the tool end tag.
}
if (contentStartIndex !== -1 && contentEndIndex !== -1 && contentEndIndex > contentStartIndex) {
currentToolUse.params[contentParamName] = toolContent.slice(contentStartIndex, contentEndIndex).trim()
}
}
// If none of the above, partial tool value is accumulating
continue // Move to next character
// partial tool value is accumulating
continue
}
}
// --- State: Parsing Text (or looking for start of a tool use) ---
// no currentToolUse
let didStartToolUse = false
const possibleToolUseOpeningTags = toolUseNames.map((name) => `<${name}>`)
for (const toolUseOpeningTag of possibleToolUseOpeningTags) {
if (accumulator.endsWith(toolUseOpeningTag)) {
// Start of a new tool use found
const toolName = toolUseOpeningTag.slice(1, -1) as ToolUseName
// start of a new tool use
currentToolUse = {
type: "tool_use",
name: toolName,
name: toolUseOpeningTag.slice(1, -1) as ToolUseName,
params: {},
partial: true,
}
currentToolUseStartIndex = accumulator.length
// This also indicates the end of the current text content block (if any)
// this also indicates the end of the current text content
if (currentTextContent) {
currentTextContent.partial = false
// Extract text content, removing the part that formed the tool opening tag
const textEndIndex = accumulator.length - toolUseOpeningTag.length
currentTextContent.content = accumulator.slice(currentTextContentStartIndex, textEndIndex).trim()
// Only add if there's actual content
if (currentTextContent.content.length > 0) {
contentBlocks.push(currentTextContent)
}
// remove the partially accumulated tool use tag from the end of text (<tool)
currentTextContent.content = currentTextContent.content
.slice(0, -toolUseOpeningTag.slice(0, -1).length)
.trim()
contentBlocks.push(currentTextContent)
currentTextContent = undefined
} else {
// Check if there was text before this tool use started
const textEndIndex = accumulator.length - toolUseOpeningTag.length
const potentialText = accumulator.slice(currentTextContentStartIndex, textEndIndex).trim()
if (potentialText.length > 0) {
contentBlocks.push({
type: "text",
content: potentialText,
partial: false, // Ended because tool use started
})
}
}
didStartToolUse = true
break // Found tool start, stop checking for others
break
}
}
if (!didStartToolUse) {
// No tool use started, so it must be text content accumulating
// (or continuing after a closed tool use)
// no tool use, so it must be text either at the beginning or between tools
if (currentTextContent === undefined) {
// Start of a new text block
currentTextContentStartIndex = i - (accumulator.length - currentTextContentStartIndex - 1) // Adjust start index based on how much we've accumulated since the last block ended or the beginning
// If accumulator starts from 0, start index is i
if (contentBlocks.length === 0 && currentToolUse === undefined) {
currentTextContentStartIndex = accumulator.length - 1 // i
} else {
// Re-calculate based on the actual start of the current text segment
// Find the end of the last block
let lastBlockEndIndex = 0
if (contentBlocks.length > 0) {
const lastBlock = contentBlocks[contentBlocks.length - 1]
// Approximation: find where the accumulator matches the end of the message string representation of the last block. This is complex.
// Simpler: Assume text starts right after the last block ended implicitly at index i.
lastBlockEndIndex = i // Where the loop *was* when the last block finished processing
// Need a more robust way to track the end index of the *raw string* corresponding to the last block.
// Let's stick to the accumulator slice approach for simplicity in this version.
// The start index should be where the current *unmatched* text began.
let lastProcessedIndex = -1
if (contentBlocks.length > 0) {
// This requires knowing the raw string length of the previous block, which V1 doesn't explicitly track easily.
// We'll approximate based on the current accumulator and start index logic.
// The issue arises if a tool tag was just closed. accumulator contains everything up to i.
// lastBlockEndIndex should point to the character *after* the closing tag of the last block.
}
// Reset start index to the beginning of the *current* potential text block
currentTextContentStartIndex = accumulator.length - 1 // Start accumulating from the current character `i`
}
// If we just closed a tool, text starts *after* its closing tag
// The logic needs refinement here for accurate start index after a tool closure.
// Let's assume for now the start index logic inside the loop handles it via slicing.
}
currentTextContent = {
type: "text",
content: "", // Content will be filled by slicing accumulator
partial: true,
}
currentTextContentStartIndex = i
}
currentTextContent = {
type: "text",
content: accumulator.slice(currentTextContentStartIndex).trim(),
partial: true,
}
// Update text content based on the accumulator from its start index
currentTextContent.content = accumulator.slice(currentTextContentStartIndex).trimStart() // Trim start to avoid leading space if text follows tool
}
} // End of loop
}
// --- Finalization after loop ---
// If a tool use was open at the end
if (currentToolUse) {
// If a parameter was open within that tool use
// stream did not complete tool call, add it as partial
if (currentParamName) {
// The remaining accumulator content belongs to this partial parameter
// tool call has a parameter that was not completed
currentToolUse.params[currentParamName] = accumulator.slice(currentParamValueStartIndex).trim()
}
// Add the potentially partial tool use block
contentBlocks.push(currentToolUse)
}
// If text content was being accumulated at the end
// Note: Only one of currentToolUse or currentTextContent can be defined here,
// as starting a tool use finalizes the preceding text block.
else if (currentTextContent) {
// Update content one last time
currentTextContent.content = accumulator.slice(currentTextContentStartIndex).trim()
// Add the potentially partial text block only if it contains content
if (currentTextContent.content.length > 0) {
contentBlocks.push(currentTextContent)
}
}
return contentBlocks
}
/**
* @description **Version 2**
* Parses an assistant message string potentially containing mixed text and tool usage blocks
* marked with XML-like tags into an array of structured content objects.
*
* This version aims for efficiency by avoiding the character-by-character accumulator of V1.
* It iterates through the string using an index `i`. At each position, it checks if the substring
* *ending* at `i` matches any known opening or closing tags for tools or parameters using `startsWith`
* with an offset.
* It uses pre-computed Maps (`toolUseOpenTags`, `toolParamOpenTags`) for quick tag lookups.
* State is managed using indices (`currentTextContentStart`, `currentToolUseStart`, `currentParamValueStart`)
* pointing to the start of the current block within the original `assistantMessage` string.
* Slicing is used to extract content only when a block (text, parameter, or tool use) is completed.
* Special handling for `write_to_file` and `new_rule` content parameters is included, using `indexOf`
* and `lastIndexOf` on the relevant slice to handle potentially nested closing tags.
* If the input string ends mid-block, the last open block is added and marked as partial.
*
* @param assistantMessage The raw string output from the assistant.
* @returns An array of `AssistantMessageContent` objects, which can be `TextContent` or `ToolUse`.
* Blocks that were not fully closed by the end of the input string will have their `partial` flag set to `true`.
*/
export function parseAssistantMessageV2(assistantMessage: string): AssistantMessageContent[] {
const contentBlocks: AssistantMessageContent[] = []
let currentTextContentStart = 0 // Index where the current text block started
let currentTextContent: TextContent | undefined = undefined
let currentToolUseStart = 0 // Index *after* the opening tag of the current tool use
let currentToolUse: ToolUse | undefined = undefined
let currentParamValueStart = 0 // Index *after* the opening tag of the current param
let currentParamName: ToolParamName | undefined = undefined
// Precompute tags for faster lookups
const toolUseOpenTags = new Map<string, ToolUseName>()
const toolParamOpenTags = new Map<string, ToolParamName>()
for (const name of toolUseNames) {
toolUseOpenTags.set(`<${name}>`, name)
}
for (const name of toolParamNames) {
toolParamOpenTags.set(`<${name}>`, name)
}
const len = assistantMessage.length
for (let i = 0; i < len; i++) {
const currentCharIndex = i
// --- State: Parsing a Tool Parameter ---
if (currentToolUse && currentParamName) {
const closeTag = `</${currentParamName}>`
// Check if the string *ending* at index `i` matches the closing tag
if (
currentCharIndex >= closeTag.length - 1 &&
assistantMessage.startsWith(
closeTag,
currentCharIndex - closeTag.length + 1, // Start checking from potential start of tag
)
) {
// Found the closing tag for the parameter
const value = assistantMessage
.slice(
currentParamValueStart, // Start after the opening tag
currentCharIndex - closeTag.length + 1, // End before the closing tag
)
.trim()
currentToolUse.params[currentParamName] = value
currentParamName = undefined // Go back to parsing tool content
// We don't continue loop here, need to check for tool close or other params at index i
} else {
continue // Still inside param value, move to next char
}
}
// --- State: Parsing a Tool Use (but not a specific parameter) ---
if (currentToolUse && !currentParamName) {
// Ensure we are not inside a parameter already
// Check if starting a new parameter
let startedNewParam = false
for (const [tag, paramName] of toolParamOpenTags.entries()) {
if (currentCharIndex >= tag.length - 1 && assistantMessage.startsWith(tag, currentCharIndex - tag.length + 1)) {
currentParamName = paramName
currentParamValueStart = currentCharIndex + 1 // Value starts after the tag
startedNewParam = true
break
}
}
if (startedNewParam) {
continue // Handled start of param, move to next char
}
// Check if closing the current tool use
const toolCloseTag = `</${currentToolUse.name}>`
if (
currentCharIndex >= toolCloseTag.length - 1 &&
assistantMessage.startsWith(toolCloseTag, currentCharIndex - toolCloseTag.length + 1)
) {
// End of the tool use found
// Special handling for content params *before* finalizing the tool
const toolContentSlice = assistantMessage.slice(
currentToolUseStart, // From after the tool opening tag
currentCharIndex - toolCloseTag.length + 1, // To before the tool closing tag
)
// Check if content parameter needs special handling (write_to_file/new_rule)
// This check is important if the closing </content> tag was missed by the parameter parsing logic
// (e.g., if content is empty or parsing logic prioritizes tool close)
const contentParamName: ToolParamName = "content"
if (
(currentToolUse.name === "write_to_file" || currentToolUse.name === "new_rule") &&
!(contentParamName in currentToolUse.params) && // Only if not already parsed
toolContentSlice.includes(`<${contentParamName}>`) // Check if tag exists
) {
const contentStartTag = `<${contentParamName}>`
const contentEndTag = `</${contentParamName}>`
const contentStart = toolContentSlice.indexOf(contentStartTag)
// Use lastIndexOf for robustness against nested tags
const contentEnd = toolContentSlice.lastIndexOf(contentEndTag)
if (contentStart !== -1 && contentEnd !== -1 && contentEnd > contentStart) {
const contentValue = toolContentSlice.slice(contentStart + contentStartTag.length, contentEnd).trim()
currentToolUse.params[contentParamName] = contentValue
}
}
currentToolUse.partial = false // Mark as complete
contentBlocks.push(currentToolUse)
currentToolUse = undefined // Reset state
currentTextContentStart = currentCharIndex + 1 // Potential text starts after this tag
continue // Move to next char
}
// If not starting a param and not closing the tool, continue accumulating tool content implicitly
continue
}
// --- State: Parsing Text / Looking for Tool Start ---
if (!currentToolUse) {
// Check if starting a new tool use
let startedNewTool = false
for (const [tag, toolName] of toolUseOpenTags.entries()) {
if (currentCharIndex >= tag.length - 1 && assistantMessage.startsWith(tag, currentCharIndex - tag.length + 1)) {
// End current text block if one was active
if (currentTextContent) {
currentTextContent.content = assistantMessage
.slice(
currentTextContentStart, // From where text started
currentCharIndex - tag.length + 1, // To before the tool tag starts
)
.trim()
currentTextContent.partial = false // Ended because tool started
if (currentTextContent.content.length > 0) {
contentBlocks.push(currentTextContent)
}
currentTextContent = undefined
} else {
// Check for any text between the last block and this tag
const potentialText = assistantMessage
.slice(
currentTextContentStart, // From where text *might* have started
currentCharIndex - tag.length + 1, // To before the tool tag starts
)
.trim()
if (potentialText.length > 0) {
contentBlocks.push({
type: "text",
content: potentialText,
partial: false,
})
}
}
// Start the new tool use
currentToolUse = {
type: "tool_use",
name: toolName,
params: {},
partial: true, // Assume partial until closing tag is found
}
currentToolUseStart = currentCharIndex + 1 // Tool content starts after the opening tag
startedNewTool = true
break
}
}
if (startedNewTool) {
continue // Handled start of tool, move to next char
}
// If not starting a tool, it must be text content
if (!currentTextContent) {
// Start a new text block if we aren't already in one
currentTextContentStart = currentCharIndex // Text starts at the current character
// Check if the current char is the start of potential text *immediately* after a tag
// This needs the previous state - simpler to let slicing handle it later.
// Resetting start index accurately is key.
// It should be the index *after* the last processed tag.
// The logic managing currentTextContentStart after closing tags handles this.
currentTextContent = {
type: "text",
content: "", // Will be determined by slicing at the end or when a tool starts
partial: true,
}
}
// Continue accumulating text implicitly; content is extracted later.
}
} // End of loop
// --- Finalization after loop ---
// Finalize any open parameter within an open tool use
if (currentToolUse && currentParamName) {
currentToolUse.params[currentParamName] = assistantMessage
.slice(currentParamValueStart) // From param start to end of string
.trim()
// Tool use remains partial
}
// Finalize any open tool use (which might contain the finalized partial param)
if (currentToolUse) {
// Tool use is partial because the loop finished before its closing tag
contentBlocks.push(currentToolUse)
}
// Finalize any trailing text content
// Only possible if a tool use wasn't open at the very end
else if (currentTextContent) {
currentTextContent.content = assistantMessage
.slice(currentTextContentStart) // From text start to end of string
.trim()
// Text is partial because the loop finished
if (currentTextContent.content.length > 0) {
contentBlocks.push(currentTextContent)
}
// Note: it doesn't matter if check for currentToolUse or currentTextContent, only one of them will be defined since only one can be partial at a time
if (currentTextContent) {
// stream did not complete text content, add it as partial
contentBlocks.push(currentTextContent)
}
return contentBlocks
@@ -2,7 +2,6 @@ import * as vscode from "vscode"
import crypto from "crypto"
import { Controller } from "../index"
import { storeSecret } from "../../storage/state"
import { EmptyRequest, String } from "../../../shared/proto/common"
/**
* Handles the user clicking the login link in the UI.
@@ -12,7 +11,7 @@ import { EmptyRequest, String } from "../../../shared/proto/common"
* @param controller The controller instance.
* @returns The login URL as a string.
*/
export async function accountLoginClicked(controller: Controller, unused: EmptyRequest): Promise<String> {
export async function accountLoginClicked(controller: Controller): Promise<String> {
// Generate nonce for state validation
const nonce = crypto.randomBytes(32).toString("hex")
await storeSecret(controller.context, "authNonce", nonce)
@@ -26,8 +25,6 @@ export async function accountLoginClicked(controller: Controller, unused: EmptyR
const authUrl = vscode.Uri.parse(
`https://app.cline.bot/auth?state=${encodeURIComponent(nonce)}&callback_url=${encodeURIComponent(`${uriScheme || "vscode"}://saoudrizwan.claude-dev/auth`)}`,
)
await vscode.env.openExternal(authUrl)
return {
value: authUrl.toString(),
}
vscode.env.openExternal(authUrl)
return authUrl.toString()
}
@@ -1,8 +1,8 @@
import { UpdateBrowserSettingsRequest } from "../../../shared/proto/browser"
import { Boolean } from "../../../shared/proto/common"
import { Controller } from "../index"
import { updateGlobalState, getGlobalState } from "../../storage/state"
import { BrowserSettings as SharedBrowserSettings, DEFAULT_BROWSER_SETTINGS } from "../../../shared/BrowserSettings"
import { updateGlobalState } from "../../storage/state"
import { BrowserSettings as SharedBrowserSettings } from "../../../shared/BrowserSettings"
/**
* Update browser settings
@@ -12,39 +12,23 @@ import { BrowserSettings as SharedBrowserSettings, DEFAULT_BROWSER_SETTINGS } fr
*/
export async function updateBrowserSettings(controller: Controller, request: UpdateBrowserSettingsRequest): Promise<Boolean> {
try {
// Get current browser settings to preserve fields not in the request
const currentSettings = (await getGlobalState(controller.context, "browserSettings")) as SharedBrowserSettings | undefined
const mergedWithDefaults = { ...DEFAULT_BROWSER_SETTINGS, ...currentSettings }
// Convert from protobuf format to shared format, merging with existing settings
const newBrowserSettings: SharedBrowserSettings = {
...mergedWithDefaults, // Start with existing settings (and defaults)
// Convert from protobuf format to shared format
const browserSettings: SharedBrowserSettings = {
viewport: {
// Apply updates from request
width: request.viewport?.width || mergedWithDefaults.viewport.width,
height: request.viewport?.height || mergedWithDefaults.viewport.height,
width: request.viewport?.width || 900,
height: request.viewport?.height || 600,
},
// Explicitly handle optional boolean and string fields from the request
remoteBrowserEnabled:
request.remoteBrowserEnabled === undefined
? mergedWithDefaults.remoteBrowserEnabled
: request.remoteBrowserEnabled,
remoteBrowserHost:
request.remoteBrowserHost === undefined ? mergedWithDefaults.remoteBrowserHost : request.remoteBrowserHost,
chromeExecutablePath:
// If chromeExecutablePath is explicitly in the request (even as ""), use it.
// Otherwise, fall back to mergedWithDefaults.
"chromeExecutablePath" in request ? request.chromeExecutablePath : mergedWithDefaults.chromeExecutablePath,
disableToolUse: request.disableToolUse === undefined ? mergedWithDefaults.disableToolUse : request.disableToolUse,
remoteBrowserEnabled: request.remoteBrowserEnabled || false,
remoteBrowserHost: request.remoteBrowserHost || undefined,
}
// Update global state with new settings
await updateGlobalState(controller.context, "browserSettings", newBrowserSettings)
await updateGlobalState(controller.context, "browserSettings", browserSettings)
// Update task browser settings if task exists
if (controller.task) {
controller.task.browserSettings = newBrowserSettings
controller.task.browserSession.browserSettings = newBrowserSettings
controller.task.browserSettings = browserSettings
controller.task.browserSession.browserSettings = browserSettings
}
// Post updated state to webview
@@ -12,7 +12,6 @@ import { handleStateServiceRequest, handleStateServiceStreamingRequest } from ".
import { handleTaskServiceRequest, handleTaskServiceStreamingRequest } from "./task/index"
import { handleWebServiceRequest, handleWebServiceStreamingRequest } from "./web/index"
import { handleModelsServiceRequest, handleModelsServiceStreamingRequest } from "./models/index"
import { handleSlashServiceRequest, handleSlashServiceStreamingRequest } from "./slash/index"
/**
* Configuration for a service handler
@@ -68,8 +67,4 @@ export const serviceHandlers: Record<string, ServiceHandlerConfig> = {
requestHandler: handleModelsServiceRequest,
streamingHandler: handleModelsServiceStreamingRequest,
},
"cline.SlashService": {
requestHandler: handleSlashServiceRequest,
streamingHandler: handleSlashServiceStreamingRequest,
},
}
+54 -6
View File
@@ -249,7 +249,7 @@ export class Controller {
// If user already opted in to telemetry, enable telemetry service
this.getStateToPostToWebview().then((state) => {
const { telemetrySetting } = state
const isOptedIn = telemetrySetting !== "disabled"
const isOptedIn = telemetrySetting === "enabled"
telemetryService.updateTelemetryState(isOptedIn)
})
break
@@ -274,6 +274,9 @@ 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)
@@ -331,6 +334,9 @@ export class Controller {
case "resetState":
await this.resetState()
break
case "refreshRequestyModels":
await this.refreshRequestyModels()
break
case "refreshClineRules":
await refreshClineRulesToggles(this.context, cwd)
await refreshExternalRulesToggles(this.context, cwd)
@@ -670,7 +676,7 @@ export class Controller {
async updateTelemetrySetting(telemetrySetting: TelemetrySetting) {
await updateGlobalState(this.context, "telemetrySetting", telemetrySetting)
const isOptedIn = telemetrySetting !== "disabled"
const isOptedIn = telemetrySetting === "enabled"
telemetryService.updateTelemetryState(isOptedIn)
}
@@ -750,7 +756,6 @@ export class Controller {
break
case "litellm":
await updateGlobalState(this.context, "previousModeModelId", apiConfiguration.liteLlmModelId)
await updateGlobalState(this.context, "previousModeModelInfo", apiConfiguration.liteLlmModelInfo)
break
case "requesty":
await updateGlobalState(this.context, "previousModeModelId", apiConfiguration.requestyModelId)
@@ -804,8 +809,7 @@ export class Controller {
await updateGlobalState(this.context, "lmStudioModelId", newModelId)
break
case "litellm":
await updateGlobalState(this.context, "previousModeModelId", apiConfiguration.liteLlmModelId)
await updateGlobalState(this.context, "previousModeModelInfo", apiConfiguration.liteLlmModelInfo)
await updateGlobalState(this.context, "liteLlmModelId", newModelId)
break
case "requesty":
await updateGlobalState(this.context, "requestyModelId", newModelId)
@@ -1145,7 +1149,6 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
return cacheDir
}
// Read OpenRouter models from disk cache
async readOpenRouterModels(): Promise<Record<string, ModelInfo> | undefined> {
const openRouterModelsFilePath = path.join(await this.ensureCacheDirectoryExists(), GlobalFileNames.openRouterModels)
const fileExists = await fileExistsAtPath(openRouterModelsFilePath)
@@ -1156,6 +1159,51 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
return undefined
}
async refreshRequestyModels() {
const parsePrice = (price: any) => {
if (price) {
return parseFloat(price) * 1_000_000
}
return undefined
}
let models: Record<string, ModelInfo> = {}
try {
const apiKey = await getSecret(this.context, "requestyApiKey")
const headers = {
Authorization: `Bearer ${apiKey}`,
}
const response = await axios.get("https://router.requesty.ai/v1/models", { headers })
if (response.data?.data) {
for (const model of response.data.data) {
const modelInfo: ModelInfo = {
maxTokens: model.max_output_tokens || undefined,
contextWindow: model.context_window,
supportsImages: model.supports_vision || undefined,
supportsPromptCache: model.supports_caching || undefined,
inputPrice: parsePrice(model.input_price),
outputPrice: parsePrice(model.output_price),
cacheWritesPrice: parsePrice(model.caching_price),
cacheReadsPrice: parsePrice(model.cached_price),
description: model.description,
}
models[model.id] = modelInfo
}
console.log("Requesty models fetched", models)
} else {
console.error("Invalid response from Requesty API")
}
} catch (error) {
console.error("Error fetching Requesty models:", error)
}
await this.postMessageToWebview({
type: "requestyModels",
requestyModels: models,
})
return models
}
// Context menus and code actions
getFileMentionFromPath(filePath: string) {
-2
View File
@@ -8,7 +8,6 @@ import { getOllamaModels } from "./getOllamaModels"
import { getVsCodeLmModels } from "./getVsCodeLmModels"
import { refreshOpenAiModels } from "./refreshOpenAiModels"
import { refreshOpenRouterModels } from "./refreshOpenRouterModels"
import { refreshRequestyModels } from "./refreshRequestyModels"
// Register all models service methods
export function registerAllMethods(): void {
@@ -18,5 +17,4 @@ export function registerAllMethods(): void {
registerMethod("getVsCodeLmModels", getVsCodeLmModels)
registerMethod("refreshOpenAiModels", refreshOpenAiModels)
registerMethod("refreshOpenRouterModels", refreshOpenRouterModels)
registerMethod("refreshRequestyModels", refreshRequestyModels)
}
@@ -29,6 +29,9 @@ export async function refreshOpenAiModels(controller: Controller, request: OpenA
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)
@@ -1,6 +1,6 @@
import { Controller } from ".."
import { EmptyRequest } from "../../../shared/proto/common"
import { OpenRouterCompatibleModelInfo, OpenRouterModelInfo } from "../../../shared/proto/models"
import { OpenRouterModels, OpenRouterModelInfo } from "../../../shared/proto/models"
import axios from "axios"
import path from "path"
import fs from "fs/promises"
@@ -13,10 +13,7 @@ import { GlobalFileNames } from "@core/storage/disk"
* @param request Empty request object
* @returns Response containing the OpenRouter models
*/
export async function refreshOpenRouterModels(
controller: Controller,
request: EmptyRequest,
): Promise<OpenRouterCompatibleModelInfo> {
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>> = {}
@@ -144,7 +141,13 @@ export async function refreshOpenRouterModels(
}
}
return OpenRouterCompatibleModelInfo.create({ models: typedModels })
// Send models to webview
await controller.postMessageToWebview({
type: "openRouterModels",
openRouterModels: typedModels,
})
return OpenRouterModels.create({ models: typedModels })
}
/**
@@ -1,55 +0,0 @@
import { Controller } from ".."
import { EmptyRequest } from "../../../shared/proto/common"
import { OpenRouterCompatibleModelInfo, OpenRouterModelInfo } from "../../../shared/proto/models"
import axios from "axios"
import { getSecret } from "@core/storage/state"
/**
* Refreshes the Requesty models and returns the updated model list
* @param controller The controller instance
* @param request Empty request object
* @returns Response containing the Requesty models
*/
export async function refreshRequestyModels(
controller: Controller,
request: EmptyRequest,
): Promise<OpenRouterCompatibleModelInfo> {
const parsePrice = (price: any) => {
if (price) {
return parseFloat(price) * 1_000_000
}
return undefined
}
let models: Record<string, OpenRouterModelInfo> = {}
try {
const apiKey = await getSecret(controller.context, "requestyApiKey")
const headers = {
Authorization: `Bearer ${apiKey}`,
}
const response = await axios.get("https://router.requesty.ai/v1/models", { headers })
if (response.data?.data) {
for (const model of response.data.data) {
const modelInfo: OpenRouterModelInfo = {
maxTokens: model.max_output_tokens || undefined,
contextWindow: model.context_window,
supportsImages: model.supports_vision || undefined,
supportsPromptCache: model.supports_caching || undefined,
inputPrice: parsePrice(model.input_price) || 0,
outputPrice: parsePrice(model.output_price) || 0,
cacheWritesPrice: parsePrice(model.caching_price) || 0,
cacheReadsPrice: parsePrice(model.cached_price) || 0,
description: model.description,
}
models[model.id] = modelInfo
}
console.log("Requesty models fetched", models)
} else {
console.error("Invalid response from Requesty API")
}
} catch (error) {
console.error("Error fetching Requesty models:", error)
}
return OpenRouterCompatibleModelInfo.create({ models })
}
-22
View File
@@ -1,22 +0,0 @@
// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
// Generated by proto/build-proto.js
import { createServiceRegistry, ServiceMethodHandler, StreamingMethodHandler } from "../grpc-service"
import { StreamingResponseHandler } from "../grpc-handler"
import { registerAllMethods } from "./methods"
// Create slash service registry
const slashService = createServiceRegistry("slash")
// Export the method handler types and registration function
export type SlashMethodHandler = ServiceMethodHandler
export type SlashStreamingMethodHandler = StreamingMethodHandler
export const registerMethod = slashService.registerMethod
// Export the request handlers
export const handleSlashServiceRequest = slashService.handleRequest
export const handleSlashServiceStreamingRequest = slashService.handleStreamingRequest
export const isStreamingMethod = slashService.isStreamingMethod
// Register all slash methods
registerAllMethods()
-12
View File
@@ -1,12 +0,0 @@
// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
// Generated by proto/build-proto.js
// Import all method implementations
import { registerMethod } from "./index"
import { reportBug } from "./reportBug"
// Register all slash service methods
export function registerAllMethods(): void {
// Register each method with the registry
registerMethod("reportBug", reportBug)
}
-10
View File
@@ -1,10 +0,0 @@
import { Controller } from ".."
import { StringRequest, Empty } from "../../../shared/proto/common"
/**
* Report bug slash command logic
*/
export async function reportBug(controller: Controller, request: StringRequest): Promise<Empty> {
await controller.task?.handleWebviewAskResponse("yesButtonClicked")
return Empty.create()
}
-1
View File
@@ -67,7 +67,6 @@ export type GlobalStateKey =
| "previousModeModelInfo"
| "liteLlmBaseUrl"
| "liteLlmModelId"
| "liteLlmModelInfo"
| "liteLlmUsePromptCache"
| "qwenApiLine"
| "requestyModelId"
-5
View File
@@ -107,7 +107,6 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
vsCodeLmModelSelector,
liteLlmBaseUrl,
liteLlmModelId,
liteLlmModelInfo,
liteLlmUsePromptCache,
userInfo,
previousModeApiProvider,
@@ -187,7 +186,6 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
getGlobalState(context, "vsCodeLmModelSelector") as Promise<vscode.LanguageModelChatSelector | undefined>,
getGlobalState(context, "liteLlmBaseUrl") as Promise<string | undefined>,
getGlobalState(context, "liteLlmModelId") as Promise<string | undefined>,
getGlobalState(context, "liteLlmModelInfo") as Promise<ModelInfo | undefined>,
getGlobalState(context, "liteLlmUsePromptCache") as Promise<boolean | undefined>,
getGlobalState(context, "userInfo") as Promise<UserInfo | undefined>,
getGlobalState(context, "previousModeApiProvider") as Promise<ApiProvider | undefined>,
@@ -306,7 +304,6 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
reasoningEffort,
liteLlmBaseUrl,
liteLlmModelId,
liteLlmModelInfo,
liteLlmApiKey,
liteLlmUsePromptCache,
asksageApiKey,
@@ -389,7 +386,6 @@ export async function updateApiConfiguration(context: vscode.ExtensionContext, a
vsCodeLmModelSelector,
liteLlmBaseUrl,
liteLlmModelId,
liteLlmModelInfo,
liteLlmApiKey,
liteLlmUsePromptCache,
qwenApiLine,
@@ -448,7 +444,6 @@ export async function updateApiConfiguration(context: vscode.ExtensionContext, a
await updateGlobalState(context, "vsCodeLmModelSelector", vsCodeLmModelSelector)
await updateGlobalState(context, "liteLlmBaseUrl", liteLlmBaseUrl)
await updateGlobalState(context, "liteLlmModelId", liteLlmModelId)
await updateGlobalState(context, "liteLlmModelInfo", liteLlmModelInfo)
await updateGlobalState(context, "liteLlmUsePromptCache", liteLlmUsePromptCache)
await updateGlobalState(context, "qwenApiLine", qwenApiLine)
await updateGlobalState(context, "requestyModelId", requestyModelId)
+4 -19
View File
@@ -60,7 +60,7 @@ import { fileExistsAtPath } from "@utils/fs"
import { createAndOpenGitHubIssue } from "@utils/github-url-utils"
import { arePathsEqual, getReadablePath, isLocatedInWorkspace } from "@utils/path"
import { fixModelHtmlEscaping, removeInvalidChars } from "@utils/string"
import { AssistantMessageContent, parseAssistantMessageV2, ToolParamName, ToolUseName } from "@core/assistant-message"
import { AssistantMessageContent, parseAssistantMessage, ToolParamName, ToolUseName } from "@core/assistant-message"
import { constructNewFileContent } from "@core/assistant-message/diff"
import { ClineIgnoreController } from "@core/ignore/ClineIgnoreController"
import { parseMentions } from "@core/mentions"
@@ -1442,28 +1442,13 @@ export class Task {
return statusCode && !message.includes(statusCode.toString()) ? `${statusCode} - ${message}` : message
}
/**
* Migrates the disableBrowserTool setting from VSCode configuration to browserSettings
*/
private async migrateDisableBrowserToolSetting(): Promise<void> {
const config = vscode.workspace.getConfiguration("cline")
const disableBrowserTool = vscode.workspace.getConfiguration("cline").get<boolean>("disableBrowserTool")
if (disableBrowserTool !== undefined) {
this.browserSettings.disableToolUse = disableBrowserTool
// Remove from VSCode configuration
await config.update("disableBrowserTool", undefined, true)
}
}
async *attemptApiRequest(previousApiReqIndex: number): ApiStream {
// Wait for MCP servers to be connected before generating system prompt
await pWaitFor(() => this.mcpHub.isConnecting !== true, { timeout: 10_000 }).catch(() => {
console.error("MCP servers failed to connect in time")
})
await this.migrateDisableBrowserToolSetting()
const disableBrowserTool = this.browserSettings.disableToolUse ?? false
const disableBrowserTool = vscode.workspace.getConfiguration("cline").get<boolean>("disableBrowserTool") ?? false
// cline browser tool uses image recognition for navigation (requires model image support).
const modelSupportsBrowserUse = this.api.getModel().info.supportsImages ?? false
@@ -3259,7 +3244,7 @@ export class Task {
await this.say("user_feedback", text ?? "", images)
pushToolResult(
formatResponse.toolResult(
`The user did not submit the bug, and provided feedback on the Github issue generated instead:\n<feedback>\n${text}\n</feedback>`,
`The user provided feedback on the Github issue generated:\n<feedback>\n${text}\n</feedback>`,
images,
),
)
@@ -3859,7 +3844,7 @@ export class Task {
assistantMessage += chunk.text
// parse raw assistant message into content blocks
const prevLength = this.assistantMessageContent.length
this.assistantMessageContent = parseAssistantMessageV2(assistantMessage)
this.assistantMessageContent = parseAssistantMessage(assistantMessage)
if (this.assistantMessageContent.length > prevLength) {
this.userMessageContentReady = false // new content we need to present, reset to false in case previous content set this to true
}
+3 -9
View File
@@ -91,9 +91,9 @@ export class DiffViewProvider {
const currentFirstVisibleLine = e.visibleRanges[0]?.start.line || 0
// If the first visible line moved upward, user scrolled up
// if (currentFirstVisibleLine < this.lastFirstVisibleLine) {
// this.shouldAutoScroll = false
// }
if (currentFirstVisibleLine < this.lastFirstVisibleLine) {
this.shouldAutoScroll = false
}
// Always update our tracking variable
this.lastFirstVisibleLine = currentFirstVisibleLine
@@ -433,12 +433,6 @@ export class DiffViewProvider {
// close editor if open?
async reset() {
// releasing memory by clearing the diff editor
try {
await this.closeAllDiffViews()
} catch (error) {
console.error("Error closing diff views:", error)
}
this.editType = undefined
this.isEditing = false
this.originalContent = undefined
+4 -18
View File
@@ -67,25 +67,11 @@ export class BrowserSession {
}
}
/**
* Migrates the chromeExecutablePath setting from VSCode configuration to browserSettings
*/
private async migrateChromeExecutablePathSetting(): Promise<void> {
const config = vscode.workspace.getConfiguration("cline")
const configPath = vscode.workspace.getConfiguration("cline").get<string>("chromeExecutablePath")
if (configPath !== undefined) {
this.browserSettings.chromeExecutablePath = configPath
// Remove from VSCode configuration
await config.update("chromeExecutablePath", undefined, true)
}
}
async getDetectedChromePath(): Promise<{ path: string; isBundled: boolean }> {
// First check browserSettings (from UI, stored in global state)
await this.migrateChromeExecutablePathSetting()
if (this.browserSettings.chromeExecutablePath && (await fileExistsAtPath(this.browserSettings.chromeExecutablePath))) {
return { path: this.browserSettings.chromeExecutablePath, isBundled: false }
// First check VSCode config
const configPath = vscode.workspace.getConfiguration("cline").get<string>("chromeExecutablePath")
if (configPath && (await fileExistsAtPath(configPath))) {
return { path: configPath, isBundled: false }
}
// Then try to find system Chrome
@@ -9,7 +9,6 @@ class PostHogClientProvider {
this.client = new PostHog(posthogConfig.apiKey, {
host: posthogConfig.host,
enableExceptionAutocapture: false,
defaultOptIn: false,
})
}
@@ -22,20 +22,7 @@ interface Collection {
properties: any
}
/**
* Represents telemetry event categories that can be individually enabled or disabled
* When adding a new category, add it both here and to the initial values in telemetryCategoryEnabled
* Ensure `if (!this.isCategoryEnabled('<category_name>')` is added to the capture method
*/
type TelemetryCategory = "checkpoints" | "browser"
class PostHogClient {
// Map to control specific telemetry categories (event types)
private telemetryCategoryEnabled: Map<TelemetryCategory, boolean> = new Map([
["checkpoints", false], // Checkpoints telemetry disabled
["browser", true], // Browser telemetry enabled
])
// Stores events when collect=true
private collectedTasks: CollectedTasks[] = []
// Event constants for tracking user interactions and system events
@@ -284,6 +271,7 @@ class PostHogClient {
}
/**
* TODO
* Records token usage metrics for cost tracking and usage analysis
* @param taskId Unique identifier for the task
* @param tokensIn Number of input tokens consumed
@@ -377,10 +365,6 @@ class PostHogClient {
durationMs?: number,
collect: boolean = false,
) {
if (!this.isCategoryEnabled("checkpoints")) {
return
}
this.capture(
{
event: PostHogClient.EVENTS.TASK.CHECKPOINT_USED,
@@ -600,10 +584,6 @@ class PostHogClient {
* @param browserSettings The browser settings being used
*/
public captureBrowserToolStart(taskId: string, browserSettings: BrowserSettings, collect: boolean = false) {
if (!this.isCategoryEnabled("browser")) {
return
}
this.capture(
{
event: PostHogClient.EVENTS.TASK.BROWSER_TOOL_START,
@@ -633,10 +613,6 @@ class PostHogClient {
},
collect: boolean = false,
) {
if (!this.isCategoryEnabled("browser")) {
return
}
this.capture(
{
event: PostHogClient.EVENTS.TASK.BROWSER_TOOL_END,
@@ -671,10 +647,6 @@ class PostHogClient {
},
collect: boolean = false,
) {
if (!this.isCategoryEnabled("browser")) {
return
}
this.capture(
{
event: PostHogClient.EVENTS.TASK.BROWSER_ERROR,
@@ -748,24 +720,10 @@ class PostHogClient {
)
}
/**
* Checks if telemetry is enabled
* @returns Boolean indicating whether telemetry is enabled
*/
public isTelemetryEnabled(): boolean {
return this.telemetryEnabled
}
/**
* Checks if a specific telemetry category is enabled
* @param category The telemetry category to check
* @returns Boolean indicating whether the specified telemetry category is enabled
*/
public isCategoryEnabled(category: TelemetryCategory): boolean {
// Default to true if category has not been explicitly configured
return this.telemetryCategoryEnabled.get(category) ?? true
}
public async sendCollectedEvents(taskId?: string): Promise<void> {
if (this.collectedTasks.length > 0) {
if (taskId) {
+4 -4
View File
@@ -22,18 +22,18 @@ export interface AutoApprovalSettings {
export const DEFAULT_AUTO_APPROVAL_SETTINGS: AutoApprovalSettings = {
version: 1,
enabled: true,
enabled: false,
actions: {
readFiles: true,
readFiles: false,
readFilesExternally: false,
editFiles: false,
editFilesExternally: false,
executeSafeCommands: true,
executeSafeCommands: false,
executeAllCommands: false,
useBrowser: false,
useMcp: false,
},
maxRequests: 20,
enableNotifications: false,
favorites: ["enableAll", "readFiles", "editFiles"],
favorites: [],
}
-4
View File
@@ -8,8 +8,6 @@ export interface BrowserSettings {
// chromeType: "chromium" | "system"
remoteBrowserHost?: string
remoteBrowserEnabled?: boolean
chromeExecutablePath?: string
disableToolUse?: boolean
}
export const DEFAULT_BROWSER_SETTINGS: BrowserSettings = {
@@ -19,9 +17,7 @@ export const DEFAULT_BROWSER_SETTINGS: BrowserSettings = {
},
remoteBrowserEnabled: false,
remoteBrowserHost: "http://localhost:9222",
chromeExecutablePath: "", // Changed from undefined to empty string
// chromeType: "chromium",
disableToolUse: false,
}
export const BROWSER_VIEWPORT_PRESETS = {
+1
View File
@@ -21,6 +21,7 @@ export interface WebviewMessage {
| "openInBrowser"
| "openMention"
| "showChatView"
| "refreshRequestyModels"
| "refreshClineRules"
| "openMcpSettings"
| "restartMcpServer"
+2 -8
View File
@@ -33,7 +33,6 @@ export interface ApiHandlerOptions {
liteLlmApiKey?: string
liteLlmUsePromptCache?: boolean
openAiHeaders?: Record<string, string> // Custom headers for OpenAI requests
liteLlmModelInfo?: LiteLLMModelInfo
anthropicBaseUrl?: string
openRouterApiKey?: string
openRouterModelId?: string
@@ -1453,12 +1452,8 @@ export const mistralModels = {
// LiteLLM
// https://docs.litellm.ai/docs/
export type LiteLLMModelId = string
export const liteLlmDefaultModelId = "anthropic/claude-3-7-sonnet-20250219"
export interface LiteLLMModelInfo extends ModelInfo {
temperature?: number
}
export const liteLlmModelInfoSaneDefaults: LiteLLMModelInfo = {
export const liteLlmDefaultModelId = "gpt-3.5-turbo"
export const liteLlmModelInfoSaneDefaults: ModelInfo = {
maxTokens: -1,
contextWindow: 128_000,
supportsImages: true,
@@ -1467,7 +1462,6 @@ export const liteLlmModelInfoSaneDefaults: LiteLLMModelInfo = {
outputPrice: 0,
cacheWritesPrice: 0,
cacheReadsPrice: 0,
temperature: 0,
}
// AskSage Models
+2 -83
View File
@@ -36,8 +36,6 @@ export interface BrowserSettings {
viewport?: Viewport | undefined
remoteBrowserHost?: string | undefined
remoteBrowserEnabled?: boolean | undefined
chromeExecutablePath?: string | undefined
disableToolUse?: boolean | undefined
}
export interface UpdateBrowserSettingsRequest {
@@ -45,8 +43,6 @@ export interface UpdateBrowserSettingsRequest {
viewport?: Viewport | undefined
remoteBrowserHost?: string | undefined
remoteBrowserEnabled?: boolean | undefined
chromeExecutablePath?: string | undefined
disableToolUse?: boolean | undefined
}
function createBaseBrowserConnectionInfo(): BrowserConnectionInfo {
@@ -386,13 +382,7 @@ export const Viewport: MessageFns<Viewport> = {
}
function createBaseBrowserSettings(): BrowserSettings {
return {
viewport: undefined,
remoteBrowserHost: undefined,
remoteBrowserEnabled: undefined,
chromeExecutablePath: undefined,
disableToolUse: undefined,
}
return { viewport: undefined, remoteBrowserHost: undefined, remoteBrowserEnabled: undefined }
}
export const BrowserSettings: MessageFns<BrowserSettings> = {
@@ -406,12 +396,6 @@ export const BrowserSettings: MessageFns<BrowserSettings> = {
if (message.remoteBrowserEnabled !== undefined) {
writer.uint32(24).bool(message.remoteBrowserEnabled)
}
if (message.chromeExecutablePath !== undefined) {
writer.uint32(34).string(message.chromeExecutablePath)
}
if (message.disableToolUse !== undefined) {
writer.uint32(40).bool(message.disableToolUse)
}
return writer
},
@@ -446,22 +430,6 @@ export const BrowserSettings: MessageFns<BrowserSettings> = {
message.remoteBrowserEnabled = reader.bool()
continue
}
case 4: {
if (tag !== 34) {
break
}
message.chromeExecutablePath = reader.string()
continue
}
case 5: {
if (tag !== 40) {
break
}
message.disableToolUse = reader.bool()
continue
}
}
if ((tag & 7) === 4 || tag === 0) {
break
@@ -478,8 +446,6 @@ export const BrowserSettings: MessageFns<BrowserSettings> = {
remoteBrowserEnabled: isSet(object.remoteBrowserEnabled)
? globalThis.Boolean(object.remoteBrowserEnabled)
: undefined,
chromeExecutablePath: isSet(object.chromeExecutablePath) ? globalThis.String(object.chromeExecutablePath) : undefined,
disableToolUse: isSet(object.disableToolUse) ? globalThis.Boolean(object.disableToolUse) : undefined,
}
},
@@ -494,12 +460,6 @@ export const BrowserSettings: MessageFns<BrowserSettings> = {
if (message.remoteBrowserEnabled !== undefined) {
obj.remoteBrowserEnabled = message.remoteBrowserEnabled
}
if (message.chromeExecutablePath !== undefined) {
obj.chromeExecutablePath = message.chromeExecutablePath
}
if (message.disableToolUse !== undefined) {
obj.disableToolUse = message.disableToolUse
}
return obj
},
@@ -512,21 +472,12 @@ export const BrowserSettings: MessageFns<BrowserSettings> = {
object.viewport !== undefined && object.viewport !== null ? Viewport.fromPartial(object.viewport) : undefined
message.remoteBrowserHost = object.remoteBrowserHost ?? undefined
message.remoteBrowserEnabled = object.remoteBrowserEnabled ?? undefined
message.chromeExecutablePath = object.chromeExecutablePath ?? undefined
message.disableToolUse = object.disableToolUse ?? undefined
return message
},
}
function createBaseUpdateBrowserSettingsRequest(): UpdateBrowserSettingsRequest {
return {
metadata: undefined,
viewport: undefined,
remoteBrowserHost: undefined,
remoteBrowserEnabled: undefined,
chromeExecutablePath: undefined,
disableToolUse: undefined,
}
return { metadata: undefined, viewport: undefined, remoteBrowserHost: undefined, remoteBrowserEnabled: undefined }
}
export const UpdateBrowserSettingsRequest: MessageFns<UpdateBrowserSettingsRequest> = {
@@ -543,12 +494,6 @@ export const UpdateBrowserSettingsRequest: MessageFns<UpdateBrowserSettingsReque
if (message.remoteBrowserEnabled !== undefined) {
writer.uint32(32).bool(message.remoteBrowserEnabled)
}
if (message.chromeExecutablePath !== undefined) {
writer.uint32(42).string(message.chromeExecutablePath)
}
if (message.disableToolUse !== undefined) {
writer.uint32(48).bool(message.disableToolUse)
}
return writer
},
@@ -591,22 +536,6 @@ export const UpdateBrowserSettingsRequest: MessageFns<UpdateBrowserSettingsReque
message.remoteBrowserEnabled = reader.bool()
continue
}
case 5: {
if (tag !== 42) {
break
}
message.chromeExecutablePath = reader.string()
continue
}
case 6: {
if (tag !== 48) {
break
}
message.disableToolUse = reader.bool()
continue
}
}
if ((tag & 7) === 4 || tag === 0) {
break
@@ -624,8 +553,6 @@ export const UpdateBrowserSettingsRequest: MessageFns<UpdateBrowserSettingsReque
remoteBrowserEnabled: isSet(object.remoteBrowserEnabled)
? globalThis.Boolean(object.remoteBrowserEnabled)
: undefined,
chromeExecutablePath: isSet(object.chromeExecutablePath) ? globalThis.String(object.chromeExecutablePath) : undefined,
disableToolUse: isSet(object.disableToolUse) ? globalThis.Boolean(object.disableToolUse) : undefined,
}
},
@@ -643,12 +570,6 @@ export const UpdateBrowserSettingsRequest: MessageFns<UpdateBrowserSettingsReque
if (message.remoteBrowserEnabled !== undefined) {
obj.remoteBrowserEnabled = message.remoteBrowserEnabled
}
if (message.chromeExecutablePath !== undefined) {
obj.chromeExecutablePath = message.chromeExecutablePath
}
if (message.disableToolUse !== undefined) {
obj.disableToolUse = message.disableToolUse
}
return obj
},
@@ -663,8 +584,6 @@ export const UpdateBrowserSettingsRequest: MessageFns<UpdateBrowserSettingsReque
object.viewport !== undefined && object.viewport !== null ? Viewport.fromPartial(object.viewport) : undefined
message.remoteBrowserHost = object.remoteBrowserHost ?? undefined
message.remoteBrowserEnabled = object.remoteBrowserEnabled ?? undefined
message.chromeExecutablePath = object.chromeExecutablePath ?? undefined
message.disableToolUse = object.disableToolUse ?? undefined
return message
},
}
+29 -42
View File
@@ -23,7 +23,7 @@ export interface VsCodeLmModel {
id: string
}
/** For OpenRouterCompatibleModelInfo structure in OpenRouterModels */
/** For ModelInfo structure in OpenRouterModels */
export interface OpenRouterModelInfo {
maxTokens: number
contextWindow: number
@@ -36,12 +36,12 @@ export interface OpenRouterModelInfo {
description: string
}
/** Shared response message for model information */
export interface OpenRouterCompatibleModelInfo {
/** Response message for OpenRouter models */
export interface OpenRouterModels {
models: { [key: string]: OpenRouterModelInfo }
}
export interface OpenRouterCompatibleModelInfo_ModelsEntry {
export interface OpenRouterModels_ModelsEntry {
key: string
value?: OpenRouterModelInfo | undefined
}
@@ -419,22 +419,22 @@ export const OpenRouterModelInfo: MessageFns<OpenRouterModelInfo> = {
},
}
function createBaseOpenRouterCompatibleModelInfo(): OpenRouterCompatibleModelInfo {
function createBaseOpenRouterModels(): OpenRouterModels {
return { models: {} }
}
export const OpenRouterCompatibleModelInfo: MessageFns<OpenRouterCompatibleModelInfo> = {
encode(message: OpenRouterCompatibleModelInfo, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
export const OpenRouterModels: MessageFns<OpenRouterModels> = {
encode(message: OpenRouterModels, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
Object.entries(message.models).forEach(([key, value]) => {
OpenRouterCompatibleModelInfo_ModelsEntry.encode({ key: key as any, value }, writer.uint32(10).fork()).join()
OpenRouterModels_ModelsEntry.encode({ key: key as any, value }, writer.uint32(10).fork()).join()
})
return writer
},
decode(input: BinaryReader | Uint8Array, length?: number): OpenRouterCompatibleModelInfo {
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 = createBaseOpenRouterCompatibleModelInfo()
const message = createBaseOpenRouterModels()
while (reader.pos < end) {
const tag = reader.uint32()
switch (tag >>> 3) {
@@ -443,7 +443,7 @@ export const OpenRouterCompatibleModelInfo: MessageFns<OpenRouterCompatibleModel
break
}
const entry1 = OpenRouterCompatibleModelInfo_ModelsEntry.decode(reader, reader.uint32())
const entry1 = OpenRouterModels_ModelsEntry.decode(reader, reader.uint32())
if (entry1.value !== undefined) {
message.models[entry1.key] = entry1.value
}
@@ -458,7 +458,7 @@ export const OpenRouterCompatibleModelInfo: MessageFns<OpenRouterCompatibleModel
return message
},
fromJSON(object: any): OpenRouterCompatibleModelInfo {
fromJSON(object: any): OpenRouterModels {
return {
models: isObject(object.models)
? Object.entries(object.models).reduce<{ [key: string]: OpenRouterModelInfo }>((acc, [key, value]) => {
@@ -469,7 +469,7 @@ export const OpenRouterCompatibleModelInfo: MessageFns<OpenRouterCompatibleModel
}
},
toJSON(message: OpenRouterCompatibleModelInfo): unknown {
toJSON(message: OpenRouterModels): unknown {
const obj: any = {}
if (message.models) {
const entries = Object.entries(message.models)
@@ -483,11 +483,11 @@ export const OpenRouterCompatibleModelInfo: MessageFns<OpenRouterCompatibleModel
return obj
},
create<I extends Exact<DeepPartial<OpenRouterCompatibleModelInfo>, I>>(base?: I): OpenRouterCompatibleModelInfo {
return OpenRouterCompatibleModelInfo.fromPartial(base ?? ({} as any))
create<I extends Exact<DeepPartial<OpenRouterModels>, I>>(base?: I): OpenRouterModels {
return OpenRouterModels.fromPartial(base ?? ({} as any))
},
fromPartial<I extends Exact<DeepPartial<OpenRouterCompatibleModelInfo>, I>>(object: I): OpenRouterCompatibleModelInfo {
const message = createBaseOpenRouterCompatibleModelInfo()
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) {
@@ -501,12 +501,12 @@ export const OpenRouterCompatibleModelInfo: MessageFns<OpenRouterCompatibleModel
},
}
function createBaseOpenRouterCompatibleModelInfo_ModelsEntry(): OpenRouterCompatibleModelInfo_ModelsEntry {
function createBaseOpenRouterModels_ModelsEntry(): OpenRouterModels_ModelsEntry {
return { key: "", value: undefined }
}
export const OpenRouterCompatibleModelInfo_ModelsEntry: MessageFns<OpenRouterCompatibleModelInfo_ModelsEntry> = {
encode(message: OpenRouterCompatibleModelInfo_ModelsEntry, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
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)
}
@@ -516,10 +516,10 @@ export const OpenRouterCompatibleModelInfo_ModelsEntry: MessageFns<OpenRouterCom
return writer
},
decode(input: BinaryReader | Uint8Array, length?: number): OpenRouterCompatibleModelInfo_ModelsEntry {
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 = createBaseOpenRouterCompatibleModelInfo_ModelsEntry()
const message = createBaseOpenRouterModels_ModelsEntry()
while (reader.pos < end) {
const tag = reader.uint32()
switch (tag >>> 3) {
@@ -548,14 +548,14 @@ export const OpenRouterCompatibleModelInfo_ModelsEntry: MessageFns<OpenRouterCom
return message
},
fromJSON(object: any): OpenRouterCompatibleModelInfo_ModelsEntry {
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: OpenRouterCompatibleModelInfo_ModelsEntry): unknown {
toJSON(message: OpenRouterModels_ModelsEntry): unknown {
const obj: any = {}
if (message.key !== "") {
obj.key = message.key
@@ -566,15 +566,11 @@ export const OpenRouterCompatibleModelInfo_ModelsEntry: MessageFns<OpenRouterCom
return obj
},
create<I extends Exact<DeepPartial<OpenRouterCompatibleModelInfo_ModelsEntry>, I>>(
base?: I,
): OpenRouterCompatibleModelInfo_ModelsEntry {
return OpenRouterCompatibleModelInfo_ModelsEntry.fromPartial(base ?? ({} as any))
create<I extends Exact<DeepPartial<OpenRouterModels_ModelsEntry>, I>>(base?: I): OpenRouterModels_ModelsEntry {
return OpenRouterModels_ModelsEntry.fromPartial(base ?? ({} as any))
},
fromPartial<I extends Exact<DeepPartial<OpenRouterCompatibleModelInfo_ModelsEntry>, I>>(
object: I,
): OpenRouterCompatibleModelInfo_ModelsEntry {
const message = createBaseOpenRouterCompatibleModelInfo_ModelsEntry()
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
@@ -713,7 +709,7 @@ export const ModelsServiceDefinition = {
name: "refreshOpenRouterModels",
requestType: EmptyRequest,
requestStream: false,
responseType: OpenRouterCompatibleModelInfo,
responseType: OpenRouterModels,
responseStream: false,
options: {},
},
@@ -726,15 +722,6 @@ export const ModelsServiceDefinition = {
responseStream: false,
options: {},
},
/** Refreshes and returns Requesty models */
refreshRequestyModels: {
name: "refreshRequestyModels",
requestType: EmptyRequest,
requestStream: false,
responseType: OpenRouterCompatibleModelInfo,
responseStream: false,
options: {},
},
},
} as const
-28
View File
@@ -1,28 +0,0 @@
// Code generated by protoc-gen-ts_proto. DO NOT EDIT.
// versions:
// protoc-gen-ts_proto v2.7.0
// protoc v3.19.1
// source: slash.proto
/* eslint-disable */
import { Empty, StringRequest } from "./common"
export const protobufPackage = "cline"
/** SlashService provides methods for managing slash */
export type SlashServiceDefinition = typeof SlashServiceDefinition
export const SlashServiceDefinition = {
name: "SlashService",
fullName: "cline.SlashService",
methods: {
/** Sends button click message */
reportBug: {
name: "reportBug",
requestType: StringRequest,
requestStream: false,
responseType: Empty,
responseStream: false,
options: {},
},
},
} as const
+1 -2
View File
@@ -1,6 +1,5 @@
// Public PostHog key (safe for open source)
export const posthogConfig = {
apiKey: "phc_qfOAGxZw2TL5O8p9KYd9ak3bPBFzfjC8fy5L6jNWY7K",
host: "https://data.cline.bot",
uiHost: "https://us.posthog.com",
host: "https://us.i.posthog.com",
}
+10
View File
@@ -53,4 +53,14 @@ describe("Extension Tests", function () {
await vscode.commands.executeCommand("cline.historyButtonClicked")
// Success if no error thrown
})
it("should handle advanced settings configuration", async () => {
// Test browser session setting
await vscode.workspace.getConfiguration().update("cline.disableBrowserTool", true, true)
const updatedConfig = vscode.workspace.getConfiguration("cline")
expect(updatedConfig.get("disableBrowserTool")).to.be.true
// Reset settings
await vscode.workspace.getConfiguration().update("cline.disableBrowserTool", undefined, true)
})
})
+1
View File
@@ -0,0 +1 @@
25463
+6 -11
View File
@@ -6,20 +6,15 @@ import { useExtensionState } from "./context/ExtensionStateContext"
export function CustomPostHogProvider({ children }: { children: ReactNode }) {
const { telemetrySetting } = useExtensionState()
const isTelemetryEnabled = telemetrySetting !== "disabled"
const isTelemetryEnabled = telemetrySetting === "enabled"
useEffect(() => {
posthog.init(posthogConfig.apiKey, {
api_host: posthogConfig.host,
ui_host: posthogConfig.uiHost,
opt_out_capturing_by_default: true,
disable_session_recording: true,
capture_pageview: false,
capture_dead_clicks: true,
})
if (isTelemetryEnabled) {
posthog.opt_in_capturing()
posthog.init(posthogConfig.apiKey, {
api_host: posthogConfig.host,
autocapture: false,
disable_session_recording: true,
})
} else {
posthog.opt_out_capturing()
}
+3 -5
View File
@@ -378,7 +378,7 @@ export const ChatRowContent = ({
marginBottom: "-1.5px",
}}></span>
),
<span className="ph-no-capture" style={{ color: normalColor, fontWeight: "bold", wordBreak: "break-word" }}>
<span style={{ color: normalColor, fontWeight: "bold", wordBreak: "break-word" }}>
Cline wants to {mcpServerUse.type === "use_mcp_tool" ? "use a tool" : "access a resource"} on the{" "}
<code style={{ wordBreak: "break-all" }}>
{getMcpServerDisplayName(mcpServerUse.serverName, mcpMarketplaceCatalog)}
@@ -493,7 +493,7 @@ export const ChatRowContent = ({
}
const toolIcon = (name: string, color?: string, rotation?: number, title?: string) => (
<span
className={`codicon codicon-${name} ph-no-capture`}
className={`codicon codicon-${name}`}
style={{
color: color ? colorMap[color as keyof typeof colorMap] || color : "var(--vscode-foreground)",
marginBottom: "-1.5px",
@@ -577,7 +577,6 @@ export const ChatRowContent = ({
}}>
{tool.path?.startsWith(".") && <span>.</span>}
<span
className="ph-no-capture"
style={{
whiteSpace: "nowrap",
overflow: "hidden",
@@ -1000,13 +999,12 @@ export const ChatRowContent = ({
}}
/>
</span>
<span className="ph-no-capture">{message.text}</span>
{message.text}
</div>
) : (
<div style={{ display: "flex", alignItems: "center" }}>
<span style={{ fontWeight: "bold", marginRight: "4px" }}>Thinking:</span>
<span
className="ph-no-capture"
style={{
whiteSpace: "nowrap",
overflow: "hidden",
+5 -2
View File
@@ -18,7 +18,7 @@ import { combineCommandSequences } from "@shared/combineCommandSequences"
import { getApiMetrics } from "@shared/getApiMetrics"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { vscode } from "@/utils/vscode"
import { TaskServiceClient, SlashServiceClient } from "@/services/grpc-client"
import { TaskServiceClient } from "@/services/grpc-client"
import HistoryPreview from "@/components/history/HistoryPreview"
import { normalizeApiConfiguration } from "@/components/settings/ApiOptions"
import Announcement from "@/components/chat/Announcement"
@@ -562,7 +562,10 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
})
break
case "report_bug":
await SlashServiceClient.reportBug({ value: lastMessage?.text }).catch((err) => console.error(err))
vscode.postMessage({
type: "reportBug",
text: lastMessage?.text,
})
break
}
setSendingDisabled(true)
@@ -93,11 +93,8 @@ const ContextMenu: React.FC<ContextMenuProps> = ({
if (option.value) {
return (
<div style={{ display: "flex", flexDirection: "column", gap: 0 }}>
<span className="ph-no-capture" style={{ lineHeight: "1.2" }}>
{option.label}
</span>
<span style={{ lineHeight: "1.2" }}>{option.label}</span>
<span
className="ph-no-capture"
style={{
fontSize: "0.85em",
opacity: 0.7,
@@ -121,7 +118,6 @@ const ContextMenu: React.FC<ContextMenuProps> = ({
<span>/</span>
{option.value?.startsWith("/.") && <span>.</span>}
<span
className="ph-no-capture"
style={{
whiteSpace: "nowrap",
overflow: "hidden",
@@ -51,8 +51,6 @@ export const OptionsButtons = ({
</div> */}
{options.map((option, index) => (
<OptionButton
id={`options-button-${index}`}
className="options-button"
key={index}
isSelected={option === selected}
isNotSelectable={hasSelected || !isActive}
@@ -65,7 +63,7 @@ export const OptionsButtons = ({
text: option + (inputValue ? `: ${inputValue?.trim()}` : ""),
})
}}>
<span className="ph-no-capture">{option}</span>
{option}
</OptionButton>
))}
</div>
@@ -51,8 +51,7 @@ const SlashCommandMenu: React.FC<SlashCommandMenuProps> = ({ onSelect, selectedI
filteredCommands.map((command, index) => (
<div
key={command.name}
id={`slash-command-menu-item-${index}`}
className={`slash-command-menu-item py-2 px-3 cursor-pointer flex flex-col border-b border-[var(--vscode-editorGroup-border)] ${
className={`py-2 px-3 cursor-pointer flex flex-col border-b border-[var(--vscode-editorGroup-border)] ${
// Corrected padding
index === selectedIndex
? "bg-[var(--vscode-quickInputList-focusBackground)] text-[var(--vscode-quickInputList-focusForeground)]"
@@ -60,11 +59,9 @@ const SlashCommandMenu: React.FC<SlashCommandMenuProps> = ({ onSelect, selectedI
} hover:bg-[var(--vscode-list-hoverBackground)]`}
onClick={() => handleClick(command)}
onMouseEnter={() => setSelectedIndex(index)}>
<div className="font-bold whitespace-nowrap overflow-hidden text-ellipsis">
<span className="ph-no-capture">/{command.name}</span>
</div>
<div className="font-bold whitespace-nowrap overflow-hidden text-ellipsis">/{command.name}</div>
<div className="text-[0.85em] text-[var(--vscode-descriptionForeground)] whitespace-normal overflow-hidden text-ellipsis">
<span className="ph-no-capture">{command.description}</span>
{command.description}
</div>
</div>
))
@@ -256,11 +256,7 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
Task
{!isTaskExpanded && ":"}
</span>
{!isTaskExpanded && (
<span className="ph-no-capture" style={{ marginLeft: 4 }}>
{highlightText(task.text, false)}
</span>
)}
{!isTaskExpanded && <span style={{ marginLeft: 4 }}>{highlightText(task.text, false)}</span>}
</div>
</div>
{isCostAvailable && (
@@ -306,7 +302,7 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
wordBreak: "break-word",
overflowWrap: "anywhere",
}}>
<span className="ph-no-capture">{highlightText(task.text, false)}</span>
{highlightText(task.text, false)}
</div>
{!isTextExpanded && showSeeMore && (
<div
@@ -75,7 +75,7 @@ const UserMessage: React.FC<UserMessageProps> = ({ text, images, messageTs, send
setIsEditing(false)
} else if (e.key === "Enter" && e.metaKey && !checkpointTrackerErrorMessage) {
handleRestoreWorkspace("taskAndWorkspace")
} else if (e.key === "Enter" && !e.shiftKey && !e.nativeEvent.isComposing && e.keyCode !== 229) {
} else if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault()
handleRestoreWorkspace("task")
}
@@ -141,9 +141,7 @@ const UserMessage: React.FC<UserMessageProps> = ({ text, images, messageTs, send
</div>
</>
) : (
<span className="ph-no-capture" style={{ display: "block" }}>
{highlightText(editedText || text)}
</span>
<span style={{ display: "block" }}>{highlightText(editedText || text)}</span>
)}
{images && images.length > 0 && <Thumbnails images={images} style={{ marginTop: "8px" }} />}
</div>
@@ -1,44 +0,0 @@
/**
* UserMessage IME composition Enter test
* --------------------------------------------------
* Confirm that sendMessageFromChatRow is not called
* even if you confirm the IME conversion (Enter) in message re-edit mode.
*/
import React from "react"
import { render, fireEvent } from "@testing-library/react"
import { describe, it, expect, vi } from "vitest"
vi.mock("@/context/ExtensionStateContext", () => ({
__esModule: true,
useExtensionState: () => ({
state: {},
dispatch: vi.fn(),
}),
}))
import UserMessage from "../UserMessage"
describe("UserMessage IME composition handling", () => {
it("does NOT send when IME composition Enter is pressed while editing", () => {
const sendMessageFromChatRow = vi.fn()
const { getByText } = render(
<UserMessage text="変換テスト" images={[]} messageTs={Date.now()} sendMessageFromChatRow={sendMessageFromChatRow} />,
)
const editable = getByText("変換テスト") as HTMLElement
editable.setAttribute("contenteditable", "true")
editable.focus()
fireEvent.compositionStart(editable)
fireEvent.keyDown(editable, {
key: "Enter",
keyCode: 13,
nativeEvent: { isComposing: true },
})
fireEvent.compositionEnd(editable)
expect(sendMessageFromChatRow).not.toHaveBeenCalled()
})
})
@@ -5,7 +5,7 @@ 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_FOREGROUND_MUTED } from "@/utils/vscStyles"
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"
@@ -27,13 +27,6 @@ export interface ActionMetadata {
}
const ACTION_METADATA: ActionMetadata[] = [
{
id: "enableAll",
label: "Enable all",
shortName: "All",
description: "Enable all actions.",
icon: "codicon-checklist",
},
{
id: "readFiles",
label: "Read project files",
@@ -94,16 +87,22 @@ const ACTION_METADATA: ActionMetadata[] = [
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 NOTIFICATIONS_SETTING: ActionMetadata = {
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)
@@ -266,8 +265,8 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => {
// Render a favorited item with a checkbox
const renderFavoritedItem = (favId: string) => {
const actions = [...ACTION_METADATA.flatMap((a) => [a, a.subAction]), NOTIFICATIONS_SETTING]
const action = actions.find((a) => a?.id === favId)
// Regular action item
const action = ACTION_METADATA.flatMap((a) => [a, a.subAction]).find((a) => a?.id === favId)
if (!action) return null
return (
@@ -281,43 +280,6 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => {
)
}
// Render a favorited item with a checkbox
const getQuickAccessItems = () => {
const notificationsEnabled = autoApprovalSettings.enableNotifications
const enabledActionsNames = Object.keys(autoApprovalSettings.actions).filter(
(key) => autoApprovalSettings.actions[key as keyof AutoApprovalSettings["actions"]],
)
const enabledActions = enabledActionsNames.map((action) => {
return ACTION_METADATA.flatMap((a) => [a, a.subAction]).find((a) => a?.id === action)
})
let minusFavorites = enabledActions.filter((action) => !favorites.includes(action?.id ?? "") && action?.shortName)
if (notificationsEnabled) {
minusFavorites.push(NOTIFICATIONS_SETTING)
}
return [
...favorites.map((favId) => renderFavoritedItem(favId)),
minusFavorites.length > 0 ? (
<span style={{ color: getAsVar(VSC_FOREGROUND_MUTED), paddingLeft: "10px", opacity: 0.6 }} key="separator">
</span>
) : null,
...minusFavorites.map((action, index) => (
<span
style={{
color: getAsVar(VSC_FOREGROUND_MUTED),
opacity: 0.6,
}}
key={action?.id}>
{action?.shortName}
{index < minusFavorites.length - 1 && ","}
</span>
)),
]
}
const isChecked = (action: ActionMetadata): boolean => {
if (action.id === "enableNotifications") {
return autoApprovalSettings.enableNotifications
@@ -357,20 +319,38 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => {
justifyContent: "space-between",
gap: "8px",
}}>
<div
style={{
display: "flex",
flexWrap: "nowrap",
alignItems: "center",
overflowX: "auto",
msOverflowStyle: "none",
scrollbarWidth: "none",
WebkitOverflowScrolling: "touch",
gap: "4px",
whiteSpace: "nowrap", // Prevent text wrapping
}}>
{getQuickAccessItems()}
</div>
{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>
)}
@@ -381,10 +361,7 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => {
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",
display: "flex",
flexDirection: "column",
gap: "4px",
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
<>
@@ -403,15 +380,18 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => {
placement="top">
<span style={{ color: getAsVar(VSC_FOREGROUND) }}>Auto-approve</span>
</HeroTooltip>
<span className="codicon codicon-chevron-down" style={{ paddingRight: "4px" }} />
<span className="codicon codicon-chevron-down" />
</div>
<div
ref={itemsContainerRef}
style={{
columnCount: containerWidth > breakpoint ? 2 : 1,
columnGap: "4px",
margin: "4px 0 8px 0",
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 */}
@@ -432,47 +412,40 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => {
{/* All items in a single list - CSS Grid will handle the column distribution */}
{ACTION_METADATA.map((action) => (
<AutoApproveMenuItem
key={action.id}
action={action}
isChecked={isChecked}
isFavorited={isFavorited}
onToggle={updateAction}
onToggleFavorite={toggleFavorite}
/>
<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: "8px 0",
margin: "10px 0",
opacity: 0.2,
}}
/>
<AutoApproveMenuItem
key={NOTIFICATIONS_SETTING.id}
action={NOTIFICATIONS_SETTING}
isChecked={isChecked}
isFavorited={isFavorited}
onToggle={updateAction}
onToggleFavorite={toggleFavorite}
/>
<HeroTooltip
content="Cline will automatically make this many API requests before asking for approval to proceed with the task."
placement="top">
<div
style={{
margin: "2px 10px 10px 5px",
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%", paddingRight: "35px" }}
style={{ flex: "1", width: "100%" }}
value={autoApprovalSettings.maxRequests.toString()}
onInput={(e) => {
const input = e.target as HTMLInputElement
@@ -497,13 +470,6 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => {
</HeroTooltip>
</>
)}
{isExpanded && (
<span
className="codicon codicon-chevron-up"
style={{ paddingBottom: "4px", marginLeft: "auto", marginTop: "-20px", cursor: "pointer" }}
onClick={() => setIsExpanded(false)}
/>
)}
</div>
</div>
)
@@ -67,7 +67,7 @@ const SubOptionAnimateIn = styled.div<{ show: boolean }>`
`
const ActionButtonContainer = styled.div`
padding: 2px;
margin: 4px;
`
const AutoApproveMenuItem = ({
@@ -78,18 +78,21 @@ const AutoApproveMenuItem = ({
onToggleFavorite,
condensed = false,
}: AutoApproveMenuItemProps) => {
const [isSubOptionOpen, setIsSubOptionOpen] = useState(isChecked(action))
const checked = isChecked(action)
const favorited = isFavorited?.(action)
const onChange = (e: Event) => {
e.stopPropagation()
onToggle(action, !checked)
const newChecked = !checked
setIsSubOptionOpen(newChecked)
onToggle(action, newChecked)
}
const content = (
<>
<div>
<ActionButtonContainer>
<HeroTooltip content={action.description} delay={500}>
<HeroTooltip content={action.description} delay={200}>
<CheckboxContainer isFavorited={favorited} onClick={onChange}>
<div className="left-content">
<VSCodeCheckbox checked={checked} />
@@ -97,33 +100,19 @@ const AutoApproveMenuItem = ({
<span className="label">{condensed ? action.shortName : action.label}</span>
</div>
{onToggleFavorite && !condensed && (
<HeroTooltip
delay={500}
content={
action.id === "enableAll"
? "Required"
: favorited
? "Remove from quick-access menu"
: "Add to quick-access menu"
}>
<span
className={`codicon codicon-${favorited ? "star-full" : "star-empty"} star`}
style={{
cursor: action.id === "enableAll" ? "not-allowed" : "pointer",
}}
onClick={(e) => {
e.stopPropagation()
if (action.id === "enableAll") return
onToggleFavorite?.(action.id)
}}
/>
</HeroTooltip>
<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={checked}>
<SubOptionAnimateIn show={isSubOptionOpen}>
<AutoApproveMenuItem
action={action.subAction}
isChecked={isChecked}
@@ -133,7 +122,7 @@ const AutoApproveMenuItem = ({
/>
</SubOptionAnimateIn>
)}
</>
</div>
)
return content
@@ -74,7 +74,7 @@ const RuleRow: React.FC<{
}`}>
<span className="flex-1 overflow-hidden break-all whitespace-normal flex items-center mr-1" title={rulePath}>
{getRuleTypeIcon() && <span className="mr-1.5">{getRuleTypeIcon()}</span>}
<span className="ph-no-capture">{displayName}</span>
{displayName}
</span>
{/* Toggle Switch */}
@@ -151,9 +151,7 @@ const CodeBlock = memo(({ source, forceWrap = false }: CodeBlockProps) => {
maxHeight: forceWrap ? "none" : "100%",
backgroundColor: CODE_BLOCK_BG_COLOR,
}}>
<StyledMarkdown className="ph-no-capture" forceWrap={forceWrap}>
{reactContent}
</StyledMarkdown>
<StyledMarkdown forceWrap={forceWrap}>{reactContent}</StyledMarkdown>
</div>
)
})
@@ -339,7 +339,7 @@ const MarkdownBlock = memo(({ markdown }: MarkdownBlockProps) => {
return (
<div>
<StyledMarkdown className="ph-no-capture">{reactContent}</StyledMarkdown>
<StyledMarkdown>{reactContent}</StyledMarkdown>
</div>
)
})
@@ -12,26 +12,6 @@ const BannerContainer = styled.div`
gap: 10px;
flex-shrink: 0;
margin-bottom: 6px;
position: relative;
`
const CloseButton = styled.button`
position: absolute;
top: 12px;
right: 12px;
background: none;
border: none;
color: var(--vscode-foreground);
cursor: pointer;
font-size: 16px;
display: flex;
align-items: center;
justify-content: center;
padding: 4px;
opacity: 0.7;
&:hover {
opacity: 1;
}
`
const ButtonContainer = styled.div`
@@ -45,31 +25,31 @@ const ButtonContainer = styled.div`
`
const TelemetryBanner = () => {
const handleOpenSettings = () => {
handleClose()
vscode.postMessage({ type: "openSettings" })
const [hasChosen, setHasChosen] = useState(false)
const handleAllow = () => {
setHasChosen(true)
vscode.postMessage({ type: "telemetrySetting", telemetrySetting: "enabled" satisfies TelemetrySetting })
}
const handleClose = () => {
vscode.postMessage({ type: "telemetrySetting", telemetrySetting: "enabled" satisfies TelemetrySetting })
const handleDeny = () => {
setHasChosen(true)
vscode.postMessage({ type: "telemetrySetting", telemetrySetting: "disabled" satisfies TelemetrySetting })
}
const handleOpenSettings = () => {
vscode.postMessage({ type: "openSettings" })
}
return (
<BannerContainer>
<CloseButton onClick={handleClose} aria-label="Close banner and enable telemetry">
</CloseButton>
<div>
<strong>Help Improve Cline</strong>
<i>
<br />
(and access experimental features)
</i>
<div style={{ marginTop: 4 }}>
Cline collects anonymous error and usage data to help us fix bugs and improve the extension. No code, prompts,
or personal information is ever sent.
Send anonymous error and usage data to help us fix bugs and improve the extension. No code, prompts, or
personal information is ever sent.
<div style={{ marginTop: 4 }}>
You can turn this setting off in{" "}
You can always change this in{" "}
<VSCodeLink href="#" onClick={handleOpenSettings}>
settings
</VSCodeLink>
@@ -77,6 +57,14 @@ const TelemetryBanner = () => {
</div>
</div>
</div>
<ButtonContainer>
<VSCodeButton appearance="primary" onClick={handleAllow} disabled={hasChosen}>
Allow
</VSCodeButton>
<VSCodeButton appearance="secondary" onClick={handleDeny} disabled={hasChosen}>
Deny
</VSCodeButton>
</ButtonContainer>
</BannerContainer>
)
}
@@ -105,8 +105,6 @@ const HistoryPreview = ({ showHistoryView }: HistoryPreviewProps) => {
)}
<div
id={`history-preview-task-${item.id}`}
className="history-preview-task"
style={{
fontSize: "var(--vscode-font-size)",
color: "var(--vscode-descriptionForeground)",
@@ -119,7 +117,7 @@ const HistoryPreview = ({ showHistoryView }: HistoryPreviewProps) => {
wordBreak: "break-word",
overflowWrap: "anywhere",
}}>
<span className="ph-no-capture">{item.task}</span>
{item.task}
</div>
<div
style={{
@@ -514,14 +514,11 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
whiteSpace: "pre-wrap",
wordBreak: "break-word",
overflowWrap: "anywhere",
}}>
<span
className="ph-no-capture"
dangerouslySetInnerHTML={{
__html: item.task,
}}
/>
</div>
}}
dangerouslySetInnerHTML={{
__html: item.task,
}}
/>
</div>
<div
style={{
+10 -123
View File
@@ -1531,14 +1531,6 @@ const ApiOptions = ({
{selectedProvider === "litellm" && (
<div>
<VSCodeTextField
value={apiConfiguration?.liteLlmBaseUrl || ""}
style={{ width: "100%" }}
type="url"
onInput={handleInputChange("liteLlmBaseUrl")}
placeholder={"Default: http://localhost:4000"}>
<span style={{ fontWeight: 500 }}>Base URL (optional)</span>
</VSCodeTextField>
<VSCodeTextField
value={apiConfiguration?.liteLlmApiKey || ""}
style={{ width: "100%" }}
@@ -1547,11 +1539,19 @@ const ApiOptions = ({
placeholder="Default: noop">
<span style={{ fontWeight: 500 }}>API Key</span>
</VSCodeTextField>
<VSCodeTextField
value={apiConfiguration?.liteLlmBaseUrl || ""}
style={{ width: "100%" }}
type="url"
onInput={handleInputChange("liteLlmBaseUrl")}
placeholder={"Default: http://localhost:4000"}>
<span style={{ fontWeight: 500 }}>Base URL (optional)</span>
</VSCodeTextField>
<VSCodeTextField
value={apiConfiguration?.liteLlmModelId || ""}
style={{ width: "100%" }}
onInput={handleInputChange("liteLlmModelId")}
placeholder={"e.g. anthropic/claude-3-7-sonnet-20250219"}>
placeholder={"e.g. gpt-4"}>
<span style={{ fontWeight: 500 }}>Model ID</span>
</VSCodeTextField>
@@ -1594,119 +1594,6 @@ const ApiOptions = ({
</p>
</>
<div
style={{
color: getAsVar(VSC_DESCRIPTION_FOREGROUND),
display: "flex",
margin: "10px 0",
cursor: "pointer",
alignItems: "center",
}}
onClick={() => setModelConfigurationSelected((val) => !val)}>
<span
className={`codicon ${modelConfigurationSelected ? "codicon-chevron-down" : "codicon-chevron-right"}`}
style={{
marginRight: "4px",
}}></span>
<span
style={{
fontWeight: 700,
textTransform: "uppercase",
}}>
Model Configuration
</span>
</div>
{modelConfigurationSelected && (
<>
<VSCodeCheckbox
checked={!!apiConfiguration?.liteLlmModelInfo?.supportsImages}
onChange={(e: any) => {
const isChecked = e.target.checked === true
const modelInfo = apiConfiguration?.liteLlmModelInfo
? apiConfiguration.liteLlmModelInfo
: { ...liteLlmModelInfoSaneDefaults }
modelInfo.supportsImages = isChecked
setApiConfiguration({
...apiConfiguration,
liteLlmModelInfo: modelInfo,
})
}}>
Supports Images
</VSCodeCheckbox>
<div style={{ display: "flex", gap: 10, marginTop: "5px" }}>
<VSCodeTextField
value={
apiConfiguration?.liteLlmModelInfo?.contextWindow
? apiConfiguration.liteLlmModelInfo.contextWindow.toString()
: liteLlmModelInfoSaneDefaults.contextWindow?.toString()
}
style={{ flex: 1 }}
onInput={(input: any) => {
const modelInfo = apiConfiguration?.liteLlmModelInfo
? apiConfiguration.liteLlmModelInfo
: { ...liteLlmModelInfoSaneDefaults }
modelInfo.contextWindow = Number(input.target.value)
setApiConfiguration({
...apiConfiguration,
liteLlmModelInfo: modelInfo,
})
}}>
<span style={{ fontWeight: 500 }}>Context Window Size</span>
</VSCodeTextField>
<VSCodeTextField
value={
apiConfiguration?.liteLlmModelInfo?.maxTokens
? apiConfiguration.liteLlmModelInfo.maxTokens.toString()
: liteLlmModelInfoSaneDefaults.maxTokens?.toString()
}
style={{ flex: 1 }}
onInput={(input: any) => {
const modelInfo = apiConfiguration?.liteLlmModelInfo
? apiConfiguration.liteLlmModelInfo
: { ...liteLlmModelInfoSaneDefaults }
modelInfo.maxTokens = input.target.value
setApiConfiguration({
...apiConfiguration,
liteLlmModelInfo: modelInfo,
})
}}>
<span style={{ fontWeight: 500 }}>Max Output Tokens</span>
</VSCodeTextField>
</div>
<div style={{ display: "flex", gap: 10, marginTop: "5px" }}>
<VSCodeTextField
value={
apiConfiguration?.liteLlmModelInfo?.temperature !== undefined
? apiConfiguration.liteLlmModelInfo.temperature.toString()
: liteLlmModelInfoSaneDefaults.temperature?.toString()
}
onInput={(input: any) => {
const modelInfo = apiConfiguration?.liteLlmModelInfo
? apiConfiguration.liteLlmModelInfo
: { ...liteLlmModelInfoSaneDefaults }
// Check if the input ends with a decimal point or has trailing zeros after decimal
const value = input.target.value
const shouldPreserveFormat =
value.endsWith(".") || (value.includes(".") && value.endsWith("0"))
modelInfo.temperature =
value === ""
? liteLlmModelInfoSaneDefaults.temperature
: shouldPreserveFormat
? value // Keep as string to preserve decimal format
: parseFloat(value)
setApiConfiguration({
...apiConfiguration,
liteLlmModelInfo: modelInfo,
})
}}>
<span style={{ fontWeight: 500 }}>Temperature</span>
</VSCodeTextField>
</div>
</>
)}
<p
style={{
fontSize: "12px",
@@ -2389,7 +2276,7 @@ export function normalizeApiConfiguration(apiConfiguration?: ApiConfiguration):
return {
selectedProvider: provider,
selectedModelId: apiConfiguration?.liteLlmModelId || "",
selectedModelInfo: apiConfiguration?.liteLlmModelInfo || liteLlmModelInfoSaneDefaults,
selectedModelInfo: liteLlmModelInfoSaneDefaults,
}
case "xai":
return getProviderData(xaiModels, xaiDefaultModelId)
@@ -37,22 +37,8 @@ const ConnectionStatusIndicator = ({
)
}
const CollapsibleContent = styled.div<{ isOpen: boolean }>`
overflow: hidden;
transition:
max-height 0.3s ease-in-out,
opacity 0.3s ease-in-out,
margin-top 0.3s ease-in-out,
visibility 0.3s ease-in-out;
max-height: ${({ isOpen }) => (isOpen ? "1000px" : "0")}; // Sufficiently large height
opacity: ${({ isOpen }) => (isOpen ? 1 : 0)};
margin-top: ${({ isOpen }) => (isOpen ? "15px" : "0")};
visibility: ${({ isOpen }) => (isOpen ? "visible" : "hidden")};
`
export const BrowserSettingsSection: React.FC = () => {
const { browserSettings } = useExtensionState()
const [localChromePath, setLocalChromePath] = useState(browserSettings.chromeExecutablePath || "")
const [isCheckingConnection, setIsCheckingConnection] = useState(false)
const [connectionStatus, setConnectionStatus] = useState<boolean | null>(null)
const [relaunchResult, setRelaunchResult] = useState<{ success: boolean; message: string } | null>(null)
@@ -105,14 +91,6 @@ export const BrowserSettingsSection: React.FC = () => {
})
}, [])
// Sync localChromePath with global state
useEffect(() => {
if (browserSettings.chromeExecutablePath !== localChromePath) {
setLocalChromePath(browserSettings.chromeExecutablePath || "")
}
// Removed sync for local disableToolUse state
}, [browserSettings.chromeExecutablePath, browserSettings.disableToolUse])
// Debounced connection check function
const debouncedCheckConnection = useCallback(
debounce(() => {
@@ -169,8 +147,6 @@ export const BrowserSettingsSection: React.FC = () => {
},
remoteBrowserEnabled: browserSettings.remoteBrowserEnabled,
remoteBrowserHost: browserSettings.remoteBrowserHost,
chromeExecutablePath: browserSettings.chromeExecutablePath,
disableToolUse: browserSettings.disableToolUse,
})
.then((response) => {
if (!response.value) {
@@ -193,8 +169,6 @@ export const BrowserSettingsSection: React.FC = () => {
remoteBrowserEnabled: enabled,
// If disabling, also clear the host
remoteBrowserHost: enabled ? browserSettings.remoteBrowserHost : undefined,
chromeExecutablePath: browserSettings.chromeExecutablePath,
disableToolUse: browserSettings.disableToolUse,
})
.then((response) => {
if (!response.value) {
@@ -215,55 +189,6 @@ export const BrowserSettingsSection: React.FC = () => {
},
remoteBrowserEnabled: browserSettings.remoteBrowserEnabled,
remoteBrowserHost: host,
chromeExecutablePath: browserSettings.chromeExecutablePath,
disableToolUse: browserSettings.disableToolUse,
})
.then((response) => {
if (!response.value) {
console.error("Failed to update browser settings")
}
})
.catch((error) => {
console.error("Error updating browser settings:", error)
})
}
const debouncedUpdateChromePath = useCallback(
debounce((newPath: string | undefined) => {
BrowserServiceClient.updateBrowserSettings({
metadata: {},
viewport: {
width: browserSettings.viewport.width,
height: browserSettings.viewport.height,
},
remoteBrowserEnabled: browserSettings.remoteBrowserEnabled,
remoteBrowserHost: browserSettings.remoteBrowserHost,
chromeExecutablePath: newPath,
disableToolUse: browserSettings.disableToolUse,
})
.then((response) => {
if (!response.value) {
console.error("Failed to update browser settings for chromeExecutablePath")
}
})
.catch((error) => {
console.error("Error updating browser settings for chromeExecutablePath:", error)
})
}, 500),
[browserSettings],
)
const updateChromeExecutablePath = (path: string | undefined) => {
BrowserServiceClient.updateBrowserSettings({
metadata: {},
viewport: {
width: browserSettings.viewport.width,
height: browserSettings.viewport.height,
},
remoteBrowserEnabled: browserSettings.remoteBrowserEnabled,
remoteBrowserHost: browserSettings.remoteBrowserHost,
chromeExecutablePath: path,
disableToolUse: browserSettings.disableToolUse,
})
.then((response) => {
if (!response.value) {
@@ -322,28 +247,6 @@ export const BrowserSettingsSection: React.FC = () => {
return () => clearInterval(pollInterval)
}, [browserSettings.remoteBrowserEnabled, checkConnectionOnce])
const updateDisableToolUse = (disabled: boolean) => {
BrowserServiceClient.updateBrowserSettings({
metadata: {},
viewport: {
width: browserSettings.viewport.width,
height: browserSettings.viewport.height,
},
remoteBrowserEnabled: browserSettings.remoteBrowserEnabled,
remoteBrowserHost: browserSettings.remoteBrowserHost,
chromeExecutablePath: browserSettings.chromeExecutablePath,
disableToolUse: disabled,
})
.then((response) => {
if (!response.value) {
console.error("Failed to update disableToolUse setting")
}
})
.catch((error) => {
console.error("Error updating disableToolUse setting:", error)
})
}
const relaunchChromeDebugMode = () => {
setDebugMode(true)
setRelaunchResult(null)
@@ -357,169 +260,121 @@ export const BrowserSettingsSection: React.FC = () => {
// Determine if we should show the relaunch button
const isRemoteEnabled = Boolean(browserSettings.remoteBrowserEnabled)
const shouldShowRelaunchButton = isRemoteEnabled && connectionStatus === false
const isSubSettingsOpen = !(browserSettings.disableToolUse || false)
return (
<div
id="browser-settings-section"
style={{ marginBottom: 20, borderTop: "1px solid var(--vscode-panel-border)", paddingTop: 15 }}>
<h3 style={{ color: "var(--vscode-foreground)", margin: "0 0 10px 0", fontSize: "14px" }}>Browser Settings</h3>
{/* Master Toggle */}
<div style={{ marginBottom: isSubSettingsOpen ? 0 : 10 }}>
<VSCodeCheckbox
checked={browserSettings.disableToolUse || false}
onChange={(e) => updateDisableToolUse((e.target as HTMLInputElement).checked)}>
Disable browser tool usage
</VSCodeCheckbox>
<div style={{ marginBottom: 15 }}>
<div style={{ marginBottom: 8 }}>
<label style={{ fontWeight: "500", display: "block", marginBottom: 5 }}>Viewport size</label>
<VSCodeDropdown
style={{ width: "100%" }}
value={
Object.entries(BROWSER_VIEWPORT_PRESETS).find(([_, size]) => {
const typedSize = size as { width: number; height: number }
return (
typedSize.width === browserSettings.viewport.width &&
typedSize.height === browserSettings.viewport.height
)
})?.[0]
}
onChange={(event) => handleViewportChange(event as Event)}>
{Object.entries(BROWSER_VIEWPORT_PRESETS).map(([name]) => (
<VSCodeOption key={name} value={name}>
{name}
</VSCodeOption>
))}
</VSCodeDropdown>
</div>
<p
style={{
fontSize: "12px",
color: "var(--vscode-descriptionForeground)",
margin: "4px 0 0 0px",
margin: 0,
}}>
Prevent Cline from using browser actions (e.g. launch, click, type).
Set the size of the browser viewport for screenshots and interactions.
</p>
</div>
<CollapsibleContent isOpen={isSubSettingsOpen}>
<div style={{ marginBottom: 15 }}>
<div style={{ marginBottom: 8 }}>
<label style={{ fontWeight: "500", display: "block", marginBottom: 5 }}>Viewport size</label>
<VSCodeDropdown
style={{ width: "100%" }}
value={
Object.entries(BROWSER_VIEWPORT_PRESETS).find(([_, size]) => {
const typedSize = size as { width: number; height: number }
return (
typedSize.width === browserSettings.viewport.width &&
typedSize.height === browserSettings.viewport.height
)
})?.[0]
}
onChange={(event) => handleViewportChange(event as Event)}>
{Object.entries(BROWSER_VIEWPORT_PRESETS).map(([name]) => (
<VSCodeOption key={name} value={name}>
{name}
</VSCodeOption>
))}
</VSCodeDropdown>
</div>
<p
style={{
fontSize: "12px",
color: "var(--vscode-descriptionForeground)",
margin: 0,
}}>
Set the size of the browser viewport for screenshots and interactions.
</p>
<div style={{ marginBottom: 0 }}>
<div style={{ marginBottom: 4, display: "flex", alignItems: "center", justifyContent: "space-between" }}>
<VSCodeCheckbox
checked={browserSettings.remoteBrowserEnabled}
onChange={(e) => updateRemoteBrowserEnabled((e.target as HTMLInputElement).checked)}>
Use remote browser connection
</VSCodeCheckbox>
<ConnectionStatusIndicator
isChecking={isCheckingConnection}
isConnected={connectionStatus}
remoteBrowserEnabled={browserSettings.remoteBrowserEnabled}
/>
</div>
<div style={{ marginBottom: 0 }}>
{" "}
{/* This div now contains Remote Connection & Chrome Path */}
<div style={{ marginBottom: 4, display: "flex", alignItems: "center", justifyContent: "space-between" }}>
<VSCodeCheckbox
checked={browserSettings.remoteBrowserEnabled}
onChange={(e) => updateRemoteBrowserEnabled((e.target as HTMLInputElement).checked)}>
Use remote browser connection
</VSCodeCheckbox>
<ConnectionStatusIndicator
isChecking={isCheckingConnection}
isConnected={connectionStatus}
remoteBrowserEnabled={browserSettings.remoteBrowserEnabled}
/>
</div>
<p
style={{
fontSize: "12px",
color: "var(--vscode-descriptionForeground)",
margin: "0 0 6px 0px",
}}>
Enable Cline to use your Chrome
{isBundled ? "(not detected on your machine)" : detectedChromePath ? ` (${detectedChromePath})` : ""}. You
can specify a custom path below. Using a remote browser connection requires starting Chrome in debug mode
{browserSettings.remoteBrowserEnabled ? (
<>
{" "}
manually (<code>--remote-debugging-port=9222</code>) or using the button below. Enter the host
address or leave it blank for automatic discovery.
</>
) : (
"."
)}
</p>
{/* Moved remote-specific settings to appear directly after enabling remote connection */}
{browserSettings.remoteBrowserEnabled && (
<div style={{ marginLeft: 0, marginTop: 8 }}>
<VSCodeTextField
value={browserSettings.remoteBrowserHost || ""}
placeholder="http://localhost:9222"
style={{ width: "100%", marginBottom: 8 }}
onChange={(e: any) => updateRemoteBrowserHost(e.target.value || undefined)}
/>
{shouldShowRelaunchButton && (
<div style={{ display: "flex", gap: "10px", marginBottom: 8, justifyContent: "center" }}>
<VSCodeButton style={{ flex: 1 }} disabled={debugMode} onClick={relaunchChromeDebugMode}>
{debugMode ? "Relaunching Browser..." : "Relaunch Browser with Debug Mode"}
</VSCodeButton>
</div>
)}
{relaunchResult && (
<div
style={{
padding: "8px",
marginBottom: "8px",
backgroundColor: relaunchResult.success ? "rgba(0, 128, 0, 0.1)" : "rgba(255, 0, 0, 0.1)",
color: relaunchResult.success
? "var(--vscode-terminal-ansiGreen)"
: "var(--vscode-terminal-ansiRed)",
borderRadius: "3px",
fontSize: "11px",
whiteSpace: "pre-wrap",
wordBreak: "break-word",
}}>
{relaunchResult.message}
</div>
)}
<p
style={{
fontSize: "12px",
color: "var(--vscode-descriptionForeground)",
margin: 0,
}}></p>
</div>
<p
style={{
fontSize: "12px",
color: "var(--vscode-descriptionForeground)",
margin: "0 0 6px 0px",
}}>
Enable Cline to use your Chrome
{isBundled ? "(not detected on your machine)" : detectedChromePath ? ` (${detectedChromePath})` : ""}. This
requires starting Chrome in debug mode
{browserSettings.remoteBrowserEnabled ? (
<>
{" "}
manually (<code>--remote-debugging-port=9222</code>) or using the button below. Enter the host address
or leave it blank for automatic discovery.
</>
) : (
"."
)}
{/* Chrome Executable Path section now follows remote-specific settings */}
<div style={{ marginBottom: 8, marginTop: 8 }}>
<label htmlFor="chrome-executable-path" style={{ fontWeight: "500", display: "block", marginBottom: 5 }}>
Chrome Executable Path (Optional)
</label>
</p>
{browserSettings.remoteBrowserEnabled && (
<div style={{ marginLeft: 0 }}>
<VSCodeTextField
id="chrome-executable-path"
value={localChromePath}
placeholder="e.g., /usr/bin/google-chrome or C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe"
style={{ width: "100%" }}
onChange={(e: any) => {
const newValue = e.target.value || ""
setLocalChromePath(newValue)
debouncedUpdateChromePath(newValue) // Send "" if empty, not undefined
}}
value={browserSettings.remoteBrowserHost || ""}
placeholder="http://localhost:9222"
style={{ width: "100%", marginBottom: 8 }}
onChange={(e: any) => updateRemoteBrowserHost(e.target.value || undefined)}
/>
{shouldShowRelaunchButton && (
<div style={{ display: "flex", gap: "10px", marginBottom: 8, justifyContent: "center" }}>
<VSCodeButton style={{ flex: 1 }} disabled={debugMode} onClick={relaunchChromeDebugMode}>
{debugMode ? "Relaunching Browser..." : "Relaunch Browser with Debug Mode"}
</VSCodeButton>
</div>
)}
{relaunchResult && (
<div
style={{
padding: "8px",
marginBottom: "8px",
backgroundColor: relaunchResult.success ? "rgba(0, 128, 0, 0.1)" : "rgba(255, 0, 0, 0.1)",
color: relaunchResult.success
? "var(--vscode-terminal-ansiGreen)"
: "var(--vscode-terminal-ansiRed)",
borderRadius: "3px",
fontSize: "11px",
whiteSpace: "pre-wrap",
wordBreak: "break-word",
}}>
{relaunchResult.message}
</div>
)}
<p
style={{
fontSize: "12px",
color: "var(--vscode-descriptionForeground)",
margin: "4px 0 0 0",
}}>
Leave blank to auto-detect.
</p>
margin: 0,
}}></p>
</div>
</div>
</CollapsibleContent>
)}
</div>
</div>
)
}
@@ -6,7 +6,7 @@ import { useMount } from "react-use"
import styled from "styled-components"
import { requestyDefaultModelId } from "../../../../src/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"
import { CODE_BLOCK_BG_COLOR } from "../common/CodeBlock"
@@ -43,9 +43,7 @@ const RequestyModelPicker: React.FC<RequestyModelPickerProps> = ({ isPopup }) =>
}, [apiConfiguration])
useMount(() => {
ModelsServiceClient.refreshRequestyModels({}).catch((err) => {
console.error("Failed to refresh Requesty models:", err)
})
vscode.postMessage({ type: "refreshRequestyModels" })
})
useEffect(() => {
-3
View File
@@ -11,7 +11,6 @@ import { StateServiceDefinition } from "@shared/proto/state"
import { TaskServiceDefinition } from "@shared/proto/task"
import { WebServiceDefinition } from "@shared/proto/web"
import { ModelsServiceDefinition } from "@shared/proto/models"
import { SlashServiceDefinition } from "@shared/proto/slash"
const AccountServiceClient = createGrpcClient(AccountServiceDefinition)
const BrowserServiceClient = createGrpcClient(BrowserServiceDefinition)
@@ -22,7 +21,6 @@ const StateServiceClient = createGrpcClient(StateServiceDefinition)
const TaskServiceClient = createGrpcClient(TaskServiceDefinition)
const WebServiceClient = createGrpcClient(WebServiceDefinition)
const ModelsServiceClient = createGrpcClient(ModelsServiceDefinition)
const SlashServiceClient = createGrpcClient(SlashServiceDefinition)
export {
AccountServiceClient,
@@ -34,5 +32,4 @@ export {
TaskServiceClient,
WebServiceClient,
ModelsServiceClient,
SlashServiceClient,
}