Compare commits

...

23 Commits

Author SHA1 Message Date
celestial-vault 2f2f7f2a1b merge conflicts 2025-06-06 18:58:23 -07:00
Andrei Eternal 4d7ced7e15 [Protobus] migrate executeQuickWin (#4088)
* protobuf executeQuickWin

* whitespaces

---------

Co-authored-by: Andrei Eternal <eternal@cline.bot>
2025-06-06 18:46:31 -07:00
canvrno d7e1fff011 Task init promise error in task truncation (#4097) 2025-06-06 17:28:36 -07:00
Evan 8ca96dfd9f remove proto gen files (#4045)
Co-authored-by: Elephant Lumps <celestial_vault@Elephants-MacBook-Pro.local>
2025-06-06 13:34:53 -07:00
canvrno 8c559d6916 one small fix (#4086) 2025-06-06 12:59:26 -07:00
canvrno 2158165fc4 [PROTOBUS] updateSettings & didUpdateSettings (#4073)
* updateSettings proto mvp

* changeset
2025-06-06 12:43:03 -07:00
canvrno 78792c240f Spring cleaning (#3723)
* Spring cleaning

* changeset

* Fixed incorrect return on ScrollToSettings proto

* cleanup
2025-06-06 11:50:59 -07:00
Sarah Fortune 0abd13e1f2 Better environments for the vibes (#4077)
* Get root of the cline storage directory from an env var.

Set the cline directory env var when running the standalone app from the vscode launcher.
Add missing vscode SDK stub.

* Stop spamming the logs.
2025-06-06 10:15:29 -07:00
Beatrix 7e37cec1ab Fix hardcoded context menu index for File option selection (#4065)
* Fix hardcoded context menu index for File option selection

The ChatTextArea component was using a hardcoded index 3 to select the "File" option by default in the context menu, but this was incorrect - the File option is actually at index 5 in the menu options array. This caused the wrong option (Git) to be selected by default when pressing Escape or when no query is provided.

Additionally, the hardcoded approach was fragile and would break if the context menu order changed in the future.

Key changes:

- Added `DEFAULT_CONTEXT_MENU_OPTIONS` array in context-mentions.ts to define the canonical menu order
- Added helper function `getDefaultContextMenuOptionIndex()` dynamically finds the correct index for any option type
- Updated ChatTextArea to use `DEFAULT_CONTEXT_MENU_OPTION` constant instead of hardcoded 3
- Updated `getContextMenuOptions()` to use the new centralized array

* changeset
2025-06-06 00:44:24 -07:00
canvrno b0926b1647 Fixed telemetry popup bug (#4070) 2025-06-05 22:06:24 -07:00
Alberto Valiña Lema 582a3b190d Collapsible Panel for MCP Responses (#3528)
* feat: the response of the mcps is represented by a collapsible

* feat: the response of the mcps is represented by a collapsible

* fix: message for error parsing response

* fix: format

* feat: added cline.mcp.defaultPanelState settings

* chore: merge

* feat: improve global state

* chore: re-trigger  workflow

* chore: lint fix

* revert: unnecessary changes

* Update webview-ui/src/components/mcp/chat-display/McpResponseDisplay.tsx

Co-authored-by: Toshii <94262432+0xToshii@users.noreply.github.com>

* chore: change MCP Response Display Mode to a button

* fix: improve ui mcp response panel

* refactor: change name and type for property mcpDefaultPanelState to mcpResponsesCollapsed

* chore: rename props

---------

Co-authored-by: Toshii <94262432+0xToshii@users.noreply.github.com>
2025-06-05 17:14:43 -07:00
Evan 8c8a398f90 Migrate webviewDidLaunch protobus (#4063)
* migrate openRouterModels

* remove conversion functions and unused imports

* migrate webviewDidLaunch

---------

Co-authored-by: Elephant Lumps <celestial_vault@Elephants-MacBook-Pro.local>
2025-06-05 13:47:53 -10:00
Evan 742a72b4ec migrate openRouterModels protobus (#4058)
* migrate openRouterModels

* remove conversion functions and unused imports

---------

Co-authored-by: Elephant Lumps <celestial_vault@Elephants-MacBook-Pro.local>
2025-06-05 15:47:11 -07:00
Sarah Fortune 84a00b05e0 Add better stubs for the workspace. (#4061)
The extension doesn't like it when the workspace/workspace config is undefined.
Fix the warning about the name in package.json.
Change some logging in the gRPC server.
2025-06-05 15:19:09 -07:00
Evan e21fa3fff9 migrate theme message protobus (#4012)
Co-authored-by: Elephant Lumps <celestial_vault@Elephants-MacBook-Pro.local>
2025-06-05 13:12:19 -07:00
Beatrix 5b3647fdff Add active files to file context menu (#4048)
* Add active files to file context menu

Add support to include active files (open tabs) within the WorkspaceTracker. This enables the tracker to send the webview with a file list that has active files listed at the top.

The changes include:

- Listening for tab group changes using `vscode.window.tabGroups.onDidChangeTabs` to trigger workspace updates.
- Introducing an `activeFiles` getter that retrieves the file paths of all currently open text editor tabs.
- Modifying the `workspaceDidUpdate` function to include both `activeFiles` and `filePaths` when posting the `workspaceUpdated` message to the webview.

* use set

* add changeset
2025-06-06 00:49:17 +05:30
github-actions[bot] 14b6e71416 v3.17.11 Release Notes
* changeset version bump

* Updating CHANGELOG.md format

* Update CHANGELOG.md and version for 3.17.11 patch release - Add Gemini 2.5 Pro Preview 06-05

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: pashpashpash <nik@nugbase.com>
2025-06-05 11:51:40 -07:00
Ara 4869aba90f Adding Gemini 2.5 pro preview 06-05 (#4053) 2025-06-05 11:37:06 -07:00
Evan c68e427d13 migrate mcpMarketplaceCatalog (#4023)
Co-authored-by: Elephant Lumps <celestial_vault@Elephants-MacBook-Pro.local>
2025-06-04 23:17:13 -07:00
Evan 0dca4dedbd migrate partialMessage (#4033)
Co-authored-by: Elephant Lumps <celestial_vault@Elephants-MacBook-Pro.local>
2025-06-04 23:02:37 -07:00
Evan c6e7b5249e Migrate settingsButtonClicked protobus (#3976)
* migrate settingsButtonClicked

* changeset

* add provider type filtering

---------

Co-authored-by: Elephant Lumps <celestial_vault@Elephants-MacBook-Pro.local>
2025-06-04 20:49:07 -07:00
Elephant Lumps 70e8d8bc13 fix linter warning about empty proto object creation 2025-06-03 16:32:46 -07:00
Elephant Lumps 9b3d93d4d0 migrate workspaceUpdated 2025-06-03 16:26:31 -07:00
59 changed files with 2158 additions and 539 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Migrated updateSettings to protos, removed didUpdateSettings, altered Plan/Act toggling in settings menu
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Fixed issue where telemetry warning popup was created for every new Cline window
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
Prioritize active files in file context menu
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Added promise to task init to prevent race condition with checkpoints
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Spring cleaning
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
Context menu is default to File option on start up
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
the response of the mcps is displayed with a collapsible which allows to focus on the model responses.
+2 -1
View File
@@ -52,7 +52,8 @@
"env": {
"GRPC_TRACE": "all",
"GRPC_VERBOSITY": "DEBUG",
"NODE_PATH": "${workspaceFolder}/dist-standalone/node_modules"
"NODE_PATH": "${workspaceFolder}/dist-standalone/node_modules",
"CLINE_DIR": "${userHome}/.cline-standalone"
},
"program": "standalone.js"
}
+4
View File
@@ -1,5 +1,9 @@
# 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!)
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "claude-dev",
"version": "3.17.10",
"version": "3.17.11",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "claude-dev",
"version": "3.17.10",
"version": "3.17.11",
"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.10",
"version": "3.17.11",
"icon": "assets/icons/icon.png",
"engines": {
"vscode": "^1.84.0"
+5
View File
@@ -63,3 +63,8 @@ message StringArrays {
repeated string values1 = 1;
repeated string values2 = 2;
}
message KeyValuePair {
string key = 1;
string value = 2;
}
+3
View File
@@ -55,6 +55,9 @@ service FileService {
// Opens a task's conversation history file on disk
rpc openTaskHistory(StringRequest) returns (Empty);
// Subscribe to workspace file updates
rpc subscribeToWorkspaceUpdates(EmptyRequest) returns (stream StringArray);
}
// Response for refreshRules operation
+3
View File
@@ -16,6 +16,9 @@ service McpService {
rpc toggleToolAutoApprove(ToggleToolAutoApproveRequest) returns (McpServers);
rpc refreshMcpMarketplace(EmptyRequest) returns (McpMarketplaceCatalog);
rpc openMcpSettings(EmptyRequest) returns (Empty);
// Subscribe to MCP marketplace catalog updates
rpc subscribeToMcpMarketplaceCatalog(EmptyRequest) returns (stream McpMarketplaceCatalog);
}
message ToggleMcpServerRequest {
+35 -8
View File
@@ -20,6 +20,8 @@ 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
@@ -35,17 +37,42 @@ 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 {
int32 max_tokens = 1;
int32 context_window = 2;
bool supports_images = 3;
optional int32 max_tokens = 1;
optional int32 context_window = 2;
optional bool supports_images = 3;
bool supports_prompt_cache = 4;
double input_price = 5;
double output_price = 6;
double cache_writes_price = 7;
double cache_reads_price = 8;
string description = 9;
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;
}
// Shared response message for model information
+124
View File
@@ -13,6 +13,7 @@ service StateService {
rpc togglePlanActMode(TogglePlanActModeRequest) returns (Empty);
rpc updateTerminalConnectionTimeout(Int64Request) returns (Int64);
rpc updateAutoApprovalSettings(AutoApprovalSettingsRequest) returns (Empty);
rpc updateSettings(UpdateSettingsRequest) returns (Empty);
}
message State {
@@ -64,3 +65,126 @@ 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,6 +33,8 @@ 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
@@ -107,3 +109,10 @@ 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;
}
+213 -1
View File
@@ -18,10 +18,210 @@ message WebviewProviderTypeRequest {
WebviewProviderType providerType = 2;
}
// Enum for ClineMessage type
enum ClineMessageType {
ASK = 0;
SAY = 1;
}
// Enum for ClineAsk types
enum ClineAsk {
FOLLOWUP = 0;
PLAN_MODE_RESPOND = 1;
COMMAND = 2;
COMMAND_OUTPUT = 3;
COMPLETION_RESULT = 4;
TOOL = 5;
API_REQ_FAILED = 6;
RESUME_TASK = 7;
RESUME_COMPLETED_TASK = 8;
MISTAKE_LIMIT_REACHED = 9;
AUTO_APPROVAL_MAX_REQ_REACHED = 10;
BROWSER_ACTION_LAUNCH = 11;
USE_MCP_SERVER = 12;
NEW_TASK = 13;
CONDENSE = 14;
REPORT_BUG = 15;
}
// Enum for ClineSay types
enum ClineSay {
TASK = 0;
ERROR = 1;
API_REQ_STARTED = 2;
API_REQ_FINISHED = 3;
TEXT = 4;
REASONING = 5;
COMPLETION_RESULT_SAY = 6;
USER_FEEDBACK = 7;
USER_FEEDBACK_DIFF = 8;
API_REQ_RETRIED = 9;
COMMAND_SAY = 10;
COMMAND_OUTPUT_SAY = 11;
TOOL_SAY = 12;
SHELL_INTEGRATION_WARNING = 13;
BROWSER_ACTION_LAUNCH_SAY = 14;
BROWSER_ACTION = 15;
BROWSER_ACTION_RESULT = 16;
MCP_SERVER_REQUEST_STARTED = 17;
MCP_SERVER_RESPONSE = 18;
USE_MCP_SERVER_SAY = 19;
DIFF_ERROR = 20;
DELETED_API_REQS = 21;
CLINEIGNORE_ERROR = 22;
CHECKPOINT_CREATED = 23;
LOAD_MCP_DOCUMENTATION = 24;
INFO = 25;
}
// Enum for ClineSayTool tool types
enum ClineSayToolType {
EDITED_EXISTING_FILE = 0;
NEW_FILE_CREATED = 1;
READ_FILE = 2;
LIST_FILES_TOP_LEVEL = 3;
LIST_FILES_RECURSIVE = 4;
LIST_CODE_DEFINITION_NAMES = 5;
SEARCH_FILES = 6;
WEB_FETCH = 7;
}
// Enum for browser actions
enum BrowserAction {
LAUNCH = 0;
CLICK = 1;
TYPE = 2;
SCROLL_DOWN = 3;
SCROLL_UP = 4;
CLOSE = 5;
}
// Enum for MCP server request types
enum McpServerRequestType {
USE_MCP_TOOL = 0;
ACCESS_MCP_RESOURCE = 1;
}
// Enum for API request cancel reasons
enum ClineApiReqCancelReason {
STREAMING_FAILED = 0;
USER_CANCELLED = 1;
RETRIES_EXHAUSTED = 2;
}
// Message for conversation history deleted range
message ConversationHistoryDeletedRange {
int32 start_index = 1;
int32 end_index = 2;
}
// Message for ClineSayTool
message ClineSayTool {
ClineSayToolType tool = 1;
string path = 2;
string diff = 3;
string content = 4;
string regex = 5;
string file_pattern = 6;
bool operation_is_located_in_workspace = 7;
}
// Message for ClineSayBrowserAction
message ClineSayBrowserAction {
BrowserAction action = 1;
string coordinate = 2;
string text = 3;
}
// Message for BrowserActionResult
message BrowserActionResult {
string screenshot = 1;
string logs = 2;
string current_url = 3;
string current_mouse_position = 4;
}
// Message for ClineAskUseMcpServer
message ClineAskUseMcpServer {
string server_name = 1;
McpServerRequestType type = 2;
string tool_name = 3;
string arguments = 4;
string uri = 5;
}
// Message for ClinePlanModeResponse
message ClinePlanModeResponse {
string response = 1;
repeated string options = 2;
string selected = 3;
}
// Message for ClineAskQuestion
message ClineAskQuestion {
string question = 1;
repeated string options = 2;
string selected = 3;
}
// Message for ClineAskNewTask
message ClineAskNewTask {
string context = 1;
}
// Message for API request retry status
message ApiReqRetryStatus {
int32 attempt = 1;
int32 max_attempts = 2;
int32 delay_sec = 3;
string error_snippet = 4;
}
// Message for ClineApiReqInfo
message ClineApiReqInfo {
string request = 1;
int32 tokens_in = 2;
int32 tokens_out = 3;
int32 cache_writes = 4;
int32 cache_reads = 5;
double cost = 6;
ClineApiReqCancelReason cancel_reason = 7;
string streaming_failed_message = 8;
ApiReqRetryStatus retry_status = 9;
}
// Main ClineMessage type
message ClineMessage {
int64 ts = 1;
ClineMessageType type = 2;
ClineAsk ask = 3;
ClineSay say = 4;
string text = 5;
string reasoning = 6;
repeated string images = 7;
repeated string files = 8;
bool partial = 9;
string last_checkpoint_hash = 10;
bool is_checkpoint_checked_out = 11;
bool is_operation_outside_workspace = 12;
int32 conversation_history_index = 13;
ConversationHistoryDeletedRange conversation_history_deleted_range = 14;
// Additional fields for specific ask/say types
ClineSayTool say_tool = 15;
ClineSayBrowserAction say_browser_action = 16;
BrowserActionResult browser_action_result = 17;
ClineAskUseMcpServer ask_use_mcp_server = 18;
ClinePlanModeResponse plan_mode_response = 19;
ClineAskQuestion ask_question = 20;
ClineAskNewTask ask_new_task = 21;
ClineApiReqInfo api_req_info = 22;
}
// UiService provides methods for managing UI interactions
service UiService {
// Scrolls to a specific settings section in the settings view
rpc scrollToSettings(StringRequest) returns (Empty);
rpc scrollToSettings(StringRequest) returns (KeyValuePair);
// Marks the current announcement as shown and returns whether an announcement should still be shown
rpc onDidShowAnnouncement(EmptyRequest) returns (Boolean);
@@ -40,4 +240,16 @@ service UiService {
// Subscribe to account button click events
rpc subscribeToAccountButtonClicked(EmptyRequest) returns (stream Empty);
// Subscribe to settings button clicked events
rpc subscribeToSettingsButtonClicked(WebviewProviderTypeRequest) returns (stream Empty);
// Subscribe to partial message updates (streaming Cline messages as they're built)
rpc subscribeToPartialMessage(EmptyRequest) returns (stream ClineMessage);
// Subscribe to theme change events
rpc subscribeToTheme(EmptyRequest) returns (stream String);
// Initialize webview when it launches
rpc initializeWebview(EmptyRequest) returns (Empty);
}
@@ -0,0 +1,58 @@
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)
}
+10 -165
View File
@@ -1,24 +1,17 @@
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"
@@ -26,39 +19,29 @@ import { ChatContent } from "@shared/ChatContent"
import { ChatSettings } from "@shared/ChatSettings"
import { ExtensionMessage, ExtensionState, Platform } from "@shared/ExtensionMessage"
import { HistoryItem } from "@shared/HistoryItem"
import { McpDownloadResponse, McpMarketplaceCatalog, McpServer } from "@shared/mcp"
import { McpMarketplaceCatalog } 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 { getTotalTasksSize } from "@utils/storage"
import {
ensureMcpServersDirectoryExists,
ensureSettingsDirectoryExists,
GlobalFileNames,
ensureWorkflowsDirectoryExists,
} from "../storage/disk"
import { ensureMcpServersDirectoryExists, ensureSettingsDirectoryExists, GlobalFileNames } from "../storage/disk"
import {
getAllExtensionState,
getGlobalState,
getSecret,
getWorkspaceState,
resetExtensionState,
storeSecret,
updateApiConfiguration,
updateGlobalState,
updateWorkspaceState,
} from "../storage/state"
import { Task, cwd } from "../task"
import { Task } 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 { 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"
import { sendMcpMarketplaceCatalogEvent } from "./mcp/subscribeToMcpMarketplaceCatalog"
/*
https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
@@ -85,7 +68,7 @@ export class Controller {
this.outputChannel.appendLine("ClineProvider instantiated")
this.postMessage = postMessage
this.workspaceTracker = new WorkspaceTracker((msg) => this.postMessageToWebview(msg))
this.workspaceTracker = new WorkspaceTracker()
this.mcpHub = new McpHub(
() => ensureMcpServersDirectoryExists(),
() => ensureSettingsDirectoryExists(this.context),
@@ -220,71 +203,6 @@ export class Controller {
await this.setUserInfo(message.user || undefined)
await this.postStateToWebview()
break
case "webviewDidLaunch":
this.postStateToWebview()
this.workspaceTracker?.populateFilePaths() // don't await
getTheme().then((theme) =>
this.postMessageToWebview({
type: "theme",
text: JSON.stringify(theme),
}),
)
// 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) {
this.postMessageToWebview({
type: "mcpMarketplaceCatalog",
mcpMarketplaceCatalog: 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)
@@ -367,57 +285,6 @@ 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?",
@@ -449,13 +316,6 @@ 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)
@@ -759,10 +619,6 @@ export class Controller {
console.error("Failed to fetch MCP marketplace:", error)
if (!silent) {
const errorMessage = error instanceof Error ? error.message : "Failed to fetch MCP marketplace"
await this.postMessageToWebview({
type: "mcpMarketplaceCatalog",
error: errorMessage,
})
vscode.window.showErrorMessage(errorMessage)
}
return undefined
@@ -808,10 +664,7 @@ export class Controller {
try {
const catalog = await this.fetchMcpMarketplaceFromApi(true)
if (catalog) {
await this.postMessageToWebview({
type: "mcpMarketplaceCatalog",
mcpMarketplaceCatalog: catalog,
})
await sendMcpMarketplaceCatalogEvent(catalog)
}
} catch (error) {
console.error("Failed to silently refresh MCP marketplace:", error)
@@ -839,27 +692,17 @@ export class Controller {
| McpMarketplaceCatalog
| undefined
if (!forceRefresh && cachedCatalog?.items) {
await this.postMessageToWebview({
type: "mcpMarketplaceCatalog",
mcpMarketplaceCatalog: cachedCatalog,
})
await sendMcpMarketplaceCatalogEvent(cachedCatalog)
return
}
const catalog = await this.fetchMcpMarketplaceFromApi(false)
if (catalog) {
await this.postMessageToWebview({
type: "mcpMarketplaceCatalog",
mcpMarketplaceCatalog: catalog,
})
await sendMcpMarketplaceCatalogEvent(catalog)
}
} catch (error) {
console.error("Failed to handle cached MCP marketplace:", error)
const errorMessage = error instanceof Error ? error.message : "Failed to handle cached MCP marketplace"
await this.postMessageToWebview({
type: "mcpMarketplaceCatalog",
error: errorMessage,
})
vscode.window.showErrorMessage(errorMessage)
}
}
@@ -1184,6 +1027,7 @@ export class Controller {
shellIntegrationTimeout,
terminalReuseEnabled,
isNewUser,
mcpResponsesCollapsed,
} = await getAllExtensionState(this.context)
const localClineRulesToggles =
@@ -1229,6 +1073,7 @@ export class Controller {
shellIntegrationTimeout,
terminalReuseEnabled,
isNewUser,
mcpResponsesCollapsed,
}
}
@@ -0,0 +1,55 @@
import { Controller } from "../index"
import { EmptyRequest } from "@shared/proto/common"
import { McpMarketplaceCatalog } from "@shared/proto/mcp"
import { StreamingResponseHandler, getRequestRegistry } from "../grpc-handler"
// Keep track of active subscriptions
const activeMcpMarketplaceSubscriptions = new Set<StreamingResponseHandler>()
/**
* Subscribe to MCP marketplace catalog 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 subscribeToMcpMarketplaceCatalog(
controller: Controller,
request: EmptyRequest,
responseStream: StreamingResponseHandler,
requestId?: string,
): Promise<void> {
// Add this subscription to the active subscriptions
activeMcpMarketplaceSubscriptions.add(responseStream)
// Register cleanup when the connection is closed
const cleanup = () => {
activeMcpMarketplaceSubscriptions.delete(responseStream)
}
// Register the cleanup function with the request registry if we have a requestId
if (requestId) {
getRequestRegistry().registerRequest(requestId, cleanup, { type: "mcp_marketplace_subscription" }, responseStream)
}
}
/**
* Send an MCP marketplace catalog event to all active subscribers
*/
export async function sendMcpMarketplaceCatalogEvent(catalog: McpMarketplaceCatalog): Promise<void> {
// Send the event to all active subscribers
const promises = Array.from(activeMcpMarketplaceSubscriptions).map(async (responseStream) => {
try {
await responseStream(
catalog,
false, // Not the last message
)
} catch (error) {
console.error("Error sending MCP marketplace catalog event:", error)
// Remove the subscription if there was an error
activeMcpMarketplaceSubscriptions.delete(responseStream)
}
})
await Promise.all(promises)
}
@@ -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, Partial<OpenRouterModelInfo>> = {}
let models: Record<string, OpenRouterModelInfo> = {}
try {
const response = await axios.get("https://openrouter.ai/api/v1/models")
@@ -32,15 +32,20 @@ export async function refreshOpenRouterModels(
return undefined
}
for (const rawModel of rawModels) {
const modelInfo: Partial<OpenRouterModelInfo> = {
maxTokens: rawModel.top_provider?.max_completion_tokens,
contextWindow: rawModel.context_length,
supportsImages: rawModel.architecture?.modality?.includes("image"),
const modelInfo = OpenRouterModelInfo.create({
maxTokens: rawModel.top_provider?.max_completion_tokens ?? 0,
contextWindow: rawModel.context_length ?? 0,
supportsImages: rawModel.architecture?.modality?.includes("image") ?? false,
supportsPromptCache: false,
inputPrice: parsePrice(rawModel.pricing?.prompt),
outputPrice: parsePrice(rawModel.pricing?.completion),
description: rawModel.description,
}
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 ?? [],
})
switch (rawModel.id) {
case "anthropic/claude-sonnet-4":
@@ -118,7 +123,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", models)
console.log("OpenRouter models fetched and saved", JSON.stringify(models).slice(0, 300))
} catch (error) {
console.error("Error fetching OpenRouter models:", error)
@@ -129,30 +134,13 @@ export async function refreshOpenRouterModels(
}
}
// 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 })
return OpenRouterCompatibleModelInfo.create({ models })
}
/**
* Reads cached OpenRouter models from disk
*/
async function readOpenRouterModels(controller: Controller): Promise<Record<string, Partial<OpenRouterModelInfo>> | undefined> {
async function readOpenRouterModels(controller: Controller): Promise<Record<string, OpenRouterModelInfo> | undefined> {
const openRouterModelsFilePath = path.join(await ensureCacheDirectoryExists(controller), GlobalFileNames.openRouterModels)
const fileExists = await fileExistsAtPath(openRouterModelsFilePath)
if (fileExists) {
@@ -0,0 +1,60 @@
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)
}
@@ -0,0 +1,85 @@
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
}
}
@@ -0,0 +1,35 @@
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
}
}
@@ -0,0 +1,73 @@
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 } from "../../../shared/proto/common"
import { StringRequest, KeyValuePair } 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 An object with action and value fields for the UI to process
* @returns KeyValuePair with action and value fields for the UI to process
*/
export async function scrollToSettings(controller: Controller, request: StringRequest): Promise<Record<string, string>> {
return {
action: "scrollToSettings",
export async function scrollToSettings(controller: Controller, request: StringRequest): Promise<KeyValuePair> {
return KeyValuePair.create({
key: "scrollToSettings",
value: request.value || "",
}
})
}
@@ -50,7 +50,7 @@ export async function sendChatButtonClickedEvent(controllerId: string): Promise<
}
try {
const event: Empty = Empty.create({})
const event = Empty.create({})
await responseStream(
event,
false, // Not the last message
@@ -50,7 +50,7 @@ export async function sendHistoryButtonClickedEvent(webviewType?: WebviewProvide
}
try {
const event: Empty = Empty.create({})
const event = 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 = Empty.create({})
const event = Empty.create({})
// Process all subscriptions, filtering based on the source
const promises = Array.from(mcpButtonClickedSubscriptions.entries()).map(async ([responseStream, providerType]) => {
@@ -0,0 +1,56 @@
import { Controller } from "../index"
import { EmptyRequest } from "@shared/proto/common"
import { ClineMessage } from "@shared/proto/ui"
import { StreamingResponseHandler, getRequestRegistry } from "../grpc-handler"
// Keep track of active partial message subscriptions
const activePartialMessageSubscriptions = new Set<StreamingResponseHandler>()
/**
* Subscribe to partial message 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 subscribeToPartialMessage(
controller: Controller,
request: EmptyRequest,
responseStream: StreamingResponseHandler,
requestId?: string,
): Promise<void> {
// Add this subscription to the active subscriptions
activePartialMessageSubscriptions.add(responseStream)
// Register cleanup when the connection is closed
const cleanup = () => {
activePartialMessageSubscriptions.delete(responseStream)
}
// Register the cleanup function with the request registry if we have a requestId
if (requestId) {
getRequestRegistry().registerRequest(requestId, cleanup, { type: "partial_message_subscription" }, responseStream)
}
}
/**
* Send a partial message event to all active subscribers
* @param partialMessage The ClineMessage to send
*/
export async function sendPartialMessageEvent(partialMessage: ClineMessage): Promise<void> {
// Send the event to all active subscribers
const promises = Array.from(activePartialMessageSubscriptions).map(async (responseStream) => {
try {
await responseStream(
partialMessage,
false, // Not the last message
)
} catch (error) {
console.error("Error sending partial message event:", error)
// Remove the subscription if there was an error
activePartialMessageSubscriptions.delete(responseStream)
}
})
await Promise.all(promises)
}
@@ -0,0 +1,61 @@
import { Empty } from "@shared/proto/common"
import { WebviewProviderType, WebviewProviderTypeRequest } from "@shared/proto/ui"
import { StreamingResponseHandler, getRequestRegistry } from "../grpc-handler"
import type { Controller } from "../index"
// Track subscriptions with their provider type
const subscriptions = new Map<StreamingResponseHandler, WebviewProviderType>()
/**
* Subscribe to settings button clicked events
* @param controller The controller instance
* @param request The request with provider type
* @param responseStream The streaming response handler
* @param requestId The ID of the request (passed by the gRPC handler)
*/
export async function subscribeToSettingsButtonClicked(
controller: Controller,
request: WebviewProviderTypeRequest,
responseStream: StreamingResponseHandler,
requestId?: string,
): Promise<void> {
const providerType = request.providerType
console.log(`[DEBUG] set up settings button subscription for ${WebviewProviderType[providerType]} webview`)
// Store the subscription with its provider type
subscriptions.set(responseStream, providerType)
// Register cleanup when the connection is closed
const cleanup = () => {
subscriptions.delete(responseStream)
}
// Register the cleanup function with the request registry if we have a requestId
if (requestId) {
getRequestRegistry().registerRequest(requestId, cleanup, { type: "settings_button_clicked_subscription" }, responseStream)
}
}
/**
* Send a settings button clicked event to active subscribers of matching provider type
* @param webviewType The type of webview that triggered the event
*/
export async function sendSettingsButtonClickedEvent(webviewType?: WebviewProviderType): Promise<void> {
// Process all subscriptions, filtering based on the source
const promises = Array.from(subscriptions.entries()).map(async ([responseStream, providerType]) => {
// If webviewType is provided, only send to subscribers of the same type
if (webviewType !== undefined && webviewType !== providerType) {
return // Skip subscribers of different types
}
try {
const event = Empty.create({})
await responseStream(event, false) // Not the last message
} catch (error) {
console.error(`Error sending settings button clicked event to ${WebviewProviderType[providerType]}:`, error)
subscriptions.delete(responseStream)
}
})
await Promise.all(promises)
}
@@ -0,0 +1,76 @@
import { Controller } from "../index"
import { EmptyRequest, String } from "@shared/proto/common"
import { StreamingResponseHandler, getRequestRegistry } from "../grpc-handler"
import { getTheme } from "@integrations/theme/getTheme"
// Keep track of active theme subscriptions
const activeThemeSubscriptions = new Set<StreamingResponseHandler>()
/**
* Subscribe to theme change 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 subscribeToTheme(
controller: Controller,
request: EmptyRequest,
responseStream: StreamingResponseHandler,
requestId?: string,
): Promise<void> {
// Add this subscription to the active subscriptions
activeThemeSubscriptions.add(responseStream)
// Register cleanup when the connection is closed
const cleanup = () => {
activeThemeSubscriptions.delete(responseStream)
}
// Register the cleanup function with the request registry if we have a requestId
if (requestId) {
getRequestRegistry().registerRequest(requestId, cleanup, { type: "theme_subscription" }, responseStream)
}
// Send the current theme immediately upon subscription
const theme = await getTheme()
if (theme) {
try {
const themeEvent = String.create({
value: JSON.stringify(theme),
})
await responseStream(
themeEvent,
false, // Not the last message
)
} catch (error) {
console.error("Error sending initial theme:", error)
activeThemeSubscriptions.delete(responseStream)
}
}
}
/**
* Send a theme event to all active subscribers
* @param themeJson The JSON-stringified theme data
*/
export async function sendThemeEvent(themeJson: string): Promise<void> {
// Send the event to all active subscribers
const promises = Array.from(activeThemeSubscriptions).map(async (responseStream) => {
try {
const event = String.create({
value: themeJson,
})
await responseStream(
event,
false, // Not the last message
)
} catch (error) {
console.error("Error sending theme event:", error)
// Remove the subscription if there was an error
activeThemeSubscriptions.delete(responseStream)
}
})
await Promise.all(promises)
}
+1
View File
@@ -91,6 +91,7 @@ export type GlobalStateKey =
| "favoritedModelIds"
| "requestTimeoutMs"
| "shellIntegrationTimeout"
| "mcpResponsesCollapsed"
| "terminalReuseEnabled"
| "isNewUser"
+4
View File
@@ -164,6 +164,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
shellIntegrationTimeout,
enableCheckpointsSettingRaw,
mcpMarketplaceEnabledRaw,
mcpResponsesCollapsedRaw,
globalWorkflowToggles,
terminalReuseEnabled,
] = await Promise.all([
@@ -255,6 +256,7 @@ 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>,
])
@@ -277,6 +279,7 @@ 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
@@ -387,6 +390,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
previousModeAwsBedrockCustomSelected,
previousModeAwsBedrockCustomModelBaseId,
mcpMarketplaceEnabled: mcpMarketplaceEnabled,
mcpResponsesCollapsed: mcpResponsesCollapsed,
telemetrySetting: telemetrySetting || "unset",
planActSeparateModelsSetting,
enableCheckpointsSetting: enableCheckpointsSetting,
+29 -12
View File
@@ -72,6 +72,8 @@ import { ClineIgnoreController } from "@core/ignore/ClineIgnoreController"
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 { 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"
import { ModelContextTracker } from "@core/context/context-tracking/ModelContextTracker"
@@ -163,6 +165,7 @@ export class Task {
checkpointTrackerErrorMessage?: string
conversationHistoryDeletedRange?: [number, number]
isInitialized = false
private initTaskPromise?: Promise<void>
isAwaitingPlanResponse = false
didRespondToPlanAskBySwitchingMode = false
@@ -294,9 +297,9 @@ export class Task {
// Continue with task initialization
if (historyItem) {
this.resumeTaskFromHistory()
this.initTaskPromise = this.resumeTaskFromHistory()
} else if (task || images || files) {
this.startTask(task, images, files)
this.initTaskPromise = this.startTask(task, images, files)
}
// initialize telemetry
@@ -385,6 +388,10 @@ 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)
@@ -519,6 +526,10 @@ export class Task {
}
async presentMultifileDiff(messageTs: number, seeNewChangesSinceLastTaskCompletion: boolean) {
if (this.initTaskPromise && !this.isInitialized) {
await this.initTaskPromise
}
const relinquishButton = () => {
this.postMessageToWebview({ type: "relinquishControl" })
}
@@ -648,6 +659,10 @@ export class Task {
}
async doesLatestTaskCompletionHaveNewChanges() {
if (this.initTaskPromise && !this.isInitialized) {
await this.initTaskPromise
}
if (!this.enableCheckpoints) {
return false
}
@@ -743,10 +758,8 @@ export class Task {
// todo be more efficient about saving and posting only new data or one whole message at a time so ignore partial for saves, and only post parts of partial message instead of whole array in new listener
// await this.saveClineMessagesAndUpdateHistory()
// await this.postStateToWebview()
await this.postMessageToWebview({
type: "partialMessage",
partialMessage: lastMessage,
})
const protoMessage = convertClineMessageToProto(lastMessage)
await sendPartialMessageEvent(protoMessage)
throw new Error("Current ask promise was ignored 1")
} else {
// this is a new partial message, so add it with partial state
@@ -787,10 +800,8 @@ export class Task {
lastMessage.partial = false
await this.saveClineMessagesAndUpdateHistory()
// await this.postStateToWebview()
await this.postMessageToWebview({
type: "partialMessage",
partialMessage: lastMessage,
})
const protoMessage = convertClineMessageToProto(lastMessage)
await sendPartialMessageEvent(protoMessage)
} else {
// this is a new partial=false message, so add it like normal
this.askResponse = undefined
@@ -866,7 +877,8 @@ export class Task {
lastMessage.images = images
lastMessage.files = files
lastMessage.partial = partial
await this.postMessageToWebview({ type: "partialMessage", partialMessage: lastMessage })
const protoMessage = convertClineMessageToProto(lastMessage)
await sendPartialMessageEvent(protoMessage)
} else {
// this is a new partial message, so add it with partial state
const sayTs = Date.now()
@@ -896,7 +908,8 @@ export class Task {
// instead of streaming partialMessage events, we do a save and post like normal to persist to disk
await this.saveClineMessagesAndUpdateHistory()
// await this.postStateToWebview()
await this.postMessageToWebview({ type: "partialMessage", partialMessage: lastMessage }) // more performant than an entire postStateToWebview
const protoMessage = convertClineMessageToProto(lastMessage)
await sendPartialMessageEvent(protoMessage) // more performant than an entire postStateToWebview
} else {
// this is a new partial=false message, so add it like normal
const sayTs = Date.now()
@@ -1200,6 +1213,10 @@ 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
+6 -5
View File
@@ -8,6 +8,7 @@ import { findLast } from "@shared/array"
import { readFile } from "fs/promises"
import path from "node:path"
import { WebviewProviderType } from "@/shared/webview/types"
import { sendThemeEvent } from "@core/controller/ui/subscribeToTheme"
/*
https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
@@ -139,11 +140,11 @@ export class WebviewProvider implements vscode.WebviewViewProvider {
vscode.workspace.onDidChangeConfiguration(
async (e) => {
if (e && e.affectsConfiguration("workbench.colorTheme")) {
// Sends latest theme name to webview
await this.controller.postMessageToWebview({
type: "theme",
text: JSON.stringify(await getTheme()),
})
// Send theme update via gRPC subscription
const theme = await getTheme()
if (theme) {
await sendThemeEvent(JSON.stringify(theme))
}
}
if (e && e.affectsConfiguration("cline.mcpMarketplace.enabled")) {
// Update state when marketplace tab setting changes
+5 -14
View File
@@ -16,6 +16,7 @@ import { sendChatButtonClickedEvent } from "./core/controller/ui/subscribeToChat
import { ErrorService } from "./services/error/ErrorService"
import { initializeTestMode, cleanupTestMode } from "./services/test/TestMode"
import { telemetryService } from "./services/posthog/telemetry/TelemetryService"
import { sendSettingsButtonClickedEvent } from "./core/controller/ui/subscribeToSettingsButtonClicked"
import { v4 as uuidv4 } from "uuid"
import { WebviewProviderType as WebviewProviderTypeEnum } from "@shared/proto/ui"
import { WebviewProviderType } from "./shared/webview/types"
@@ -169,20 +170,10 @@ export async function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(
vscode.commands.registerCommand("cline.settingsButtonClicked", (webview: any) => {
WebviewProvider.getAllInstances().forEach((instance) => {
const openSettings = async (instance?: WebviewProvider) => {
instance?.controller.postMessageToWebview({
type: "action",
action: "settingsButtonClicked",
})
}
const isSidebar = !webview
if (isSidebar) {
openSettings(WebviewProvider.getSidebarInstance())
} else {
WebviewProvider.getTabInstances().forEach(openSettings)
}
})
const isSidebar = !webview
const webviewType = isSidebar ? WebviewProviderTypeEnum.SIDEBAR : WebviewProviderTypeEnum.TAB
sendSettingsButtonClickedEvent(webviewType)
}),
)
+18 -10
View File
@@ -1,7 +1,7 @@
import * as vscode from "vscode"
import * as path from "path"
import { listFiles } from "@services/glob/list-files"
import { ExtensionMessage } from "@shared/ExtensionMessage"
import { sendWorkspaceUpdateEvent } from "@core/controller/file/subscribeToWorkspaceUpdates"
const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0)
@@ -10,8 +10,15 @@ class WorkspaceTracker {
private disposables: vscode.Disposable[] = []
private filePaths: Set<string> = new Set()
constructor(private readonly postMessageToWebview: (message: ExtensionMessage) => Promise<void>) {
this.postMessageToWebview = postMessageToWebview
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() {
this.registerListeners()
}
@@ -36,6 +43,9 @@ 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,
@@ -80,17 +90,15 @@ class WorkspaceTracker {
this.workspaceDidUpdate()
}
private workspaceDidUpdate() {
private async workspaceDidUpdate() {
if (!cwd) {
return
}
this.postMessageToWebview({
type: "workspaceUpdated",
filePaths: Array.from(this.filePaths).map((file) => {
const relativePath = path.relative(cwd, file).toPosix()
return file.endsWith("/") ? relativePath + "/" : relativePath
}),
const filePaths = Array.from(new Set([...this.activeFiles, ...this.filePaths])).map((file) => {
const relativePath = path.relative(cwd, file).toPosix()
return file.endsWith("/") ? relativePath + "/" : relativePath
})
await sendWorkspaceUpdateEvent(filePaths)
}
private normalizeFilePath(filePath: string): string {
@@ -138,17 +138,20 @@ class TelemetryService {
if (globalTelemetryEnabled) {
this.telemetryEnabled = didUserOptIn
} else {
// 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")
}
})
// 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
}
// Update PostHog client state based on telemetry preference
+38 -32
View File
@@ -37,25 +37,28 @@ function createToolCallTracker(webviewProvider: WebviewProvider): {
// Intercept messages to track tool usage
const originalPostMessageToWebview = webviewProvider.controller.postMessageToWebview
webviewProvider.controller.postMessageToWebview = async (message: ExtensionMessage) => {
// Track tool calls
if (message.type === "partialMessage" && message.partialMessage?.say === "tool") {
const toolName = (message.partialMessage.text as any)?.tool
if (toolName) {
tracker.toolCalls[toolName] = (tracker.toolCalls[toolName] || 0) + 1
}
}
// NOTE: Tool tracking via partialMessage has been migrated to gRPC streaming
// This interceptor is kept for potential future use with other message types
// Track tool failures
if (message.type === "partialMessage" && message.partialMessage?.say === "error") {
const errorText = message.partialMessage.text
if (errorText && errorText.includes("Error executing tool")) {
const match = errorText.match(/Error executing tool: (\w+)/)
if (match && match[1]) {
const toolName = match[1]
tracker.toolFailures[toolName] = (tracker.toolFailures[toolName] || 0) + 1
}
}
}
// Track tool calls - commented out as partialMessage is now handled via gRPC
// if (message.type === "partialMessage" && message.partialMessage?.say === "tool") {
// const toolName = (message.partialMessage.text as any)?.tool
// if (toolName) {
// tracker.toolCalls[toolName] = (tracker.toolCalls[toolName] || 0) + 1
// }
// }
// Track tool failures - commented out as partialMessage is now handled via gRPC
// if (message.type === "partialMessage" && message.partialMessage?.say === "error") {
// const errorText = message.partialMessage.text
// if (errorText && errorText.includes("Error executing tool")) {
// const match = errorText.match(/Error executing tool: (\w+)/)
// if (match && match[1]) {
// const toolName = match[1]
// tracker.toolFailures[toolName] = (tracker.toolFailures[toolName] || 0) + 1
// }
// }
// }
return originalPostMessageToWebview.call(webviewProvider.controller, message)
}
@@ -504,22 +507,25 @@ export function createMessageCatcher(webviewProvider: WebviewProvider): vscode.D
// Intercept outgoing messages from extension to webview
webviewProvider.controller.postMessageToWebview = async (message: ExtensionMessage) => {
// Check for completion_result message
if (message.type === "partialMessage" && message.partialMessage?.say === "completion_result") {
// Complete the current task
completeTask()
}
// NOTE: Completion and ask message detection has been migrated to gRPC streaming
// This interceptor is kept for potential future use with other message types
// Check for ask messages that require user intervention
if (message.type === "partialMessage" && message.partialMessage?.type === "ask" && !message.partialMessage.partial) {
const askType = message.partialMessage.ask as ClineAsk
const askText = message.partialMessage.text
// Check for completion_result message - commented out as partialMessage is now handled via gRPC
// if (message.type === "partialMessage" && message.partialMessage?.say === "completion_result") {
// // Complete the current task
// completeTask()
// }
// Automatically respond to different types of asks
setTimeout(async () => {
await autoRespondToAsk(webviewProvider, askType, askText)
}, 100) // Small delay to ensure the message is processed first
}
// Check for ask messages that require user intervention - commented out as partialMessage is now handled via gRPC
// if (message.type === "partialMessage" && message.partialMessage?.type === "ask" && !message.partialMessage.partial) {
// const askType = message.partialMessage.ask as ClineAsk
// const askText = message.partialMessage.text
// // Automatically respond to different types of asks
// setTimeout(async () => {
// await autoRespondToAsk(webviewProvider, askType, askText)
// }, 100) // Small delay to ensure the message is processed first
// }
return originalPostMessageToWebview.call(webviewProvider.controller, message)
}
+2 -23
View File
@@ -17,37 +17,23 @@ export interface ExtensionMessage {
| "action"
| "state"
| "selectedImages"
| "ollamaModels"
| "lmStudioModels"
| "theme"
| "workspaceUpdated"
| "partialMessage"
| "openRouterModels"
| "openAiModels"
| "requestyModels"
| "mcpServers"
| "relinquishControl"
| "mcpMarketplaceCatalog"
| "mcpDownloadDetails"
| "commitSearchResults"
| "openGraphData"
| "didUpdateSettings"
| "userCreditsBalance"
| "userCreditsUsage"
| "userCreditsPayments"
| "fileSearchResults"
| "grpc_response" // New type for gRPC responses
text?: string
action?: "settingsButtonClicked" | "didBecomeVisible" | "accountLogoutClicked" | "focusChatInput"
action?: "didBecomeVisible" | "accountLogoutClicked" | "focusChatInput"
state?: ExtensionState
images?: string[]
files?: string[]
ollamaModels?: string[]
lmStudioModels?: string[]
vsCodeLmModels?: { vendor?: string; family?: string; version?: string; id?: string }[]
filePaths?: string[]
partialMessage?: ClineMessage
openRouterModels?: Record<string, ModelInfo>
openAiModels?: string[]
requestyModels?: Record<string, ModelInfo>
mcpServers?: McpServer[]
@@ -56,14 +42,6 @@ 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
@@ -129,6 +107,7 @@ export interface ExtensionState {
globalWorkflowToggles: ClineRulesToggles
localCursorRulesToggles: ClineRulesToggles
localWindsurfRulesToggles: ClineRulesToggles
mcpResponsesCollapsed?: boolean
}
export interface ClineMessage {
+1 -9
View File
@@ -9,24 +9,17 @@ 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
@@ -53,6 +46,7 @@ export interface WebviewMessage {
planActSeparateModelsSetting?: boolean
enableCheckpointsSetting?: boolean
mcpMarketplaceEnabled?: boolean
mcpResponsesCollapsed?: boolean
telemetrySetting?: TelemetrySetting
customInstructionsSetting?: string
mentionsRequestId?: string
@@ -76,8 +70,6 @@ export interface WebviewMessage {
enabled?: boolean
filename?: string
payload?: { command: string; title: string }
offset?: number
shellIntegrationTimeout?: number
terminalReuseEnabled?: boolean
+48
View File
@@ -575,6 +575,30 @@ 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,
@@ -719,6 +743,30 @@ 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,
@@ -0,0 +1,253 @@
import { ClineMessage as AppClineMessage, ClineAsk as AppClineAsk, ClineSay as AppClineSay } from "@shared/ExtensionMessage"
import { ClineMessage as ProtoClineMessage, ClineMessageType, ClineAsk, ClineSay } from "@shared/proto/ui"
// Helper function to convert ClineAsk string to enum
function convertClineAskToProtoEnum(ask: AppClineAsk | undefined): ClineAsk | undefined {
if (!ask) {
return undefined
}
const mapping: Record<AppClineAsk, ClineAsk> = {
followup: ClineAsk.FOLLOWUP,
plan_mode_respond: ClineAsk.PLAN_MODE_RESPOND,
command: ClineAsk.COMMAND,
command_output: ClineAsk.COMMAND_OUTPUT,
completion_result: ClineAsk.COMPLETION_RESULT,
tool: ClineAsk.TOOL,
api_req_failed: ClineAsk.API_REQ_FAILED,
resume_task: ClineAsk.RESUME_TASK,
resume_completed_task: ClineAsk.RESUME_COMPLETED_TASK,
mistake_limit_reached: ClineAsk.MISTAKE_LIMIT_REACHED,
auto_approval_max_req_reached: ClineAsk.AUTO_APPROVAL_MAX_REQ_REACHED,
browser_action_launch: ClineAsk.BROWSER_ACTION_LAUNCH,
use_mcp_server: ClineAsk.USE_MCP_SERVER,
new_task: ClineAsk.NEW_TASK,
condense: ClineAsk.CONDENSE,
report_bug: ClineAsk.REPORT_BUG,
}
const result = mapping[ask]
if (result === undefined) {
console.warn(`Unknown ClineAsk value: ${ask}`)
}
return result
}
// Helper function to convert ClineAsk enum to string
function convertProtoEnumToClineAsk(ask: ClineAsk): AppClineAsk | undefined {
if (ask === ClineAsk.UNRECOGNIZED) {
console.warn("Received UNRECOGNIZED ClineAsk enum value")
return undefined
}
const mapping: Record<Exclude<ClineAsk, ClineAsk.UNRECOGNIZED>, AppClineAsk> = {
[ClineAsk.FOLLOWUP]: "followup",
[ClineAsk.PLAN_MODE_RESPOND]: "plan_mode_respond",
[ClineAsk.COMMAND]: "command",
[ClineAsk.COMMAND_OUTPUT]: "command_output",
[ClineAsk.COMPLETION_RESULT]: "completion_result",
[ClineAsk.TOOL]: "tool",
[ClineAsk.API_REQ_FAILED]: "api_req_failed",
[ClineAsk.RESUME_TASK]: "resume_task",
[ClineAsk.RESUME_COMPLETED_TASK]: "resume_completed_task",
[ClineAsk.MISTAKE_LIMIT_REACHED]: "mistake_limit_reached",
[ClineAsk.AUTO_APPROVAL_MAX_REQ_REACHED]: "auto_approval_max_req_reached",
[ClineAsk.BROWSER_ACTION_LAUNCH]: "browser_action_launch",
[ClineAsk.USE_MCP_SERVER]: "use_mcp_server",
[ClineAsk.NEW_TASK]: "new_task",
[ClineAsk.CONDENSE]: "condense",
[ClineAsk.REPORT_BUG]: "report_bug",
}
return mapping[ask]
}
// Helper function to convert ClineSay string to enum
function convertClineSayToProtoEnum(say: AppClineSay | undefined): ClineSay | undefined {
if (!say) {
return undefined
}
const mapping: Record<AppClineSay, ClineSay> = {
task: ClineSay.TASK,
error: ClineSay.ERROR,
api_req_started: ClineSay.API_REQ_STARTED,
api_req_finished: ClineSay.API_REQ_FINISHED,
text: ClineSay.TEXT,
reasoning: ClineSay.REASONING,
completion_result: ClineSay.COMPLETION_RESULT_SAY,
user_feedback: ClineSay.USER_FEEDBACK,
user_feedback_diff: ClineSay.USER_FEEDBACK_DIFF,
api_req_retried: ClineSay.API_REQ_RETRIED,
command: ClineSay.COMMAND_SAY,
command_output: ClineSay.COMMAND_OUTPUT_SAY,
tool: ClineSay.TOOL_SAY,
shell_integration_warning: ClineSay.SHELL_INTEGRATION_WARNING,
browser_action_launch: ClineSay.BROWSER_ACTION_LAUNCH_SAY,
browser_action: ClineSay.BROWSER_ACTION,
browser_action_result: ClineSay.BROWSER_ACTION_RESULT,
mcp_server_request_started: ClineSay.MCP_SERVER_REQUEST_STARTED,
mcp_server_response: ClineSay.MCP_SERVER_RESPONSE,
use_mcp_server: ClineSay.USE_MCP_SERVER_SAY,
diff_error: ClineSay.DIFF_ERROR,
deleted_api_reqs: ClineSay.DELETED_API_REQS,
clineignore_error: ClineSay.CLINEIGNORE_ERROR,
checkpoint_created: ClineSay.CHECKPOINT_CREATED,
load_mcp_documentation: ClineSay.LOAD_MCP_DOCUMENTATION,
info: ClineSay.INFO,
}
const result = mapping[say]
if (result === undefined) {
console.warn(`Unknown ClineSay value: ${say}`)
}
return result
}
// Helper function to convert ClineSay enum to string
function convertProtoEnumToClineSay(say: ClineSay): AppClineSay | undefined {
if (say === ClineSay.UNRECOGNIZED) {
console.warn("Received UNRECOGNIZED ClineSay enum value")
return undefined
}
const mapping: Record<Exclude<ClineSay, ClineSay.UNRECOGNIZED>, AppClineSay> = {
[ClineSay.TASK]: "task",
[ClineSay.ERROR]: "error",
[ClineSay.API_REQ_STARTED]: "api_req_started",
[ClineSay.API_REQ_FINISHED]: "api_req_finished",
[ClineSay.TEXT]: "text",
[ClineSay.REASONING]: "reasoning",
[ClineSay.COMPLETION_RESULT_SAY]: "completion_result",
[ClineSay.USER_FEEDBACK]: "user_feedback",
[ClineSay.USER_FEEDBACK_DIFF]: "user_feedback_diff",
[ClineSay.API_REQ_RETRIED]: "api_req_retried",
[ClineSay.COMMAND_SAY]: "command",
[ClineSay.COMMAND_OUTPUT_SAY]: "command_output",
[ClineSay.TOOL_SAY]: "tool",
[ClineSay.SHELL_INTEGRATION_WARNING]: "shell_integration_warning",
[ClineSay.BROWSER_ACTION_LAUNCH_SAY]: "browser_action_launch",
[ClineSay.BROWSER_ACTION]: "browser_action",
[ClineSay.BROWSER_ACTION_RESULT]: "browser_action_result",
[ClineSay.MCP_SERVER_REQUEST_STARTED]: "mcp_server_request_started",
[ClineSay.MCP_SERVER_RESPONSE]: "mcp_server_response",
[ClineSay.USE_MCP_SERVER_SAY]: "use_mcp_server",
[ClineSay.DIFF_ERROR]: "diff_error",
[ClineSay.DELETED_API_REQS]: "deleted_api_reqs",
[ClineSay.CLINEIGNORE_ERROR]: "clineignore_error",
[ClineSay.CHECKPOINT_CREATED]: "checkpoint_created",
[ClineSay.LOAD_MCP_DOCUMENTATION]: "load_mcp_documentation",
[ClineSay.INFO]: "info",
}
return mapping[say]
}
/**
* Convert application ClineMessage to proto ClineMessage
*/
export function convertClineMessageToProto(message: AppClineMessage): ProtoClineMessage {
// For sending messages, we need to provide values for required proto fields
const askEnum = message.ask ? convertClineAskToProtoEnum(message.ask) : undefined
const sayEnum = message.say ? convertClineSayToProtoEnum(message.say) : undefined
// Determine appropriate enum values based on message type
let finalAskEnum: ClineAsk = ClineAsk.FOLLOWUP // Proto default
let finalSayEnum: ClineSay = ClineSay.TEXT // Proto default
if (message.type === "ask") {
finalAskEnum = askEnum ?? ClineAsk.FOLLOWUP // Use FOLLOWUP as default for ask messages
} else if (message.type === "say") {
finalSayEnum = sayEnum ?? ClineSay.TEXT // Use TEXT as default for say messages
}
const protoMessage: ProtoClineMessage = {
ts: message.ts,
type: message.type === "ask" ? ClineMessageType.ASK : ClineMessageType.SAY,
ask: finalAskEnum,
say: finalSayEnum,
text: message.text ?? "",
reasoning: message.reasoning ?? "",
images: message.images ?? [],
files: message.files ?? [],
partial: message.partial ?? false,
lastCheckpointHash: message.lastCheckpointHash ?? "",
isCheckpointCheckedOut: message.isCheckpointCheckedOut ?? false,
isOperationOutsideWorkspace: message.isOperationOutsideWorkspace ?? false,
conversationHistoryIndex: message.conversationHistoryIndex ?? 0,
conversationHistoryDeletedRange: message.conversationHistoryDeletedRange
? {
startIndex: message.conversationHistoryDeletedRange[0],
endIndex: message.conversationHistoryDeletedRange[1],
}
: undefined,
}
return protoMessage
}
/**
* Convert proto ClineMessage to application ClineMessage
*/
export function convertProtoToClineMessage(protoMessage: ProtoClineMessage): AppClineMessage {
const message: AppClineMessage = {
ts: protoMessage.ts,
type: protoMessage.type === ClineMessageType.ASK ? "ask" : "say",
}
// Convert ask enum to string
if (protoMessage.type === ClineMessageType.ASK) {
const ask = convertProtoEnumToClineAsk(protoMessage.ask)
if (ask !== undefined) {
message.ask = ask
}
}
// Convert say enum to string
if (protoMessage.type === ClineMessageType.SAY) {
const say = convertProtoEnumToClineSay(protoMessage.say)
if (say !== undefined) {
message.say = say
}
}
// Convert other fields - preserve empty strings as they may be intentional
if (protoMessage.text !== "") {
message.text = protoMessage.text
}
if (protoMessage.reasoning !== "") {
message.reasoning = protoMessage.reasoning
}
if (protoMessage.images.length > 0) {
message.images = protoMessage.images
}
if (protoMessage.files.length > 0) {
message.files = protoMessage.files
}
if (protoMessage.partial) {
message.partial = protoMessage.partial
}
if (protoMessage.lastCheckpointHash !== "") {
message.lastCheckpointHash = protoMessage.lastCheckpointHash
}
if (protoMessage.isCheckpointCheckedOut) {
message.isCheckpointCheckedOut = protoMessage.isCheckpointCheckedOut
}
if (protoMessage.isOperationOutsideWorkspace) {
message.isOperationOutsideWorkspace = protoMessage.isOperationOutsideWorkspace
}
if (protoMessage.conversationHistoryIndex !== 0) {
message.conversationHistoryIndex = protoMessage.conversationHistoryIndex
}
// Convert conversationHistoryDeletedRange from object to tuple
if (protoMessage.conversationHistoryDeletedRange) {
message.conversationHistoryDeletedRange = [
protoMessage.conversationHistoryDeletedRange.startIndex,
protoMessage.conversationHistoryDeletedRange.endIndex,
]
}
return message
}
@@ -0,0 +1,263 @@
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,
}
}
+3 -4
View File
@@ -32,12 +32,11 @@ 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:", message)
log("postMessage stub called:", JSON.stringify(message).slice(0, 200))
return Promise.resolve(true)
}
+11 -2
View File
@@ -1,13 +1,22 @@
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"
const DATA_DIR = process.env.DATA_DIR ?? "."
const EXTENSION_DIR = process.env.EXTENSION_DIR ?? "."
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 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,6 +53,7 @@ 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 = createStub("vscode.workspace.workspaceFolders")
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,10 +894,22 @@ 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")
vscode.workspace.getConfiguration = function (section, scope) {
console.log("Called stubbed function: vscode.workspace.getConfiguration")
return createStub("unknown")
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.onDidChangeConfiguration = createStub("vscode.workspace.onDidChangeConfiguration")
vscode.workspace.registerTaskProvider = function (type, provider) {
console.log("Called stubbed function: vscode.workspace.registerTaskProvider")
@@ -10,6 +10,7 @@ import { FileServiceClient, StateServiceClient } from "@/services/grpc-client"
import {
ContextMenuOptionType,
getContextMenuOptions,
getContextMenuOptionIndex,
insertMention,
insertMentionDirectly,
removeMention,
@@ -60,6 +61,9 @@ 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
@@ -341,22 +345,6 @@ 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" },
@@ -530,7 +518,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
if (event.key === "Escape") {
// event.preventDefault()
setSelectedType(null)
setSelectedMenuIndex(3) // File by default
setSelectedMenuIndex(DEFAULT_CONTEXT_MENU_OPTION)
return
}
@@ -770,7 +758,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
})
}, 200) // 200ms debounce
} else {
setSelectedMenuIndex(3) // Set to "File" option by default
setSelectedMenuIndex(DEFAULT_CONTEXT_MENU_OPTION)
}
} else {
setSearchQuery("")
@@ -1,4 +1,6 @@
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"
@@ -28,6 +30,10 @@ const ResponseHeader = styled.div`
text-overflow: ellipsis;
margin-right: 8px;
}
.header-icon {
margin-right: 6px;
}
`
const ToggleSwitch = styled.div`
@@ -111,7 +117,9 @@ interface UrlMatch {
}
const McpResponseDisplay: React.FC<McpResponseDisplayProps> = ({ responseText }) => {
const [isLoading, setIsLoading] = useState(true)
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 [displayMode, setDisplayMode] = useState<"rich" | "plain">(() => {
// Get saved preference from localStorage, default to 'rich'
const savedMode = localStorage.getItem("mcpDisplayMode")
@@ -124,14 +132,11 @@ 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")
@@ -139,13 +144,23 @@ 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 (displayMode === "plain") {
if (!isExpanded || displayMode === "plain") {
setIsLoading(false)
setUrlMatches([]) // Clear any existing matches when in plain mode
return
@@ -153,12 +168,10 @@ 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[] = []
@@ -267,12 +280,24 @@ const McpResponseDisplay: React.FC<McpResponseDisplayProps> = ({ responseText })
processingCanceled = true
console.log("Cleaning up URL processing")
}
}, [responseText, displayMode, forceUpdateCounter])
}, [responseText, displayMode, forceUpdateCounter, isExpanded])
// 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" || isLoading) {
if (displayMode === "plain") {
return <UrlText>{responseText}</UrlText>
}
@@ -287,7 +312,7 @@ const McpResponseDisplay: React.FC<McpResponseDisplayProps> = ({ responseText })
}
// For rich display mode, show the text with embedded content
if (!isLoading) {
if (displayMode === "rich") {
// We already know displayMode is "rich" if we get here
// Create an array of text segments and embedded content
const segments: JSX.Element[] = []
@@ -385,30 +410,48 @@ const McpResponseDisplay: React.FC<McpResponseDisplayProps> = ({ responseText })
try {
return (
<ResponseContainer>
<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
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>
<div className="response-content">{renderContent()}</div>
{isExpanded && <div className="response-content">{renderContent()}</div>}
</ResponseContainer>
)
} catch (error) {
console.log("Error rendering MCP response - falling back to plain text")
console.log("Error rendering MCP response - falling back to plain text") // Restored comment
// Fallback for critical rendering errors
return (
<ResponseContainer>
<ResponseHeader>
<span className="header-title">Response</span>
<ResponseHeader onClick={toggleExpand}>
<div className="header-title">
<span className={`codicon codicon-chevron-${isExpanded ? "down" : "right"} header-icon`}></span>
Response (Error)
</div>
</ResponseHeader>
<div className="response-content">
<div>Error parsing response:</div>
<UrlText>{responseText}</UrlText>
</div>
{isExpanded && (
<div className="response-content">
<div style={{ color: "var(--vscode-errorForeground)" }}>Error parsing response:</div>
<UrlText>{responseText}</UrlText>
</div>
)}
</ResponseContainer>
)
}
@@ -14,8 +14,7 @@ import { vscode } from "@/utils/vscode"
import McpMarketplaceCard from "./McpMarketplaceCard"
import McpSubmitCard from "./McpSubmitCard"
const McpMarketplaceView = () => {
const { mcpServers } = useExtensionState()
const [items, setItems] = useState<McpMarketplaceItem[]>([])
const { mcpServers, mcpMarketplaceCatalog } = useExtensionState()
const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [isRefreshing, setIsRefreshing] = useState(false)
@@ -23,6 +22,8 @@ const McpMarketplaceView = () => {
const [selectedCategory, setSelectedCategory] = useState<string | null>(null)
const [sortBy, setSortBy] = useState<"newest" | "stars" | "name" | "downloadCount">("downloadCount")
const items = mcpMarketplaceCatalog?.items || []
const categories = useMemo(() => {
const uniqueCategories = new Set(items.map((item) => item.category))
return Array.from(uniqueCategories).sort()
@@ -58,16 +59,7 @@ const McpMarketplaceView = () => {
useEffect(() => {
const handleMessage = (event: MessageEvent) => {
const message = event.data
if (message.type === "mcpMarketplaceCatalog") {
if (message.error) {
setError(message.error)
} else {
setItems(message.mcpMarketplaceCatalog?.items || [])
setError(null)
}
setIsLoading(false)
setIsRefreshing(false)
} else if (message.type === "mcpDownloadDetails") {
if (message.type === "mcpDownloadDetails") {
if (message.error) {
setError(message.error)
}
@@ -76,7 +68,7 @@ const McpMarketplaceView = () => {
window.addEventListener("message", handleMessage)
// Fetch marketplace catalog
// Fetch marketplace catalog on initial load
fetchMarketplace()
return () => {
@@ -84,6 +76,15 @@ const McpMarketplaceView = () => {
}
}, [])
useEffect(() => {
// Update loading state when catalog arrives
if (mcpMarketplaceCatalog?.items) {
setIsLoading(false)
setIsRefreshing(false)
setError(null)
}
}, [mcpMarketplaceCatalog])
const fetchMarketplace = (forceRefresh: boolean = false) => {
if (forceRefresh) {
setIsRefreshing(true)
@@ -9,6 +9,8 @@ const FeatureSettingsSection = () => {
setEnableCheckpointsSetting,
mcpMarketplaceEnabled,
setMcpMarketplaceEnabled,
mcpResponsesCollapsed,
setMcpResponsesCollapsed,
chatSettings,
setChatSettings,
} = useExtensionState()
@@ -42,6 +44,19 @@ 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 } from "@shared/proto/state"
import { PlanActMode, TogglePlanActModeRequest, UpdateSettingsRequest } 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,6 +21,8 @@ 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
@@ -130,6 +132,8 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
setShellIntegrationTimeout,
terminalReuseEnabled,
setTerminalReuseEnabled,
mcpResponsesCollapsed,
setMcpResponsesCollapsed,
setApiConfiguration,
} = useExtensionState()
@@ -141,15 +145,14 @@ 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 [pendingTabChange, setPendingTabChange] = useState<"plan" | "act" | null>(null)
const handleSubmit = (withoutDone: boolean = false) => {
const handleSubmit = async (withoutDone: boolean = false) => {
const apiValidationResult = validateApiConfiguration(apiConfiguration)
const modelIdValidationResult = validateModelId(apiConfiguration, openRouterModels)
@@ -177,17 +180,26 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
apiConfigurationToSubmit = undefined
}
vscode.postMessage({
type: "updateSettings",
planActSeparateModelsSetting,
customInstructionsSetting: customInstructions,
telemetrySetting,
enableCheckpointsSetting,
mcpMarketplaceEnabled,
shellIntegrationTimeout,
terminalReuseEnabled,
apiConfiguration: apiConfigurationToSubmit,
})
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)
}
if (!withoutDone) {
onDone()
@@ -208,6 +220,7 @@ 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
@@ -220,6 +233,7 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
planActSeparateModelsSetting,
enableCheckpointsSetting,
mcpMarketplaceEnabled,
mcpResponsesCollapsed,
chatSettings,
shellIntegrationTimeout,
terminalReuseEnabled,
@@ -260,6 +274,9 @@ 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()
}
@@ -277,6 +294,7 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
setApiConfiguration,
setEnableCheckpointsSetting,
setMcpMarketplaceEnabled,
setMcpResponsesCollapsed,
])
// Handle confirmation dialog actions
@@ -306,59 +324,42 @@ 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) {
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)
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)
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
}
},
[pendingTabChange],
)
}
break
}
}, [])
useEvent("message", handleMessage)
@@ -370,12 +371,27 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
}
}
const handlePlanActModeChange = (tab: "plan" | "act") => {
const handlePlanActModeChange = async (tab: "plan" | "act") => {
if (tab === chatSettings.mode) {
return
}
setPendingTabChange(tab)
handleSubmit(true)
// 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)
}
}
// Track active tab
+211 -63
View File
@@ -1,8 +1,16 @@
import React, { createContext, useCallback, useContext, useEffect, useRef, useState } from "react"
import { useEvent } from "react-use"
import { StateServiceClient, ModelsServiceClient, UiServiceClient } from "../services/grpc-client"
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"
import { DEFAULT_BROWSER_SETTINGS } from "@shared/BrowserSettings"
import { ChatSettings, DEFAULT_CHAT_SETTINGS } from "@shared/ChatSettings"
@@ -20,6 +28,7 @@ import {
import { McpMarketplaceCatalog, McpServer, McpViewTab } from "../../../src/shared/mcp"
import { convertTextMateToHljs } from "../utils/textMateToHljs"
import { vscode } from "../utils/vscode"
import { OpenRouterCompatibleModelInfo } from "@shared/proto/models"
interface ExtensionStateContextType extends ExtensionState {
didHydrateState: boolean
@@ -50,6 +59,7 @@ 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
@@ -90,6 +100,9 @@ const ExtensionStateContext = createContext<ExtensionStateContextType | undefine
export const ExtensionStateContextProvider: React.FC<{
children: React.ReactNode
}> = ({ children }) => {
// Get the current webview provider type
const currentProviderType =
window.WEBVIEW_PROVIDER_TYPE === "sidebar" ? WebviewProviderTypeEnum.SIDEBAR : WebviewProviderTypeEnum.TAB
// UI view state
const [showMcp, setShowMcp] = useState(false)
const [mcpTab, setMcpTab] = useState<McpViewTab | undefined>(undefined)
@@ -174,6 +187,7 @@ 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)
@@ -193,47 +207,6 @@ export const ExtensionStateContextProvider: React.FC<{
const handleMessage = useCallback((event: MessageEvent) => {
const message: ExtensionMessage = event.data
switch (message.type) {
case "action": {
switch (message.action!) {
case "settingsButtonClicked":
navigateToSettings()
break
}
break
}
case "theme": {
if (message.text) {
setTheme(convertTextMateToHljs(JSON.parse(message.text)))
}
break
}
case "workspaceUpdated": {
setFilePaths(message.filePaths ?? [])
break
}
case "partialMessage": {
const partialMessage = message.partialMessage!
setState((prevState) => {
// worth noting it will never be possible for a more up-to-date message to be sent here or in normal messages post since the presentAssistantContent function uses lock
const lastIndex = findLastIndex(prevState.clineMessages, (msg) => msg.ts === partialMessage.ts)
if (lastIndex !== -1) {
const newClineMessages = [...prevState.clineMessages]
newClineMessages[lastIndex] = partialMessage
return { ...prevState, clineMessages: newClineMessages }
}
return prevState
})
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)
@@ -251,12 +224,6 @@ export const ExtensionStateContextProvider: React.FC<{
setMcpServers(message.mcpServers ?? [])
break
}
case "mcpMarketplaceCatalog": {
if (message.mcpMarketplaceCatalog) {
setMcpMarketplaceCatalog(message.mcpMarketplaceCatalog)
}
break
}
}
}, [])
@@ -268,6 +235,12 @@ export const ExtensionStateContextProvider: React.FC<{
const historyButtonClickedSubscriptionRef = useRef<(() => void) | null>(null)
const chatButtonUnsubscribeRef = useRef<(() => void) | null>(null)
const accountButtonClickedSubscriptionRef = useRef<(() => void) | null>(null)
const settingsButtonClickedSubscriptionRef = useRef<(() => void) | null>(null)
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)
// Subscribe to state updates and UI events using the gRPC streaming API
useEffect(() => {
@@ -397,8 +370,135 @@ export const ExtensionStateContextProvider: React.FC<{
onComplete: () => {},
})
// Still send the webviewDidLaunch message for other initialization
vscode.postMessage({ type: "webviewDidLaunch" })
// 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({
providerType: currentProviderType,
}),
{
onResponse: () => {
// When settings button is clicked, navigate to settings
navigateToSettings()
},
onError: (error) => {
console.error("Error in settings button clicked subscription:", error)
},
onComplete: () => {
console.log("Settings button clicked subscription completed")
},
},
)
// Subscribe to partial message events
partialMessageUnsubscribeRef.current = UiServiceClient.subscribeToPartialMessage(EmptyRequest.create({}), {
onResponse: (protoMessage) => {
try {
console.log("[PARTIAL] Received partialMessage event from gRPC stream")
// Validate critical fields
if (!protoMessage.ts || protoMessage.ts <= 0) {
console.error("Invalid timestamp in partial message:", protoMessage)
return
}
const partialMessage = convertProtoToClineMessage(protoMessage)
console.log("[PARTIAL] Partial message:", partialMessage)
console.log("\n")
setState((prevState) => {
// worth noting it will never be possible for a more up-to-date message to be sent here or in normal messages post since the presentAssistantContent function uses lock
const lastIndex = findLastIndex(prevState.clineMessages, (msg) => msg.ts === partialMessage.ts)
if (lastIndex !== -1) {
const newClineMessages = [...prevState.clineMessages]
newClineMessages[lastIndex] = partialMessage
return { ...prevState, clineMessages: newClineMessages }
}
return prevState
})
} catch (error) {
console.error("Failed to process partial message:", error, protoMessage)
}
},
onError: (error) => {
console.error("Error in partialMessage subscription:", error)
},
onComplete: () => {
console.log("[DEBUG] partialMessage subscription completed")
},
})
// Subscribe to MCP marketplace catalog updates
mcpMarketplaceUnsubscribeRef.current = McpServiceClient.subscribeToMcpMarketplaceCatalog(EmptyRequest.create({}), {
onResponse: (catalog) => {
console.log("[DEBUG] Received MCP marketplace catalog update from gRPC stream")
setMcpMarketplaceCatalog(catalog)
},
onError: (error) => {
console.error("Error in MCP marketplace catalog subscription:", error)
},
onComplete: () => {
console.log("MCP marketplace catalog subscription completed")
},
})
// Subscribe to theme changes
themeSubscriptionRef.current = UiServiceClient.subscribeToTheme(EmptyRequest.create({}), {
onResponse: (response) => {
if (response.value) {
try {
const themeData = JSON.parse(response.value)
setTheme(convertTextMateToHljs(themeData))
console.log("[DEBUG] Received theme update from gRPC stream")
} catch (error) {
console.error("Error parsing theme data:", error)
}
}
},
onError: (error) => {
console.error("Error in theme subscription:", error)
},
onComplete: () => {
console.log("Theme subscription completed")
},
})
// 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)
})
// Set up account button clicked subscription
accountButtonClickedSubscriptionRef.current = UiServiceClient.subscribeToAccountButtonClicked(EmptyRequest.create(), {
@@ -437,15 +537,40 @@ export const ExtensionStateContextProvider: React.FC<{
accountButtonClickedSubscriptionRef.current()
accountButtonClickedSubscriptionRef.current = null
}
if (settingsButtonClickedSubscriptionRef.current) {
settingsButtonClickedSubscriptionRef.current()
settingsButtonClickedSubscriptionRef.current = null
}
if (partialMessageUnsubscribeRef.current) {
partialMessageUnsubscribeRef.current()
partialMessageUnsubscribeRef.current = null
}
if (mcpMarketplaceUnsubscribeRef.current) {
mcpMarketplaceUnsubscribeRef.current()
mcpMarketplaceUnsubscribeRef.current = null
}
if (themeSubscriptionRef.current) {
themeSubscriptionRef.current()
themeSubscriptionRef.current = null
}
if (openRouterModelsUnsubscribeRef.current) {
openRouterModelsUnsubscribeRef.current()
openRouterModelsUnsubscribeRef.current = null
}
if (workspaceUpdatesUnsubscribeRef.current) {
workspaceUpdatesUnsubscribeRef.current()
workspaceUpdatesUnsubscribeRef.current = null
}
}
}, [])
const refreshOpenRouterModels = useCallback(() => {
ModelsServiceClient.refreshOpenRouterModels(EmptyRequest.create({}))
.then((res) => {
.then((response: OpenRouterCompatibleModelInfo) => {
const models = response.models
setOpenRouterModels({
[openRouterDefaultModelId]: openRouterDefaultModelInfo, // in case the extension sent a model list without the default model
...res.models,
...models,
})
})
.catch((error: Error) => console.error("Failed to refresh OpenRouter models:", error))
@@ -519,6 +644,12 @@ export const ExtensionStateContextProvider: React.FC<{
...prevState,
mcpMarketplaceEnabled: value,
})),
setMcpResponsesCollapsed: (value) => {
setState((prevState) => ({
...prevState,
mcpResponsesCollapsed: value,
}))
},
setShowAnnouncement,
setShouldShowAnnouncement: (value) =>
setState((prevState) => ({
@@ -539,21 +670,38 @@ export const ExtensionStateContextProvider: React.FC<{
setMcpMarketplaceCatalog: (catalog: McpMarketplaceCatalog) => setMcpMarketplaceCatalog(catalog),
setShowMcp,
closeMcpView,
setChatSettings: (value) => {
setChatSettings: async (value) => {
setState((prevState) => ({
...prevState,
chatSettings: value,
}))
vscode.postMessage({
type: "updateSettings",
chatSettings: value,
apiConfiguration: state.apiConfiguration,
customInstructionsSetting: state.customInstructions,
telemetrySetting: state.telemetrySetting,
planActSeparateModelsSetting: state.planActSeparateModelsSetting,
enableCheckpointsSetting: state.enableCheckpointsSetting,
mcpMarketplaceEnabled: state.mcpMarketplaceEnabled,
})
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)
}
},
setGlobalClineRulesToggles: (toggles) =>
setState((prevState) => ({
+14 -8
View File
@@ -75,6 +75,19 @@ 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,
@@ -114,14 +127,7 @@ export function getContextMenuOptions(
return commits.length > 0 ? [workingChanges, ...commits] : [workingChanges]
}
return [
{ type: ContextMenuOptionType.URL },
{ type: ContextMenuOptionType.Problems },
{ type: ContextMenuOptionType.Terminal },
{ type: ContextMenuOptionType.Git },
{ type: ContextMenuOptionType.Folder },
{ type: ContextMenuOptionType.File },
]
return DEFAULT_CONTEXT_MENU_OPTIONS.map((type) => ({ type }))
}
const lowerQuery = query.toLowerCase()