mirror of
https://github.com/cline/cline.git
synced 2026-09-07 12:58:33 +08:00
Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2f2f7f2a1b | |||
| 4d7ced7e15 | |||
| d7e1fff011 | |||
| 8ca96dfd9f | |||
| 8c559d6916 | |||
| 2158165fc4 | |||
| 78792c240f | |||
| 0abd13e1f2 | |||
| 7e37cec1ab | |||
| b0926b1647 | |||
| 582a3b190d | |||
| 8c8a398f90 | |||
| 742a72b4ec | |||
| 84a00b05e0 | |||
| e21fa3fff9 | |||
| 5b3647fdff | |||
| 14b6e71416 | |||
| 4869aba90f | |||
| c68e427d13 | |||
| 0dca4dedbd | |||
| c6e7b5249e | |||
| 70e8d8bc13 | |||
| 9b3d93d4d0 |
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Migrated updateSettings to protos, removed didUpdateSettings, altered Plan/Act toggling in settings menu
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Fixed issue where telemetry warning popup was created for every new Cline window
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Prioritize active files in file context menu
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Added promise to task init to prevent race condition with checkpoints
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Spring cleaning
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Context menu is default to File option on start up
|
||||
@@ -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.
|
||||
Vendored
+2
-1
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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!)
|
||||
|
||||
Generated
+2
-2
@@ -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
@@ -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"
|
||||
|
||||
@@ -63,3 +63,8 @@ message StringArrays {
|
||||
repeated string values1 = 1;
|
||||
repeated string values2 = 2;
|
||||
}
|
||||
|
||||
message KeyValuePair {
|
||||
string key = 1;
|
||||
string value = 2;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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({})
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -91,6 +91,7 @@ export type GlobalStateKey =
|
||||
| "favoritedModelIds"
|
||||
| "requestTimeoutMs"
|
||||
| "shellIntegrationTimeout"
|
||||
| "mcpResponsesCollapsed"
|
||||
| "terminalReuseEnabled"
|
||||
| "isNewUser"
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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)
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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}`)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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,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
-13
@@ -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
|
||||
|
||||
@@ -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) => ({
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user