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