Compare commits

..

3 Commits

Author SHA1 Message Date
Elephant Lumps 611bb53f10 merge conflicts 2025-06-05 10:46:35 -07:00
Elephant Lumps 6787bb51ee merge conflicts 2025-06-03 23:48:38 -07:00
Elephant Lumps d6c640d5b7 migrate theme message protobus 2025-06-03 13:50:09 -07:00
80 changed files with 1046 additions and 2187 deletions
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Migrated updateSettings to protos, removed didUpdateSettings, altered Plan/Act toggling in settings menu
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Fixed issue where telemetry warning popup was created for every new Cline window
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": minor
---
Prioritize active files in file context menu
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
toggleWorkflow protobus migration
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
Migrate settingsButtonClicked to protobus
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Added promise to task init to prevent race condition with checkpoints
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Spring cleaning
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Fix bug where replace_in_file would not be able to handle for out-of-order SEARCH/REPLACE blocks
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": minor
---
Context menu is default to File option on start up
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": minor
---
Migrate fetchLatestServersFromHub to protobus
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": minor
---
the response of the mcps is displayed with a collapsible which allows to focus on the model responses.
+1 -2
View File
@@ -52,8 +52,7 @@
"env": {
"GRPC_TRACE": "all",
"GRPC_VERBOSITY": "DEBUG",
"NODE_PATH": "${workspaceFolder}/dist-standalone/node_modules",
"CLINE_DIR": "${userHome}/.cline-standalone"
"NODE_PATH": "${workspaceFolder}/dist-standalone/node_modules"
},
"program": "standalone.js"
}
-4
View File
@@ -1,9 +1,5 @@
# Changelog
## [3.17.11]
- Add support for Gemini 2.5 Pro Preview 06-05 model to Vertex AI and Google Gemini providers
## [3.17.10]
- Add support for Qwen 3 series models with thinking mode options (Thanks @Jonny-china!)
+33
View File
@@ -0,0 +1,33 @@
// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
// Generated by proto/build-proto.js
import { StreamingResponseHandler } from "./host-grpc-handler"
import { handleUriServiceRequest, handleUriServiceStreamingRequest } from "./uri/index"
import { handleWatchServiceRequest, handleWatchServiceStreamingRequest } from "./watch/index"
/**
* Configuration for a host service handler
*/
export interface HostServiceHandlerConfig {
requestHandler: (method: string, message: any) => Promise<any>
streamingHandler: (
method: string,
message: any,
responseStream: StreamingResponseHandler,
requestId?: string,
) => Promise<void>
}
/**
* Map of host service names to their handler configurations
*/
export const hostServiceHandlers: Record<string, HostServiceHandlerConfig> = {
"host.UriService": {
requestHandler: handleUriServiceRequest,
streamingHandler: handleUriServiceStreamingRequest,
},
"host.WatchService": {
requestHandler: handleWatchServiceRequest,
streamingHandler: handleWatchServiceStreamingRequest,
},
}
+22
View File
@@ -0,0 +1,22 @@
// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
// Generated by proto/build-proto.js
import { createServiceRegistry, ServiceMethodHandler, StreamingMethodHandler } from "../host-grpc-service"
import { StreamingResponseHandler } from "../host-grpc-handler"
import { registerAllMethods } from "./methods"
// Create uri service registry
const uriService = createServiceRegistry("uri")
// Export the method handler types and registration function
export type UriMethodHandler = ServiceMethodHandler
export type UriStreamingMethodHandler = StreamingMethodHandler
export const registerMethod = uriService.registerMethod
// Export the request handlers
export const handleUriServiceRequest = uriService.handleRequest
export const handleUriServiceStreamingRequest = uriService.handleStreamingRequest
export const isStreamingMethod = uriService.isStreamingMethod
// Register all uri methods
registerAllMethods()
+16
View File
@@ -0,0 +1,16 @@
// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
// Generated by proto/build-proto.js
// Import all method implementations
import { registerMethod } from "./index"
import { file } from "./file"
import { joinPath } from "./joinPath"
import { parse } from "./parse"
// Register all uri service methods
export function registerAllMethods(): void {
// Register each method with the registry
registerMethod("file", file)
registerMethod("joinPath", joinPath)
registerMethod("parse", parse)
}
+22
View File
@@ -0,0 +1,22 @@
// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
// Generated by proto/build-proto.js
import { createServiceRegistry, ServiceMethodHandler, StreamingMethodHandler } from "../host-grpc-service"
import { StreamingResponseHandler } from "../host-grpc-handler"
import { registerAllMethods } from "./methods"
// Create watch service registry
const watchService = createServiceRegistry("watch")
// Export the method handler types and registration function
export type WatchMethodHandler = ServiceMethodHandler
export type WatchStreamingMethodHandler = StreamingMethodHandler
export const registerMethod = watchService.registerMethod
// Export the request handlers
export const handleWatchServiceRequest = watchService.handleRequest
export const handleWatchServiceStreamingRequest = watchService.handleStreamingRequest
export const isStreamingMethod = watchService.isStreamingMethod
// Register all watch methods
registerAllMethods()
+15
View File
@@ -0,0 +1,15 @@
// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
// Generated by proto/build-proto.js
// Import all method implementations
import { registerMethod } from "./index"
import { subscribeToFile } from "./subscribeToFile"
// Streaming methods for this service
export const streamingMethods = ["subscribeToFile"]
// Register all watch service methods
export function registerAllMethods(): void {
// Register each method with the registry
registerMethod("subscribeToFile", subscribeToFile, { isStreaming: true })
}
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "claude-dev",
"version": "3.17.11",
"version": "3.17.10",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "claude-dev",
"version": "3.17.11",
"version": "3.17.10",
"license": "Apache-2.0",
"dependencies": {
"@anthropic-ai/bedrock-sdk": "^0.12.4",
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "claude-dev",
"displayName": "Cline",
"description": "Autonomous coding agent right in your IDE, capable of creating/editing files, running commands, using the browser, and more with your permission every step of the way.",
"version": "3.17.11",
"version": "3.17.10",
"icon": "assets/icons/icon.png",
"engines": {
"vscode": "^1.84.0"
-5
View File
@@ -63,8 +63,3 @@ message StringArrays {
repeated string values1 = 1;
repeated string values2 = 2;
}
message KeyValuePair {
string key = 1;
string value = 2;
}
-14
View File
@@ -55,12 +55,6 @@ service FileService {
// Opens a task's conversation history file on disk
rpc openTaskHistory(StringRequest) returns (Empty);
// Toggles a workflow on or off
rpc toggleWorkflow(ToggleWorkflowRequest) returns (ClineRulesToggles);
// Subscribe to workspace file updates
rpc subscribeToWorkspaceUpdates(EmptyRequest) returns (stream StringArray);
}
// Response for refreshRules operation
@@ -167,11 +161,3 @@ message ToggleCursorRuleRequest {
string rule_path = 2; // Path to the rule file
bool enabled = 3; // Whether to enable or disable the rule
}
// Request to toggle a workflow on or off
message ToggleWorkflowRequest {
Metadata metadata = 1;
string workflow_path = 2;
bool enabled = 3;
bool is_global = 4;
}
-1
View File
@@ -19,7 +19,6 @@ service McpService {
// Subscribe to MCP marketplace catalog updates
rpc subscribeToMcpMarketplaceCatalog(EmptyRequest) returns (stream McpMarketplaceCatalog);
rpc getLatestMcpServers(Empty) returns (McpServers);
}
message ToggleMcpServerRequest {
+8 -35
View File
@@ -20,8 +20,6 @@ service ModelsService {
rpc refreshOpenAiModels(OpenAiModelsRequest) returns (StringArray);
// Refreshes and returns Requesty models
rpc refreshRequestyModels(EmptyRequest) returns (OpenRouterCompatibleModelInfo);
// Subscribe to OpenRouter models updates
rpc subscribeToOpenRouterModels(EmptyRequest) returns (stream OpenRouterCompatibleModelInfo);
}
// List of VS Code LM models
@@ -37,42 +35,17 @@ message VsCodeLmModel {
string id = 4;
}
// Price tier for tiered pricing models
message PriceTier {
int32 token_limit = 1; // Upper limit (inclusive) of input tokens for this price
double price = 2; // Price per million tokens for this tier
}
// Thinking configuration for models that support thinking/reasoning
message ThinkingConfig {
optional int32 max_budget = 1; // Max allowed thinking budget tokens
optional double output_price = 2; // Output price per million tokens when budget > 0
repeated PriceTier output_price_tiers = 3; // Optional: Tiered output price when budget > 0
}
// Model tier for tiered pricing structures
message ModelTier {
int32 context_window = 1;
optional double input_price = 2;
optional double output_price = 3;
optional double cache_writes_price = 4;
optional double cache_reads_price = 5;
}
// For OpenRouterCompatibleModelInfo structure in OpenRouterModels
message OpenRouterModelInfo {
optional int32 max_tokens = 1;
optional int32 context_window = 2;
optional bool supports_images = 3;
int32 max_tokens = 1;
int32 context_window = 2;
bool supports_images = 3;
bool supports_prompt_cache = 4;
optional double input_price = 5;
optional double output_price = 6;
optional double cache_writes_price = 7;
optional double cache_reads_price = 8;
optional string description = 9;
optional ThinkingConfig thinking_config = 10;
optional bool supports_global_endpoint = 11;
repeated ModelTier tiers = 12;
double input_price = 5;
double output_price = 6;
double cache_writes_price = 7;
double cache_reads_price = 8;
string description = 9;
}
// Shared response message for model information
-124
View File
@@ -13,7 +13,6 @@ service StateService {
rpc togglePlanActMode(TogglePlanActModeRequest) returns (Empty);
rpc updateTerminalConnectionTimeout(Int64Request) returns (Int64);
rpc updateAutoApprovalSettings(AutoApprovalSettingsRequest) returns (Empty);
rpc updateSettings(UpdateSettingsRequest) returns (Empty);
}
message State {
@@ -65,126 +64,3 @@ message AutoApprovalSettingsRequest {
bool enable_notifications = 6;
repeated string favorites = 7;
}
// Message for updating settings
message UpdateSettingsRequest {
Metadata metadata = 1;
optional ApiConfiguration api_configuration = 2;
optional string custom_instructions_setting = 3;
optional string telemetry_setting = 4;
optional bool plan_act_separate_models_setting = 5;
optional bool enable_checkpoints_setting = 6;
optional bool mcp_marketplace_enabled = 7;
optional ChatSettings chat_settings = 8;
optional int64 shell_integration_timeout = 9;
optional bool terminal_reuse_enabled = 10;
optional bool mcp_responses_collapsed = 11;
}
// Complete API Configuration message
message ApiConfiguration {
// Core API fields
optional string api_provider = 1;
optional string api_model_id = 2;
optional string api_key = 3; // anthropic
optional string api_base_url = 4;
// Provider-specific API keys
optional string cline_api_key = 5;
optional string openrouter_api_key = 6;
optional string anthropic_base_url = 7;
optional string openai_api_key = 8;
optional string openai_native_api_key = 9;
optional string gemini_api_key = 10;
optional string deepseek_api_key = 11;
optional string requesty_api_key = 12;
optional string together_api_key = 13;
optional string fireworks_api_key = 14;
optional string qwen_api_key = 15;
optional string doubao_api_key = 16;
optional string mistral_api_key = 17;
optional string nebius_api_key = 18;
optional string asksage_api_key = 19;
optional string xai_api_key = 20;
optional string sambanova_api_key = 21;
optional string cerebras_api_key = 22;
// Model IDs
optional string openrouter_model_id = 23;
optional string openai_model_id = 24;
optional string anthropic_model_id = 25;
optional string bedrock_model_id = 26;
optional string vertex_model_id = 27;
optional string gemini_model_id = 28;
optional string ollama_model_id = 29;
optional string lm_studio_model_id = 30;
optional string litellm_model_id = 31;
optional string requesty_model_id = 32;
optional string together_model_id = 33;
optional string fireworks_model_id = 34;
// AWS Bedrock fields
optional bool aws_bedrock_custom_selected = 35;
optional string aws_bedrock_custom_model_base_id = 36;
optional string aws_access_key = 37;
optional string aws_secret_key = 38;
optional string aws_session_token = 39;
optional string aws_region = 40;
optional bool aws_use_cross_region_inference = 41;
optional bool aws_bedrock_use_prompt_cache = 42;
optional bool aws_use_profile = 43;
optional string aws_profile = 44;
optional string aws_bedrock_endpoint = 45;
// Vertex AI fields
optional string vertex_project_id = 46;
optional string vertex_region = 47;
// Base URLs and endpoints
optional string openai_base_url = 48;
optional string ollama_base_url = 49;
optional string lm_studio_base_url = 50;
optional string gemini_base_url = 51;
optional string litellm_base_url = 52;
optional string asksage_api_url = 53;
// LiteLLM specific fields
optional string litellm_api_key = 54;
optional bool litellm_use_prompt_cache = 55;
// Model configuration
optional int64 thinking_budget_tokens = 56;
optional string reasoning_effort = 57;
optional int64 request_timeout_ms = 58;
// Fireworks specific
optional int64 fireworks_model_max_completion_tokens = 59;
optional int64 fireworks_model_max_tokens = 60;
// Azure specific
optional string azure_api_version = 61;
// Ollama specific
optional string ollama_api_options_ctx_num = 62;
// Qwen specific
optional string qwen_api_line = 63;
// OpenRouter specific
optional string openrouter_provider_sorting = 64;
// VSCode LM (stored as JSON string due to complex type)
optional string vscode_lm_model_selector = 65;
// Model info objects (stored as JSON strings)
optional string openrouter_model_info = 66;
optional string openai_model_info = 67;
optional string requesty_model_info = 68;
optional string litellm_model_info = 69;
// OpenAI headers (stored as JSON string)
optional string openai_headers = 70;
// Favorited model IDs
repeated string favorited_model_ids = 71;
}
-9
View File
@@ -33,8 +33,6 @@ service TaskService {
rpc taskFeedback(StringRequest) returns (Empty);
// Shows task completion changes diff in a view
rpc taskCompletionViewChanges(Int64Request) returns (Empty);
// Executes a quick win task with command and title
rpc executeQuickWin(ExecuteQuickWinRequest) returns (Empty);
}
// Request message for creating a new task
@@ -109,10 +107,3 @@ message AskResponseRequest {
repeated string images = 4;
repeated string files = 5;
}
// Request for executing a quick win task
message ExecuteQuickWinRequest {
Metadata metadata = 1;
string command = 2;
string title = 3;
}
+1 -7
View File
@@ -221,7 +221,7 @@ message ClineMessage {
// UiService provides methods for managing UI interactions
service UiService {
// Scrolls to a specific settings section in the settings view
rpc scrollToSettings(StringRequest) returns (KeyValuePair);
rpc scrollToSettings(StringRequest) returns (Empty);
// Marks the current announcement as shown and returns whether an announcement should still be shown
rpc onDidShowAnnouncement(EmptyRequest) returns (Boolean);
@@ -249,10 +249,4 @@ service UiService {
// Subscribe to theme change events
rpc subscribeToTheme(EmptyRequest) returns (stream String);
// Initialize webview when it launches
rpc initializeWebview(EmptyRequest) returns (Empty);
// Subscribe to relinquish control events
rpc subscribeToRelinquishControl(EmptyRequest) returns (stream Empty);
}
+1 -6
View File
@@ -27,11 +27,7 @@ export class ClineHandler implements ApiHandler {
}
@withRetry()
async *createMessage(
systemPrompt: string,
messages: Anthropic.Messages.MessageParam[],
systemPromptCacheOnly: boolean = false,
): ApiStream {
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
this.lastGenerationId = undefined
const stream = await createOpenRouterStream(
@@ -39,7 +35,6 @@ export class ClineHandler implements ApiHandler {
systemPrompt,
messages,
this.getModel(),
systemPromptCacheOnly,
this.options.reasoningEffort,
this.options.thinkingBudgetTokens,
this.options.openRouterProviderSorting,
+1 -6
View File
@@ -27,11 +27,7 @@ export class OpenRouterHandler implements ApiHandler {
}
@withRetry()
async *createMessage(
systemPrompt: string,
messages: Anthropic.Messages.MessageParam[],
systemPromptCacheOnly: boolean = false,
): ApiStream {
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
this.lastGenerationId = undefined
const stream = await createOpenRouterStream(
@@ -39,7 +35,6 @@ export class OpenRouterHandler implements ApiHandler {
systemPrompt,
messages,
this.getModel(),
systemPromptCacheOnly,
this.options.reasoningEffort,
this.options.thinkingBudgetTokens,
this.options.openRouterProviderSorting,
+16 -19
View File
@@ -9,7 +9,6 @@ export async function createOpenRouterStream(
systemPrompt: string,
messages: Anthropic.Messages.MessageParam[],
model: { id: string; info: ModelInfo },
systemPromptCacheOnly: boolean,
reasoningEffort?: string,
thinkingBudgetTokens?: number,
openRouterProviderSorting?: string,
@@ -56,25 +55,23 @@ export async function createOpenRouterStream(
}
// Add cache_control to the last two user messages
// (note: this works because we only ever add one user message at a time, but if we added multiple we'd need to mark the user message before the last assistant message)
if (!systemPromptCacheOnly) {
const lastTwoUserMessages = openAiMessages.filter((msg) => msg.role === "user").slice(-2)
lastTwoUserMessages.forEach((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()
const lastTwoUserMessages = openAiMessages.filter((msg) => msg.role === "user").slice(-2)
lastTwoUserMessages.forEach((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" }
if (!lastTextPart) {
lastTextPart = { type: "text", text: "..." }
msg.content.push(lastTextPart)
}
})
}
// @ts-ignore-next-line
lastTextPart["cache_control"] = { type: "ephemeral" }
}
})
break
default:
break
@@ -136,7 +133,7 @@ export async function createOpenRouterStream(
}
// Removes messages in the middle when close to context window limit. Should not be applied to models that support prompt caching since it would continuously break the cache.
let shouldApplyMiddleOutTransform = !model.info.supportsPromptCache || systemPromptCacheOnly
let shouldApplyMiddleOutTransform = !model.info.supportsPromptCache
// except for deepseek (which we set supportsPromptCache to true for), where because the context window is so small our truncation algo might miss and we should use openrouter's middle-out transform as a fallback to ensure we don't exceed the context window (FIXME: once we have a more robust token estimator we should not rely on this)
if (model.id === "deepseek/deepseek-chat") {
shouldApplyMiddleOutTransform = true
+3 -135
View File
@@ -1,9 +1,9 @@
import { constructNewFileContent as cnfc } from "./diff"
import { constructNewFileContent as cnfc2 } from "./diff"
import { describe, it } from "mocha"
import { expect } from "chai"
async function cnfc2(diffContent: string, originalContent: string, isFinal: boolean): Promise<string> {
return cnfc(diffContent, originalContent, isFinal, "v2")
async function cnfc(diffContent: string, originalContent: string, isFinal: boolean): Promise<string> {
return cnfc2(diffContent, originalContent, isFinal, "v1")
}
describe("constructNewFileContent", () => {
@@ -175,136 +175,4 @@ replaced
expect(err).to.be.an("error")
}
})
it("should handle missing final REPLACE marker when isFinal is true", async () => {
const original = "line1\nline2\nline3"
const diff = `------- SEARCH
line2
=======
replaced`
// Note: missing +++++++ REPLACE marker
const result1 = await cnfc(diff, original, true) // isFinal = true
// Should still work and replace line2 with "replaced"
const expected = "line1\nreplaced\nline3"
expect(result1).to.equal(expected)
})
it("should handle missing final REPLACE marker with multiple lines of replacement", async () => {
const original = "function test() {\n\tconst a = 1;\n\treturn a;\n}"
const diff = `------- SEARCH
const a = 1;
return a;
=======
const a = 42;
console.log('updated');
return a;`
// Note: missing +++++++ REPLACE marker
const result1 = await cnfc(diff, original, true) // isFinal = true
const expected = "function test() {\n\tconst a = 42;\n\tconsole.log('updated');\n\treturn a;\n}"
expect(result1).to.equal(expected)
})
// it("should NOT process incomplete replacement when isFinal is false", async () => {
// const original = "line1\nline2\nline3"
// const diff = `------- SEARCH
// line2
// =======
// replaced`
// // Note: missing +++++++ REPLACE marker AND isFinal = false
// const result1 = await cnfc(diff, original, false) // isFinal = false
// // Should not make any changes since the block is incomplete
// const expected = "line1\nline2\nline3"
// expect(result1).to.equal(expected)
// })
})
// Test cases for out-of-order search/replace blocks
describe("Diff Format Out of Order Cases", () => {
it("should handle out-of-order replacements with different positions", async () => {
const isFinal = true
const original = "first\nsecond\nthird\nfourth\n"
const diff = `------- SEARCH
fourth
=======
new fourth
+++++++ REPLACE
------- SEARCH
second
=======
new second
+++++++ REPLACE`
const result1 = await cnfc(diff, original, isFinal)
const expectedResult = "first\nnew second\nthird\nnew fourth\n"
expect(result1).to.equal(expectedResult)
})
it("should handle multiple out-of-order replacements", async () => {
const isFinal = true
const original = "one\ntwo\nthree\nfour\nfive\n"
const diff = `------- SEARCH
four
=======
fourth
+++++++ REPLACE
------- SEARCH
two
=======
second
+++++++ REPLACE
------- SEARCH
five
=======
fifth
+++++++ REPLACE`
const result1 = await cnfc(diff, original, isFinal)
const expectedResult = "one\nsecond\nthree\nfourth\nfifth\n"
expect(result1).to.equal(expectedResult)
})
it("should handle out-of-order replacements with indentation", async () => {
const isFinal = true
const original = "function test() {\n\tconst a = 1;\n\tconst b = 2;\n\tconst c = 3;\n\n}"
const diff = `------- SEARCH
const c = 3;
=======
const c = 30;
+++++++ REPLACE
------- SEARCH
const a = 1;
=======
const a = 10;
+++++++ REPLACE`
const result1 = await cnfc(diff, original, isFinal)
const expectedResult = "function test() {\n\tconst a = 10;\n\tconst b = 2;\n\tconst c = 30;\n\n}"
expect(result1).to.equal(expectedResult)
})
it("should handle out-of-order replacements with empty lines", async () => {
const isFinal = true
const original = "header\n\nbody\n\nfooter\n"
const diff = `------- SEARCH
footer
=======
new footer
+++++++ REPLACE
------- SEARCH
body
=======
new body content
+++++++ REPLACE`
const result1 = await cnfc(diff, original, isFinal)
const expectedResult = "header\nnew body content\nnew footer\n"
expect(result1).to.equal(expectedResult)
})
})
+28 -111
View File
@@ -5,24 +5,6 @@ const REPLACE_BLOCK_END = "+++++++ REPLACE"
const SEARCH_BLOCK_CHAR = "-"
const REPLACE_BLOCK_CHAR = "+"
// Replace the exact string constants with flexible regex patterns
const SEARCH_BLOCK_START_REGEX = /^[-]{3,} SEARCH$/
const SEARCH_BLOCK_END_REGEX = /^[=]{3,}$/
const REPLACE_BLOCK_END_REGEX = /^[+]{3,} REPLACE$/
// Helper functions to check if a line matches the flexible patterns
function isSearchBlockStart(line: string): boolean {
return SEARCH_BLOCK_START_REGEX.test(line)
}
function isSearchBlockEnd(line: string): boolean {
return SEARCH_BLOCK_END_REGEX.test(line)
}
function isReplaceBlockEnd(line: string): boolean {
return REPLACE_BLOCK_END_REGEX.test(line)
}
/**
* Attempts a line-trimmed fallback match for the given search content in the original content.
* It tries to match `searchContent` lines against a block of lines in `originalContent` starting
@@ -229,7 +211,7 @@ export async function constructNewFileContent(
diffContent: string,
originalContent: string,
isFinal: boolean,
version: "v1" | "v2" = "v1",
version: "v1" | "v2" = "v2",
): Promise<string> {
const constructor = constructNewFileContentVersionMapping[version]
if (!constructor) {
@@ -246,6 +228,9 @@ const constructNewFileContentVersionMapping: Record<
v2: constructNewFileContentV2,
} as const
/**
* @deprecated
*/
async function constructNewFileContentV1(diffContent: string, originalContent: string, isFinal: boolean): Promise<string> {
let result = ""
let lastProcessedIndex = 0
@@ -258,10 +243,6 @@ async function constructNewFileContentV1(diffContent: string, originalContent: s
let searchMatchIndex = -1
let searchEndIndex = -1
// Track all replacements to handle out-of-order edits
let replacements: Array<{ start: number; end: number; content: string }> = []
let pendingOutOfOrderReplacement = false
let lines = diffContent.split("\n")
// If the last line looks like a partial marker but isn't recognized,
@@ -270,22 +251,22 @@ async function constructNewFileContentV1(diffContent: string, originalContent: s
if (
lines.length > 0 &&
(lastLine.startsWith(SEARCH_BLOCK_CHAR) || lastLine.startsWith("=") || lastLine.startsWith(REPLACE_BLOCK_CHAR)) &&
!isSearchBlockStart(lastLine) &&
!isSearchBlockEnd(lastLine) &&
!isReplaceBlockEnd(lastLine)
lastLine !== SEARCH_BLOCK_START &&
lastLine !== SEARCH_BLOCK_END &&
lastLine !== REPLACE_BLOCK_END
) {
lines.pop()
}
for (const line of lines) {
if (isSearchBlockStart(line)) {
if (line === SEARCH_BLOCK_START) {
inSearch = true
currentSearchContent = ""
currentReplaceContent = ""
continue
}
if (isSearchBlockEnd(line)) {
if (line === SEARCH_BLOCK_END) {
inSearch = false
inReplace = true
@@ -333,51 +314,31 @@ async function constructNewFileContentV1(diffContent: string, originalContent: s
if (blockMatch) {
;[searchMatchIndex, searchEndIndex] = blockMatch
} else {
// Last resort: search the entire file from the beginning
const fullFileIndex = originalContent.indexOf(currentSearchContent, 0)
if (fullFileIndex !== -1) {
// Found in the file - could be out of order
searchMatchIndex = fullFileIndex
searchEndIndex = fullFileIndex + currentSearchContent.length
if (searchMatchIndex < lastProcessedIndex) {
pendingOutOfOrderReplacement = true
}
} else {
throw new Error(
`The SEARCH block:\n${currentSearchContent.trimEnd()}\n...does not match anything in the file.`,
)
}
throw new Error(
`The SEARCH block:\n${currentSearchContent.trimEnd()}\n...does not match anything in the file or was searched out of order in the provided blocks.`,
)
}
}
}
}
// Check if this is an out-of-order replacement
if (searchMatchIndex < lastProcessedIndex) {
pendingOutOfOrderReplacement = true
}
// For in-order replacements, output everything up to the match location
if (!pendingOutOfOrderReplacement) {
result += originalContent.slice(lastProcessedIndex, searchMatchIndex)
}
// Output everything up to the match location
result += originalContent.slice(lastProcessedIndex, searchMatchIndex)
continue
}
if (isReplaceBlockEnd(line)) {
if (line === REPLACE_BLOCK_END) {
// Finished one replace block
// Store this replacement
replacements.push({
start: searchMatchIndex,
end: searchEndIndex,
content: currentReplaceContent,
})
// // Remove the artificially added linebreak in the last line of the REPLACE block
// if (result.endsWith("\r\n")) {
// result = result.slice(0, -2)
// } else if (result.endsWith("\n")) {
// result = result.slice(0, -1)
// }
// If this was an in-order replacement, advance lastProcessedIndex
if (!pendingOutOfOrderReplacement) {
lastProcessedIndex = searchEndIndex
}
// Advance lastProcessedIndex to after the matched section
lastProcessedIndex = searchEndIndex
// Reset for next block
inSearch = false
@@ -386,7 +347,6 @@ async function constructNewFileContentV1(diffContent: string, originalContent: s
currentReplaceContent = ""
searchMatchIndex = -1
searchEndIndex = -1
pendingOutOfOrderReplacement = false
continue
}
@@ -398,59 +358,16 @@ async function constructNewFileContentV1(diffContent: string, originalContent: s
currentSearchContent += line + "\n"
} else if (inReplace) {
currentReplaceContent += line + "\n"
// Only output replacement lines immediately for in-order replacements
if (searchMatchIndex !== -1 && !pendingOutOfOrderReplacement) {
// Output replacement lines immediately if we know the insertion point
if (searchMatchIndex !== -1) {
result += line + "\n"
}
}
}
// If this is the final chunk, we need to apply all replacements and build the final result
if (isFinal) {
// Handle the case where we're still in replace mode when processing ends
// and this is the final chunk - treat it as if we encountered the REPLACE marker
if (inReplace && searchMatchIndex !== -1) {
// Store this replacement
replacements.push({
start: searchMatchIndex,
end: searchEndIndex,
content: currentReplaceContent,
})
// If this was an in-order replacement, advance lastProcessedIndex
if (!pendingOutOfOrderReplacement) {
lastProcessedIndex = searchEndIndex
}
// Reset state
inSearch = false
inReplace = false
currentSearchContent = ""
currentReplaceContent = ""
searchMatchIndex = -1
searchEndIndex = -1
pendingOutOfOrderReplacement = false
}
// end of handling missing replace marker
// Sort replacements by start position
replacements.sort((a, b) => a.start - b.start)
// Rebuild the entire result by applying all replacements
result = ""
let currentPos = 0
for (const replacement of replacements) {
// Add original content up to this replacement
result += originalContent.slice(currentPos, replacement.start)
// Add the replacement content
result += replacement.content
// Move position to after the replaced section
currentPos = replacement.end
}
// Add any remaining original content
result += originalContent.slice(currentPos)
// If this is the final chunk, append any remaining original content
if (isFinal && lastProcessedIndex < originalContent.length) {
result += originalContent.slice(lastProcessedIndex)
}
return result
@@ -1,9 +1,9 @@
import { constructNewFileContent as cnfc } from "./diff"
import { constructNewFileContent as cnfc2 } from "./diff"
import { describe, it } from "mocha"
import { expect } from "chai"
async function cnfc2(diffContent: string, originalContent: string, isFinal: boolean): Promise<string> {
return cnfc(diffContent, originalContent, isFinal, "v2")
async function cnfc(diffContent: string, originalContent: string, isFinal: boolean): Promise<string> {
return cnfc2(diffContent, originalContent, isFinal, "v1")
}
describe("Diff Format Edge Cases", () => {
@@ -17,9 +17,8 @@ new content
+++++++ REPLACE`
const result1 = await cnfc(diff, original, isFinal)
const result2 = await cnfc2(diff, original, isFinal)
const expectedResult = "before\nnew content\nafter"
expect(result1).to.equal(expectedResult)
expect(result2).to.equal(expectedResult)
expect(result1).to.equal("new content\n")
expect(result2).to.equal("before\nnew content\nafter")
})
it("should handle SEARCH prefix symbols - more than 7", async () => {
@@ -32,9 +31,8 @@ new content
+++++++ REPLACE`
const result1 = await cnfc(diff, original, isFinal)
const result2 = await cnfc2(diff, original, isFinal)
const expectedResult = "before\nnew content\nafter"
expect(result1).to.equal(expectedResult)
expect(result2).to.equal(expectedResult)
expect(result1).to.equal("new content\n")
expect(result2).to.equal("before\nnew content\nafter")
})
it("should handle SEARCH - less than 7 and REPLACE = less than 7", async () => {
@@ -47,9 +45,8 @@ new content
+++++++ REPLACE`
const result1 = await cnfc(diff, original, isFinal)
const result2 = await cnfc2(diff, original, isFinal)
const expectedResult = "before\nnew content\nafter"
expect(result1).to.equal(expectedResult)
expect(result2).to.equal(expectedResult)
expect(result1).to.equal("r")
expect(result2).to.equal("before\nnew content\nafter")
})
it("should handle SEARCH - less than 7 and REPLACE = more than 7", async () => {
@@ -62,7 +59,7 @@ new content
+++++++ REPLACE`
const result1 = await cnfc(diff, original, isFinal)
const result2 = await cnfc2(diff, original, isFinal)
expect(result1).to.equal("before\nnew content\nafter")
expect(result1).to.equal("r")
expect(result2).to.equal("before\nnew content\nafter")
})
@@ -76,9 +73,8 @@ new content
+++++++ REPLACE`
const result1 = await cnfc(diff, original, isFinal)
const result2 = await cnfc2(diff, original, isFinal)
const expectedResult = "before\nnew content\nafter"
expect(result1).to.equal(expectedResult)
expect(result2).to.equal(expectedResult)
expect(result1).to.equal("r")
expect(result2).to.equal("before\nnew content\nafter")
})
it("should handle SEARCH - more than 7 and REPLACE = less than 7", async () => {
@@ -91,9 +87,8 @@ new content
+++++++ REPLACE`
const result1 = await cnfc(diff, original, isFinal)
const result2 = await cnfc2(diff, original, isFinal)
const expectedResult = "before\nnew content\nafter"
expect(result1).to.equal(expectedResult)
expect(result2).to.equal(expectedResult)
expect(result1).to.equal("r")
expect(result2).to.equal("before\nnew content\nafter")
})
it("should handle consecutive SEARCH-REPLACE with second block SEARCH - less than 7", async () => {
@@ -111,9 +106,8 @@ second new content
+++++++ REPLACE`
const result1 = await cnfc(diff, original, isFinal)
const result2 = await cnfc2(diff, original, isFinal)
const expectedResult = "before\nfirst new content\nafter\nsecond new content\nend"
expect(result1).to.equal(expectedResult)
expect(result2).to.equal(expectedResult)
expect(result1).to.equal("before\nfirst new content\nsecond new content\n")
expect(result2).to.equal("before\nfirst new content\nafter\nsecond new content\nend")
})
it("should handle consecutive SEARCH-REPLACE with second block SEARCH - less than 7 and REPLACE = less than 7", async () => {
@@ -131,8 +125,7 @@ second new content
+++++++ REPLACE`
const result1 = await cnfc(diff, original, isFinal)
const result2 = await cnfc2(diff, original, isFinal)
const expectedResult = "before\nfirst new content\nafter\nsecond new content\nend"
expect(result1).to.equal(expectedResult)
expect(result2).to.equal(expectedResult)
expect(result1).to.equal("before\nfirst new content\nd")
expect(result2).to.equal("before\nfirst new content\nafter\nsecond new content\nend")
})
})
@@ -1,361 +1,361 @@
// import { constructNewFileContent as cnfc } from "./diff"
// import { describe, it } from "mocha"
// import { expect } from "chai"
import { constructNewFileContent as cnfc2 } from "./diff"
import { describe, it } from "mocha"
import { expect } from "chai"
// async function cnfc2(diffContent: string, originalContent: string, isFinal: boolean): Promise<string> {
// return cnfc(diffContent, originalContent, isFinal, "v2")
// }
async function cnfc(diffContent: string, originalContent: string, isFinal: boolean): Promise<string> {
return cnfc2(diffContent, originalContent, isFinal, "v1")
}
// describe("Diff Format Edge Cases", () => {
// it("should handle missing search block", async () => {
// const original = "line1\nline2"
// const diff = `=======
// new content
// +++++++ REPLACE`
// const result1 = await cnfc(diff, original, true)
// expect(result1).to.equal("new content\n")
// try {
// await cnfc2(diff, original, true)
// expect.fail("Expected an error to be thrown")
// } catch (err) {
// expect(err).to.be.an("error")
// }
// })
describe("Diff Format Edge Cases", () => {
it("should handle missing search block", async () => {
const original = "line1\nline2"
const diff = `=======
new content
+++++++ REPLACE`
const result1 = await cnfc(diff, original, true)
expect(result1).to.equal("new content\n")
try {
await cnfc2(diff, original, true)
expect.fail("Expected an error to be thrown")
} catch (err) {
expect(err).to.be.an("error")
}
})
// it("should handle consecutive search blocks", async () => {
// const original = "text"
// const diff = `------- SEARCH
// =======
// replaced
// +++++++ REPLACE
// ------- SEARCH
// =======
// another
// +++++++ REPLACE`
// const result1 = await cnfc(diff, original, true)
// expect(result1).to.equal("replaced\nanother\n")
// try {
// await cnfc2(diff, original, true)
// expect.fail("Expected an error to be thrown")
// } catch (err) {
// expect(err).to.be.an("error")
// }
// })
it("should handle consecutive search blocks", async () => {
const original = "text"
const diff = `------- SEARCH
=======
replaced
+++++++ REPLACE
------- SEARCH
=======
another
+++++++ REPLACE`
const result1 = await cnfc(diff, original, true)
expect(result1).to.equal("replaced\nanother\n")
try {
await cnfc2(diff, original, true)
expect.fail("Expected an error to be thrown")
} catch (err) {
expect(err).to.be.an("error")
}
})
// it("should handle reverse markers order", async () => {
// const original = "content"
// const diff = `+++++++ SEARCH
// =======
// invalid
// ------- REPLACE`
// const result1 = await cnfc(diff, original, true)
// expect(result1).to.equal("invalid\ncontent")
// try {
// await cnfc2(diff, original, true)
// expect.fail("Expected an error to be thrown")
// } catch (err) {
// expect(err).to.be.an("error")
// }
// })
it("should handle reverse markers order", async () => {
const original = "content"
const diff = `+++++++ SEARCH
=======
invalid
------- REPLACE`
const result1 = await cnfc(diff, original, true)
expect(result1).to.equal("invalid\ncontent")
try {
await cnfc2(diff, original, true)
expect.fail("Expected an error to be thrown")
} catch (err) {
expect(err).to.be.an("error")
}
})
// it("should handle incomplete block structure", async () => {
// const original = "valid text"
// const diff = `------- SEARCH
// text
// +++++++ REPLACE`
// const result1 = await cnfc(diff, original, true)
// expect(result1).to.equal("t")
// try {
// await cnfc2(diff, original, true)
// expect.fail("Expected an error to be thrown")
// } catch (err) {
// expect(err).to.be.an("error")
// }
// })
it("should handle incomplete block structure", async () => {
const original = "valid text"
const diff = `------- SEARCH
text
+++++++ REPLACE`
const result1 = await cnfc(diff, original, true)
expect(result1).to.equal("t")
try {
await cnfc2(diff, original, true)
expect.fail("Expected an error to be thrown")
} catch (err) {
expect(err).to.be.an("error")
}
})
// it("should handle empty search block", async () => {
// const original = "any content"
// const diff = `------- SEARCH
// =======
// inserted
// +++++++ REPLACE`
// const result1 = await cnfc(diff, original, true)
// const result2 = await cnfc2(diff, original, true)
// expect(result1).to.equal("inserted\n")
// expect(result1).to.equal(result2)
// })
it("should handle empty search block", async () => {
const original = "any content"
const diff = `------- SEARCH
=======
inserted
+++++++ REPLACE`
const result1 = await cnfc(diff, original, true)
const result2 = await cnfc2(diff, original, true)
expect(result1).to.equal("inserted\n")
expect(result1).to.equal(result2)
})
// it("should handle mixed line endings", async () => {
// const original = "line1\r\nline2"
// const diff = `------- SEARCH
// line1\r
// =======
// line1
// +++++++ REPLACE`
// const result1 = await cnfc(diff, original, true)
// const result2 = await cnfc2(diff, original, true)
// expect(result1).to.equal("line1\nline2")
// expect(result1).to.equal(result2)
// })
it("should handle mixed line endings", async () => {
const original = "line1\r\nline2"
const diff = `------- SEARCH
line1\r
=======
line1
+++++++ REPLACE`
const result1 = await cnfc(diff, original, true)
const result2 = await cnfc2(diff, original, true)
expect(result1).to.equal("line1\nline2")
expect(result1).to.equal(result2)
})
// it("should handle special characters in search", async () => {
// const original = "text with $^.*\nend"
// const diff = `------- SEARCH
// $^.*
// =======
// replaced
// +++++++ REPLACE`
// const result1 = await cnfc(diff, original, true)
// const result2 = await cnfc2(diff, original, true)
// expect(result1).to.equal("text with replaced\nend")
// expect(result1).to.equal(result2)
// })
it("should handle special characters in search", async () => {
const original = "text with $^.*\nend"
const diff = `------- SEARCH
$^.*
=======
replaced
+++++++ REPLACE`
const result1 = await cnfc(diff, original, true)
const result2 = await cnfc2(diff, original, true)
expect(result1).to.equal("text with replaced\nend")
expect(result1).to.equal(result2)
})
// it("should handle special regex chars and nested search markers", async () => {
// const original = `text with $^.*\n--- SEARCH\nend`
// const diff = `------- SEARCH
// $^.*
// =======
// replaced
// +++++++ REPLACE
it("should handle special regex chars and nested search markers", async () => {
const original = `text with $^.*\n--- SEARCH\nend`
const diff = `------- SEARCH
$^.*
=======
replaced
+++++++ REPLACE
// ------- SEARCH
// --- SEARCH
// =======
// before
// +++++++ REPLACE`
// const result1 = await cnfc(diff, original, true)
// const result2 = await cnfc2(diff, original, true)
// expect(result1).to.equal("text with replaced\nbefore\nend")
// expect(result1).to.equal(result2)
// })
------- SEARCH
--- SEARCH
=======
before
+++++++ REPLACE`
const result1 = await cnfc(diff, original, true)
const result2 = await cnfc2(diff, original, true)
expect(result1).to.equal("text with replaced\nbefore\nend")
expect(result1).to.equal(result2)
})
// it("cnfc2 should handle invalid search marker format", async () => {
// const original = `text with $^.*\n--- SEARCH\nend`
// const diff = `--- SEARCH
// $^.*
// =======
// replaced
// +++++++ REPLACE
it("cnfc2 should handle invalid search marker format", async () => {
const original = `text with $^.*\n--- SEARCH\nend`
const diff = `--- SEARCH
$^.*
=======
replaced
+++++++ REPLACE
// ------- SEARCH
// --- SEARCH
// =======
// before
// +++++++ REPLACE`
// try {
// await cnfc(diff, original, true)
// expect.fail("Expected an error to be thrown")
// } catch (err) {
// expect(err).to.be.an("error")
// }
// const result2 = await cnfc2(diff, original, true)
// expect(result2).to.equal("text with replaced\nbefore\nend")
// })
------- SEARCH
--- SEARCH
=======
before
+++++++ REPLACE`
try {
await cnfc(diff, original, true)
expect.fail("Expected an error to be thrown")
} catch (err) {
expect(err).to.be.an("error")
}
const result2 = await cnfc2(diff, original, true)
expect(result2).to.equal("text with replaced\nbefore\nend")
})
// it("cnfc2 should throw error for incomplete search marker", async () => {
// const original = `text with $^.*\n--- SEARCH\nend`
// const diff = `--- SEARCH
// $^.*
// =======
// replaced
// +++++++ REPLACE
it("cnfc2 should throw error for incomplete search marker", async () => {
const original = `text with $^.*\n--- SEARCH\nend`
const diff = `--- SEARCH
$^.*
=======
replaced
+++++++ REPLACE
// ------ SEARCH
// --- SEARCH
// =======
// before
// +++++++ REPLACE`
// const result1 = await cnfc(diff, original, true)
// expect(result1).to.equal("replaced\nbefore\n")
// try {
// await cnfc2(diff, original, true)
// expect.fail("Expected an error to be thrown")
// } catch (err) {
// expect(err).to.be.an("error")
// }
// })
------ SEARCH
--- SEARCH
=======
before
+++++++ REPLACE`
const result1 = await cnfc(diff, original, true)
expect(result1).to.equal("replaced\nbefore\n")
try {
await cnfc2(diff, original, true)
expect.fail("Expected an error to be thrown")
} catch (err) {
expect(err).to.be.an("error")
}
})
// it("cnfc2 should handle custom nested search markers", async () => {
// const original = `text with $^.*\n--- SEARCH2\nend`
// const diff = `--- SEARCH
// $^.*
// =======
// replaced
// +++++++ REPLACE
it("cnfc2 should handle custom nested search markers", async () => {
const original = `text with $^.*\n--- SEARCH2\nend`
const diff = `--- SEARCH
$^.*
=======
replaced
+++++++ REPLACE
// ------ SEARCH
// --- SEARCH2
// =======
// before
// +++++++ REPLACE`
// const result1 = await cnfc(diff, original, true)
// const result2 = await cnfc2(diff, original, true)
// expect(result1).to.equal("replaced\nbefore\n")
// expect(result2).to.equal("text with replaced\nbefore\nend")
// })
------ SEARCH
--- SEARCH2
=======
before
+++++++ REPLACE`
const result1 = await cnfc(diff, original, true)
const result2 = await cnfc2(diff, original, true)
expect(result1).to.equal("replaced\nbefore\n")
expect(result2).to.equal("text with replaced\nbefore\nend")
})
// it("cnfc2 should handle text containing nested search markers", async () => {
// const original = `text with $^.*\ntext with --- SEARCH2\nend`
// const diff = `--- SEARCH
// $^.*
// =======
// replaced
// +++++++ REPLACE
it("cnfc2 should handle text containing nested search markers", async () => {
const original = `text with $^.*\ntext with --- SEARCH2\nend`
const diff = `--- SEARCH
$^.*
=======
replaced
+++++++ REPLACE
// ------ SEARCH
// text with --- SEARCH2
// =======
// before
// +++++++ REPLACE`
// const result1 = await cnfc(diff, original, true)
// const result2 = await cnfc2(diff, original, true)
// expect(result1).to.equal("replaced\nbefore\n")
// expect(result2).to.equal("text with replaced\nbefore\nend")
// })
------ SEARCH
text with --- SEARCH2
=======
before
+++++++ REPLACE`
const result1 = await cnfc(diff, original, true)
const result2 = await cnfc2(diff, original, true)
expect(result1).to.equal("replaced\nbefore\n")
expect(result2).to.equal("text with replaced\nbefore\nend")
})
// it("cnfc2 should handle missing replacement marker in lenient mode", async () => {
// const original = `text with $^.*\ntext with --- SEARCH2\nend`
// const diff = `--- SEARCH
// $^.*
// =======
// replaced
// +++++++ REPLACE
it("cnfc2 should handle missing replacement marker in lenient mode", async () => {
const original = `text with $^.*\ntext with --- SEARCH2\nend`
const diff = `--- SEARCH
$^.*
=======
replaced
+++++++ REPLACE
// ------ SEARCH
// text with --- SEARCH2
// =======
// before`
// const result1 = await cnfc(diff, original, false)
// const result2 = await cnfc2(diff, original, false)
// expect(result1).to.equal("replaced\nbefore\n")
// expect(result2).to.equal("text with replaced\nbefore\n")
// })
------ SEARCH
text with --- SEARCH2
=======
before`
const result1 = await cnfc(diff, original, false)
const result2 = await cnfc2(diff, original, false)
expect(result1).to.equal("replaced\nbefore\n")
expect(result2).to.equal("text with replaced\nbefore\n")
})
// it("cnfc2 should throw error for missing replacement marker in strict mode", async () => {
// const original = `text with $^.*\ntext with --- SEARCH2\nend`
// const diff = `--- SEARCH
// $^.*
// =======
// replaced
// +++++++ REPLACE
it("cnfc2 should throw error for missing replacement marker in strict mode", async () => {
const original = `text with $^.*\ntext with --- SEARCH2\nend`
const diff = `--- SEARCH
$^.*
=======
replaced
+++++++ REPLACE
// ------ SEARCH
// text with --- SEARCH2
// =======
// before`
// const result1 = await cnfc(diff, original, true)
// expect(result1).to.equal("replaced\nbefore\n")
// try {
// await cnfc2(diff, original, true)
// expect.fail("Expected an error to be thrown")
// } catch (err) {
// expect(err).to.be.an("error")
// }
// })
------ SEARCH
text with --- SEARCH2
=======
before`
const result1 = await cnfc(diff, original, true)
expect(result1).to.equal("replaced\nbefore\n")
try {
await cnfc2(diff, original, true)
expect.fail("Expected an error to be thrown")
} catch (err) {
expect(err).to.be.an("error")
}
})
// it("cnfc2 should handle long text with multiple search-replace blocks", async () => {
// const original = `This is a long text with multiple sections.
// Section 1: Lorem ipsum dolor sit amet
// Section 2: consectetur adipiscing elit
// Section 3: sed do eiusmod tempor
// Section 4: incididunt ut labore
// Section 5: et dolore magna aliqua`
it("cnfc2 should handle long text with multiple search-replace blocks", async () => {
const original = `This is a long text with multiple sections.
Section 1: Lorem ipsum dolor sit amet
Section 2: consectetur adipiscing elit
Section 3: sed do eiusmod tempor
Section 4: incididunt ut labore
Section 5: et dolore magna aliqua`
// const diff = `--- SEARCH
// Section 1: Lorem ipsum dolor sit amet
// =======
// Section 1: Replaced text
// +++++++ REPLACE
const diff = `--- SEARCH
Section 1: Lorem ipsum dolor sit amet
=======
Section 1: Replaced text
+++++++ REPLACE
// ------- SEARCH
// Section 3: sed do eiusmod tempor
// =======
// Section 3: Modified content
// +++++++ REPLACE
------- SEARCH
Section 3: sed do eiusmod tempor
=======
Section 3: Modified content
+++++++ REPLACE
// ------- SEARCH
// Section 5: et dolore magna aliqua
// =======
// Section 5: Final replacement
// +++++++ REPLACE`
------- SEARCH
Section 5: et dolore magna aliqua
=======
Section 5: Final replacement
+++++++ REPLACE`
// const expected = `This is a long text with multiple sections.
// Section 1: Replaced text
// Section 2: consectetur adipiscing elit
// Section 3: Modified content
// Section 4: incididunt ut labore
// Section 5: Final replacement
// `
const expected = `This is a long text with multiple sections.
Section 1: Replaced text
Section 2: consectetur adipiscing elit
Section 3: Modified content
Section 4: incididunt ut labore
Section 5: Final replacement
`
// const result = await cnfc2(diff, original, true)
// expect(result).to.equal(expected)
// })
const result = await cnfc2(diff, original, true)
expect(result).to.equal(expected)
})
// // Test diff containing special regex characters and nested search markers
// const diff = `--- SEARCH
// $^.*
// =======
// replaced
// +++++++ REPLACE
// Test diff containing special regex characters and nested search markers
const diff = `--- SEARCH
$^.*
=======
replaced
+++++++ REPLACE
// ------ SEARCH
// --- SEARCH
// =======
// before
// +++++++ REPLACE`
// // expected1 shows the incremental results when processing the diff line by line
// // Each element represents the result after processing that line number
// const expected1 = [
// "",
// "",
// "",
// "replaced\n",
// "replaced\n",
// "replaced\n",
// "replaced\n",
// "replaced\n",
// "replaced\n",
// "replaced\nbefore\n",
// ]
// // expected2 shows the results when processing with original content
// // Each element represents the result after processing that line number
// const expected2 = [
// "",
// "",
// "text with ",
// "text with replaced\n",
// "text with replaced\n",
// "text with replaced\n",
// "text with replaced\n",
// "text with replaced\n",
// new Error(),
// new Error(),
// ]
// const diffLines = diff.split("\n")
// for (let i = 1; i < diffLines.length; i++) {
// it(`cnfc2 should handle partial diff configuration (line ${i})`, async () => {
// const original = `text with $^.*\n--- SEARCH\nend`
// const result1 = await cnfc(diffLines.slice(0, i).join("\n"), original, i === diffLines.length - 1)
// expect(result1).to.equal(expected1[i - 1])
// })
// }
------ SEARCH
--- SEARCH
=======
before
+++++++ REPLACE`
// expected1 shows the incremental results when processing the diff line by line
// Each element represents the result after processing that line number
const expected1 = [
"",
"",
"",
"replaced\n",
"replaced\n",
"replaced\n",
"replaced\n",
"replaced\n",
"replaced\n",
"replaced\nbefore\n",
]
// expected2 shows the results when processing with original content
// Each element represents the result after processing that line number
const expected2 = [
"",
"",
"text with ",
"text with replaced\n",
"text with replaced\n",
"text with replaced\n",
"text with replaced\n",
"text with replaced\n",
new Error(),
new Error(),
]
const diffLines = diff.split("\n")
for (let i = 1; i < diffLines.length; i++) {
it(`cnfc2 should handle partial diff configuration (line ${i})`, async () => {
const original = `text with $^.*\n--- SEARCH\nend`
const result1 = await cnfc(diffLines.slice(0, i).join("\n"), original, i === diffLines.length - 1)
expect(result1).to.equal(expected1[i - 1])
})
}
// for (let i = 1; i < diffLines.length; i++) {
// it(`cnfc2 should handle partial diff configuration (line ${i})`, async () => {
// const original = `text with $^.*\n--- SEARCH\nend`
// let expected = expected2[i - 1]
// if (expected instanceof Error) {
// try {
// await cnfc2(diffLines.slice(0, i).join("\n"), original, true)
// expect.fail("Expected an error to be thrown")
// } catch (err) {
// expect(err).to.be.an("error")
// }
// } else {
// const result2 = await cnfc2(diffLines.slice(0, i).join("\n"), original, i === diffLines.length - 1)
// expect(result2).to.equal(expected)
// }
// })
// }
// })
for (let i = 1; i < diffLines.length; i++) {
it(`cnfc2 should handle partial diff configuration (line ${i})`, async () => {
const original = `text with $^.*\n--- SEARCH\nend`
let expected = expected2[i - 1]
if (expected instanceof Error) {
try {
await cnfc2(diffLines.slice(0, i).join("\n"), original, true)
expect.fail("Expected an error to be thrown")
} catch (err) {
expect(err).to.be.an("error")
}
} else {
const result2 = await cnfc2(diffLines.slice(0, i).join("\n"), original, i === diffLines.length - 1)
expect(result2).to.equal(expected)
}
})
}
})
@@ -1,58 +0,0 @@
import { Controller } from "../index"
import { EmptyRequest, StringArray } from "@shared/proto/common"
import { StreamingResponseHandler, getRequestRegistry } from "../grpc-handler"
// Keep track of active subscriptions
const activeWorkspaceUpdateSubscriptions = new Set<StreamingResponseHandler>()
/**
* Subscribe to workspace file updates
* @param controller The controller instance
* @param request The empty request
* @param responseStream The streaming response handler
* @param requestId The ID of the request (passed by the gRPC handler)
*/
export async function subscribeToWorkspaceUpdates(
controller: Controller,
request: EmptyRequest,
responseStream: StreamingResponseHandler,
requestId?: string,
): Promise<void> {
// Add this subscription to the active subscriptions
activeWorkspaceUpdateSubscriptions.add(responseStream)
// Register cleanup when the connection is closed
const cleanup = () => {
activeWorkspaceUpdateSubscriptions.delete(responseStream)
}
// Register the cleanup function with the request registry if we have a requestId
if (requestId) {
getRequestRegistry().registerRequest(requestId, cleanup, { type: "workspace_update_subscription" }, responseStream)
}
}
/**
* Send a workspace update event to all active subscribers
* @param filePaths Array of file paths to send
*/
export async function sendWorkspaceUpdateEvent(filePaths: string[]): Promise<void> {
// Send the event to all active subscribers
const promises = Array.from(activeWorkspaceUpdateSubscriptions).map(async (responseStream) => {
try {
const event = StringArray.create({
values: filePaths,
})
await responseStream(
event,
false, // Not the last message
)
} catch (error) {
console.error("Error sending workspace update event:", error)
// Remove the subscription if there was an error
activeWorkspaceUpdateSubscriptions.delete(responseStream)
}
})
await Promise.all(promises)
}
@@ -1,44 +0,0 @@
import { Controller } from ".."
import { Metadata } from "../../../shared/proto/common"
import { ToggleWorkflowRequest, ClineRulesToggles } from "../../../shared/proto/file"
import { getWorkspaceState, updateWorkspaceState, getGlobalState, updateGlobalState } from "../../../core/storage/state"
import { ClineRulesToggles as AppClineRulesToggles } from "../../../shared/cline-rules"
/**
* Toggles a workflow on or off
* @param controller The controller instance
* @param request The request containing the workflow path and enabled state
* @returns The updated workflow toggles
*/
export async function toggleWorkflow(controller: Controller, request: ToggleWorkflowRequest): Promise<ClineRulesToggles> {
const { workflowPath, enabled, isGlobal } = request
if (!workflowPath || typeof enabled !== "boolean") {
console.error("toggleWorkflow: Missing or invalid parameters", {
workflowPath,
enabled: typeof enabled === "boolean" ? enabled : `Invalid: ${typeof enabled}`,
})
throw new Error("Missing or invalid parameters for toggleWorkflow")
}
// Update the toggles based on isGlobal flag
if (isGlobal) {
// Global workflows
const toggles = ((await getGlobalState(controller.context, "globalWorkflowToggles")) as AppClineRulesToggles) || {}
toggles[workflowPath] = enabled
await updateGlobalState(controller.context, "globalWorkflowToggles", toggles)
await controller.postStateToWebview()
// Return the global toggles
return ClineRulesToggles.create({ toggles: toggles })
} else {
// Workspace workflows
const toggles = ((await getWorkspaceState(controller.context, "workflowToggles")) as AppClineRulesToggles) || {}
toggles[workflowPath] = enabled
await updateWorkspaceState(controller.context, "workflowToggles", toggles)
await controller.postStateToWebview()
// Return the workspace toggles
return ClineRulesToggles.create({ toggles: toggles })
}
}
+193 -8
View File
@@ -1,17 +1,24 @@
import { Anthropic } from "@anthropic-ai/sdk"
import axios from "axios"
import { v4 as uuidv4 } from "uuid"
import fs from "fs/promises"
import { setTimeout as setTimeoutPromise } from "node:timers/promises"
import pWaitFor from "p-wait-for"
import * as path from "path"
import * as vscode from "vscode"
import { handleGrpcRequest, handleGrpcRequestCancel } from "./grpc-handler"
import { handleModelsServiceRequest } from "./models"
import { EmptyRequest } from "@shared/proto/common"
import { buildApiHandler } from "@api/index"
import { cleanupLegacyCheckpoints } from "@integrations/checkpoints/CheckpointMigration"
import { downloadTask } from "@integrations/misc/export-markdown"
import { fetchOpenGraphData } from "@integrations/misc/link-preview"
import { handleFileServiceRequest } from "./file"
import { getTheme } from "@integrations/theme/getTheme"
import WorkspaceTracker from "@integrations/workspace/WorkspaceTracker"
import { ClineAccountService } from "@services/account/ClineAccountService"
import { BrowserSession } from "@services/browser/BrowserSession"
import { McpHub } from "@services/mcp/McpHub"
import { telemetryService } from "@/services/posthog/telemetry/TelemetryService"
import { ApiProvider, ModelInfo } from "@shared/api"
@@ -19,30 +26,40 @@ import { ChatContent } from "@shared/ChatContent"
import { ChatSettings } from "@shared/ChatSettings"
import { ExtensionMessage, ExtensionState, Platform } from "@shared/ExtensionMessage"
import { HistoryItem } from "@shared/HistoryItem"
import { McpMarketplaceCatalog } from "@shared/mcp"
import { McpDownloadResponse, McpMarketplaceCatalog, McpServer } from "@shared/mcp"
import { TelemetrySetting } from "@shared/TelemetrySetting"
import { WebviewMessage } from "@shared/WebviewMessage"
import { fileExistsAtPath } from "@utils/fs"
import { getWorkingState } from "@utils/git"
import { extractCommitMessage } from "@integrations/git/commit-message-generator"
import { ensureMcpServersDirectoryExists, ensureSettingsDirectoryExists, GlobalFileNames } from "../storage/disk"
import { getTotalTasksSize } from "@utils/storage"
import {
ensureMcpServersDirectoryExists,
ensureSettingsDirectoryExists,
GlobalFileNames,
ensureWorkflowsDirectoryExists,
} from "../storage/disk"
import {
getAllExtensionState,
getGlobalState,
getSecret,
getWorkspaceState,
resetExtensionState,
storeSecret,
updateApiConfiguration,
updateGlobalState,
updateWorkspaceState,
} from "../storage/state"
import { Task } from "../task"
import { Task, cwd } from "../task"
import { ClineRulesToggles } from "@shared/cline-rules"
import { sendStateUpdate } from "./state/subscribeToState"
import { sendAddToInputEvent } from "./ui/subscribeToAddToInput"
import { sendAuthCallbackEvent } from "./account/subscribeToAuthCallback"
import { sendChatButtonClickedEvent } from "./ui/subscribeToChatButtonClicked"
import { sendMcpMarketplaceCatalogEvent } from "./mcp/subscribeToMcpMarketplaceCatalog"
import { sendRelinquishControlEvent } from "./ui/subscribeToRelinquishControl"
import { refreshClineRulesToggles } from "@core/context/instructions/user-instructions/cline-rules"
import { refreshExternalRulesToggles } from "@core/context/instructions/user-instructions/external-rules"
import { refreshWorkflowToggles } from "@core/context/instructions/user-instructions/workflows"
/*
https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
@@ -69,7 +86,7 @@ export class Controller {
this.outputChannel.appendLine("ClineProvider instantiated")
this.postMessage = postMessage
this.workspaceTracker = new WorkspaceTracker()
this.workspaceTracker = new WorkspaceTracker((msg) => this.postMessageToWebview(msg))
this.mcpHub = new McpHub(
() => ensureMcpServersDirectoryExists(),
() => ensureSettingsDirectoryExists(this.context),
@@ -204,6 +221,62 @@ export class Controller {
await this.setUserInfo(message.user || undefined)
await this.postStateToWebview()
break
case "webviewDidLaunch":
this.postStateToWebview()
this.workspaceTracker?.populateFilePaths() // don't await
// post last cached models in case the call to endpoint fails
this.readOpenRouterModels().then((openRouterModels) => {
if (openRouterModels) {
this.postMessageToWebview({
type: "openRouterModels",
openRouterModels,
})
}
})
// gui relies on model info to be up-to-date to provide the most accurate pricing, so we need to fetch the latest details on launch.
// we do this for all users since many users switch between api providers and if they were to switch back to openrouter it would be showing outdated model info if we hadn't retrieved the latest at this point
// (see normalizeApiConfiguration > openrouter)
// Prefetch marketplace and OpenRouter models
getGlobalState(this.context, "mcpMarketplaceCatalog").then((mcpMarketplaceCatalog) => {
if (mcpMarketplaceCatalog) {
sendMcpMarketplaceCatalogEvent(mcpMarketplaceCatalog as McpMarketplaceCatalog)
}
})
this.silentlyRefreshMcpMarketplace()
handleModelsServiceRequest(this, "refreshOpenRouterModels", EmptyRequest.create()).then(async (response) => {
if (response && response.models) {
// update model info in state (this needs to be done here since we don't want to update state while settings is open, and we may refresh models there)
const { apiConfiguration } = await getAllExtensionState(this.context)
if (apiConfiguration.openRouterModelId && response.models[apiConfiguration.openRouterModelId]) {
await updateGlobalState(
this.context,
"openRouterModelInfo",
response.models[apiConfiguration.openRouterModelId],
)
await this.postStateToWebview()
}
}
})
// Initialize telemetry service with user's current setting
this.getStateToPostToWebview().then((state) => {
const { telemetrySetting } = state
const isOptedIn = telemetrySetting !== "disabled"
telemetryService.updateTelemetryState(isOptedIn)
})
break
case "newTask":
// Code that should run in response to the hello message command
//vscode.window.showInformationMessage(message.text!)
// Send a message to our webview.
// You can send any JSON serializable data.
// Could also do this in extension .ts
//this.postMessageToWebview({ type: "text", text: `Extension: ${Date.now()}` })
// initializing new instance of Cline will make sure that any agentically running promises in old instance don't affect our new task. this essentially creates a fresh slate for the new task
await this.initTask(message.text, message.images, message.files)
break
case "apiConfiguration":
if (message.apiConfiguration) {
await updateApiConfiguration(this.context, message.apiConfiguration)
@@ -221,7 +294,63 @@ export class Controller {
await this.fetchMcpMarketplace(message.bool)
break
}
// case "openMcpMarketplaceServerDetails": {
// if (message.text) {
// const response = await fetch(`https://api.cline.bot/v1/mcp/marketplace/item?mcpId=${message.mcpId}`)
// const details: McpDownloadResponse = await response.json()
// if (details.readmeContent) {
// // Disable markdown preview markers
// const config = vscode.workspace.getConfiguration("markdown")
// await config.update("preview.markEditorSelection", false, true)
// // Create URI with base64 encoded markdown content
// const uri = vscode.Uri.parse(
// `${DIFF_VIEW_URI_SCHEME}:${details.name} README?${Buffer.from(details.readmeContent).toString("base64")}`,
// )
// // close existing
// const tabs = vscode.window.tabGroups.all
// .flatMap((tg) => tg.tabs)
// .filter((tab) => tab.label && tab.label.includes("README") && tab.label.includes("Preview"))
// for (const tab of tabs) {
// await vscode.window.tabGroups.close(tab)
// }
// // Show only the preview
// await vscode.commands.executeCommand("markdown.showPreview", uri, {
// sideBySide: true,
// preserveFocus: true,
// })
// }
// }
// this.postMessageToWebview({ type: "relinquishControl" })
// break
// }
case "toggleWorkflow": {
const { workflowPath, enabled, isGlobal } = message
if (workflowPath && typeof enabled === "boolean" && typeof isGlobal === "boolean") {
if (isGlobal) {
const globalWorkflowToggles =
((await getGlobalState(this.context, "globalWorkflowToggles")) as ClineRulesToggles) || {}
globalWorkflowToggles[workflowPath] = enabled
await updateGlobalState(this.context, "globalWorkflowToggles", globalWorkflowToggles)
await this.postStateToWebview()
} else {
const toggles = ((await getWorkspaceState(this.context, "workflowToggles")) as ClineRulesToggles) || {}
toggles[workflowPath] = enabled
await updateWorkspaceState(this.context, "workflowToggles", toggles)
await this.postStateToWebview()
}
}
break
}
case "fetchLatestMcpServersFromHub": {
this.mcpHub?.sendLatestMcpServers()
break
}
// telemetry
case "telemetrySetting": {
if (message.telemetrySetting) {
@@ -230,6 +359,57 @@ export class Controller {
await this.postStateToWebview()
break
}
case "updateSettings": {
// api config
if (message.apiConfiguration) {
await updateApiConfiguration(this.context, message.apiConfiguration)
if (this.task) {
this.task.api = buildApiHandler(message.apiConfiguration)
}
}
// custom instructions
await this.updateCustomInstructions(message.customInstructionsSetting)
// telemetry setting
if (message.telemetrySetting) {
await this.updateTelemetrySetting(message.telemetrySetting)
}
// plan act setting
await updateGlobalState(this.context, "planActSeparateModelsSetting", message.planActSeparateModelsSetting)
if (typeof message.enableCheckpointsSetting === "boolean") {
await updateGlobalState(this.context, "enableCheckpointsSetting", message.enableCheckpointsSetting)
}
if (typeof message.mcpMarketplaceEnabled === "boolean") {
await updateGlobalState(this.context, "mcpMarketplaceEnabled", message.mcpMarketplaceEnabled)
}
// chat settings (including preferredLanguage and openAIReasoningEffort)
if (message.chatSettings) {
await updateGlobalState(this.context, "chatSettings", message.chatSettings)
if (this.task) {
this.task.chatSettings = message.chatSettings
}
}
// terminal settings
if (typeof message.shellIntegrationTimeout === "number") {
await updateGlobalState(this.context, "shellIntegrationTimeout", message.shellIntegrationTimeout)
}
if (typeof message.terminalReuseEnabled === "boolean") {
await updateGlobalState(this.context, "terminalReuseEnabled", message.terminalReuseEnabled)
}
// after settings are updated, post state to webview
await this.postStateToWebview()
await this.postMessageToWebview({ type: "didUpdateSettings" })
break
}
case "clearAllTaskHistory": {
const answer = await vscode.window.showWarningMessage(
"What would you like to delete?",
@@ -246,7 +426,7 @@ export class Controller {
await this.deleteAllTaskHistory()
await this.postStateToWebview()
}
sendRelinquishControlEvent()
this.postMessageToWebview({ type: "relinquishControl" })
break
}
case "grpc_request": {
@@ -261,6 +441,13 @@ export class Controller {
}
break
}
case "executeQuickWin":
if (message.payload) {
const { command, title } = message.payload
this.outputChannel.appendLine(`Received executeQuickWin: command='${command}', title='${title}'`)
await this.initTask(title)
}
break
// Add more switch case statements here as more webview message commands
// are created within the webview context (i.e. inside media/main.js)
@@ -972,7 +1159,6 @@ export class Controller {
shellIntegrationTimeout,
terminalReuseEnabled,
isNewUser,
mcpResponsesCollapsed,
} = await getAllExtensionState(this.context)
const localClineRulesToggles =
@@ -1018,7 +1204,6 @@ export class Controller {
shellIntegrationTimeout,
terminalReuseEnabled,
isNewUser,
mcpResponsesCollapsed,
}
}
@@ -1,25 +0,0 @@
import type { Empty } from "@shared/proto/common"
import { McpServers } from "@shared/proto/mcp"
import type { Controller } from "../index"
import { convertMcpServersToProtoMcpServers } from "@/shared/proto-conversions/mcp/mcp-server-conversion"
/**
* RPC handler for getting the latest MCP servers
* @param controller The controller instance
* @param _request Empty request
* @returns McpServers response with list of all MCP servers
*/
export async function getLatestMcpServers(controller: Controller, _request: Empty): Promise<McpServers> {
try {
// Get sorted servers from mcpHub using the RPC variant
const mcpServers = (await controller.mcpHub?.getLatestMcpServersRPC()) || []
// Convert to proto format
const protoServers = convertMcpServersToProtoMcpServers(mcpServers)
return McpServers.create({ mcpServers: protoServers })
} catch (error) {
console.error("Error fetching latest MCP servers:", error)
throw error
}
}
@@ -15,11 +15,11 @@ import { GlobalFileNames } from "@core/storage/disk"
*/
export async function refreshOpenRouterModels(
controller: Controller,
_request: EmptyRequest,
request: EmptyRequest,
): Promise<OpenRouterCompatibleModelInfo> {
const openRouterModelsFilePath = path.join(await ensureCacheDirectoryExists(controller), GlobalFileNames.openRouterModels)
let models: Record<string, OpenRouterModelInfo> = {}
let models: Record<string, Partial<OpenRouterModelInfo>> = {}
try {
const response = await axios.get("https://openrouter.ai/api/v1/models")
@@ -32,20 +32,15 @@ export async function refreshOpenRouterModels(
return undefined
}
for (const rawModel of rawModels) {
const modelInfo = OpenRouterModelInfo.create({
maxTokens: rawModel.top_provider?.max_completion_tokens ?? 0,
contextWindow: rawModel.context_length ?? 0,
supportsImages: rawModel.architecture?.modality?.includes("image") ?? false,
const modelInfo: Partial<OpenRouterModelInfo> = {
maxTokens: rawModel.top_provider?.max_completion_tokens,
contextWindow: rawModel.context_length,
supportsImages: rawModel.architecture?.modality?.includes("image"),
supportsPromptCache: false,
inputPrice: parsePrice(rawModel.pricing?.prompt) ?? 0,
outputPrice: parsePrice(rawModel.pricing?.completion) ?? 0,
cacheWritesPrice: 0,
cacheReadsPrice: 0,
description: rawModel.description ?? "",
thinkingConfig: rawModel.thinking_config ?? undefined,
supportsGlobalEndpoint: rawModel.supports_global_endpoint ?? undefined,
tiers: rawModel.tiers ?? [],
})
inputPrice: parsePrice(rawModel.pricing?.prompt),
outputPrice: parsePrice(rawModel.pricing?.completion),
description: rawModel.description,
}
switch (rawModel.id) {
case "anthropic/claude-sonnet-4":
@@ -123,7 +118,7 @@ export async function refreshOpenRouterModels(
console.error("Invalid response from OpenRouter API")
}
await fs.writeFile(openRouterModelsFilePath, JSON.stringify(models))
console.log("OpenRouter models fetched and saved", JSON.stringify(models).slice(0, 300))
console.log("OpenRouter models fetched and saved", models)
} catch (error) {
console.error("Error fetching OpenRouter models:", error)
@@ -134,13 +129,30 @@ export async function refreshOpenRouterModels(
}
}
return OpenRouterCompatibleModelInfo.create({ models })
// Convert the Record<string, Partial<OpenRouterModelInfo>> to Record<string, OpenRouterModelInfo>
// by filling in any missing required fields with defaults
const typedModels: Record<string, OpenRouterModelInfo> = {}
for (const [key, model] of Object.entries(models)) {
typedModels[key] = {
maxTokens: model.maxTokens ?? 0,
contextWindow: model.contextWindow ?? 0,
supportsImages: model.supportsImages ?? false,
supportsPromptCache: model.supportsPromptCache ?? false,
inputPrice: model.inputPrice ?? 0,
outputPrice: model.outputPrice ?? 0,
cacheWritesPrice: model.cacheWritesPrice ?? 0,
cacheReadsPrice: model.cacheReadsPrice ?? 0,
description: model.description ?? "",
}
}
return OpenRouterCompatibleModelInfo.create({ models: typedModels })
}
/**
* Reads cached OpenRouter models from disk
*/
async function readOpenRouterModels(controller: Controller): Promise<Record<string, OpenRouterModelInfo> | undefined> {
async function readOpenRouterModels(controller: Controller): Promise<Record<string, Partial<OpenRouterModelInfo>> | undefined> {
const openRouterModelsFilePath = path.join(await ensureCacheDirectoryExists(controller), GlobalFileNames.openRouterModels)
const fileExists = await fileExistsAtPath(openRouterModelsFilePath)
if (fileExists) {
@@ -1,60 +0,0 @@
import { Controller } from "../index"
import { EmptyRequest } from "@shared/proto/common"
import { OpenRouterCompatibleModelInfo } from "@shared/proto/models"
import { StreamingResponseHandler, getRequestRegistry } from "../grpc-handler"
// Keep track of active OpenRouter models subscriptions
const activeOpenRouterModelsSubscriptions = new Set<StreamingResponseHandler>()
/**
* Subscribe to OpenRouter models events
* @param controller The controller instance
* @param request The empty request
* @param responseStream The streaming response handler
* @param requestId The ID of the request (passed by the gRPC handler)
*/
export async function subscribeToOpenRouterModels(
controller: Controller,
request: EmptyRequest,
responseStream: StreamingResponseHandler,
requestId?: string,
): Promise<void> {
console.log("[DEBUG] set up OpenRouter models subscription")
// Add this subscription to the active subscriptions
activeOpenRouterModelsSubscriptions.add(responseStream)
// Register cleanup when the connection is closed
const cleanup = () => {
activeOpenRouterModelsSubscriptions.delete(responseStream)
console.log("[DEBUG] Cleaned up OpenRouter models subscription")
}
// Register the cleanup function with the request registry if we have a requestId
if (requestId) {
getRequestRegistry().registerRequest(requestId, cleanup, { type: "openRouterModels_subscription" }, responseStream)
}
}
/**
* Send an OpenRouter models event to all active subscribers
* @param models The OpenRouter models to send
*/
export async function sendOpenRouterModelsEvent(models: OpenRouterCompatibleModelInfo): Promise<void> {
// Send the event to all active subscribers
const promises = Array.from(activeOpenRouterModelsSubscriptions).map(async (responseStream) => {
try {
await responseStream(
models,
false, // Not the last message
)
console.log("[DEBUG] sending OpenRouter models event")
} catch (error) {
console.error("Error sending OpenRouter models event:", error)
// Remove the subscription if there was an error
activeOpenRouterModelsSubscriptions.delete(responseStream)
}
})
await Promise.all(promises)
}
@@ -1,85 +0,0 @@
import { Controller } from ".."
import { Empty } from "../../../shared/proto/common"
import { UpdateSettingsRequest } from "../../../shared/proto/state"
import { updateApiConfiguration } from "../../storage/state"
import { buildApiHandler } from "../../../api"
import { convertProtoApiConfigurationToApiConfiguration } from "../../../shared/proto-conversions/state/settings-conversion"
import { convertProtoChatSettingsToChatSettings } from "../../../shared/proto-conversions/state/chat-settings-conversion"
import { TelemetrySetting } from "@/shared/TelemetrySetting"
/**
* Updates multiple extension settings in a single request
* @param controller The controller instance
* @param request The request containing the settings to update
* @returns An empty response
*/
export async function updateSettings(controller: Controller, request: UpdateSettingsRequest): Promise<Empty> {
try {
// Update API configuration
if (request.apiConfiguration) {
const apiConfiguration = convertProtoApiConfigurationToApiConfiguration(request.apiConfiguration)
await updateApiConfiguration(controller.context, apiConfiguration)
if (controller.task) {
controller.task.api = buildApiHandler(apiConfiguration)
}
}
// Update custom instructions
if (request.customInstructionsSetting !== undefined) {
await controller.updateCustomInstructions(request.customInstructionsSetting)
}
// Update telemetry setting
if (request.telemetrySetting) {
await controller.updateTelemetrySetting(request.telemetrySetting as TelemetrySetting)
}
// Update plan/act separate models setting
if (request.planActSeparateModelsSetting !== undefined) {
await controller.context.globalState.update("planActSeparateModelsSetting", request.planActSeparateModelsSetting)
}
// Update checkpoints setting
if (request.enableCheckpointsSetting !== undefined) {
await controller.context.globalState.update("enableCheckpointsSetting", request.enableCheckpointsSetting)
}
// Update MCP marketplace setting
if (request.mcpMarketplaceEnabled !== undefined) {
await controller.context.globalState.update("mcpMarketplaceEnabled", request.mcpMarketplaceEnabled)
}
// Update MCP responses collapsed setting
if (request.mcpResponsesCollapsed !== undefined) {
await controller.context.globalState.update("mcpResponsesCollapsed", request.mcpResponsesCollapsed)
}
// Update chat settings
if (request.chatSettings) {
const chatSettings = convertProtoChatSettingsToChatSettings(request.chatSettings)
await controller.context.globalState.update("chatSettings", chatSettings)
if (controller.task) {
controller.task.chatSettings = chatSettings
}
}
// Update terminal timeout setting
if (request.shellIntegrationTimeout !== undefined) {
await controller.context.globalState.update("shellIntegrationTimeout", Number(request.shellIntegrationTimeout))
}
// Update terminal reuse setting
if (request.terminalReuseEnabled !== undefined) {
await controller.context.globalState.update("terminalReuseEnabled", request.terminalReuseEnabled)
}
// Post updated state to webview
await controller.postStateToWebview()
return Empty.create()
} catch (error) {
console.error("Failed to update settings:", error)
throw error
}
}
@@ -1,35 +0,0 @@
import { ExecuteQuickWinRequest } from "@shared/proto/task"
import { Empty } from "@shared/proto/common"
import type { Controller } from "../index"
/**
* Executes a quick win task with command and title
* @param controller The controller instance
* @param request The execute quick win request
* @returns Empty response
*
* @example
* // Usage from webview:
* import { TaskServiceClient } from "@/services/grpc-client"
* import { ExecuteQuickWinRequest } from "@shared/proto/task"
*
* const request: ExecuteQuickWinRequest = {
* command: "npm install",
* title: "Install dependencies"
* }
*
* TaskServiceClient.executeQuickWin(request)
* .then(() => console.log("Quick win executed successfully"))
* .catch(error => console.error("Failed to execute quick win:", error))
*/
export async function executeQuickWin(controller: Controller, request: ExecuteQuickWinRequest): Promise<Empty> {
try {
const { command, title } = request
console.log(`Received executeQuickWin: command='${command}', title='${title}'`)
await controller.initTask(title)
return Empty.create({})
} catch (error) {
console.error("Failed to execute quick win:", error)
throw error
}
}
@@ -1,73 +0,0 @@
import type { Controller } from "../index"
import { EmptyRequest, Empty } from "@shared/proto/common"
import { handleModelsServiceRequest } from "../models"
import { getAllExtensionState, getGlobalState, updateGlobalState } from "../../storage/state"
import { sendOpenRouterModelsEvent } from "../models/subscribeToOpenRouterModels"
import { sendMcpMarketplaceCatalogEvent } from "../mcp/subscribeToMcpMarketplaceCatalog"
import { telemetryService } from "@/services/posthog/telemetry/TelemetryService"
import { OpenRouterCompatibleModelInfo } from "@/shared/proto/models"
import { McpMarketplaceCatalog } from "@shared/mcp"
/**
* Initialize webview when it launches
* @param controller The controller instance
* @param request The empty request
* @returns Empty response
*/
export async function initializeWebview(controller: Controller, request: EmptyRequest): Promise<Empty> {
try {
// Populate file paths for workspace tracker (don't await)
controller.workspaceTracker?.populateFilePaths()
// Post last cached models in case the call to endpoint fails
controller.readOpenRouterModels().then((openRouterModels) => {
if (openRouterModels) {
sendOpenRouterModelsEvent(OpenRouterCompatibleModelInfo.create({ models: openRouterModels }))
}
})
// Refresh OpenRouter models from API
handleModelsServiceRequest(controller, "refreshOpenRouterModels", EmptyRequest.create()).then(async (response) => {
if (response && response.models) {
// Update model info in state (this needs to be done here since we don't want to update state while settings is open, and we may refresh models there)
const { apiConfiguration } = await getAllExtensionState(controller.context)
if (apiConfiguration.openRouterModelId && response.models[apiConfiguration.openRouterModelId]) {
await updateGlobalState(
controller.context,
"openRouterModelInfo",
response.models[apiConfiguration.openRouterModelId],
)
await controller.postStateToWebview()
}
}
})
// GUI relies on model info to be up-to-date to provide the most accurate pricing, so we need to fetch the latest details on launch.
// We do this for all users since many users switch between api providers and if they were to switch back to openrouter it would be showing outdated model info if we hadn't retrieved the latest at this point
// (see normalizeApiConfiguration > openrouter)
// Prefetch marketplace and OpenRouter models
// Send cached MCP marketplace catalog if available
getGlobalState(controller.context, "mcpMarketplaceCatalog").then((mcpMarketplaceCatalog) => {
if (mcpMarketplaceCatalog) {
sendMcpMarketplaceCatalogEvent(mcpMarketplaceCatalog as McpMarketplaceCatalog)
}
})
// Silently refresh MCP marketplace catalog
controller.silentlyRefreshMcpMarketplace()
// Initialize telemetry service with user's current setting
controller.getStateToPostToWebview().then((state) => {
const { telemetrySetting } = state
const isOptedIn = telemetrySetting !== "disabled"
telemetryService.updateTelemetryState(isOptedIn)
})
return Empty.create({})
} catch (error) {
console.error("Failed to initialize webview:", error)
// Return empty response even on error to not break the frontend
return Empty.create({})
}
}
+6 -6
View File
@@ -1,15 +1,15 @@
import { Controller } from ".."
import { StringRequest, KeyValuePair } from "../../../shared/proto/common"
import { StringRequest } from "../../../shared/proto/common"
/**
* Executes a scroll to settings action
* @param controller The controller instance
* @param request The request containing the ID of the settings section to scroll to
* @returns KeyValuePair with action and value fields for the UI to process
* @returns An object with action and value fields for the UI to process
*/
export async function scrollToSettings(controller: Controller, request: StringRequest): Promise<KeyValuePair> {
return KeyValuePair.create({
key: "scrollToSettings",
export async function scrollToSettings(controller: Controller, request: StringRequest): Promise<Record<string, string>> {
return {
action: "scrollToSettings",
value: request.value || "",
})
}
}
@@ -50,7 +50,7 @@ export async function sendChatButtonClickedEvent(controllerId: string): Promise<
}
try {
const event = Empty.create({})
const event: Empty = Empty.create({})
await responseStream(
event,
false, // Not the last message
@@ -50,7 +50,7 @@ export async function sendHistoryButtonClickedEvent(webviewType?: WebviewProvide
}
try {
const event = Empty.create({})
const event: Empty = Empty.create({})
await responseStream(
event,
false, // Not the last message
@@ -41,7 +41,7 @@ export async function subscribeToMcpButtonClicked(
* @param webviewType The type of webview that triggered the event (SIDEBAR or TAB)
*/
export async function sendMcpButtonClickedEvent(webviewType?: WebviewProviderType): Promise<void> {
const event = Empty.create({})
const event: Empty = Empty.create({})
// Process all subscriptions, filtering based on the source
const promises = Array.from(mcpButtonClickedSubscriptions.entries()).map(async ([responseStream, providerType]) => {
@@ -1,55 +0,0 @@
import { Controller } from "../index"
import { EmptyRequest, Empty } from "@shared/proto/common"
import { StreamingResponseHandler, getRequestRegistry } from "../grpc-handler"
// Keep track of active subscriptions
const activeRelinquishControlSubscriptions = new Set<StreamingResponseHandler>()
/**
* Subscribe to relinquish control events
* @param controller The controller instance
* @param request The empty request
* @param responseStream The streaming response handler
* @param requestId The ID of the request (passed by the gRPC handler)
*/
export async function subscribeToRelinquishControl(
controller: Controller,
request: EmptyRequest,
responseStream: StreamingResponseHandler,
requestId?: string,
): Promise<void> {
// Add this subscription to the active subscriptions
activeRelinquishControlSubscriptions.add(responseStream)
// Register cleanup when the connection is closed
const cleanup = () => {
activeRelinquishControlSubscriptions.delete(responseStream)
}
// Register the cleanup function with the request registry if we have a requestId
if (requestId) {
getRequestRegistry().registerRequest(requestId, cleanup, { type: "relinquish_control_subscription" }, responseStream)
}
}
/**
* Send a relinquish control event to all active subscribers
*/
export async function sendRelinquishControlEvent(): Promise<void> {
// Send the event to all active subscribers
const promises = Array.from(activeRelinquishControlSubscriptions).map(async (responseStream) => {
try {
const event = Empty.create({})
await responseStream(
event,
false, // Not the last message
)
} catch (error) {
console.error("Error sending relinquish control event:", error)
// Remove the subscription if there was an error
activeRelinquishControlSubscriptions.delete(responseStream)
}
})
await Promise.all(promises)
}
-1
View File
@@ -91,7 +91,6 @@ export type GlobalStateKey =
| "favoritedModelIds"
| "requestTimeoutMs"
| "shellIntegrationTimeout"
| "mcpResponsesCollapsed"
| "terminalReuseEnabled"
| "isNewUser"
-4
View File
@@ -164,7 +164,6 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
shellIntegrationTimeout,
enableCheckpointsSettingRaw,
mcpMarketplaceEnabledRaw,
mcpResponsesCollapsedRaw,
globalWorkflowToggles,
terminalReuseEnabled,
] = await Promise.all([
@@ -256,7 +255,6 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
getGlobalState(context, "shellIntegrationTimeout") as Promise<number | undefined>,
getGlobalState(context, "enableCheckpointsSetting") as Promise<boolean | undefined>,
getGlobalState(context, "mcpMarketplaceEnabled") as Promise<boolean | undefined>,
getGlobalState(context, "mcpResponsesCollapsed") as Promise<boolean | undefined>,
getGlobalState(context, "globalWorkflowToggles") as Promise<ClineRulesToggles | undefined>,
getGlobalState(context, "terminalReuseEnabled") as Promise<boolean | undefined>,
])
@@ -279,7 +277,6 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
const mcpMarketplaceEnabled = await migrateMcpMarketplaceEnableSetting(mcpMarketplaceEnabledRaw)
const enableCheckpointsSetting = await migrateEnableCheckpointsSetting(enableCheckpointsSettingRaw)
const mcpResponsesCollapsed = mcpResponsesCollapsedRaw ?? false
// Plan/Act separate models setting is a boolean indicating whether the user wants to use different models for plan and act. Existing users expect this to be enabled, while we want new users to opt in to this being disabled by default.
// On win11 state sometimes initializes as empty string instead of undefined
@@ -390,7 +387,6 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
previousModeAwsBedrockCustomSelected,
previousModeAwsBedrockCustomModelBaseId,
mcpMarketplaceEnabled: mcpMarketplaceEnabled,
mcpResponsesCollapsed: mcpResponsesCollapsed,
telemetrySetting: telemetrySetting || "unset",
planActSeparateModelsSetting,
enableCheckpointsSetting: enableCheckpointsSetting,
+5 -23
View File
@@ -73,7 +73,6 @@ import { parseMentions } from "@core/mentions"
import { formatResponse } from "@core/prompts/responses"
import { addUserInstructions, SYSTEM_PROMPT } from "@core/prompts/system"
import { sendPartialMessageEvent } from "@core/controller/ui/subscribeToPartialMessage"
import { sendRelinquishControlEvent } from "@core/controller/ui/subscribeToRelinquishControl"
import { convertClineMessageToProto } from "@shared/proto-conversions/cline-message"
import { getContextWindowInfo } from "@core/context/context-management/context-window-utils"
import { FileContextTracker } from "@core/context/context-tracking/FileContextTracker"
@@ -166,7 +165,6 @@ export class Task {
checkpointTrackerErrorMessage?: string
conversationHistoryDeletedRange?: [number, number]
isInitialized = false
private initTaskPromise?: Promise<void>
isAwaitingPlanResponse = false
didRespondToPlanAskBySwitchingMode = false
@@ -298,9 +296,9 @@ export class Task {
// Continue with task initialization
if (historyItem) {
this.initTaskPromise = this.resumeTaskFromHistory()
this.resumeTaskFromHistory()
} else if (task || images || files) {
this.initTaskPromise = this.startTask(task, images, files)
this.startTask(task, images, files)
}
// initialize telemetry
@@ -389,10 +387,6 @@ export class Task {
}
async restoreCheckpoint(messageTs: number, restoreType: ClineCheckpointRestore, offset?: number) {
if (this.initTaskPromise && !this.isInitialized) {
await this.initTaskPromise
}
const messageIndex = this.clineMessages.findIndex((m) => m.ts === messageTs) - (offset || 0)
// Find the last message before messageIndex that has a lastCheckpointHash
const lastHashIndex = findLastIndex(this.clineMessages.slice(0, messageIndex), (m) => m.lastCheckpointHash !== undefined)
@@ -518,21 +512,17 @@ export class Task {
await this.saveClineMessagesAndUpdateHistory()
sendRelinquishControlEvent()
await this.postMessageToWebview({ type: "relinquishControl" })
this.cancelTask() // the task is already cancelled by the provider beforehand, but we need to re-init to get the updated messages
} else {
sendRelinquishControlEvent()
await this.postMessageToWebview({ type: "relinquishControl" })
}
}
async presentMultifileDiff(messageTs: number, seeNewChangesSinceLastTaskCompletion: boolean) {
if (this.initTaskPromise && !this.isInitialized) {
await this.initTaskPromise
}
const relinquishButton = () => {
sendRelinquishControlEvent()
this.postMessageToWebview({ type: "relinquishControl" })
}
if (!this.enableCheckpoints) {
vscode.window.showInformationMessage("Checkpoints are disabled in settings. Cannot show diff.")
@@ -660,10 +650,6 @@ export class Task {
}
async doesLatestTaskCompletionHaveNewChanges() {
if (this.initTaskPromise && !this.isInitialized) {
await this.initTaskPromise
}
if (!this.enableCheckpoints) {
return false
}
@@ -1214,10 +1200,6 @@ export class Task {
// Checkpoints
async saveCheckpoint(isAttemptCompletionMessage: boolean = false) {
if (this.initTaskPromise && !this.isInitialized) {
await this.initTaskPromise
}
if (!this.enableCheckpoints) {
// If checkpoints are disabled, do nothing.
return
+10 -18
View File
@@ -1,7 +1,7 @@
import * as vscode from "vscode"
import * as path from "path"
import { listFiles } from "@services/glob/list-files"
import { sendWorkspaceUpdateEvent } from "@core/controller/file/subscribeToWorkspaceUpdates"
import { ExtensionMessage } from "@shared/ExtensionMessage"
const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0)
@@ -10,15 +10,8 @@ class WorkspaceTracker {
private disposables: vscode.Disposable[] = []
private filePaths: Set<string> = new Set()
private get activeFiles() {
return new Set(
vscode.window.tabGroups.activeTabGroup.tabs
.filter((tab) => tab.input instanceof vscode.TabInputText)
.map((tab) => (tab.input as vscode.TabInputText).uri.fsPath),
)
}
constructor() {
constructor(private readonly postMessageToWebview: (message: ExtensionMessage) => Promise<void>) {
this.postMessageToWebview = postMessageToWebview
this.registerListeners()
}
@@ -43,9 +36,6 @@ class WorkspaceTracker {
// Listen for file renaming
this.disposables.push(vscode.workspace.onDidRenameFiles(this.onFilesRenamed.bind(this)))
// Listen for tab groups changes
this.disposables.push(vscode.window.tabGroups.onDidChangeTabs(this.workspaceDidUpdate.bind(this)))
/*
An event that is emitted when a workspace folder is added or removed.
**Note:** this event will not fire if the first workspace folder is added, removed or changed,
@@ -90,15 +80,17 @@ class WorkspaceTracker {
this.workspaceDidUpdate()
}
private async workspaceDidUpdate() {
private workspaceDidUpdate() {
if (!cwd) {
return
}
const filePaths = Array.from(new Set([...this.activeFiles, ...this.filePaths])).map((file) => {
const relativePath = path.relative(cwd, file).toPosix()
return file.endsWith("/") ? relativePath + "/" : relativePath
this.postMessageToWebview({
type: "workspaceUpdated",
filePaths: Array.from(this.filePaths).map((file) => {
const relativePath = path.relative(cwd, file).toPosix()
return file.endsWith("/") ? relativePath + "/" : relativePath
}),
})
await sendWorkspaceUpdateEvent(filePaths)
}
private normalizeFilePath(filePath: string): string {
-11
View File
@@ -616,17 +616,6 @@ export class McpHub {
await this.notifyWebviewOfServerChanges()
}
async getLatestMcpServersRPC(): Promise<McpServer[]> {
const settings = await this.readAndValidateMcpSettingsFile()
if (!settings) {
// Return empty array if settings can't be read or validated
return []
}
const serverOrder = Object.keys(settings.mcpServers || {})
return this.getSortedMcpServers(serverOrder)
}
// Using server
// Public methods for server management
@@ -138,20 +138,17 @@ class TelemetryService {
if (globalTelemetryEnabled) {
this.telemetryEnabled = didUserOptIn
} else {
// Only show warning if user has opted in to Cline telemetry but VS Code telemetry is disabled
if (didUserOptIn) {
void vscode.window
.showWarningMessage(
"Anonymous Cline error and usage reporting is enabled, but VSCode telemetry is disabled. To enable error and usage reporting for this extension, enable VSCode telemetry in settings.",
"Open Settings",
)
.then((selection) => {
if (selection === "Open Settings") {
void vscode.commands.executeCommand("workbench.action.openSettings", "telemetry.telemetryLevel")
}
})
}
this.telemetryEnabled = false
// Show warning to user that global telemetry is disabled
void vscode.window
.showWarningMessage(
"VSCode telemetry is disabled. To enable telemetry for this extension, first enable VSCode telemetry in settings.",
"Open Settings",
)
.then((selection) => {
if (selection === "Open Settings") {
void vscode.commands.executeCommand("workbench.action.openSettings", "telemetry.telemetryLevel")
}
})
}
// Update PostHog client state based on telemetry preference
+19 -1
View File
@@ -17,13 +17,22 @@ export interface ExtensionMessage {
| "action"
| "state"
| "selectedImages"
| "ollamaModels"
| "lmStudioModels"
| "workspaceUpdated"
| "openRouterModels"
| "openAiModels"
| "requestyModels"
| "mcpServers"
| "relinquishControl"
| "mcpDownloadDetails"
| "commitSearchResults"
| "openGraphData"
| "didUpdateSettings"
| "userCreditsBalance"
| "userCreditsUsage"
| "userCreditsPayments"
| "fileSearchResults"
| "grpc_response" // New type for gRPC responses
text?: string
action?: "didBecomeVisible" | "accountLogoutClicked" | "focusChatInput"
@@ -33,6 +42,8 @@ export interface ExtensionMessage {
ollamaModels?: string[]
lmStudioModels?: string[]
vsCodeLmModels?: { vendor?: string; family?: string; version?: string; id?: string }[]
filePaths?: string[]
openRouterModels?: Record<string, ModelInfo>
openAiModels?: string[]
requestyModels?: Record<string, ModelInfo>
mcpServers?: McpServer[]
@@ -41,6 +52,14 @@ export interface ExtensionMessage {
error?: string
mcpDownloadDetails?: McpDownloadResponse
commits?: GitCommit[]
openGraphData?: {
title?: string
description?: string
image?: string
url?: string
siteName?: string
type?: string
}
url?: string
isImage?: boolean
userCreditsBalance?: BalanceResponse
@@ -106,7 +125,6 @@ export interface ExtensionState {
globalWorkflowToggles: ClineRulesToggles
localCursorRulesToggles: ClineRulesToggles
localWindsurfRulesToggles: ClineRulesToggles
mcpResponsesCollapsed?: boolean
}
export interface ClineMessage {
+11 -1
View File
@@ -9,15 +9,24 @@ import { McpViewTab } from "./mcp"
export interface WebviewMessage {
type:
| "apiConfiguration"
| "webviewDidLaunch"
| "newTask"
| "condense"
| "reportBug"
| "requestVsCodeLmModels"
| "authStateChanged"
| "fetchMcpMarketplace"
| "searchCommits"
| "fetchLatestMcpServersFromHub"
| "telemetrySetting"
| "updateSettings"
| "clearAllTaskHistory"
| "fetchUserCreditsData"
| "searchFiles"
| "grpc_request"
| "grpc_request_cancel"
| "toggleWorkflow"
| "executeQuickWin"
text?: string
disabled?: boolean
@@ -44,7 +53,6 @@ export interface WebviewMessage {
planActSeparateModelsSetting?: boolean
enableCheckpointsSetting?: boolean
mcpMarketplaceEnabled?: boolean
mcpResponsesCollapsed?: boolean
telemetrySetting?: TelemetrySetting
customInstructionsSetting?: string
mentionsRequestId?: string
@@ -68,6 +76,8 @@ export interface WebviewMessage {
enabled?: boolean
filename?: string
payload?: { command: string; title: string }
offset?: number
shellIntegrationTimeout?: number
terminalReuseEnabled?: boolean
-48
View File
@@ -575,30 +575,6 @@ export const vertexModels = {
},
],
},
"gemini-2.5-pro-preview-06-05": {
maxTokens: 65536,
contextWindow: 1_048_576,
supportsImages: true,
supportsPromptCache: true,
supportsGlobalEndpoint: true,
inputPrice: 2.5,
outputPrice: 15,
cacheReadsPrice: 0.31,
tiers: [
{
contextWindow: 200000,
inputPrice: 1.25,
outputPrice: 10,
cacheReadsPrice: 0.31,
},
{
contextWindow: Infinity,
inputPrice: 2.5,
outputPrice: 15,
cacheReadsPrice: 0.625,
},
],
},
"gemini-2.5-flash-preview-04-17": {
maxTokens: 65536,
contextWindow: 1_048_576,
@@ -743,30 +719,6 @@ export const geminiModels = {
},
],
},
"gemini-2.5-pro-preview-06-05": {
maxTokens: 65536,
contextWindow: 1_048_576,
supportsImages: true,
supportsPromptCache: true,
supportsGlobalEndpoint: true,
inputPrice: 2.5,
outputPrice: 15,
cacheReadsPrice: 0.31,
tiers: [
{
contextWindow: 200000,
inputPrice: 1.25,
outputPrice: 10,
cacheReadsPrice: 0.31,
},
{
contextWindow: Infinity,
inputPrice: 2.5,
outputPrice: 15,
cacheReadsPrice: 0.625,
},
],
},
"gemini-2.5-flash-preview-05-20": {
maxTokens: 65536,
contextWindow: 1_048_576,
@@ -1,263 +0,0 @@
import { ApiConfiguration } from "@shared/api"
import { ChatSettings } from "@shared/ChatSettings"
import {
ApiConfiguration as ProtoApiConfiguration,
ChatSettings as ProtoChatSettings,
PlanActMode,
} from "../../../shared/proto/state"
/**
* Converts domain ApiConfiguration objects to proto ApiConfiguration objects
*/
export function convertApiConfigurationToProtoApiConfiguration(config: ApiConfiguration): ProtoApiConfiguration {
return ProtoApiConfiguration.create({
// Core API fields
apiProvider: config.apiProvider,
apiModelId: config.apiModelId,
apiKey: config.apiKey,
// Provider-specific API keys
clineApiKey: config.clineApiKey,
openrouterApiKey: config.openRouterApiKey,
anthropicBaseUrl: config.anthropicBaseUrl,
openaiApiKey: config.openAiApiKey,
openaiNativeApiKey: config.openAiNativeApiKey,
geminiApiKey: config.geminiApiKey,
deepseekApiKey: config.deepSeekApiKey,
requestyApiKey: config.requestyApiKey,
togetherApiKey: config.togetherApiKey,
fireworksApiKey: config.fireworksApiKey,
qwenApiKey: config.qwenApiKey,
doubaoApiKey: config.doubaoApiKey,
mistralApiKey: config.mistralApiKey,
nebiusApiKey: config.nebiusApiKey,
asksageApiKey: config.asksageApiKey,
xaiApiKey: config.xaiApiKey,
sambanovaApiKey: config.sambanovaApiKey,
cerebrasApiKey: config.cerebrasApiKey,
// Model IDs - each provider has its own field
openrouterModelId: config.openRouterModelId,
openaiModelId: config.openAiModelId,
anthropicModelId: config.apiModelId,
bedrockModelId: config.apiModelId,
vertexModelId: config.apiModelId,
geminiModelId: config.apiModelId,
ollamaModelId: config.ollamaModelId,
lmStudioModelId: config.lmStudioModelId,
litellmModelId: config.liteLlmModelId,
requestyModelId: config.requestyModelId,
togetherModelId: config.togetherModelId,
fireworksModelId: config.fireworksModelId,
// AWS Bedrock fields
awsBedrockCustomSelected: config.awsBedrockCustomSelected,
awsBedrockCustomModelBaseId: config.awsBedrockCustomModelBaseId,
awsAccessKey: config.awsAccessKey,
awsSecretKey: config.awsSecretKey,
awsSessionToken: config.awsSessionToken,
awsRegion: config.awsRegion,
awsUseCrossRegionInference: config.awsUseCrossRegionInference,
awsBedrockUsePromptCache: config.awsBedrockUsePromptCache,
awsUseProfile: config.awsUseProfile,
awsProfile: config.awsProfile,
awsBedrockEndpoint: config.awsBedrockEndpoint,
// Vertex AI fields
vertexProjectId: config.vertexProjectId,
vertexRegion: config.vertexRegion,
// Base URLs and endpoints
openaiBaseUrl: config.openAiBaseUrl,
ollamaBaseUrl: config.ollamaBaseUrl,
lmStudioBaseUrl: config.lmStudioBaseUrl,
geminiBaseUrl: config.geminiBaseUrl,
litellmBaseUrl: config.liteLlmBaseUrl,
asksageApiUrl: config.asksageApiUrl,
// LiteLLM specific fields
litellmApiKey: config.liteLlmApiKey,
litellmUsePromptCache: config.liteLlmUsePromptCache,
// Model configuration
thinkingBudgetTokens: config.thinkingBudgetTokens ? Number(config.thinkingBudgetTokens) : undefined,
reasoningEffort: config.reasoningEffort,
requestTimeoutMs: config.requestTimeoutMs ? Number(config.requestTimeoutMs) : undefined,
// Fireworks specific
fireworksModelMaxCompletionTokens: config.fireworksModelMaxCompletionTokens
? Number(config.fireworksModelMaxCompletionTokens)
: undefined,
fireworksModelMaxTokens: config.fireworksModelMaxTokens ? Number(config.fireworksModelMaxTokens) : undefined,
// Azure specific
azureApiVersion: config.azureApiVersion,
// Ollama specific
ollamaApiOptionsCtxNum: config.ollamaApiOptionsCtxNum,
// Qwen specific
qwenApiLine: config.qwenApiLine,
// OpenRouter specific
openrouterProviderSorting: config.openRouterProviderSorting,
// Complex objects stored as JSON strings
vscodeLmModelSelector: config.vsCodeLmModelSelector ? JSON.stringify(config.vsCodeLmModelSelector) : undefined,
openrouterModelInfo: config.openRouterModelInfo ? JSON.stringify(config.openRouterModelInfo) : undefined,
openaiModelInfo: config.openAiModelInfo ? JSON.stringify(config.openAiModelInfo) : undefined,
requestyModelInfo: config.requestyModelInfo ? JSON.stringify(config.requestyModelInfo) : undefined,
litellmModelInfo: config.liteLlmModelInfo ? JSON.stringify(config.liteLlmModelInfo) : undefined,
openaiHeaders: config.openAiHeaders ? JSON.stringify(config.openAiHeaders) : undefined,
// Arrays
favoritedModelIds: config.favoritedModelIds || [],
})
}
/**
* Converts proto ApiConfiguration objects to domain ApiConfiguration objects
*/
export function convertProtoApiConfigurationToApiConfiguration(protoConfig: ProtoApiConfiguration): ApiConfiguration {
// eslint-disable-next-line eslint-rules/no-protobuf-object-literals
const config: ApiConfiguration = {
// Core API fields
apiProvider: protoConfig.apiProvider as any,
apiModelId: protoConfig.apiModelId,
apiKey: protoConfig.apiKey,
// Provider-specific API keys
clineApiKey: protoConfig.clineApiKey,
openRouterApiKey: protoConfig.openrouterApiKey,
anthropicBaseUrl: protoConfig.anthropicBaseUrl,
openAiApiKey: protoConfig.openaiApiKey,
openAiNativeApiKey: protoConfig.openaiNativeApiKey,
geminiApiKey: protoConfig.geminiApiKey,
deepSeekApiKey: protoConfig.deepseekApiKey,
requestyApiKey: protoConfig.requestyApiKey,
togetherApiKey: protoConfig.togetherApiKey,
fireworksApiKey: protoConfig.fireworksApiKey,
qwenApiKey: protoConfig.qwenApiKey,
doubaoApiKey: protoConfig.doubaoApiKey,
mistralApiKey: protoConfig.mistralApiKey,
nebiusApiKey: protoConfig.nebiusApiKey,
asksageApiKey: protoConfig.asksageApiKey,
xaiApiKey: protoConfig.xaiApiKey,
sambanovaApiKey: protoConfig.sambanovaApiKey,
cerebrasApiKey: protoConfig.cerebrasApiKey,
// Model IDs
openRouterModelId: protoConfig.openrouterModelId,
openAiModelId: protoConfig.openaiModelId,
ollamaModelId: protoConfig.ollamaModelId,
lmStudioModelId: protoConfig.lmStudioModelId,
liteLlmModelId: protoConfig.litellmModelId,
requestyModelId: protoConfig.requestyModelId,
togetherModelId: protoConfig.togetherModelId,
fireworksModelId: protoConfig.fireworksModelId,
// AWS Bedrock fields
awsBedrockCustomSelected: protoConfig.awsBedrockCustomSelected,
awsBedrockCustomModelBaseId: protoConfig.awsBedrockCustomModelBaseId as any,
awsAccessKey: protoConfig.awsAccessKey,
awsSecretKey: protoConfig.awsSecretKey,
awsSessionToken: protoConfig.awsSessionToken,
awsRegion: protoConfig.awsRegion,
awsUseCrossRegionInference: protoConfig.awsUseCrossRegionInference,
awsBedrockUsePromptCache: protoConfig.awsBedrockUsePromptCache,
awsUseProfile: protoConfig.awsUseProfile,
awsProfile: protoConfig.awsProfile,
awsBedrockEndpoint: protoConfig.awsBedrockEndpoint,
// Vertex AI fields
vertexProjectId: protoConfig.vertexProjectId,
vertexRegion: protoConfig.vertexRegion,
// Base URLs and endpoints
openAiBaseUrl: protoConfig.openaiBaseUrl,
ollamaBaseUrl: protoConfig.ollamaBaseUrl,
lmStudioBaseUrl: protoConfig.lmStudioBaseUrl,
geminiBaseUrl: protoConfig.geminiBaseUrl,
liteLlmBaseUrl: protoConfig.litellmBaseUrl,
asksageApiUrl: protoConfig.asksageApiUrl,
// LiteLLM specific fields
liteLlmApiKey: protoConfig.litellmApiKey,
liteLlmUsePromptCache: protoConfig.litellmUsePromptCache,
// Model configuration
thinkingBudgetTokens: protoConfig.thinkingBudgetTokens ? Number(protoConfig.thinkingBudgetTokens) : undefined,
reasoningEffort: protoConfig.reasoningEffort,
requestTimeoutMs: protoConfig.requestTimeoutMs ? Number(protoConfig.requestTimeoutMs) : undefined,
// Fireworks specific
fireworksModelMaxCompletionTokens: protoConfig.fireworksModelMaxCompletionTokens
? Number(protoConfig.fireworksModelMaxCompletionTokens)
: undefined,
fireworksModelMaxTokens: protoConfig.fireworksModelMaxTokens ? Number(protoConfig.fireworksModelMaxTokens) : undefined,
// Azure specific
azureApiVersion: protoConfig.azureApiVersion,
// Ollama specific
ollamaApiOptionsCtxNum: protoConfig.ollamaApiOptionsCtxNum,
// Qwen specific
qwenApiLine: protoConfig.qwenApiLine,
// OpenRouter specific
openRouterProviderSorting: protoConfig.openrouterProviderSorting,
// Arrays
favoritedModelIds: protoConfig.favoritedModelIds || [],
}
// Handle complex JSON objects
try {
if (protoConfig.vscodeLmModelSelector) {
config.vsCodeLmModelSelector = JSON.parse(protoConfig.vscodeLmModelSelector)
}
if (protoConfig.openrouterModelInfo) {
config.openRouterModelInfo = JSON.parse(protoConfig.openrouterModelInfo)
}
if (protoConfig.openaiModelInfo) {
config.openAiModelInfo = JSON.parse(protoConfig.openaiModelInfo)
}
if (protoConfig.requestyModelInfo) {
config.requestyModelInfo = JSON.parse(protoConfig.requestyModelInfo)
}
if (protoConfig.litellmModelInfo) {
config.liteLlmModelInfo = JSON.parse(protoConfig.litellmModelInfo)
}
if (protoConfig.openaiHeaders) {
config.openAiHeaders = JSON.parse(protoConfig.openaiHeaders)
}
} catch (error) {
console.error("Failed to parse complex JSON objects in API configuration:", error)
}
return config
}
/**
* Converts domain ChatSettings objects to proto ChatSettings objects
*/
export function convertChatSettingsToProtoChatSettings(chatSettings: ChatSettings): ProtoChatSettings {
return ProtoChatSettings.create({
mode: chatSettings.mode === "plan" ? PlanActMode.PLAN : PlanActMode.ACT,
preferredLanguage: chatSettings.preferredLanguage,
openAiReasoningEffort: chatSettings.openAIReasoningEffort,
})
}
/**
* Converts proto ChatSettings objects to domain ChatSettings objects
*/
export function convertProtoChatSettingsToChatSettings(protoChatSettings: ProtoChatSettings): ChatSettings {
// eslint-disable-next-line eslint-rules/no-protobuf-object-literals
return {
mode: protoChatSettings.mode === PlanActMode.PLAN ? "plan" : "act",
preferredLanguage: protoChatSettings.preferredLanguage,
openAIReasoningEffort: protoChatSettings.openAiReasoningEffort as "low" | "medium" | "high" | undefined,
}
}
@@ -0,0 +1,11 @@
// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
// Generated by proto/build-proto.js
import { createGrpcClient } from "./host-grpc-client-base"
import { UriServiceDefinition } from "@shared/proto/host/uri"
import { WatchServiceDefinition } from "@shared/proto/host/watch"
const UriServiceClient = createGrpcClient(UriServiceDefinition)
const WatchServiceClient = createGrpcClient(WatchServiceDefinition)
export { UriServiceClient, WatchServiceClient }
+4 -3
View File
@@ -32,11 +32,12 @@ function main() {
const host = "127.0.0.1:50051"
server.bindAsync(host, grpc.ServerCredentials.createInsecure(), (err) => {
if (err) {
log(`Error: Failed to bind to ${host}, port may be unavailable. ${err.message}`)
log(`Error: Failed to bind to ${host}, port may be unavailable ${err.message}`)
process.exit(1)
} else {
server.start()
log(`gRPC server listening on ${host}`)
}
server.start()
log(`gRPC server listening on ${host}`)
})
}
+1 -1
View File
@@ -15,7 +15,7 @@ const outputChannel: vscode.OutputChannel = {
}
function postMessage(message: ExtensionMessage): Promise<boolean> {
log("postMessage stub called:", JSON.stringify(message).slice(0, 200))
log("postMessage stub called:", message)
return Promise.resolve(true)
}
+2 -11
View File
@@ -1,22 +1,13 @@
import { URI } from "vscode-uri"
import path from "path"
import { mkdirSync } from "fs"
import type { Extension, ExtensionContext } from "vscode"
import { ExtensionKind, ExtensionMode } from "vscode"
import { outputChannel, postMessage } from "./vscode-context-stubs"
import { EnvironmentVariableCollection, MementoStore, readJson, SecretStore } from "./vscode-context-utils"
import { log } from "./utils"
if (!process.env.CLINE_DIR) {
console.warn("Environment variable CLINE_DIR was not set.")
process.exit(1)
}
const DATA_DIR = path.join(process.env.CLINE_DIR, "data")
mkdirSync(DATA_DIR, { recursive: true })
log("Using settings dir:", DATA_DIR)
const EXTENSION_DIR = path.join(process.env.CLINE_DIR, "core")
const DATA_DIR = process.env.DATA_DIR ?? "."
const EXTENSION_DIR = process.env.EXTENSION_DIR ?? "."
const EXTENSION_MODE = process.env.IS_DEV === "true" ? ExtensionMode.Development : ExtensionMode.Production
const extension: Extension<void> = {
+1 -1
View File
@@ -1,5 +1,5 @@
{
"name": "cline-standalone",
"name": "Cline standalone",
"version": "1.0.0",
"main": "standalone.js",
"dependencies": {
@@ -53,7 +53,6 @@ vscode.window = {
tabGroups: {
all: [],
close: async () => {},
onDidChangeTabs: createStub("vscode.env.tabGroups.onDidChangeTabs"),
},
withProgress: async (_options, task) => {
console.log("Stubbed withProgress")
@@ -816,7 +816,7 @@ vscode.TextDocumentSaveReason = { Manual: 0, AfterDelay: 0, FocusOut: 0 }
vscode.workspace = {}
vscode.workspace.fs = createStub("vscode.workspace.fs")
vscode.workspace.rootPath = createStub("vscode.workspace.rootPath")
vscode.workspace.workspaceFolders = []
vscode.workspace.workspaceFolders = createStub("vscode.workspace.workspaceFolders")
vscode.workspace.name = createStub("vscode.workspace.name")
vscode.workspace.workspaceFile = createStub("vscode.workspace.workspaceFile")
vscode.workspace.onDidChangeWorkspaceFolders = createStub("vscode.workspace.onDidChangeWorkspaceFolders")
@@ -894,22 +894,10 @@ vscode.workspace.onWillDeleteFiles = createStub("vscode.workspace.onWillDeleteFi
vscode.workspace.onDidDeleteFiles = createStub("vscode.workspace.onDidDeleteFiles")
vscode.workspace.onWillRenameFiles = createStub("vscode.workspace.onWillRenameFiles")
vscode.workspace.onDidRenameFiles = createStub("vscode.workspace.onDidRenameFiles")
const workspaceConfigStore = {}
vscode.workspace.getConfiguration = function (section) {
return {
get: (key, defaultValue) => {
return workspaceConfigStore[`${section}.${key}`] ?? defaultValue
},
update: (key, value, global) => {
workspaceConfigStore[`${section}.${key}`] = value
},
has: (key) => {
return `${section}.${key}` in workspaceConfigStore
},
}
vscode.workspace.getConfiguration = function (section, scope) {
console.log("Called stubbed function: vscode.workspace.getConfiguration")
return createStub("unknown")
}
vscode.workspace.onDidChangeConfiguration = createStub("vscode.workspace.onDidChangeConfiguration")
vscode.workspace.registerTaskProvider = function (type, provider) {
console.log("Called stubbed function: vscode.workspace.registerTaskProvider")
+12 -7
View File
@@ -237,7 +237,7 @@ export const ChatRowContent = ({
sendMessageFromChatRow,
onSetQuote,
}: ChatRowContentProps) => {
const { mcpServers, mcpMarketplaceCatalog, onRelinquishControl } = useExtensionState()
const { mcpServers, mcpMarketplaceCatalog } = useExtensionState()
const [seeNewChangesDisabled, setSeeNewChangesDisabled] = useState(false)
const [quoteButtonState, setQuoteButtonState] = useState<QuoteButtonState>({
visible: false,
@@ -269,12 +269,17 @@ export const ChatRowContent = ({
const type = message.type === "ask" ? message.ask : message.say
// Use the onRelinquishControl hook instead of message event
useEffect(() => {
return onRelinquishControl(() => {
setSeeNewChangesDisabled(false)
})
}, [onRelinquishControl])
const handleMessage = useCallback((event: MessageEvent) => {
const message: ExtensionMessage = event.data
switch (message.type) {
case "relinquishControl": {
setSeeNewChangesDisabled(false)
break
}
}
}, [])
useEvent("message", handleMessage)
// --- Quote Button Logic ---
// MOVE handleQuoteClick INSIDE ChatRowContent
@@ -10,7 +10,6 @@ import { FileServiceClient, StateServiceClient } from "@/services/grpc-client"
import {
ContextMenuOptionType,
getContextMenuOptions,
getContextMenuOptionIndex,
insertMention,
insertMentionDirectly,
removeMention,
@@ -61,9 +60,6 @@ const getImageDimensions = (dataUrl: string): Promise<{ width: number; height: n
})
}
// Set to "File" option by default
const DEFAULT_CONTEXT_MENU_OPTION = getContextMenuOptionIndex(ContextMenuOptionType.File)
interface ChatTextAreaProps {
inputValue: string
activeQuote: string | null
@@ -345,6 +341,22 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
}
}, [selectedType, searchQuery])
const handleMessage = useCallback((event: MessageEvent) => {
const message: ExtensionMessage = event.data
switch (message.type) {
case "fileSearchResults": {
// Only update results if they match the current query or if there's no mentionsRequestId - better UX
if (!message.mentionsRequestId || message.mentionsRequestId === currentSearchQueryRef.current) {
setFileSearchResults(message.results || [])
setSearchLoading(false)
}
break
}
}
}, [])
useEvent("message", handleMessage)
const queryItems = useMemo(() => {
return [
{ type: ContextMenuOptionType.Problems, value: "problems" },
@@ -518,7 +530,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
if (event.key === "Escape") {
// event.preventDefault()
setSelectedType(null)
setSelectedMenuIndex(DEFAULT_CONTEXT_MENU_OPTION)
setSelectedMenuIndex(3) // File by default
return
}
@@ -758,7 +770,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
})
}, 200) // 200ms debounce
} else {
setSelectedMenuIndex(DEFAULT_CONTEXT_MENU_OPTION)
setSelectedMenuIndex(3) // Set to "File" option by default
}
} else {
setSearchQuery("")
@@ -3,16 +3,12 @@ import { useClickAway, useWindowSize } from "react-use"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { CODE_BLOCK_BG_COLOR } from "@/components/common/CodeBlock"
import ServersToggleList from "@/components/mcp/configuration/tabs/installed/ServersToggleList"
import { McpServiceClient } from "@/services/grpc-client"
import { vscode } from "@/utils/vscode"
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
import Tooltip from "@/components/common/Tooltip"
import { McpServers } from "@shared/proto/mcp"
import { convertProtoMcpServersToMcpServers } from "@shared/proto-conversions/mcp/mcp-server-conversion"
import { EmptyRequest } from "@shared/proto/common"
const ServersToggleModal: React.FC = () => {
const { mcpServers, navigateToMcp, setMcpServers } = useExtensionState()
const { mcpServers, navigateToMcp } = useExtensionState()
const [isVisible, setIsVisible] = useState(false)
const buttonRef = useRef<HTMLDivElement>(null)
const modalRef = useRef<HTMLDivElement>(null)
@@ -39,16 +35,7 @@ const ServersToggleModal: React.FC = () => {
useEffect(() => {
if (isVisible) {
McpServiceClient.getLatestMcpServers(EmptyRequest.create({}))
.then((response: McpServers) => {
if (response.mcpServers) {
const mcpServers = convertProtoMcpServersToMcpServers(response.mcpServers)
setMcpServers(mcpServers)
}
})
.catch((error) => {
console.error("Failed to fetch MCP servers:", error)
})
vscode.postMessage({ type: "fetchLatestMcpServersFromHub" })
}
}, [isVisible])
@@ -10,7 +10,6 @@ import {
ToggleClineRuleRequest,
ToggleCursorRuleRequest,
ToggleWindsurfRuleRequest,
ToggleWorkflowRequest,
} from "@shared/proto/file"
import { VSCodeButton, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
import React, { useEffect, useRef, useState } from "react"
@@ -139,12 +138,10 @@ const ClineRulesToggleModal: React.FC = () => {
}
const toggleWindsurfRule = (rulePath: string, enabled: boolean) => {
FileServiceClient.toggleWindsurfRule(
ToggleWindsurfRuleRequest.create({
rulePath,
enabled,
} as ToggleWindsurfRuleRequest),
)
FileServiceClient.toggleWindsurfRule({
rulePath,
enabled,
} as ToggleWindsurfRuleRequest)
.then((response: ClineRulesToggles) => {
if (response.toggles) {
setLocalWindsurfRulesToggles(response.toggles)
@@ -156,25 +153,12 @@ const ClineRulesToggleModal: React.FC = () => {
}
const toggleWorkflow = (isGlobal: boolean, workflowPath: string, enabled: boolean) => {
FileServiceClient.toggleWorkflow(
ToggleWorkflowRequest.create({
workflowPath,
enabled,
isGlobal,
}),
)
.then((response) => {
if (response.toggles) {
if (isGlobal) {
setGlobalWorkflowToggles(response.toggles)
} else {
setLocalWorkflowToggles(response.toggles)
}
}
})
.catch((err: Error) => {
console.error("Failed to toggle workflow:", err)
})
vscode.postMessage({
type: "toggleWorkflow",
workflowPath,
enabled,
isGlobal,
})
}
// Close modal when clicking outside
@@ -1,14 +1,15 @@
import { CODE_BLOCK_BG_COLOR } from "@/components/common/CodeBlock"
import { CheckpointsServiceClient } from "@/services/grpc-client"
import { flip, offset, shift, useFloating } from "@floating-ui/react"
import { ExtensionMessage } from "@shared/ExtensionMessage"
import { CheckpointRestoreRequest } from "@shared/proto/checkpoints"
import { Int64Request } from "@shared/proto/common"
import { ClineCheckpointRestore } from "@shared/WebviewMessage"
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
import { useEffect, useRef, useState } from "react"
import { useCallback, useEffect, useRef, useState } from "react"
import { createPortal } from "react-dom"
import { useEvent } from "react-use"
import styled from "styled-components"
import { useExtensionState } from "@/context/ExtensionStateContext"
interface CheckmarkControlProps {
messageTs?: number
@@ -24,7 +25,6 @@ export const CheckmarkControl = ({ messageTs, isCheckpointCheckedOut }: Checkmar
const [hasMouseEntered, setHasMouseEntered] = useState(false)
const containerRef = useRef<HTMLDivElement>(null)
const tooltipRef = useRef<HTMLDivElement>(null)
const { onRelinquishControl } = useExtensionState()
const { refs, floatingStyles, update, placement } = useFloating({
placement: "bottom-end",
@@ -52,16 +52,15 @@ export const CheckmarkControl = ({ messageTs, isCheckpointCheckedOut }: Checkmar
}
}, [showRestoreConfirm, update])
// Use the onRelinquishControl hook instead of message event
useEffect(() => {
return onRelinquishControl(() => {
const handleMessage = useCallback((event: MessageEvent<ExtensionMessage>) => {
if (event.data.type === "relinquishControl") {
setCompareDisabled(false)
setRestoreTaskDisabled(false)
setRestoreWorkspaceDisabled(false)
setRestoreBothDisabled(false)
setShowRestoreConfirm(false)
})
}, [onRelinquishControl])
}
}, [])
const handleRestoreTask = async () => {
setRestoreTaskDisabled(true)
@@ -142,6 +141,8 @@ export const CheckmarkControl = ({ messageTs, isCheckpointCheckedOut }: Checkmar
setHasMouseEntered(false)
}
useEvent("message", handleMessage)
return (
<Container isMenuOpen={showRestoreConfirm} $isCheckedOut={isCheckpointCheckedOut} onMouseLeave={handleControlsMouseLeave}>
<i
@@ -1,12 +1,12 @@
import { CODE_BLOCK_BG_COLOR } from "@/components/common/CodeBlock"
import { CheckpointsServiceClient } from "@/services/grpc-client"
import { ExtensionMessage } from "@shared/ExtensionMessage"
import { CheckpointRestoreRequest } from "@shared/proto/checkpoints"
import { Int64Request } from "@shared/proto/common"
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
import { useEffect, useRef, useState } from "react"
import { useClickAway } from "react-use"
import { useCallback, useRef, useState } from "react"
import { useClickAway, useEvent } from "react-use"
import styled from "styled-components"
import { useExtensionState } from "@/context/ExtensionStateContext"
interface CheckpointOverlayProps {
messageTs?: number
@@ -21,7 +21,6 @@ export const CheckpointOverlay = ({ messageTs }: CheckpointOverlayProps) => {
const [hasMouseEntered, setHasMouseEntered] = useState(false)
const containerRef = useRef<HTMLDivElement>(null)
const tooltipRef = useRef<HTMLDivElement>(null)
const { onRelinquishControl } = useExtensionState()
useClickAway(containerRef, () => {
if (showRestoreConfirm) {
@@ -30,16 +29,21 @@ export const CheckpointOverlay = ({ messageTs }: CheckpointOverlayProps) => {
}
})
// Use the onRelinquishControl hook instead of message event
useEffect(() => {
return onRelinquishControl(() => {
setCompareDisabled(false)
setRestoreTaskDisabled(false)
setRestoreWorkspaceDisabled(false)
setRestoreBothDisabled(false)
setShowRestoreConfirm(false)
})
}, [onRelinquishControl])
const handleMessage = useCallback((event: MessageEvent) => {
const message: ExtensionMessage = event.data
switch (message.type) {
case "relinquishControl": {
setCompareDisabled(false)
setRestoreTaskDisabled(false)
setRestoreWorkspaceDisabled(false)
setRestoreBothDisabled(false)
setShowRestoreConfirm(false)
break
}
}
}, [])
useEvent("message", handleMessage)
const handleRestoreTask = async () => {
setRestoreTaskDisabled(true)
@@ -40,7 +40,7 @@ const CodeAccordian = ({
const numberOfEdits = useMemo(() => {
if (code) {
return (code.match(/[-]{3,} SEARCH/g) || []).length || undefined
return (code.match(/\+{7} REPLACE/g) || []).length || undefined
}
return undefined
}, [code])
@@ -49,7 +49,7 @@ const CustomFilterRadio = ({ checked, onChange, icon, label }: CustomFilterRadio
const HistoryView = ({ onDone }: HistoryViewProps) => {
const extensionStateContext = useExtensionState()
const { taskHistory, filePaths, onRelinquishControl } = extensionStateContext
const { taskHistory, filePaths } = extensionStateContext
const [searchQuery, setSearchQuery] = useState("")
const [sortOption, setSortOption] = useState<SortOption>("newest")
const [lastNonRelevantSort, setLastNonRelevantSort] = useState<SortOption | null>("newest")
@@ -130,12 +130,12 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
[showFavoritesOnly, loadTaskHistory],
)
// Use the onRelinquishControl hook instead of message event
useEffect(() => {
return onRelinquishControl(() => {
const handleMessage = useCallback((event: MessageEvent<ExtensionMessage>) => {
if (event.data.type === "relinquishControl") {
setDeleteAllDisabled(false)
})
}, [onRelinquishControl])
}
}, [])
useEvent("message", handleMessage)
const { totalTasksSize, setTotalTasksSize } = extensionStateContext
@@ -1,6 +1,4 @@
import React, { useEffect, useState, useCallback } from "react"
import { VSCodeProgressRing } from "@vscode/webview-ui-toolkit/react" // Import ProgressRing
import { useExtensionState } from "../../../context/ExtensionStateContext"
import LinkPreview from "./LinkPreview"
import ImagePreview from "./ImagePreview"
import styled from "styled-components"
@@ -30,10 +28,6 @@ const ResponseHeader = styled.div`
text-overflow: ellipsis;
margin-right: 8px;
}
.header-icon {
margin-right: 6px;
}
`
const ToggleSwitch = styled.div`
@@ -117,9 +111,7 @@ interface UrlMatch {
}
const McpResponseDisplay: React.FC<McpResponseDisplayProps> = ({ responseText }) => {
const { mcpResponsesCollapsed } = useExtensionState() // Get setting from context
const [isExpanded, setIsExpanded] = useState(!mcpResponsesCollapsed) // Initialize with context setting
const [isLoading, setIsLoading] = useState(false) // Initial loading state for rich content
const [isLoading, setIsLoading] = useState(true)
const [displayMode, setDisplayMode] = useState<"rich" | "plain">(() => {
// Get saved preference from localStorage, default to 'rich'
const savedMode = localStorage.getItem("mcpDisplayMode")
@@ -132,11 +124,14 @@ const McpResponseDisplay: React.FC<McpResponseDisplayProps> = ({ responseText })
const toggleDisplayMode = useCallback(() => {
const newMode = displayMode === "rich" ? "plain" : "rich"
// Force an immediate re-render
setForceUpdateCounter((prev) => prev + 1)
// Update display mode and save preference
setDisplayMode(newMode)
localStorage.setItem("mcpDisplayMode", newMode)
// If switching to plain mode, cancel any ongoing processing
if (newMode === "plain") {
console.log("Switching to plain mode - cancelling URL processing")
@@ -144,23 +139,13 @@ const McpResponseDisplay: React.FC<McpResponseDisplayProps> = ({ responseText })
} else {
// If switching to rich mode, the useEffect will re-run and fetch data
console.log("Switching to rich mode - will start URL processing")
setUrlMatches([])
}
}, [displayMode])
const toggleExpand = useCallback(() => {
setIsExpanded((prev) => !prev)
}, [])
// Effect to update isExpanded if mcpResponsesCollapsed changes from context
useEffect(() => {
setIsExpanded(!mcpResponsesCollapsed)
}, [])
// Find all URLs in the text and determine if they're images
useEffect(() => {
// Skip all processing if in plain mode
if (!isExpanded || displayMode === "plain") {
if (displayMode === "plain") {
setIsLoading(false)
setUrlMatches([]) // Clear any existing matches when in plain mode
return
@@ -168,10 +153,12 @@ const McpResponseDisplay: React.FC<McpResponseDisplayProps> = ({ responseText })
// Use a direct boolean for cancellation that's scoped to this effect run
let processingCanceled = false
const processResponse = async () => {
console.log("Processing MCP response for URL extraction")
setIsLoading(true)
setError(null)
try {
const text = responseText || ""
const matches: UrlMatch[] = []
@@ -280,24 +267,12 @@ const McpResponseDisplay: React.FC<McpResponseDisplayProps> = ({ responseText })
processingCanceled = true
console.log("Cleaning up URL processing")
}
}, [responseText, displayMode, forceUpdateCounter, isExpanded])
}, [responseText, displayMode, forceUpdateCounter])
// Function to render content based on display mode
const renderContent = () => {
if (!isExpanded) {
return null // Don't render content if not expanded
}
if (isLoading && displayMode === "rich") {
return (
<div style={{ display: "flex", justifyContent: "center", alignItems: "center", height: "50px" }}>
<VSCodeProgressRing />
</div>
)
}
// For plain text mode, just show the text
if (displayMode === "plain") {
if (displayMode === "plain" || isLoading) {
return <UrlText>{responseText}</UrlText>
}
@@ -312,7 +287,7 @@ const McpResponseDisplay: React.FC<McpResponseDisplayProps> = ({ responseText })
}
// For rich display mode, show the text with embedded content
if (displayMode === "rich") {
if (!isLoading) {
// We already know displayMode is "rich" if we get here
// Create an array of text segments and embedded content
const segments: JSX.Element[] = []
@@ -410,48 +385,30 @@ const McpResponseDisplay: React.FC<McpResponseDisplayProps> = ({ responseText })
try {
return (
<ResponseContainer>
<ResponseHeader
onClick={toggleExpand}
style={{
borderBottom: isExpanded ? "1px dashed var(--vscode-editorGroup-border)" : "none",
marginBottom: isExpanded ? "8px" : "0px",
}}>
<div className="header-title">
<span className={`codicon codicon-chevron-${isExpanded ? "down" : "right"} header-icon`}></span>
Response
</div>
<div style={{ minWidth: isExpanded ? "auto" : "0", visibility: isExpanded ? "visible" : "hidden" }}>
<ToggleSwitch onClick={(e) => e.stopPropagation()}>
<span className="toggle-label">{displayMode === "rich" ? "Rich Display" : "Plain Text"}</span>
<div
className={`toggle-container ${displayMode === "rich" ? "active" : ""}`}
onClick={toggleDisplayMode}>
<div className="toggle-handle"></div>
</div>
</ToggleSwitch>
</div>
<ResponseHeader>
<span className="header-title">Response</span>
<ToggleSwitch>
<span className="toggle-label">{displayMode === "rich" ? "Rich Display" : "Plain Text"}</span>
<div className={`toggle-container ${displayMode === "rich" ? "active" : ""}`} onClick={toggleDisplayMode}>
<div className="toggle-handle"></div>
</div>
</ToggleSwitch>
</ResponseHeader>
{isExpanded && <div className="response-content">{renderContent()}</div>}
<div className="response-content">{renderContent()}</div>
</ResponseContainer>
)
} catch (error) {
console.log("Error rendering MCP response - falling back to plain text") // Restored comment
// Fallback for critical rendering errors
console.log("Error rendering MCP response - falling back to plain text")
return (
<ResponseContainer>
<ResponseHeader onClick={toggleExpand}>
<div className="header-title">
<span className={`codicon codicon-chevron-${isExpanded ? "down" : "right"} header-icon`}></span>
Response (Error)
</div>
<ResponseHeader>
<span className="header-title">Response</span>
</ResponseHeader>
{isExpanded && (
<div className="response-content">
<div style={{ color: "var(--vscode-errorForeground)" }}>Error parsing response:</div>
<UrlText>{responseText}</UrlText>
</div>
)}
<div className="response-content">
<div>Error parsing response:</div>
<UrlText>{responseText}</UrlText>
</div>
</ResponseContainer>
)
}
@@ -9,8 +9,6 @@ import styled from "styled-components"
import AddRemoteServerForm from "./tabs/add-server/AddRemoteServerForm"
import InstalledServersView from "./tabs/installed/InstalledServersView"
import McpMarketplaceView from "./tabs/marketplace/McpMarketplaceView"
import { convertProtoMcpServersToMcpServers } from "@shared/proto-conversions/mcp/mcp-server-conversion"
import { McpServers } from "@shared/proto/mcp"
type McpViewProps = {
onDone: () => void
@@ -18,7 +16,7 @@ type McpViewProps = {
}
const McpConfigurationView = ({ onDone, initialTab }: McpViewProps) => {
const { mcpMarketplaceEnabled, setMcpServers } = useExtensionState()
const { mcpMarketplaceEnabled } = useExtensionState()
const [activeTab, setActiveTab] = useState<McpViewTab>(initialTab || (mcpMarketplaceEnabled ? "marketplace" : "installed"))
const handleTabChange = (tab: McpViewTab) => {
@@ -39,22 +37,14 @@ const McpConfigurationView = ({ onDone, initialTab }: McpViewProps) => {
if (mcpMarketplaceEnabled) {
McpServiceClient.refreshMcpMarketplace(EmptyRequest.create({}))
.then((response) => {
// Types are structurally identical, use response directly
setMcpMarketplaceCatalog(response)
})
.catch((error) => {
console.error("Error refreshing MCP marketplace:", error)
})
McpServiceClient.getLatestMcpServers(EmptyRequest.create({}))
.then((response: McpServers) => {
if (response.mcpServers) {
const mcpServers = convertProtoMcpServersToMcpServers(response.mcpServers)
setMcpServers(mcpServers)
}
})
.catch((error) => {
console.error("Failed to fetch MCP servers:", error)
})
vscode.postMessage({ type: "fetchLatestMcpServersFromHub" })
}
}, [mcpMarketplaceEnabled])
@@ -1,10 +1,9 @@
import { McpServiceClient } from "@/services/grpc-client"
import { McpMarketplaceItem, McpServer } from "@shared/mcp"
import { StringRequest } from "@shared/proto/common"
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
import { useCallback, useMemo, useRef, useState } from "react"
import { useEvent } from "react-use"
import styled from "styled-components"
import { useExtensionState } from "@/context/ExtensionStateContext"
interface McpMarketplaceCardProps {
item: McpMarketplaceItem
@@ -16,7 +15,6 @@ const McpMarketplaceCard = ({ item, installedServers }: McpMarketplaceCardProps)
const [isDownloading, setIsDownloading] = useState(false)
const [isLoading, setIsLoading] = useState(false)
const githubLinkRef = useRef<HTMLDivElement>(null)
const { onRelinquishControl } = useExtensionState()
const handleMessage = useCallback((event: MessageEvent) => {
const message = event.data
@@ -24,17 +22,14 @@ const McpMarketplaceCard = ({ item, installedServers }: McpMarketplaceCardProps)
case "mcpDownloadDetails":
setIsDownloading(false)
break
case "relinquishControl":
setIsLoading(false)
break
}
}, [])
useEvent("message", handleMessage)
useEffect(() => {
return onRelinquishControl(() => {
setIsLoading(false)
})
}, [onRelinquishControl])
const githubAuthorUrl = useMemo(() => {
const url = new URL(item.githubUrl)
const pathParts = url.pathname.split("/")
@@ -9,8 +9,6 @@ const FeatureSettingsSection = () => {
setEnableCheckpointsSetting,
mcpMarketplaceEnabled,
setMcpMarketplaceEnabled,
mcpResponsesCollapsed,
setMcpResponsesCollapsed,
chatSettings,
setChatSettings,
} = useExtensionState()
@@ -44,19 +42,6 @@ const FeatureSettingsSection = () => {
Enables the MCP Marketplace tab for discovering and installing MCP servers.
</p>
</div>
<div style={{ marginTop: 10 }}>
<VSCodeCheckbox
checked={mcpResponsesCollapsed}
onChange={(e: any) => {
const checked = e.target.checked === true
setMcpResponsesCollapsed(checked)
}}>
Collapse MCP Responses
</VSCodeCheckbox>
<p className="text-xs text-[var(--vscode-descriptionForeground)]">
Sets the default display mode for MCP response panels
</p>
</div>
<div style={{ marginTop: 10 }}>
<label
htmlFor="openai-reasoning-effort-dropdown"
@@ -7,7 +7,7 @@ import { validateApiConfiguration, validateModelId } from "@/utils/validate"
import { vscode } from "@/utils/vscode"
import { ExtensionMessage } from "@shared/ExtensionMessage"
import { EmptyRequest } from "@shared/proto/common"
import { PlanActMode, TogglePlanActModeRequest, UpdateSettingsRequest } from "@shared/proto/state"
import { PlanActMode, TogglePlanActModeRequest } from "@shared/proto/state"
import { VSCodeButton, VSCodeCheckbox, VSCodeLink, VSCodeTextArea } from "@vscode/webview-ui-toolkit/react"
import { CheckCheck, FlaskConical, Info, LucideIcon, Settings, SquareMousePointer, SquareTerminal, Webhook } from "lucide-react"
import { memo, useCallback, useEffect, useRef, useState } from "react"
@@ -21,8 +21,6 @@ import PreferredLanguageSetting from "./PreferredLanguageSetting" // Added impor
import Section from "./Section"
import SectionHeader from "./SectionHeader"
import TerminalSettingsSection from "./TerminalSettingsSection"
import { convertApiConfigurationToProtoApiConfiguration } from "@shared/proto-conversions/state/settings-conversion"
import { convertChatSettingsToProtoChatSettings } from "@shared/proto-conversions/state/chat-settings-conversion"
const { IS_DEV } = process.env
// Styles for the tab system
@@ -132,8 +130,6 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
setShellIntegrationTimeout,
terminalReuseEnabled,
setTerminalReuseEnabled,
mcpResponsesCollapsed,
setMcpResponsesCollapsed,
setApiConfiguration,
} = useExtensionState()
@@ -145,14 +141,15 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
planActSeparateModelsSetting,
enableCheckpointsSetting,
mcpMarketplaceEnabled,
mcpResponsesCollapsed,
chatSettings,
shellIntegrationTimeout,
terminalReuseEnabled,
})
const [apiErrorMessage, setApiErrorMessage] = useState<string | undefined>(undefined)
const [modelIdErrorMessage, setModelIdErrorMessage] = useState<string | undefined>(undefined)
const handleSubmit = async (withoutDone: boolean = false) => {
const [pendingTabChange, setPendingTabChange] = useState<"plan" | "act" | null>(null)
const handleSubmit = (withoutDone: boolean = false) => {
const apiValidationResult = validateApiConfiguration(apiConfiguration)
const modelIdValidationResult = validateModelId(apiConfiguration, openRouterModels)
@@ -180,26 +177,17 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
apiConfigurationToSubmit = undefined
}
try {
await StateServiceClient.updateSettings(
UpdateSettingsRequest.create({
planActSeparateModelsSetting,
customInstructionsSetting: customInstructions,
telemetrySetting,
enableCheckpointsSetting,
mcpMarketplaceEnabled,
shellIntegrationTimeout,
terminalReuseEnabled,
mcpResponsesCollapsed,
apiConfiguration: apiConfigurationToSubmit
? convertApiConfigurationToProtoApiConfiguration(apiConfigurationToSubmit)
: undefined,
chatSettings: chatSettings ? convertChatSettingsToProtoChatSettings(chatSettings) : undefined,
}),
)
} catch (error) {
console.error("Failed to update settings:", error)
}
vscode.postMessage({
type: "updateSettings",
planActSeparateModelsSetting,
customInstructionsSetting: customInstructions,
telemetrySetting,
enableCheckpointsSetting,
mcpMarketplaceEnabled,
shellIntegrationTimeout,
terminalReuseEnabled,
apiConfiguration: apiConfigurationToSubmit,
})
if (!withoutDone) {
onDone()
@@ -220,7 +208,6 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
planActSeparateModelsSetting !== originalState.current.planActSeparateModelsSetting ||
enableCheckpointsSetting !== originalState.current.enableCheckpointsSetting ||
mcpMarketplaceEnabled !== originalState.current.mcpMarketplaceEnabled ||
mcpResponsesCollapsed !== originalState.current.mcpResponsesCollapsed ||
JSON.stringify(chatSettings) !== JSON.stringify(originalState.current.chatSettings) ||
shellIntegrationTimeout !== originalState.current.shellIntegrationTimeout ||
terminalReuseEnabled !== originalState.current.terminalReuseEnabled
@@ -233,7 +220,6 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
planActSeparateModelsSetting,
enableCheckpointsSetting,
mcpMarketplaceEnabled,
mcpResponsesCollapsed,
chatSettings,
shellIntegrationTimeout,
terminalReuseEnabled,
@@ -274,9 +260,6 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
if (typeof setTerminalReuseEnabled === "function") {
setTerminalReuseEnabled(originalState.current.terminalReuseEnabled ?? true)
}
if (typeof setMcpResponsesCollapsed === "function") {
setMcpResponsesCollapsed(originalState.current.mcpResponsesCollapsed ?? false)
}
// Close settings view
onDone()
}
@@ -294,7 +277,6 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
setApiConfiguration,
setEnableCheckpointsSetting,
setMcpMarketplaceEnabled,
setMcpResponsesCollapsed,
])
// Handle confirmation dialog actions
@@ -324,42 +306,59 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
If we only want to run code once on mount we can use react-use's useEffectOnce or useMount
*/
const handleMessage = useCallback((event: MessageEvent) => {
const message: ExtensionMessage = event.data
switch (message.type) {
// Handle tab navigation through targetSection prop instead
case "grpc_response":
if (message.grpc_response?.message?.action === "scrollToSettings") {
const tabId = message.grpc_response?.message?.value
if (tabId) {
console.log("Opening settings tab from GRPC response:", tabId)
// Check if the value corresponds to a valid tab ID
const isValidTabId = SETTINGS_TABS.some((tab) => tab.id === tabId)
const handleMessage = useCallback(
(event: MessageEvent) => {
const message: ExtensionMessage = event.data
switch (message.type) {
case "didUpdateSettings":
if (pendingTabChange) {
StateServiceClient.togglePlanActMode(
TogglePlanActModeRequest.create({
chatSettings: {
mode: pendingTabChange === "plan" ? PlanActMode.PLAN : PlanActMode.ACT,
preferredLanguage: chatSettings.preferredLanguage,
openAiReasoningEffort: chatSettings.openAIReasoningEffort,
},
}),
)
setPendingTabChange(null)
}
break
// Handle tab navigation through targetSection prop instead
case "grpc_response":
if (message.grpc_response?.message?.action === "scrollToSettings") {
const tabId = message.grpc_response?.message?.value
if (tabId) {
console.log("Opening settings tab from GRPC response:", tabId)
// Check if the value corresponds to a valid tab ID
const isValidTabId = SETTINGS_TABS.some((tab) => tab.id === tabId)
if (isValidTabId) {
// Set the active tab directly
setActiveTab(tabId)
} else {
// Fall back to the old behavior of scrolling to an element
setTimeout(() => {
const element = document.getElementById(tabId)
if (element) {
element.scrollIntoView({ behavior: "smooth" })
if (isValidTabId) {
// Set the active tab directly
setActiveTab(tabId)
} else {
// Fall back to the old behavior of scrolling to an element
setTimeout(() => {
const element = document.getElementById(tabId)
if (element) {
element.scrollIntoView({ behavior: "smooth" })
element.style.transition = "background-color 0.5s ease"
element.style.backgroundColor = "var(--vscode-textPreformat-background)"
element.style.transition = "background-color 0.5s ease"
element.style.backgroundColor = "var(--vscode-textPreformat-background)"
setTimeout(() => {
element.style.backgroundColor = "transparent"
}, 1200)
}
}, 300)
setTimeout(() => {
element.style.backgroundColor = "transparent"
}, 1200)
}
}, 300)
}
}
}
}
break
}
}, [])
break
}
},
[pendingTabChange],
)
useEvent("message", handleMessage)
@@ -371,27 +370,12 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
}
}
const handlePlanActModeChange = async (tab: "plan" | "act") => {
const handlePlanActModeChange = (tab: "plan" | "act") => {
if (tab === chatSettings.mode) {
return
}
// Update settings first to ensure any changes to the current tab are saved
await handleSubmit(true)
try {
await StateServiceClient.togglePlanActMode(
TogglePlanActModeRequest.create({
chatSettings: {
mode: tab === "plan" ? PlanActMode.PLAN : PlanActMode.ACT,
preferredLanguage: chatSettings.preferredLanguage,
openAiReasoningEffort: chatSettings.openAIReasoningEffort,
},
}),
)
} catch (error) {
console.error("Failed to toggle Plan/Act mode:", error)
}
setPendingTabChange(tab)
handleSubmit(true)
}
// Track active tab
+28 -128
View File
@@ -1,14 +1,6 @@
import React, { createContext, useCallback, useContext, useEffect, useRef, useState } from "react"
import { useEvent } from "react-use"
import {
StateServiceClient,
ModelsServiceClient,
UiServiceClient,
FileServiceClient,
McpServiceClient,
} from "../services/grpc-client"
import { EmptyRequest } from "@shared/proto/common"
import { UpdateSettingsRequest } from "@shared/proto/state"
import { WebviewProviderType as WebviewProviderTypeEnum, WebviewProviderTypeRequest } from "@shared/proto/ui"
import { convertProtoToClineMessage } from "@shared/proto-conversions/cline-message"
import { DEFAULT_AUTO_APPROVAL_SETTINGS } from "@shared/AutoApprovalSettings"
@@ -26,9 +18,9 @@ import {
requestyDefaultModelInfo,
} from "../../../src/shared/api"
import { McpMarketplaceCatalog, McpServer, McpViewTab } from "../../../src/shared/mcp"
import { ModelsServiceClient, StateServiceClient, UiServiceClient, McpServiceClient } from "../services/grpc-client"
import { convertTextMateToHljs } from "../utils/textMateToHljs"
import { vscode } from "../utils/vscode"
import { OpenRouterCompatibleModelInfo } from "@shared/proto/models"
interface ExtensionStateContextType extends ExtensionState {
didHydrateState: boolean
@@ -59,7 +51,6 @@ interface ExtensionStateContextType extends ExtensionState {
setPlanActSeparateModelsSetting: (value: boolean) => void
setEnableCheckpointsSetting: (value: boolean) => void
setMcpMarketplaceEnabled: (value: boolean) => void
setMcpResponsesCollapsed: (value: boolean) => void
setShellIntegrationTimeout: (value: number) => void
setTerminalReuseEnabled: (value: boolean) => void
setChatSettings: (value: ChatSettings) => void
@@ -93,9 +84,6 @@ interface ExtensionStateContextType extends ExtensionState {
hideAccount: () => void
hideAnnouncement: () => void
closeMcpView: () => void
// Event callbacks
onRelinquishControl: (callback: () => void) => () => void
}
const ExtensionStateContext = createContext<ExtensionStateContextType | undefined>(undefined)
@@ -190,7 +178,6 @@ export const ExtensionStateContextProvider: React.FC<{
shellIntegrationTimeout: 4000, // default timeout for shell integration
terminalReuseEnabled: true, // default to enabled for backward compatibility
isNewUser: false,
mcpResponsesCollapsed: false, // Default value (expanded), will be overwritten by extension state
})
const [didHydrateState, setDidHydrateState] = useState(false)
const [showWelcome, setShowWelcome] = useState(false)
@@ -210,6 +197,18 @@ export const ExtensionStateContextProvider: React.FC<{
const handleMessage = useCallback((event: MessageEvent) => {
const message: ExtensionMessage = event.data
switch (message.type) {
case "workspaceUpdated": {
setFilePaths(message.filePaths ?? [])
break
}
case "openRouterModels": {
const updatedModels = message.openRouterModels ?? {}
setOpenRouterModels({
[openRouterDefaultModelId]: openRouterDefaultModelInfo, // in case the extension sent a model list without the default model
...updatedModels,
})
break
}
case "openAiModels": {
const updatedModels = message.openAiModels ?? []
setOpenAiModels(updatedModels)
@@ -242,20 +241,6 @@ export const ExtensionStateContextProvider: React.FC<{
const partialMessageUnsubscribeRef = useRef<(() => void) | null>(null)
const mcpMarketplaceUnsubscribeRef = useRef<(() => void) | null>(null)
const themeSubscriptionRef = useRef<(() => void) | null>(null)
const openRouterModelsUnsubscribeRef = useRef<(() => void) | null>(null)
const workspaceUpdatesUnsubscribeRef = useRef<(() => void) | null>(null)
const relinquishControlUnsubscribeRef = useRef<(() => void) | null>(null)
// Add ref for callbacks
const relinquishControlCallbacks = useRef<Set<() => void>>(new Set())
// Create hook function
const onRelinquishControl = useCallback((callback: () => void) => {
relinquishControlCallbacks.current.add(callback)
return () => {
relinquishControlCallbacks.current.delete(callback)
}
}, [])
// Subscribe to state updates and UI events using the gRPC streaming API
useEffect(() => {
@@ -385,18 +370,6 @@ export const ExtensionStateContextProvider: React.FC<{
onComplete: () => {},
})
// Subscribe to workspace file updates
workspaceUpdatesUnsubscribeRef.current = FileServiceClient.subscribeToWorkspaceUpdates(EmptyRequest.create({}), {
onResponse: (response) => {
console.log("[DEBUG] Received workspace update event from gRPC stream")
setFilePaths(response.values || [])
},
onError: (error) => {
console.error("Error in workspace updates subscription:", error)
},
onComplete: () => {},
})
// Set up settings button clicked subscription
settingsButtonClickedSubscriptionRef.current = UiServiceClient.subscribeToSettingsButtonClicked(
WebviewProviderTypeRequest.create({
@@ -488,32 +461,8 @@ export const ExtensionStateContextProvider: React.FC<{
},
})
// Subscribe to OpenRouter models updates
openRouterModelsUnsubscribeRef.current = ModelsServiceClient.subscribeToOpenRouterModels(EmptyRequest.create({}), {
onResponse: (response: OpenRouterCompatibleModelInfo) => {
console.log("[DEBUG] Received OpenRouter models update from gRPC stream")
const models = response.models
setOpenRouterModels({
[openRouterDefaultModelId]: openRouterDefaultModelInfo, // in case the extension sent a model list without the default model
...models,
})
},
onError: (error) => {
console.error("Error in OpenRouter models subscription:", error)
},
onComplete: () => {
console.log("OpenRouter models subscription completed")
},
})
// Initialize webview using gRPC
UiServiceClient.initializeWebview(EmptyRequest.create({}))
.then(() => {
console.log("[DEBUG] Webview initialization completed via gRPC")
})
.catch((error) => {
console.error("Failed to initialize webview via gRPC:", error)
})
// Still send the webviewDidLaunch message for other initialization
vscode.postMessage({ type: "webviewDidLaunch" })
// Set up account button clicked subscription
accountButtonClickedSubscriptionRef.current = UiServiceClient.subscribeToAccountButtonClicked(EmptyRequest.create(), {
@@ -530,18 +479,6 @@ export const ExtensionStateContextProvider: React.FC<{
},
})
// Subscribe to relinquish control events
relinquishControlUnsubscribeRef.current = UiServiceClient.subscribeToRelinquishControl(EmptyRequest.create({}), {
onResponse: () => {
// Call all registered callbacks
relinquishControlCallbacks.current.forEach((callback) => callback())
},
onError: (error) => {
console.error("Error in relinquishControl subscription:", error)
},
onComplete: () => {},
})
// Clean up subscriptions when component unmounts
return () => {
if (stateSubscriptionRef.current) {
@@ -580,28 +517,15 @@ export const ExtensionStateContextProvider: React.FC<{
themeSubscriptionRef.current()
themeSubscriptionRef.current = null
}
if (openRouterModelsUnsubscribeRef.current) {
openRouterModelsUnsubscribeRef.current()
openRouterModelsUnsubscribeRef.current = null
}
if (workspaceUpdatesUnsubscribeRef.current) {
workspaceUpdatesUnsubscribeRef.current()
workspaceUpdatesUnsubscribeRef.current = null
}
if (relinquishControlUnsubscribeRef.current) {
relinquishControlUnsubscribeRef.current()
relinquishControlUnsubscribeRef.current = null
}
}
}, [])
const refreshOpenRouterModels = useCallback(() => {
ModelsServiceClient.refreshOpenRouterModels(EmptyRequest.create({}))
.then((response: OpenRouterCompatibleModelInfo) => {
const models = response.models
.then((res) => {
setOpenRouterModels({
[openRouterDefaultModelId]: openRouterDefaultModelInfo, // in case the extension sent a model list without the default model
...models,
...res.models,
})
})
.catch((error: Error) => console.error("Failed to refresh OpenRouter models:", error))
@@ -675,12 +599,6 @@ export const ExtensionStateContextProvider: React.FC<{
...prevState,
mcpMarketplaceEnabled: value,
})),
setMcpResponsesCollapsed: (value) => {
setState((prevState) => ({
...prevState,
mcpResponsesCollapsed: value,
}))
},
setShowAnnouncement,
setShouldShowAnnouncement: (value) =>
setState((prevState) => ({
@@ -701,38 +619,21 @@ export const ExtensionStateContextProvider: React.FC<{
setMcpMarketplaceCatalog: (catalog: McpMarketplaceCatalog) => setMcpMarketplaceCatalog(catalog),
setShowMcp,
closeMcpView,
setChatSettings: async (value) => {
setChatSettings: (value) => {
setState((prevState) => ({
...prevState,
chatSettings: value,
}))
try {
// Import the conversion functions
const { convertApiConfigurationToProtoApiConfiguration } = await import(
"@shared/proto-conversions/state/settings-conversion"
)
const { convertChatSettingsToProtoChatSettings } = await import(
"@shared/proto-conversions/state/chat-settings-conversion"
)
await StateServiceClient.updateSettings(
UpdateSettingsRequest.create({
chatSettings: convertChatSettingsToProtoChatSettings(value),
apiConfiguration: state.apiConfiguration
? convertApiConfigurationToProtoApiConfiguration(state.apiConfiguration)
: undefined,
customInstructionsSetting: state.customInstructions,
telemetrySetting: state.telemetrySetting,
planActSeparateModelsSetting: state.planActSeparateModelsSetting,
enableCheckpointsSetting: state.enableCheckpointsSetting,
mcpMarketplaceEnabled: state.mcpMarketplaceEnabled,
mcpResponsesCollapsed: state.mcpResponsesCollapsed,
}),
)
} catch (error) {
console.error("Failed to update chat settings:", error)
}
vscode.postMessage({
type: "updateSettings",
chatSettings: value,
apiConfiguration: state.apiConfiguration,
customInstructionsSetting: state.customInstructions,
telemetrySetting: state.telemetrySetting,
planActSeparateModelsSetting: state.planActSeparateModelsSetting,
enableCheckpointsSetting: state.enableCheckpointsSetting,
mcpMarketplaceEnabled: state.mcpMarketplaceEnabled,
})
},
setGlobalClineRulesToggles: (toggles) =>
setState((prevState) => ({
@@ -767,7 +668,6 @@ export const ExtensionStateContextProvider: React.FC<{
setMcpTab,
setTotalTasksSize,
refreshOpenRouterModels,
onRelinquishControl,
}
return <ExtensionStateContext.Provider value={contextValue}>{children}</ExtensionStateContext.Provider>
+8 -14
View File
@@ -75,19 +75,6 @@ export interface ContextMenuQueryItem {
description?: string
}
const DEFAULT_CONTEXT_MENU_OPTIONS = [
ContextMenuOptionType.URL,
ContextMenuOptionType.Problems,
ContextMenuOptionType.Terminal,
ContextMenuOptionType.Git,
ContextMenuOptionType.Folder,
ContextMenuOptionType.File,
]
export function getContextMenuOptionIndex(option: ContextMenuOptionType) {
return DEFAULT_CONTEXT_MENU_OPTIONS.findIndex((item) => item === option)
}
export function getContextMenuOptions(
query: string,
selectedType: ContextMenuOptionType | null = null,
@@ -127,7 +114,14 @@ export function getContextMenuOptions(
return commits.length > 0 ? [workingChanges, ...commits] : [workingChanges]
}
return DEFAULT_CONTEXT_MENU_OPTIONS.map((type) => ({ type }))
return [
{ type: ContextMenuOptionType.URL },
{ type: ContextMenuOptionType.Problems },
{ type: ContextMenuOptionType.Terminal },
{ type: ContextMenuOptionType.Git },
{ type: ContextMenuOptionType.Folder },
{ type: ContextMenuOptionType.File },
]
}
const lowerQuery = query.toLowerCase()