Compare commits

..

1 Commits

Author SHA1 Message Date
0xtoshii 09724b41fb remove warning 2025-07-17 17:44:54 -07:00
143 changed files with 25962 additions and 5970 deletions
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
fix: mcp servers are not started when disabled
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Improve the Claude Code error messages
+6 -26
View File
@@ -56,29 +56,6 @@ jobs:
path: webview-ui/node_modules
key: ${{ runner.os }}-npm-webview-${{ hashFiles('webview-ui/package-lock.json') }}
# Cache VS Code installation
- name: Cache VS Code
uses: actions/cache@v4
id: vscode-cache
with:
path: .vscode-test
key: vscode-${{ runner.os }}-stable-${{ hashFiles('.vscode-test.mjs', 'package.json') }}
restore-keys: |
vscode-${{ runner.os }}-stable-
# Cache Playwright browsers
- name: Cache Playwright browsers
uses: actions/cache@v4
id: playwright-cache
with:
path: |
~/.cache/ms-playwright
~/Library/Caches/ms-playwright
~/AppData/Local/ms-playwright
key: playwright-browsers-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
restore-keys: |
playwright-browsers-${{ runner.os }}-
- name: Install root dependencies
if: steps.root-cache.outputs.cache-hit != 'true'
run: npm ci
@@ -91,14 +68,17 @@ jobs:
if: matrix.runner == 'ubuntu'
run: sudo apt-get update && sudo apt-get install -y xvfb
# Run optimized E2E tests (eliminates redundant builds)
# Build the extension before running tests
- name: Build Tests and Extension
run: npm run pretest
- name: Run E2E tests - Linux
if: matrix.runner == 'ubuntu'
run: xvfb-run -a npm run test:e2e:optimal
run: xvfb-run -a npm run test:e2e
- name: Run E2E tests - Non-Linux
if: matrix.runner != 'ubuntu'
run: npm run test:e2e:optimal
run: npm run test:e2e
- uses: actions/upload-artifact@v4
if: ${{ failure() }}
-1
View File
@@ -94,7 +94,6 @@ jobs:
env:
VSCE_PAT: ${{ secrets.VSCE_PAT }}
OVSX_PAT: ${{ secrets.OVSX_PAT }}
CLINE_ENVIRONMENT: production
run: |
# Required to generate the .vsix
vsce package --out "cline-${{ steps.get_version.outputs.version }}.vsix"
+7 -1
View File
@@ -22,14 +22,20 @@ coverage
*evals.env
## Generated files ##
# Generated files
src/generated/
# Core
src/core/controller/*/methods.ts
src/core/controller/*/index.ts
src/core/controller/grpc-service-config.ts
# Shared
src/shared/proto/*.ts
src/shared/proto/host/*.ts
# Webview
webview-ui/src/services/grpc-client.ts
# Host bridge
src/hosts/vscode/client/host-grpc-client.ts
src/standalone/server-setup.ts
# E2E Tests
test-results
+1 -3
View File
@@ -5,6 +5,4 @@ webview-ui/build/
package-lock.json
src/core/prompts/system.ts
src/core/prompts/model_prompts/claude4.ts
evals/
docs/
out/
evals/
+2 -4
View File
@@ -14,8 +14,7 @@
"preLaunchTask": "${defaultBuildTask}",
"env": {
"IS_DEV": "true",
"DEV_WORKSPACE_FOLDER": "${workspaceFolder}",
"CLINE_ENVIRONMENT": "production"
"DEV_WORKSPACE_FOLDER": "${workspaceFolder}"
}
},
{
@@ -38,8 +37,7 @@
"env": {
"IS_DEV": "true",
"TEMP_PROFILE": "true",
"DEV_WORKSPACE_FOLDER": "${workspaceFolder}",
"CLINE_ENVIRONMENT": "production"
"DEV_WORKSPACE_FOLDER": "${workspaceFolder}"
}
},
{
-6
View File
@@ -1,11 +1,5 @@
# Changelog
## [3.19.7]
- Add Hugging Face as a new API provider with support for their inference API models
- Improve Claude Code error messages with better guidance for common setup issues (Thanks @BarreiroT!)
- Fix authentication sync issues when using multiple VSCode windows
## [3.19.6]
- Improve Kimi K2 model provider routing with additional provider options for better availability and performance
+1 -2
View File
@@ -159,8 +159,7 @@
"provider-config/openai",
"provider-config/openai-compatible",
"provider-config/openrouter",
"provider-config/requesty",
"provider-config/sap-aicore"
"provider-config/requesty"
]
},
{
+10118
View File
File diff suppressed because it is too large Load Diff
+2 -4
View File
@@ -4,15 +4,13 @@
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "mintlify dev",
"check": "mintlify broken-links",
"rename": "mintlify rename"
"dev": "mintlify dev"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"mintlify": "^4.2.23"
"mintlify": "^4.0.538"
}
}
-39
View File
@@ -1,39 +0,0 @@
---
title: "SAP AI Core"
description: "Learn how to configure and use LLM models from Generative AI Hub in SAP AI Core with Cline."
---
SAP AI Core and the generative AI hub help you to integrate LLMs and AI into new business processes in a cost-efficient manner.
**Website:** [SAP Help Portal](https://help.sap.com/docs/sap-ai-core/sap-ai-core-service-guide/what-is-sap-ai-core)
### Getting a Service Binding
> 💡 **Information**
>
> SAP AI Core, and Generative AI Hub, are offerings from SAP BTP.
> You need an active SAP BTP contract and a existing subaccount with a SAP AI Core instance to perform these steps.
1. **Access:** Go to your subaccount via [BTP Cloud Cockpit](cockpit.btp.cloud.sap/cockpit)
2. **Create a Service Binding:** Go to "Instances and Subscriptions", select your SAP AI Core service instance and click on Service Bindings > Create.
3. **Copy the Service Binding:** Copy the service binding values.
### Supported Models
SAP AI Core supports a large and growing number of models.
Refer to the [Generative AI Hub Supported Models page](https://me.sap.com/notes/3437766) for the complete and up-to-date list.
### Configuration in Cline
1. **Open Cline Settings:** Click the settings icon (⚙️) in the Cline panel.
2. **Select Provider:** Choose "SAP AI Core" from the "API Provider" dropdown.
3. **Enter Client Id:** Add the `.clientid` field from the service binding into the "AI Core Client Id" field.
4. **Enter Client Secret:** Add the `.clientsecret` field from the service binding into the "AI Core Client Secret" field.
5. **Enter Base URL:** Add the `.serviceurls.AI_API_URL` field from the service binding into the "AI Core Base URL" field.
6. **Enter Auth URL:** Add the `.url` field from the service binding into the "AI Core Auth URL" field.
7. **Enter Resource Group:** Add the resource group where you have your model deployments. See [Create a Deployment for a Generative AI Model](https://help.sap.com/docs/sap-ai-core/sap-ai-core-service-guide/create-deployment-for-generative-ai-model-in-sap-ai-core).
8. **Select Model:** Choose your desired model from the "Model" dropdown.
### Tips and Notes
- **Model Selection:** SAP AI Core offers a wide range of models. You won't be able to use the model, even if selected, if a deployment doesn't exist in the provided resource group.
+16 -61
View File
@@ -11,11 +11,8 @@ const disallowedApis = {
"vscode.workspace.fs.stat": {
messageId: "useFsUtils",
},
"vscode.workspace.fs.writeFile": {
messageId: "useFsUtils",
},
"vscode.workspace.workspaceFolders": {
messageId: "useHostBridgeWorkspace",
messageId: "useHostBridge",
},
"vscode.workspace.asRelativePath": {
messageId: "usePathUtils",
@@ -23,29 +20,6 @@ const disallowedApis = {
"vscode.workspace.getWorkspaceFolder": {
messageId: "usePathUtils",
},
"vscode.window.showTextDocument": {
messageId: "useHostBridge",
},
"vscode.workspace.applyEdit": {
messageId: "useHostBridge",
},
// "vscode.env.openExternal": {
// messageId: "useUtils",
// },
// "vscode.window.showWarningMessage": {
// messageId: "useHostBridgeShowMessage",
// },
"vscode.window.showOpenDialog": {
messageId: "useHostBridgeShowMessage",
},
// There are too many warnings for these calls, uncomment the following
// when the migration is finished.
// "vscode.window.showErrorMessage": {
// messageId: "useHostBridgeShowMessage",
// },
// "vscode.window.showInformationMessage": {
// messageId: "useHostBridgeShowMessage",
// },
}
module.exports = createRule({
@@ -63,28 +37,16 @@ module.exports = createRule({
"Example: AccountServiceClient.methodName(RequestType.create({...})) instead of vscode.postMessage({type: '...'}).\n" +
"Found: {{code}}",
useFsUtils:
"Use utilities in @/utils/fs instead of vscode.workspace.fs\n" +
"Use utilities in @/utils/fs instead of vscode.workspace.fs.stat.\n" +
"Example: import { isDirectory } from '@/utils/fs' or use the file system methods from the host bridge provider.\n" +
"Found: {{code}}",
usePathUtils:
"Use path utilities from @/utils/path instead of VSCode workspace path methods.\n" +
"This provides consistent path handling across different environments.\n" +
"Found: {{code}}",
useHostBridgeWorkspace:
useHostBridge:
"Use getHostBridgeProvider().workspaceClient.getWorkspacePaths({}) instead of vscode.workspace.workspaceFolders.\n" +
"This provides a consistent abstraction across VSCode and standalone environments.\n" +
"Found: {{code}}",
useHostBridgeShowMessage:
"Use getHostBridgeProvider().windowClient.showMessage instead of the vscode.window.showMessage.\n" +
"This provides a consistent abstraction across VSCode and standalone environments.\n" +
"Found: {{code}}",
useHostBridge:
"Use the host bridge instead of calling vscode APIs directly.\n" +
"This provides a consistent abstraction across VSCode and standalone environments.\n" +
"Found: {{code}}",
useUtils:
"Use utilities in @/utils instead of calling vscode APIs directly.\n" +
"This provides a consistent abstraction across VSCode and standalone environments.\n" +
usePathUtils:
"Use path utilities from @/utils/path instead of direct VSCode workspace path methods.\n" +
"This provides consistent path handling across different environments.\n" +
"Found: {{code}}",
},
schema: [],
@@ -92,10 +54,17 @@ module.exports = createRule({
defaultOptions: [],
create(context) {
// Check if current file is in an exception directory or is grpc-client-base.ts
const filename = context.filename
const isGrpcClientBase = path.basename(filename) === "grpc-client-base.ts"
// Skip checking files in src/hosts/vscode or standalone/runtime-files
const isExceptionDirectory = filename.includes("/src/hosts/vscode/") || filename.includes("/standalone/runtime-files/")
// Pattern for checking memberExpressions like vscode.workspace.fs.stat
function checkMemberExpression(node) {
if (isExcluded(context.filename)) {
// Skip if this file is being excluded.
// Skip if this file is in an exception directory or is grpc-client-base.ts
if (isGrpcClientBase || isExceptionDirectory) {
return
}
@@ -174,20 +143,6 @@ module.exports = createRule({
})
}
function isExcluded(filename) {
// Check if current file is in an exception directory or is grpc-client-base.ts
if (path.basename(filename) === "grpc-client-base.ts") {
return true
}
// Skip checking files in src/hosts/vscode or standalone/runtime-files
if (filename.includes("/src/hosts/vscode/")) {
return true
}
if (filename.includes("/standalone/runtime-files/")) {
return true
}
}
return {
// Detect basic member expressions (e.g., vscode.postMessage)
MemberExpression(node) {
@@ -197,7 +152,7 @@ module.exports = createRule({
// Detect property access through destructuring
VariableDeclarator(node) {
// Skip if this file is in an exception directory or is grpc-client-base.ts
if (isExcluded(context.filename)) {
if (isGrpcClientBase || isExceptionDirectory) {
return
}
+12557 -1031
View File
File diff suppressed because it is too large Load Diff
+5 -5
View File
@@ -2,7 +2,7 @@
"name": "claude-dev",
"displayName": "Cline",
"description": "Autonomous coding agent right in your IDE, capable of creating/editing files, running commands, using the browser, and more with your permission every step of the way.",
"version": "3.19.7",
"version": "3.19.6",
"icon": "assets/icons/icon.png",
"engines": {
"vscode": "^1.84.0"
@@ -347,7 +347,6 @@
"test:coverage": "vscode-test --coverage",
"e2e": "playwright test -c playwright.config.ts",
"test:e2e": "playwright install && vsce package --no-dependencies --out dist/e2e.vsix && node src/test/e2e/utils/build.js && playwright test",
"test:e2e:optimal": "vsce package --no-dependencies --out dist/e2e.vsix && node src/test/e2e/utils/build.js && playwright test",
"install:all": "npm install && cd webview-ui && npm install",
"dev:webview": "cd webview-ui && npm run dev",
"build:webview": "cd webview-ui && npm run build",
@@ -357,9 +356,9 @@
"prepare": "husky",
"changeset": "changeset",
"version-packages": "changeset version",
"docs": "cd docs && npm run dev",
"docs:check-links": "cd docs && npm run check",
"docs:rename-file": "cd docs && npm run rename",
"docs": "cd docs && mintlify dev",
"docs:check-links": "cd docs && mintlify broken-links",
"docs:rename-file": "cd docs && mintlify rename",
"report-issue": "node scripts/report-issue.js"
},
"lint-staged": {
@@ -397,6 +396,7 @@
"husky": "^9.1.7",
"lint-staged": "^16.1.0",
"minimatch": "^3.0.3",
"mintlify": "^4.0.515",
"npm-run-all": "^4.1.5",
"prettier": "^3.3.3",
"protoc-gen-ts": "^0.8.7",
+2 -40
View File
@@ -10,16 +10,7 @@ import "common.proto";
service DiffService {
// Open the diff view/editor.
rpc openDiff(OpenDiffRequest) returns (OpenDiffResponse);
// Get the contents of the diff view.
rpc getDocumentText(GetDocumentTextRequest) returns (GetDocumentTextResponse);
// Replace a text selection in the diff.
rpc replaceText(ReplaceTextRequest) returns (ReplaceTextResponse);
// Truncate the diff document.
rpc truncateDocument(TruncateDocumentRequest) returns (TruncateDocumentResponse);
// Save the diff document.
rpc saveDocument(SaveDocumentRequest) returns (SaveDocumentResponse);
// Close the diff editor UI.
rpc closeDiff(CloseDiffRequest) returns (CloseDiffResponse);
}
message OpenDiffRequest {
@@ -35,15 +26,6 @@ message OpenDiffResponse {
optional string diff_id = 1;
}
message GetDocumentTextRequest {
optional cline.Metadata metadata = 1;
optional string diff_id = 2;
}
message GetDocumentTextResponse {
optional string content = 1;
}
message ReplaceTextRequest {
optional cline.Metadata metadata = 1;
optional string diff_id = 2;
@@ -52,26 +34,6 @@ message ReplaceTextRequest {
optional int32 end_line = 5;
}
message ReplaceTextResponse {}
message TruncateDocumentRequest {
optional cline.Metadata metadata = 1;
optional string diff_id = 2;
optional int32 end_line = 5;
message ReplaceTextResponse {
// TBD
}
message TruncateDocumentResponse {}
message CloseDiffRequest {
optional cline.Metadata metadata = 1;
optional string diff_id = 2;
}
message CloseDiffResponse {}
message SaveDocumentRequest {
optional cline.Metadata metadata = 1;
optional string diff_id = 2;
}
message SaveDocumentResponse {}
-4
View File
@@ -6,10 +6,6 @@ option java_multiple_files = true;
import "common.proto";
/**
* The watch service is only here as example of a streaming rpc in the host bridge.
* This being replaced with a native JS file watcher.
*/
// WatchService provides methods for watching files in the IDE
service WatchService {
// Subscribe to file changes
+81 -118
View File
@@ -15,8 +15,6 @@ service ModelsService {
rpc getVsCodeLmModels(EmptyRequest) returns (VsCodeLmModelsArray);
// Refreshes and returns OpenRouter models
rpc refreshOpenRouterModels(EmptyRequest) returns (OpenRouterCompatibleModelInfo);
// Refreshes and returns Hugging Face models
rpc refreshHuggingFaceModels(EmptyRequest) returns (OpenRouterCompatibleModelInfo);
// Refreshes and returns OpenAI models
rpc refreshOpenAiModels(OpenAiModelsRequest) returns (StringArray);
// Refreshes and returns Requesty models
@@ -128,7 +126,6 @@ enum ApiProvider {
SAPAICORE = 25;
CLAUDE_CODE = 26;
MOONSHOT = 27;
HUGGINGFACE = 28;
}
// Model info for OpenAI-compatible models
@@ -168,119 +165,85 @@ message LiteLLMModelInfo {
// Main ApiConfiguration message
message ModelsApiConfiguration {
// Global configuration fields (not mode-specific)
optional string api_key = 1;
optional string cline_api_key = 2;
optional string task_id = 3;
optional string lite_llm_base_url = 4;
optional string lite_llm_api_key = 5;
optional bool lite_llm_use_prompt_cache = 6;
map<string, string> open_ai_headers = 7;
optional string anthropic_base_url = 8;
optional string open_router_api_key = 9;
optional string open_router_provider_sorting = 10;
optional string aws_access_key = 11;
optional string aws_secret_key = 12;
optional string aws_session_token = 13;
optional string aws_region = 14;
optional bool aws_use_cross_region_inference = 15;
optional bool aws_bedrock_use_prompt_cache = 16;
optional bool aws_use_profile = 17;
optional string aws_profile = 18;
optional string aws_bedrock_endpoint = 19;
optional string claude_code_path = 20;
optional string vertex_project_id = 21;
optional string vertex_region = 22;
optional string open_ai_base_url = 23;
optional string open_ai_api_key = 24;
optional string ollama_base_url = 25;
optional string ollama_api_options_ctx_num = 26;
optional string lm_studio_base_url = 27;
optional string gemini_api_key = 28;
optional string gemini_base_url = 29;
optional string open_ai_native_api_key = 30;
optional string deep_seek_api_key = 31;
optional string requesty_api_key = 32;
optional string together_api_key = 33;
optional string fireworks_api_key = 34;
optional int32 fireworks_model_max_completion_tokens = 35;
optional int32 fireworks_model_max_tokens = 36;
optional string qwen_api_key = 37;
optional string doubao_api_key = 38;
optional string mistral_api_key = 39;
optional string azure_api_version = 40;
optional string qwen_api_line = 41;
optional string nebius_api_key = 42;
optional string asksage_api_url = 43;
optional string asksage_api_key = 44;
optional string xai_api_key = 45;
optional string sambanova_api_key = 46;
optional string cerebras_api_key = 47;
optional int32 request_timeout_ms = 48;
optional string sap_ai_core_client_id = 49;
optional string sap_ai_core_client_secret = 50;
optional string sap_ai_resource_group = 51;
optional string sap_ai_core_token_url = 52;
optional string sap_ai_core_base_url = 53;
optional string moonshot_api_key = 54;
optional string moonshot_api_line = 55;
optional string aws_authentication = 56;
optional string aws_bedrock_api_key = 57;
optional string cline_account_id = 58;
optional string groq_api_key = 59;
optional string hugging_face_api_key = 60;
// Plan mode configurations
optional ApiProvider plan_mode_api_provider = 100;
optional string plan_mode_api_model_id = 101;
optional int32 plan_mode_thinking_budget_tokens = 102;
optional string plan_mode_reasoning_effort = 103;
optional LanguageModelChatSelector plan_mode_vs_code_lm_model_selector = 104;
optional bool plan_mode_aws_bedrock_custom_selected = 105;
optional string plan_mode_aws_bedrock_custom_model_base_id = 106;
optional string plan_mode_open_router_model_id = 107;
optional OpenRouterModelInfo plan_mode_open_router_model_info = 108;
optional string plan_mode_open_ai_model_id = 109;
optional OpenAiCompatibleModelInfo plan_mode_open_ai_model_info = 110;
optional string plan_mode_ollama_model_id = 111;
optional string plan_mode_lm_studio_model_id = 112;
optional string plan_mode_lite_llm_model_id = 113;
optional LiteLLMModelInfo plan_mode_lite_llm_model_info = 114;
optional string plan_mode_requesty_model_id = 115;
optional OpenRouterModelInfo plan_mode_requesty_model_info = 116;
optional string plan_mode_together_model_id = 117;
optional string plan_mode_fireworks_model_id = 118;
optional string plan_mode_sap_ai_core_model_id = 119;
optional string plan_mode_groq_model_id = 120;
optional OpenRouterModelInfo plan_mode_groq_model_info = 121;
optional string plan_mode_hugging_face_model_id = 122;
optional OpenRouterModelInfo plan_mode_hugging_face_model_info = 123;
// Act mode configurations
optional ApiProvider act_mode_api_provider = 200;
optional string act_mode_api_model_id = 201;
optional int32 act_mode_thinking_budget_tokens = 202;
optional string act_mode_reasoning_effort = 203;
optional LanguageModelChatSelector act_mode_vs_code_lm_model_selector = 204;
optional bool act_mode_aws_bedrock_custom_selected = 205;
optional string act_mode_aws_bedrock_custom_model_base_id = 206;
optional string act_mode_open_router_model_id = 207;
optional OpenRouterModelInfo act_mode_open_router_model_info = 208;
optional string act_mode_open_ai_model_id = 209;
optional OpenAiCompatibleModelInfo act_mode_open_ai_model_info = 210;
optional string act_mode_ollama_model_id = 211;
optional string act_mode_lm_studio_model_id = 212;
optional string act_mode_lite_llm_model_id = 213;
optional LiteLLMModelInfo act_mode_lite_llm_model_info = 214;
optional string act_mode_requesty_model_id = 215;
optional OpenRouterModelInfo act_mode_requesty_model_info = 216;
optional string act_mode_together_model_id = 217;
optional string act_mode_fireworks_model_id = 218;
optional string act_mode_sap_ai_core_model_id = 219;
optional string act_mode_groq_model_id = 220;
optional OpenRouterModelInfo act_mode_groq_model_info = 221;
optional string act_mode_hugging_face_model_id = 222;
optional OpenRouterModelInfo act_mode_hugging_face_model_info = 223;
repeated string favorited_model_ids = 300;
// From ApiHandlerOptions (excluding onRetryAttempt function)
optional string api_model_id = 1;
optional string api_key = 2;
optional string cline_account_id = 3;
optional string task_id = 4;
optional string lite_llm_base_url = 5;
optional string lite_llm_model_id = 6;
optional string lite_llm_api_key = 7;
optional bool lite_llm_use_prompt_cache = 8;
map<string, string> open_ai_headers = 9;
optional LiteLLMModelInfo lite_llm_model_info = 10;
optional string anthropic_base_url = 11;
optional string open_router_api_key = 12;
optional string open_router_model_id = 13;
optional OpenRouterModelInfo open_router_model_info = 14;
optional string open_router_provider_sorting = 15;
optional string aws_access_key = 16;
optional string aws_secret_key = 17;
optional string aws_session_token = 18;
optional string aws_region = 19;
optional bool aws_use_cross_region_inference = 20;
optional bool aws_bedrock_use_prompt_cache = 21;
optional bool aws_use_profile = 22;
optional string aws_profile = 23;
optional string aws_bedrock_endpoint = 24;
optional bool aws_bedrock_custom_selected = 25;
optional string aws_bedrock_custom_model_base_id = 26;
optional string vertex_project_id = 27;
optional string vertex_region = 28;
optional string open_ai_base_url = 29;
optional string open_ai_api_key = 30;
optional string open_ai_model_id = 31;
optional OpenAiCompatibleModelInfo open_ai_model_info = 32;
optional string ollama_model_id = 33;
optional string ollama_base_url = 34;
optional string ollama_api_options_ctx_num = 35;
optional string lm_studio_model_id = 36;
optional string lm_studio_base_url = 37;
optional string gemini_api_key = 38;
optional string gemini_base_url = 39;
optional string open_ai_native_api_key = 40;
optional string deep_seek_api_key = 41;
optional string requesty_api_key = 42;
optional string requesty_model_id = 43;
optional OpenRouterModelInfo requesty_model_info = 44;
optional string together_api_key = 45;
optional string together_model_id = 46;
optional string fireworks_api_key = 47;
optional string fireworks_model_id = 48;
optional int32 fireworks_model_max_completion_tokens = 49;
optional int32 fireworks_model_max_tokens = 50;
optional string qwen_api_key = 51;
optional string doubao_api_key = 52;
optional string mistral_api_key = 53;
optional string azure_api_version = 54;
optional LanguageModelChatSelector vs_code_lm_model_selector = 55;
optional string qwen_api_line = 56;
optional string nebius_api_key = 57;
optional string asksage_api_url = 58;
optional string asksage_api_key = 59;
optional string xai_api_key = 60;
optional int32 thinking_budget_tokens = 61;
optional string reasoning_effort = 62;
optional string sambanova_api_key = 63;
optional string cerebras_api_key = 64;
optional int32 request_timeout_ms = 65;
optional ApiProvider api_provider = 66;
repeated string favorited_model_ids = 67;
optional string sap_ai_core_client_id = 68;
optional string sap_ai_core_client_secret = 69;
optional string sap_ai_resource_group = 70;
optional string sap_ai_core_token_url = 71;
optional string sap_ai_core_base_url = 72;
optional string claude_code_path = 73;
optional string aws_authentication = 74;
optional string aws_bedrock_api_key = 75;
optional string moonshot_api_key = 76;
optional string moonshot_api_line = 77;
optional string groq_api_key = 78;
optional string groq_model_id = 79;
optional OpenRouterModelInfo groq_model_info = 80;
}
+115 -102
View File
@@ -118,113 +118,126 @@ message UpdateSettingsRequest {
// Complete API Configuration message
message ApiConfiguration {
// Global configuration fields (not mode-specific)
optional string api_key = 1; // anthropic
optional string cline_api_key = 2;
optional string task_id = 3;
optional string lite_llm_base_url = 4;
optional string lite_llm_api_key = 5;
optional bool lite_llm_use_prompt_cache = 6;
optional string openai_headers = 7; // JSON string
optional string anthropic_base_url = 8;
optional string openrouter_api_key = 9;
optional string openrouter_provider_sorting = 10;
optional string aws_access_key = 11;
optional string aws_secret_key = 12;
optional string aws_session_token = 13;
optional string aws_region = 14;
optional bool aws_use_cross_region_inference = 15;
optional bool aws_bedrock_use_prompt_cache = 16;
optional bool aws_use_profile = 17;
optional string aws_profile = 18;
optional string aws_bedrock_endpoint = 19;
optional string claude_code_path = 20;
optional string vertex_project_id = 21;
optional string vertex_region = 22;
optional string openai_base_url = 23;
optional string openai_api_key = 24;
optional string ollama_base_url = 25;
optional string ollama_api_options_ctx_num = 26;
optional string lm_studio_base_url = 27;
optional string gemini_api_key = 28;
optional string gemini_base_url = 29;
optional string openai_native_api_key = 30;
optional string deep_seek_api_key = 31;
optional string requesty_api_key = 32;
optional string together_api_key = 33;
optional string fireworks_api_key = 34;
optional int64 fireworks_model_max_completion_tokens = 35;
optional int64 fireworks_model_max_tokens = 36;
optional string qwen_api_key = 37;
optional string doubao_api_key = 38;
optional string mistral_api_key = 39;
optional string azure_api_version = 40;
optional string qwen_api_line = 41;
optional string nebius_api_key = 42;
optional string asksage_api_url = 43;
optional string asksage_api_key = 44;
optional string xai_api_key = 45;
optional string sambanova_api_key = 46;
optional string cerebras_api_key = 47;
optional int64 request_timeout_ms = 48;
optional string sap_ai_core_client_id = 49;
optional string sap_ai_core_client_secret = 50;
optional string sap_ai_resource_group = 51;
optional string sap_ai_core_token_url = 52;
optional string sap_ai_core_base_url = 53;
optional string moonshot_api_key = 54;
optional string moonshot_api_line = 55;
// 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;
// Plan mode configurations
optional string plan_mode_api_provider = 100;
optional string plan_mode_api_model_id = 101;
optional int64 plan_mode_thinking_budget_tokens = 102;
optional string plan_mode_reasoning_effort = 103;
optional string plan_mode_vscode_lm_model_selector = 104; // JSON string
optional bool plan_mode_aws_bedrock_custom_selected = 105;
optional string plan_mode_aws_bedrock_custom_model_base_id = 106;
optional string plan_mode_openrouter_model_id = 107;
optional string plan_mode_openrouter_model_info = 108; // JSON string
optional string plan_mode_openai_model_id = 109;
optional string plan_mode_openai_model_info = 110; // JSON string
optional string plan_mode_ollama_model_id = 111;
optional string plan_mode_lm_studio_model_id = 112;
optional string plan_mode_lite_llm_model_id = 113;
optional string plan_mode_lite_llm_model_info = 114; // JSON string
optional string plan_mode_requesty_model_id = 115;
optional string plan_mode_requesty_model_info = 116; // JSON string
optional string plan_mode_together_model_id = 117;
optional string plan_mode_fireworks_model_id = 118;
optional string plan_mode_sap_ai_core_model_id = 119;
// Provider-specific API keys
optional string cline_account_id = 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;
// Act mode configurations
optional string act_mode_api_provider = 200;
optional string act_mode_api_model_id = 201;
optional int64 act_mode_thinking_budget_tokens = 202;
optional string act_mode_reasoning_effort = 203;
optional string act_mode_vscode_lm_model_selector = 204; // JSON string
optional bool act_mode_aws_bedrock_custom_selected = 205;
optional string act_mode_aws_bedrock_custom_model_base_id = 206;
optional string act_mode_openrouter_model_id = 207;
optional string act_mode_openrouter_model_info = 208; // JSON string
optional string act_mode_openai_model_id = 209;
optional string act_mode_openai_model_info = 210; // JSON string
optional string act_mode_ollama_model_id = 211;
optional string act_mode_lm_studio_model_id = 212;
optional string act_mode_lite_llm_model_id = 213;
optional string act_mode_lite_llm_model_info = 214; // JSON string
optional string act_mode_requesty_model_id = 215;
optional string act_mode_requesty_model_info = 216; // JSON string
optional string act_mode_together_model_id = 217;
optional string act_mode_fireworks_model_id = 218;
optional string act_mode_sap_ai_core_model_id = 219;
// 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 = 300;
repeated string favorited_model_ids = 71;
// SAP AI Core specific
optional string sap_ai_core_client_id = 72;
optional string sap_ai_core_client_secret = 73;
optional string sap_ai_core_base_url = 74;
optional string sap_ai_core_token_url = 75;
optional string sap_ai_resource_group = 76;
// Claude Code specific
optional string claude_code_path = 77;
// Extension fields for Bedrock Api Keys
optional string aws_authentication = 301;
optional string aws_bedrock_api_key = 302;
optional string aws_authentication = 78;
optional string aws_bedrock_api_key = 79;
optional string cline_account_id = 303;
// Moonshot
optional string moonshot_api_key = 80;
optional string moonshot_api_line = 81;
}
+15 -12
View File
@@ -42,8 +42,6 @@ const serviceDirs = Object.keys(serviceNameMap).map((serviceKey) => path.join("s
async function main() {
console.log(chalk.bold.blue("Starting Protocol Buffer code generation..."))
await cleanup()
// Check for Apple Silicon compatibility before proceeding
checkAppleSiliconCompatibility()
@@ -52,6 +50,8 @@ async function main() {
await fs.mkdir(dir, { recursive: true })
}
await cleanup()
// Check for missing proto files for services in serviceNameMap
await ensureProtoFilesExist()
@@ -420,13 +420,20 @@ service ${serviceClassName} {
async function cleanup() {
// Clean up existing generated files
log_verbose(chalk.cyan("Cleaning up existing generated TypeScript files..."))
await rmrf(TS_OUT_DIR)
await rmrf("src/generated")
const existingFiles = await globby("**/*.ts", { cwd: TS_OUT_DIR })
for (const file of existingFiles) {
await fs.unlink(path.join(TS_OUT_DIR, file))
}
await rmdir("src/generated")
// Clean up generated files that were moved.
await rmrf("src/standalone/services/host-grpc-client.ts")
await rmrf("src/standalone/server-setup.ts")
await rmrf("src/hosts/vscode/host-grpc-service-config.ts")
await fs.rm("src/standalone/services/host-grpc-client.ts", { force: true })
await rmdir("src/standalone/services")
await fs.rm("hosts/vscode", { force: true, recursive: true })
await rmdir("hosts")
await fs.rm("src/standalone/server-setup.ts", { force: true })
await fs.rm("src/hosts/vscode/host-grpc-service-config.ts", { force: true })
const oldhostbridgefiles = [
"src/hosts/vscode/workspace/methods.ts",
"src/hosts/vscode/workspace/index.ts",
@@ -442,7 +449,7 @@ async function cleanup() {
"src/hosts/vscode/uri/index.ts",
]
for (const file of oldhostbridgefiles) {
await rmrf(file)
await fs.rm(file, { force: true })
}
}
@@ -468,10 +475,6 @@ async function rmdir(path) {
}
}
async function rmrf(path) {
await fs.rm(path, { force: true, recursive: true })
}
// Check for Apple Silicon compatibility
function checkAppleSiliconCompatibility() {
// Only run check on macOS
+1 -1
View File
@@ -62,7 +62,7 @@ let output = `// GENERATED CODE -- DO NOT EDIT!
import * as grpc from "@grpc/grpc-js"
import { cline } from "@generated/grpc-js"
import { Controller } from "@core/controller"
import { GrpcHandlerWrapper, GrpcStreamingResponseHandlerWrapper } from "@hosts/external/grpc-types"
import { GrpcHandlerWrapper, GrpcStreamingResponseHandlerWrapper } from "@/standalone/grpc-types"
${imports}
export function addProtobusServices(
+63 -98
View File
@@ -29,8 +29,6 @@ import { SapAiCoreHandler } from "./providers/sapaicore"
import { ClaudeCodeHandler } from "./providers/claude-code"
import { MoonshotHandler } from "./providers/moonshot"
import { GroqHandler } from "./providers/groq"
import { Mode } from "../shared/ChatSettings"
import { HuggingFaceHandler } from "./providers/huggingface"
export interface ApiHandler {
createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream
@@ -42,33 +40,27 @@ export interface SingleCompletionHandler {
completePrompt(prompt: string): Promise<string>
}
function createHandlerForProvider(
apiProvider: string | undefined,
options: Omit<ApiConfiguration, "apiProvider">,
mode: Mode,
): ApiHandler {
function createHandlerForProvider(apiProvider: string | undefined, options: Omit<ApiConfiguration, "apiProvider">): ApiHandler {
switch (apiProvider) {
case "anthropic":
return new AnthropicHandler({
apiKey: options.apiKey,
anthropicBaseUrl: options.anthropicBaseUrl,
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
thinkingBudgetTokens:
mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens,
apiModelId: options.apiModelId,
thinkingBudgetTokens: options.thinkingBudgetTokens,
})
case "openrouter":
return new OpenRouterHandler({
openRouterApiKey: options.openRouterApiKey,
openRouterModelId: mode === "plan" ? options.planModeOpenRouterModelId : options.actModeOpenRouterModelId,
openRouterModelInfo: mode === "plan" ? options.planModeOpenRouterModelInfo : options.actModeOpenRouterModelInfo,
openRouterModelId: options.openRouterModelId,
openRouterModelInfo: options.openRouterModelInfo,
openRouterProviderSorting: options.openRouterProviderSorting,
reasoningEffort: mode === "plan" ? options.planModeReasoningEffort : options.actModeReasoningEffort,
thinkingBudgetTokens:
mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens,
reasoningEffort: options.reasoningEffort,
thinkingBudgetTokens: options.thinkingBudgetTokens,
})
case "bedrock":
return new AwsBedrockHandler({
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
apiModelId: options.apiModelId,
awsAccessKey: options.awsAccessKey,
awsSecretKey: options.awsSecretKey,
awsSessionToken: options.awsSessionToken,
@@ -80,20 +72,16 @@ function createHandlerForProvider(
awsUseProfile: options.awsUseProfile,
awsProfile: options.awsProfile,
awsBedrockEndpoint: options.awsBedrockEndpoint,
awsBedrockCustomSelected:
mode === "plan" ? options.planModeAwsBedrockCustomSelected : options.actModeAwsBedrockCustomSelected,
awsBedrockCustomModelBaseId:
mode === "plan" ? options.planModeAwsBedrockCustomModelBaseId : options.actModeAwsBedrockCustomModelBaseId,
thinkingBudgetTokens:
mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens,
awsBedrockCustomSelected: options.awsBedrockCustomSelected,
awsBedrockCustomModelBaseId: options.awsBedrockCustomModelBaseId,
thinkingBudgetTokens: options.thinkingBudgetTokens,
})
case "vertex":
return new VertexHandler({
vertexProjectId: options.vertexProjectId,
vertexRegion: options.vertexRegion,
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
thinkingBudgetTokens:
mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens,
apiModelId: options.apiModelId,
thinkingBudgetTokens: options.thinkingBudgetTokens,
geminiApiKey: options.geminiApiKey,
geminiBaseUrl: options.geminiBaseUrl,
taskId: options.taskId,
@@ -104,21 +92,21 @@ function createHandlerForProvider(
openAiBaseUrl: options.openAiBaseUrl,
azureApiVersion: options.azureApiVersion,
openAiHeaders: options.openAiHeaders,
openAiModelId: mode === "plan" ? options.planModeOpenAiModelId : options.actModeOpenAiModelId,
openAiModelInfo: mode === "plan" ? options.planModeOpenAiModelInfo : options.actModeOpenAiModelInfo,
reasoningEffort: mode === "plan" ? options.planModeReasoningEffort : options.actModeReasoningEffort,
openAiModelId: options.openAiModelId,
openAiModelInfo: options.openAiModelInfo,
reasoningEffort: options.reasoningEffort,
})
case "ollama":
return new OllamaHandler({
ollamaBaseUrl: options.ollamaBaseUrl,
ollamaModelId: mode === "plan" ? options.planModeOllamaModelId : options.actModeOllamaModelId,
ollamaModelId: options.ollamaModelId,
ollamaApiOptionsCtxNum: options.ollamaApiOptionsCtxNum,
requestTimeoutMs: options.requestTimeoutMs,
})
case "lmstudio":
return new LmStudioHandler({
lmStudioBaseUrl: options.lmStudioBaseUrl,
lmStudioModelId: mode === "plan" ? options.planModeLmStudioModelId : options.actModeLmStudioModelId,
lmStudioModelId: options.lmStudioModelId,
})
case "gemini":
return new GeminiHandler({
@@ -126,85 +114,78 @@ function createHandlerForProvider(
vertexRegion: options.vertexRegion,
geminiApiKey: options.geminiApiKey,
geminiBaseUrl: options.geminiBaseUrl,
thinkingBudgetTokens:
mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens,
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
thinkingBudgetTokens: options.thinkingBudgetTokens,
apiModelId: options.apiModelId,
taskId: options.taskId,
})
case "openai-native":
return new OpenAiNativeHandler({
openAiNativeApiKey: options.openAiNativeApiKey,
reasoningEffort: mode === "plan" ? options.planModeReasoningEffort : options.actModeReasoningEffort,
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
reasoningEffort: options.reasoningEffort,
apiModelId: options.apiModelId,
})
case "deepseek":
return new DeepSeekHandler({
deepSeekApiKey: options.deepSeekApiKey,
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
apiModelId: options.apiModelId,
})
case "requesty":
return new RequestyHandler({
requestyApiKey: options.requestyApiKey,
reasoningEffort: mode === "plan" ? options.planModeReasoningEffort : options.actModeReasoningEffort,
thinkingBudgetTokens:
mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens,
requestyModelId: mode === "plan" ? options.planModeRequestyModelId : options.actModeRequestyModelId,
requestyModelInfo: mode === "plan" ? options.planModeRequestyModelInfo : options.actModeRequestyModelInfo,
reasoningEffort: options.reasoningEffort,
thinkingBudgetTokens: options.thinkingBudgetTokens,
requestyModelId: options.requestyModelId,
requestyModelInfo: options.requestyModelInfo,
})
case "fireworks":
return new FireworksHandler({
fireworksApiKey: options.fireworksApiKey,
fireworksModelId: mode === "plan" ? options.planModeFireworksModelId : options.actModeFireworksModelId,
fireworksModelId: options.fireworksModelId,
fireworksModelMaxCompletionTokens: options.fireworksModelMaxCompletionTokens,
fireworksModelMaxTokens: options.fireworksModelMaxTokens,
})
case "together":
return new TogetherHandler({
togetherApiKey: options.togetherApiKey,
togetherModelId: mode === "plan" ? options.planModeTogetherModelId : options.actModeTogetherModelId,
togetherModelId: options.togetherModelId,
})
case "qwen":
return new QwenHandler({
qwenApiKey: options.qwenApiKey,
qwenApiLine: options.qwenApiLine,
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
thinkingBudgetTokens:
mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens,
apiModelId: options.apiModelId,
thinkingBudgetTokens: options.thinkingBudgetTokens,
})
case "doubao":
return new DoubaoHandler({
doubaoApiKey: options.doubaoApiKey,
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
apiModelId: options.apiModelId,
})
case "mistral":
return new MistralHandler({
mistralApiKey: options.mistralApiKey,
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
apiModelId: options.apiModelId,
})
case "vscode-lm":
return new VsCodeLmHandler({
vsCodeLmModelSelector:
mode === "plan" ? options.planModeVsCodeLmModelSelector : options.actModeVsCodeLmModelSelector,
vsCodeLmModelSelector: options.vsCodeLmModelSelector,
})
case "cline":
return new ClineHandler({
clineAccountId: options.clineAccountId,
taskId: options.taskId,
reasoningEffort: mode === "plan" ? options.planModeReasoningEffort : options.actModeReasoningEffort,
thinkingBudgetTokens:
mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens,
reasoningEffort: options.reasoningEffort,
thinkingBudgetTokens: options.thinkingBudgetTokens,
openRouterProviderSorting: options.openRouterProviderSorting,
openRouterModelId: mode === "plan" ? options.planModeOpenRouterModelId : options.actModeOpenRouterModelId,
openRouterModelInfo: mode === "plan" ? options.planModeOpenRouterModelInfo : options.actModeOpenRouterModelInfo,
openRouterModelId: options.openRouterModelId,
openRouterModelInfo: options.openRouterModelInfo,
})
case "litellm":
return new LiteLlmHandler({
liteLlmApiKey: options.liteLlmApiKey,
liteLlmBaseUrl: options.liteLlmBaseUrl,
liteLlmModelId: mode === "plan" ? options.planModeLiteLlmModelId : options.actModeLiteLlmModelId,
liteLlmModelInfo: mode === "plan" ? options.planModeLiteLlmModelInfo : options.actModeLiteLlmModelInfo,
thinkingBudgetTokens:
mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens,
liteLlmModelId: options.liteLlmModelId,
liteLlmModelInfo: options.liteLlmModelInfo,
thinkingBudgetTokens: options.thinkingBudgetTokens,
liteLlmUsePromptCache: options.liteLlmUsePromptCache,
taskId: options.taskId,
})
@@ -212,48 +193,41 @@ function createHandlerForProvider(
return new MoonshotHandler({
moonshotApiKey: options.moonshotApiKey,
moonshotApiLine: options.moonshotApiLine,
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
})
case "huggingface":
return new HuggingFaceHandler({
huggingFaceApiKey: options.huggingFaceApiKey,
huggingFaceModelId: mode === "plan" ? options.planModeHuggingFaceModelId : options.actModeHuggingFaceModelId,
huggingFaceModelInfo:
mode === "plan" ? options.planModeHuggingFaceModelInfo : options.actModeHuggingFaceModelInfo,
apiModelId: options.apiModelId,
})
case "nebius":
return new NebiusHandler({
nebiusApiKey: options.nebiusApiKey,
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
apiModelId: options.apiModelId,
})
case "asksage":
return new AskSageHandler({
asksageApiKey: options.asksageApiKey,
asksageApiUrl: options.asksageApiUrl,
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
apiModelId: options.apiModelId,
})
case "xai":
return new XAIHandler({
xaiApiKey: options.xaiApiKey,
reasoningEffort: mode === "plan" ? options.planModeReasoningEffort : options.actModeReasoningEffort,
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
reasoningEffort: options.reasoningEffort,
apiModelId: options.apiModelId,
})
case "sambanova":
return new SambanovaHandler({
sambanovaApiKey: options.sambanovaApiKey,
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
apiModelId: options.apiModelId,
})
case "cerebras":
return new CerebrasHandler({
cerebrasApiKey: options.cerebrasApiKey,
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
apiModelId: options.apiModelId,
})
case "groq":
return new GroqHandler({
groqApiKey: options.groqApiKey,
groqModelId: mode === "plan" ? options.planModeGroqModelId : options.actModeGroqModelId,
groqModelInfo: mode === "plan" ? options.planModeGroqModelInfo : options.actModeGroqModelInfo,
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
groqModelId: options.groqModelId,
groqModelInfo: options.groqModelInfo,
apiModelId: options.apiModelId,
})
case "sapaicore":
return new SapAiCoreHandler({
@@ -262,46 +236,37 @@ function createHandlerForProvider(
sapAiCoreTokenUrl: options.sapAiCoreTokenUrl,
sapAiResourceGroup: options.sapAiResourceGroup,
sapAiCoreBaseUrl: options.sapAiCoreBaseUrl,
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
apiModelId: options.apiModelId,
})
case "claude-code":
return new ClaudeCodeHandler({
claudeCodePath: options.claudeCodePath,
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
thinkingBudgetTokens:
mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens,
apiModelId: options.apiModelId,
thinkingBudgetTokens: options.thinkingBudgetTokens,
})
default:
return new AnthropicHandler({
apiKey: options.apiKey,
anthropicBaseUrl: options.anthropicBaseUrl,
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
thinkingBudgetTokens:
mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens,
apiModelId: options.apiModelId,
thinkingBudgetTokens: options.thinkingBudgetTokens,
})
}
}
export function buildApiHandler(configuration: ApiConfiguration, mode: Mode): ApiHandler {
const { planModeApiProvider, actModeApiProvider, ...options } = configuration
const apiProvider = mode === "plan" ? planModeApiProvider : actModeApiProvider
export function buildApiHandler(configuration: ApiConfiguration): ApiHandler {
const { apiProvider, ...options } = configuration
// Validate thinking budget tokens against model's maxTokens to prevent API errors
// wrapped in a try-catch for safety, but this should never throw
try {
const thinkingBudgetTokens = mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens
if (thinkingBudgetTokens && thinkingBudgetTokens > 0) {
const handler = createHandlerForProvider(apiProvider, options, mode)
if (options.thinkingBudgetTokens && options.thinkingBudgetTokens > 0) {
const handler = createHandlerForProvider(apiProvider, options)
const modelInfo = handler.getModel().info
if (modelInfo.maxTokens && thinkingBudgetTokens > modelInfo.maxTokens) {
if (modelInfo.maxTokens && options.thinkingBudgetTokens > modelInfo.maxTokens) {
const clippedValue = modelInfo.maxTokens - 1
if (mode === "plan") {
options.planModeThinkingBudgetTokens = clippedValue
} else {
options.actModeThinkingBudgetTokens = clippedValue
}
options.thinkingBudgetTokens = clippedValue
} else {
return handler // don't rebuild unless its necessary
}
@@ -310,5 +275,5 @@ export function buildApiHandler(configuration: ApiConfiguration, mode: Mode): Ap
console.error("buildApiHandler error:", error)
}
return createHandlerForProvider(apiProvider, options, mode)
return createHandlerForProvider(apiProvider, options)
}
+13 -13
View File
@@ -203,7 +203,7 @@ describe("AwsBedrockHandler", () => {
})
const mockOptions: ApiHandlerOptions = {
actModeApiModelId: "anthropic.claude-3-7-sonnet-20250219-v1:0",
apiModelId: "anthropic.claude-3-7-sonnet-20250219-v1:0",
awsRegion: "us-east-1",
awsAccessKey: "test-key",
awsSecretKey: "test-secret",
@@ -214,9 +214,9 @@ describe("AwsBedrockHandler", () => {
awsBedrockUsePromptCache: false,
awsUseCrossRegionInference: false,
awsBedrockEndpoint: "",
actModeAwsBedrockCustomSelected: false,
actModeAwsBedrockCustomModelBaseId: undefined,
actModeThinkingBudgetTokens: 1600,
awsBedrockCustomSelected: false,
awsBedrockCustomModelBaseId: undefined,
thinkingBudgetTokens: 1600,
}
const mockModelInfo = {
@@ -616,8 +616,8 @@ describe("AwsBedrockHandler", () => {
it("should return raw model ID for custom models", async () => {
const customOptions: ApiHandlerOptions = {
...mockOptions,
actModeAwsBedrockCustomSelected: true,
actModeApiModelId:
awsBedrockCustomSelected: true,
apiModelId:
"arn:aws:bedrock:us-west-2:123456789012:custom-model/anthropic.claude-3-5-sonnet-20241022-v2:0/Qk8MMyLmRd",
}
const customHandler = new AwsBedrockHandler(customOptions)
@@ -631,8 +631,8 @@ describe("AwsBedrockHandler", () => {
it("should not encode custom model IDs with slashes", async () => {
const customOptions: ApiHandlerOptions = {
...mockOptions,
actModeAwsBedrockCustomSelected: true,
actModeApiModelId: "my-namespace/my-custom-model",
awsBedrockCustomSelected: true,
apiModelId: "my-namespace/my-custom-model",
}
const customHandler = new AwsBedrockHandler(customOptions)
@@ -680,8 +680,8 @@ describe("AwsBedrockHandler", () => {
it("should not apply cross-region prefix for custom models even when enabled", async () => {
const customCrossRegionOptions: ApiHandlerOptions = {
...mockOptions,
actModeAwsBedrockCustomSelected: true,
actModeApiModelId: "arn:aws:bedrock:us-west-2:123456789012:custom-model/my-model",
awsBedrockCustomSelected: true,
apiModelId: "arn:aws:bedrock:us-west-2:123456789012:custom-model/my-model",
awsUseCrossRegionInference: true,
}
const customCrossRegionHandler = new AwsBedrockHandler(customCrossRegionOptions)
@@ -693,10 +693,10 @@ describe("AwsBedrockHandler", () => {
it("should handle UltraThink model ARN correctly", async () => {
const ultraThinkOptions: ApiHandlerOptions = {
...mockOptions,
actModeAwsBedrockCustomSelected: true,
actModeApiModelId:
awsBedrockCustomSelected: true,
apiModelId:
"arn:aws:bedrock:us-west-2:123456789012:custom-model/anthropic.claude-3-5-sonnet-20241022-v2:0/Qk8MMyLmRd",
actModeAwsBedrockCustomModelBaseId: "anthropic.claude-3-5-sonnet-20241022-v2:0",
awsBedrockCustomModelBaseId: "anthropic.claude-3-5-sonnet-20241022-v2:0",
}
const ultraThinkHandler = new AwsBedrockHandler(ultraThinkOptions)
+1 -1
View File
@@ -26,7 +26,7 @@ describe("OllamaHandler", () => {
beforeEach(() => {
options = {
actModeOllamaModelId: "llama2",
ollamaModelId: "llama2",
ollamaBaseUrl: "http://localhost:11434",
}
handler = new OllamaHandler(options)
+1 -1
View File
@@ -13,7 +13,7 @@ interface AnthropicHandlerOptions {
}
export class AnthropicHandler implements ApiHandler {
private options: AnthropicHandlerOptions
private options: ApiHandlerOptions
private client: Anthropic | undefined
constructor(options: AnthropicHandlerOptions) {
+11 -1
View File
@@ -177,7 +177,17 @@ export class ClineHandler implements ApiHandler {
}
} catch (error) {
console.error("Cline API Error:", error)
throw error
const requestId = error?.request_id ? `\n | Request ID: ${error.request_id}` : ""
if (error.code === "ERR_BAD_REQUEST" || error.status === 401) {
throw new Error(CLINE_ACCOUNT_AUTH_ERROR_MESSAGE + requestId)
} else if (error.code === "insufficient_credits" || error.status === 402) {
if (error.error) {
throw new Error(JSON.stringify(error.error))
}
}
const _error = error instanceof Error ? error : new Error(String(error))
_error.message = _error.message + requestId
throw _error
}
}
+1 -1
View File
@@ -44,7 +44,7 @@ interface GeminiHandlerOptions {
* 4. Separating immediate costs from ongoing costs to avoid double-counting
*/
export class GeminiHandler implements ApiHandler {
private options: GeminiHandlerOptions
private options: ApiHandlerOptions
private client: GoogleGenAI | undefined
constructor(options: GeminiHandlerOptions) {
-142
View File
@@ -1,142 +0,0 @@
import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI from "openai"
import { withRetry } from "../retry"
import { ApiHandler } from "../"
import { ApiHandlerOptions, HuggingFaceModelId, ModelInfo, huggingFaceDefaultModelId, huggingFaceModels } from "@shared/api"
import { calculateApiCostOpenAI } from "../../utils/cost"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { ApiStream } from "../transform/stream"
interface HuggingFaceHandlerOptions {
huggingFaceApiKey?: string
huggingFaceModelId?: string
huggingFaceModelInfo?: ModelInfo
}
export class HuggingFaceHandler implements ApiHandler {
private options: HuggingFaceHandlerOptions
private client: OpenAI | undefined
private cachedModel: { id: HuggingFaceModelId; info: ModelInfo } | undefined
constructor(options: HuggingFaceHandlerOptions) {
this.options = options
}
private ensureClient(): OpenAI {
if (!this.client) {
if (!this.options.huggingFaceApiKey) {
throw new Error("Hugging Face API key is required")
}
try {
this.client = new OpenAI({
baseURL: "https://router.huggingface.co/v1",
apiKey: this.options.huggingFaceApiKey,
defaultHeaders: {
"User-Agent": "Cline/1.0",
},
})
} catch (error: any) {
throw new Error(`Error creating Hugging Face client: ${error.message}`)
}
}
return this.client
}
private async *yieldUsage(info: ModelInfo, usage: OpenAI.Completions.CompletionUsage | undefined): ApiStream {
if (!usage) {
return
}
const inputTokens = usage.prompt_tokens || 0
const outputTokens = usage.completion_tokens || 0
const totalCost = calculateApiCostOpenAI(info, inputTokens, outputTokens)
const usageData = {
type: "usage" as const,
inputTokens: inputTokens,
outputTokens: outputTokens,
cacheWriteTokens: 0,
cacheReadTokens: 0,
totalCost: totalCost,
}
yield usageData
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
try {
const client = this.ensureClient()
const model = this.getModel()
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
{ role: "system", content: systemPrompt },
...convertToOpenAiMessages(messages),
]
const requestParams = {
model: model.id,
max_tokens: model.info.maxTokens,
messages: openAiMessages,
stream: true,
stream_options: { include_usage: true },
temperature: 0,
}
const stream = (await client.chat.completions.create(requestParams)) as any
let chunkCount = 0
let totalContent = ""
for await (const chunk of stream) {
chunkCount++
const delta = chunk.choices[0]?.delta
if (delta?.content) {
totalContent += delta.content
yield {
type: "text",
text: delta.content,
}
}
if (chunk.usage) {
yield* this.yieldUsage(model.info, chunk.usage)
}
}
} catch (error: any) {
throw error
}
}
getModel(): { id: HuggingFaceModelId; info: ModelInfo } {
// Return cached model if available
if (this.cachedModel) {
return this.cachedModel
}
const modelId = this.options.huggingFaceModelId
// List all available models for debugging
const availableModels = Object.keys(huggingFaceModels)
let result: { id: HuggingFaceModelId; info: ModelInfo }
if (modelId && modelId in huggingFaceModels) {
const id = modelId as HuggingFaceModelId
const modelInfo = huggingFaceModels[id]
result = { id, info: modelInfo }
} else {
const defaultInfo = huggingFaceModels[huggingFaceDefaultModelId]
result = {
id: huggingFaceDefaultModelId,
info: defaultInfo,
}
}
// Cache the result for future calls
this.cachedModel = result
return result
}
}
-9
View File
@@ -19,15 +19,6 @@ interface OpenRouterHandlerOptions {
thinkingBudgetTokens?: number
}
interface OpenRouterHandlerOptions {
openRouterApiKey?: string
openRouterModelId?: string
openRouterModelInfo?: ModelInfo
openRouterProviderSorting?: string
reasoningEffort?: string
thinkingBudgetTokens?: number
}
export class OpenRouterHandler implements ApiHandler {
private options: OpenRouterHandlerOptions
private client: OpenAI | undefined
-6
View File
@@ -14,12 +14,6 @@ interface XAIHandlerOptions {
apiModelId?: string
}
interface XAIHandlerOptions {
xaiApiKey?: string
reasoningEffort?: string
apiModelId?: string
}
export class XAIHandler implements ApiHandler {
private options: XAIHandlerOptions
private client: OpenAI | undefined
+2 -2
View File
@@ -1,6 +1,6 @@
export type Environment = "production" | "staging" | "local"
const CLINE_ENVIRONMENT: Environment = (process.env.CLINE_ENVIRONMENT as Environment) || "production"
const CURRENT_ENVIRONMENT: Environment = "production"
interface EnvironmentConfig {
appBaseUrl: string
@@ -55,4 +55,4 @@ const configs: Record<Environment, EnvironmentConfig> = {
},
}
export const clineEnvConfig = configs[CLINE_ENVIRONMENT]
export const clineEnvConfig = configs[CURRENT_ENVIRONMENT]
+12 -8
View File
@@ -44,10 +44,12 @@ export const createRuleFile: FileMethodHandler = async (controller: Controller,
if (fileExists) {
const message = `${fileTypeName} file "${request.filename}" already exists.`
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.WARNING,
message,
})
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.WARNING,
message,
}),
)
// Still open it for editing
await handleFileServiceRequest(controller, "openFile", { value: filePath })
} else {
@@ -61,10 +63,12 @@ export const createRuleFile: FileMethodHandler = async (controller: Controller,
await handleFileServiceRequest(controller, "openFile", { value: filePath })
const message = `Created new ${request.isGlobal ? "global" : "workspace"} ${fileTypeName} file: ${request.filename}`
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.INFORMATION,
message,
})
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.INFORMATION,
message,
}),
)
}
return RuleFile.create({
+6 -4
View File
@@ -46,10 +46,12 @@ export const deleteRuleFile: FileMethodHandler = async (controller: Controller,
const fileTypeName = request.type === "workflow" ? "workflow" : "rule"
const message = `${fileTypeName} file "${fileName}" deleted successfully`
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.INFORMATION,
message,
})
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.INFORMATION,
message,
}),
)
return RuleFile.create({
filePath: request.rulePath,
+238 -100
View File
@@ -10,7 +10,7 @@ import { ClineAccountService } from "@services/account/ClineAccountService"
import { McpHub } from "@services/mcp/McpHub"
import { ApiProvider, ModelInfo } from "@shared/api"
import { ChatContent } from "@shared/ChatContent"
import { ChatSettings, Mode, StoredChatSettings } from "@shared/ChatSettings"
import { ChatSettings, StoredChatSettings } from "@shared/ChatSettings"
import { ClineRulesToggles } from "@shared/cline-rules"
import { ExtensionMessage, ExtensionState, Platform } from "@shared/ExtensionMessage"
import { HistoryItem } from "@shared/HistoryItem"
@@ -50,7 +50,6 @@ export class Controller {
private disposables: vscode.Disposable[] = []
task?: Task
workspaceTracker: WorkspaceTracker
mcpHub: McpHub
accountService: ClineAccountService
@@ -84,8 +83,8 @@ export class Controller {
})
}
async getCurrentMode(): Promise<Mode> {
return ((await getGlobalState(this.context, "mode")) as Mode | undefined) || "act"
private async getCurrentMode(): Promise<"plan" | "act"> {
return ((await getGlobalState(this.context, "mode")) as "plan" | "act" | undefined) || "act"
}
/*
@@ -113,20 +112,21 @@ export class Controller {
// TODO: update to clineAccountId and then move clineApiKey to a clear function.
await storeSecret(this.context, "clineAccountId", undefined)
await updateGlobalState(this.context, "userInfo", undefined)
await Promise.all([
updateGlobalState(this.context, "planModeApiProvider", "openrouter"),
updateGlobalState(this.context, "actModeApiProvider", "openrouter"),
])
await updateGlobalState(this.context, "apiProvider", "openrouter")
await this.postStateToWebview()
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.INFORMATION,
message: "Successfully logged out of Cline",
})
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.INFORMATION,
message: "Successfully logged out of Cline",
}),
)
} catch (error) {
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.INFORMATION,
message: "Logout failed",
})
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.INFORMATION,
message: "Logout failed",
}),
)
}
}
@@ -257,10 +257,153 @@ export class Controller {
// Capture mode switch telemetry | Capture regardless of if we know the taskId
telemetryService.captureModeSwitch(this.task?.taskId ?? "0", chatSettings.mode)
// Update API handler with new mode (buildApiHandler now selects provider based on mode)
if (this.task) {
const { apiConfiguration } = await getAllExtensionState(this.context)
this.task.api = buildApiHandler(apiConfiguration, chatSettings.mode)
// Get previous model info that we will revert to after saving current mode api info
const {
apiConfiguration,
previousModeApiProvider: newApiProvider,
previousModeModelId: newModelId,
previousModeModelInfo: newModelInfo,
previousModeVsCodeLmModelSelector: newVsCodeLmModelSelector,
previousModeThinkingBudgetTokens: newThinkingBudgetTokens,
previousModeReasoningEffort: newReasoningEffort,
previousModeAwsBedrockCustomSelected: newAwsBedrockCustomSelected,
previousModeAwsBedrockCustomModelBaseId: newAwsBedrockCustomModelBaseId,
previousModeSapAiCoreModelId: newSapAiCoreModelId,
planActSeparateModelsSetting,
} = await getAllExtensionState(this.context)
const shouldSwitchModel = planActSeparateModelsSetting === true
if (shouldSwitchModel) {
// Save the last model used in this mode
await updateGlobalState(this.context, "previousModeApiProvider", apiConfiguration.apiProvider)
await updateGlobalState(this.context, "previousModeThinkingBudgetTokens", apiConfiguration.thinkingBudgetTokens)
await updateGlobalState(this.context, "previousModeReasoningEffort", apiConfiguration.reasoningEffort)
switch (apiConfiguration.apiProvider) {
case "anthropic":
case "vertex":
case "gemini":
case "asksage":
case "openai-native":
case "qwen":
case "deepseek":
case "xai":
await updateGlobalState(this.context, "previousModeModelId", apiConfiguration.apiModelId)
break
case "bedrock":
await updateGlobalState(this.context, "previousModeModelId", apiConfiguration.apiModelId)
await updateGlobalState(
this.context,
"previousModeAwsBedrockCustomSelected",
apiConfiguration.awsBedrockCustomSelected,
)
await updateGlobalState(
this.context,
"previousModeAwsBedrockCustomModelBaseId",
apiConfiguration.awsBedrockCustomModelBaseId,
)
break
case "openrouter":
case "cline":
await updateGlobalState(this.context, "previousModeModelId", apiConfiguration.openRouterModelId)
await updateGlobalState(this.context, "previousModeModelInfo", apiConfiguration.openRouterModelInfo)
break
case "vscode-lm":
// Important we don't set modelId to this, as it's an object not string (webview expects model id to be a string)
await updateGlobalState(
this.context,
"previousModeVsCodeLmModelSelector",
apiConfiguration.vsCodeLmModelSelector,
)
break
case "openai":
await updateGlobalState(this.context, "previousModeModelId", apiConfiguration.openAiModelId)
await updateGlobalState(this.context, "previousModeModelInfo", apiConfiguration.openAiModelInfo)
break
case "ollama":
await updateGlobalState(this.context, "previousModeModelId", apiConfiguration.ollamaModelId)
break
case "lmstudio":
await updateGlobalState(this.context, "previousModeModelId", apiConfiguration.lmStudioModelId)
break
case "litellm":
await updateGlobalState(this.context, "previousModeModelId", apiConfiguration.liteLlmModelId)
await updateGlobalState(this.context, "previousModeModelInfo", apiConfiguration.liteLlmModelInfo)
break
case "requesty":
await updateGlobalState(this.context, "previousModeModelId", apiConfiguration.requestyModelId)
await updateGlobalState(this.context, "previousModeModelInfo", apiConfiguration.requestyModelInfo)
break
case "sapaicore":
await updateGlobalState(this.context, "previousModeModelId", apiConfiguration.apiModelId)
await updateGlobalState(this.context, "previousModeSapAiCoreModelId", apiConfiguration.sapAiCoreModelId)
break
}
// Restore the model used in previous mode
if (
newApiProvider ||
newModelId ||
newThinkingBudgetTokens !== undefined ||
newReasoningEffort ||
newVsCodeLmModelSelector
) {
await updateGlobalState(this.context, "apiProvider", newApiProvider)
await updateGlobalState(this.context, "thinkingBudgetTokens", newThinkingBudgetTokens)
await updateGlobalState(this.context, "reasoningEffort", newReasoningEffort)
switch (newApiProvider) {
case "anthropic":
case "vertex":
case "gemini":
case "asksage":
case "openai-native":
case "qwen":
case "deepseek":
case "xai":
await updateGlobalState(this.context, "apiModelId", newModelId)
break
case "bedrock":
await updateGlobalState(this.context, "apiModelId", newModelId)
await updateGlobalState(this.context, "awsBedrockCustomSelected", newAwsBedrockCustomSelected)
await updateGlobalState(this.context, "awsBedrockCustomModelBaseId", newAwsBedrockCustomModelBaseId)
break
case "openrouter":
case "cline":
await updateGlobalState(this.context, "openRouterModelId", newModelId)
await updateGlobalState(this.context, "openRouterModelInfo", newModelInfo)
break
case "vscode-lm":
await updateGlobalState(this.context, "vsCodeLmModelSelector", newVsCodeLmModelSelector)
break
case "openai":
await updateGlobalState(this.context, "openAiModelId", newModelId)
await updateGlobalState(this.context, "openAiModelInfo", newModelInfo)
break
case "ollama":
await updateGlobalState(this.context, "ollamaModelId", newModelId)
break
case "lmstudio":
await updateGlobalState(this.context, "lmStudioModelId", newModelId)
break
case "litellm":
await updateGlobalState(this.context, "liteLlmModelId", newModelId)
await updateGlobalState(this.context, "liteLlmModelInfo", newModelInfo)
break
case "requesty":
await updateGlobalState(this.context, "requestyModelId", newModelId)
await updateGlobalState(this.context, "requestyModelInfo", newModelInfo)
break
case "sapaicore":
await updateGlobalState(this.context, "apiModelId", newModelId)
await updateGlobalState(this.context, "sapAiCoreModelId", newSapAiCoreModelId)
break
}
if (this.task) {
const { apiConfiguration: updatedApiConfiguration } = await getAllExtensionState(this.context)
this.task.api = buildApiHandler(updatedApiConfiguration)
}
}
}
// Save only non-mode properties to global storage
@@ -324,47 +467,30 @@ export class Controller {
await this.authService.handleAuthCallback(customToken, provider ? provider : "google")
const clineProvider: ApiProvider = "cline"
await updateGlobalState(this.context, "apiProvider", clineProvider)
// Get current settings to determine how to update providers
const { planActSeparateModelsSetting } = await getAllExtensionState(this.context)
const currentMode = await this.getCurrentMode()
// Mark welcome view as completed since user has successfully logged in
await updateGlobalState(this.context, "welcomeViewCompleted", true)
if (planActSeparateModelsSetting) {
// Only update the current mode's provider
if (currentMode === "plan") {
await updateGlobalState(this.context, "planModeApiProvider", clineProvider)
} else {
await updateGlobalState(this.context, "actModeApiProvider", clineProvider)
}
} else {
// Update both modes to keep them in sync
await Promise.all([
updateGlobalState(this.context, "planModeApiProvider", clineProvider),
updateGlobalState(this.context, "actModeApiProvider", clineProvider),
])
}
// Get the updated API configuration (now includes the updated providers)
const { apiConfiguration } = await getAllExtensionState(this.context)
const updatedConfig = {
...apiConfiguration,
apiProvider: clineProvider,
}
// Mark welcome view as completed since user has successfully logged in
await updateGlobalState(this.context, "welcomeViewCompleted", true)
if (this.task) {
this.task.api = buildApiHandler(updatedConfig, currentMode)
this.task.api = buildApiHandler(updatedConfig)
}
await this.postStateToWebview()
} catch (error) {
console.error("Failed to handle auth callback:", error)
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.ERROR,
message: "Failed to log in to Cline",
})
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.ERROR,
message: "Failed to log in to Cline",
}),
)
// Even on login failure, we preserve any existing tokens
// Only clear tokens on explicit logout
}
@@ -399,10 +525,12 @@ export class Controller {
console.error("Failed to fetch MCP marketplace:", error)
if (!silent) {
const errorMessage = error instanceof Error ? error.message : "Failed to fetch MCP marketplace"
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.ERROR,
message: errorMessage,
})
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.ERROR,
message: errorMessage,
}),
)
}
return undefined
}
@@ -486,10 +614,12 @@ export class Controller {
} catch (error) {
console.error("Failed to handle cached MCP marketplace:", error)
const errorMessage = error instanceof Error ? error.message : "Failed to handle cached MCP marketplace"
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.ERROR,
message: errorMessage,
})
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.ERROR,
message: errorMessage,
}),
)
}
}
@@ -510,21 +640,14 @@ export class Controller {
}
const openrouter: ApiProvider = "openrouter"
const currentMode = await this.getCurrentMode()
await Promise.all([
updateGlobalState(this.context, "planModeApiProvider", openrouter),
updateGlobalState(this.context, "actModeApiProvider", openrouter),
])
await updateGlobalState(this.context, "apiProvider", openrouter)
await storeSecret(this.context, "openRouterApiKey", apiKey)
await this.postStateToWebview()
if (this.task) {
// Get the updated API configuration (now includes the updated providers)
const { apiConfiguration } = await getAllExtensionState(this.context)
const updatedConfig = {
...apiConfiguration,
this.task.api = buildApiHandler({
apiProvider: openrouter,
openRouterApiKey: apiKey,
}
this.task.api = buildApiHandler(updatedConfig, currentMode)
})
}
// await this.postMessageToWebview({ type: "action", action: "settingsButtonClicked" }) // bad ux if user is on welcome
}
@@ -859,20 +982,24 @@ export class Controller {
// Check if there's a workspace folder open
const cwd = await getCwd()
if (!cwd) {
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.ERROR,
message: "No workspace folder open",
})
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.ERROR,
message: "No workspace folder open",
}),
)
return
}
// Get the git diff
const gitDiff = await getWorkingState(cwd)
if (gitDiff === "No changes in working directory") {
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.INFORMATION,
message: "No changes in workspace for commit message",
})
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.INFORMATION,
message: "No changes in workspace for commit message",
}),
)
return
}
@@ -900,10 +1027,9 @@ Commit message:`
// Get the current API configuration
const { apiConfiguration } = await getAllExtensionState(this.context)
const currentMode = await this.getCurrentMode()
// Build the API handler
const apiHandler = buildApiHandler(apiConfiguration, currentMode)
const apiHandler = buildApiHandler(apiConfiguration)
// Create a system prompt
const systemPrompt =
@@ -941,46 +1067,58 @@ Commit message:`
const repo = api.repositories[0]
repo.inputBox.value = commitMessage
const message = "Commit message generated and applied"
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.INFORMATION,
message,
})
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.INFORMATION,
message,
}),
)
} else {
const message = "No Git repositories found"
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.ERROR,
message,
})
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.ERROR,
message,
}),
)
}
} else {
const message = "Git extension not found"
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.ERROR,
message,
})
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.ERROR,
message,
}),
)
}
} else {
const message = "Failed to generate commit message"
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.ERROR,
message,
})
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.ERROR,
message,
}),
)
}
} catch (innerError) {
const innerErrorMessage = innerError instanceof Error ? innerError.message : String(innerError)
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.ERROR,
message: `Failed to generate commit message: ${innerErrorMessage}`,
})
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.ERROR,
message: `Failed to generate commit message: ${innerErrorMessage}`,
}),
)
}
},
)
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.ERROR,
message: `Failed to generate commit message: ${errorMessage}`,
})
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.ERROR,
message: `Failed to generate commit message: ${errorMessage}`,
}),
)
}
}
}
@@ -1,112 +0,0 @@
import { Controller } from ".."
import { EmptyRequest } from "../../../shared/proto/common"
import { OpenRouterCompatibleModelInfo, OpenRouterModelInfo } from "../../../shared/proto/models"
import axios from "axios"
import path from "path"
import fs from "fs/promises"
import { fileExistsAtPath } from "@utils/fs"
import { GlobalFileNames } from "@core/storage/disk"
import { huggingFaceModels } from "@shared/api"
/**
* Ensures the cache directory exists and returns its path
*/
async function ensureCacheDirectoryExists(controller: Controller): Promise<string> {
const cacheDir = path.join(controller.context.globalStorageUri.fsPath, "cache")
try {
await fs.mkdir(cacheDir, { recursive: true })
} catch (error) {
// Directory might already exist
}
return cacheDir
}
/**
* Refreshes the Hugging Face models and returns the updated model list
* @param controller The controller instance
* @param request Empty request object
* @returns Response containing the Hugging Face models
*/
export async function refreshHuggingFaceModels(
controller: Controller,
_request: EmptyRequest,
): Promise<OpenRouterCompatibleModelInfo> {
const huggingFaceModelsFilePath = path.join(await ensureCacheDirectoryExists(controller), "huggingface_models.json")
let models: Record<string, OpenRouterModelInfo> = {}
try {
// Fetch models from Hugging Face API
const response = await axios.get("https://router.huggingface.co/v1/models", {
timeout: 10000,
})
if (response.data?.data) {
const rawModels = response.data.data
// Transform HF models to OpenRouter-compatible format
for (const rawModel of rawModels) {
const modelInfo = OpenRouterModelInfo.create({
maxTokens: 8192, // HF doesn't provide max_tokens, use default
contextWindow: 128_000, // FIXME: HF doesn't provide context window, use default
supportsImages: false, // Most models don't support images
supportsPromptCache: false,
inputPrice: 0, // Will be set based on providers
outputPrice: 0, // Will be set based on providers
cacheWritesPrice: 0,
cacheReadsPrice: 0,
description: `Available on providers: ${rawModel.providers?.join(", ") || "unknown"}`,
})
// Add model-specific configurations if we have them in our static models
if (rawModel.id in huggingFaceModels) {
const staticModel = huggingFaceModels[rawModel.id as keyof typeof huggingFaceModels]
modelInfo.maxTokens = staticModel.maxTokens
modelInfo.contextWindow = staticModel.contextWindow
modelInfo.supportsImages = staticModel.supportsImages
modelInfo.supportsPromptCache = staticModel.supportsPromptCache
modelInfo.inputPrice = staticModel.inputPrice
modelInfo.outputPrice = staticModel.outputPrice
modelInfo.description = staticModel.description || modelInfo.description
}
models[rawModel.id] = modelInfo
}
// Save to cache
await fs.writeFile(huggingFaceModelsFilePath, JSON.stringify(models, null, 2))
}
} catch (error) {
console.error("Error fetching Hugging Face models:", error)
// Try to load from cache
try {
if (await fileExistsAtPath(huggingFaceModelsFilePath)) {
const cachedModels = await fs.readFile(huggingFaceModelsFilePath, "utf-8")
const parsedModels = JSON.parse(cachedModels)
models = parsedModels
}
} catch (cacheError) {
console.error("Error loading cached Hugging Face models:", cacheError)
}
// If no cache available, use static models as fallback
if (Object.keys(models).length === 0) {
for (const [modelId, modelInfo] of Object.entries(huggingFaceModels)) {
models[modelId] = OpenRouterModelInfo.create({
maxTokens: modelInfo.maxTokens,
contextWindow: modelInfo.contextWindow,
supportsImages: modelInfo.supportsImages,
supportsPromptCache: modelInfo.supportsPromptCache,
inputPrice: modelInfo.inputPrice,
outputPrice: modelInfo.outputPrice,
cacheWritesPrice: (modelInfo as any).cacheWritesPrice || 0,
cacheReadsPrice: (modelInfo as any).cacheReadsPrice || 0,
description: modelInfo.description || "",
})
}
}
}
return OpenRouterCompatibleModelInfo.create({ models })
}
@@ -29,8 +29,7 @@ export async function updateApiConfigurationProto(
// Update the task's API handler if there's an active task
if (controller.task) {
const currentMode = await controller.getCurrentMode()
controller.task.api = buildApiHandler(appApiConfiguration, currentMode)
controller.task.api = buildApiHandler(appApiConfiguration)
}
// Post updated state to webview
+24 -16
View File
@@ -15,16 +15,20 @@ import { getHostBridgeProvider } from "@/hosts/host-providers"
export async function resetState(controller: Controller, request: ResetStateRequest): Promise<Empty> {
try {
if (request.global) {
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.INFORMATION,
message: "Resetting global state...",
})
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.INFORMATION,
message: "Resetting global state...",
}),
)
await resetGlobalState(controller.context)
} else {
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.INFORMATION,
message: "Resetting workspace state...",
})
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.INFORMATION,
message: "Resetting workspace state...",
}),
)
await resetWorkspaceState(controller.context)
}
@@ -33,10 +37,12 @@ export async function resetState(controller: Controller, request: ResetStateRequ
controller.task = undefined
}
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.INFORMATION,
message: "State reset",
})
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.INFORMATION,
message: "State reset",
}),
)
await controller.postStateToWebview()
await sendChatButtonClickedEvent(controller.id)
@@ -44,10 +50,12 @@ export async function resetState(controller: Controller, request: ResetStateRequ
return Empty.create()
} catch (error) {
console.error("Error resetting state:", error)
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.ERROR,
message: `Failed to reset state: ${error instanceof Error ? error.message : String(error)}`,
})
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.ERROR,
message: `Failed to reset state: ${error instanceof Error ? error.message : String(error)}`,
}),
)
throw error
}
}
@@ -27,10 +27,12 @@ export async function updateDefaultTerminalProfile(
// Show information message if terminals were closed
if (closedCount > 0) {
const message = `Closed ${closedCount} ${closedCount === 1 ? "terminal" : "terminals"} with different profile.`
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.INFORMATION,
message,
})
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.INFORMATION,
message,
}),
)
}
// Show warning if there are busy terminals that couldn't be closed
@@ -38,10 +40,12 @@ export async function updateDefaultTerminalProfile(
const message =
`${busyTerminals.length} busy ${busyTerminals.length === 1 ? "terminal has" : "terminals have"} a different profile. ` +
`Close ${busyTerminals.length === 1 ? "it" : "them"} to use the new profile for all commands.`
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.WARNING,
message,
})
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.WARNING,
message,
}),
)
}
}
+1 -2
View File
@@ -21,8 +21,7 @@ export async function updateSettings(controller: Controller, request: UpdateSett
await updateApiConfiguration(controller.context, apiConfiguration)
if (controller.task) {
const currentMode = await controller.getCurrentMode()
controller.task.api = buildApiHandler(apiConfiguration, currentMode)
controller.task.api = buildApiHandler(apiConfiguration)
}
}
@@ -33,7 +33,7 @@ export async function deleteAllTaskHistory(controller: Controller): Promise<Dele
},
}),
)
).selectedOption
)?.selectedOption
// Default VS Code Cancel button returns `undefined` - don't delete anything
if (userChoice === undefined) {
@@ -67,15 +67,17 @@ export async function deleteAllTaskHistory(controller: Controller): Promise<Dele
} else {
// No favorited tasks found - show warning and ask user what to do
const answer = (
await getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.WARNING,
message: "No favorited tasks found. Would you like to delete all tasks anyway?",
options: {
modal: true,
items: ["Delete All Tasks"],
},
})
).selectedOption
await getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.WARNING,
message: "No favorited tasks found. Would you like to delete all tasks anyway?",
options: {
modal: true,
items: ["Delete All Tasks"],
},
}),
)
)?.selectedOption
// User cancelled - don't delete anything
if (answer === undefined) {
@@ -103,10 +105,12 @@ export async function deleteAllTaskHistory(controller: Controller): Promise<Dele
await fs.rm(checkpointsDirPath, { recursive: true, force: true })
}
} catch (error) {
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.ERROR,
message: `Encountered error while deleting task history, there may be some files left behind. Error: ${error instanceof Error ? error.message : String(error)}`,
})
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.ERROR,
message: `Encountered error while deleting task history, there may be some files left behind. Error: ${error instanceof Error ? error.message : String(error)}`,
}),
)
}
// Update webview
@@ -28,11 +28,13 @@ export const deleteTasksWithIds: TaskMethodHandler = async (
? "Are you sure you want to delete this task? This action cannot be undone."
: `Are you sure you want to delete these ${taskCount} tasks? This action cannot be undone.`
const userChoice = await getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.WARNING,
message,
options: { modal: true, items: ["Delete"] },
})
const userChoice = await getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.WARNING,
message,
options: { modal: true, items: ["Delete"] },
}),
)
if (userChoice === undefined) {
return Empty.create()
+13 -65
View File
@@ -30,77 +30,25 @@ export async function initializeWebview(controller: Controller, request: EmptyRe
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, planActSeparateModelsSetting } = await getAllExtensionState(controller.context)
const currentMode = await controller.getCurrentMode()
if (planActSeparateModelsSetting) {
// Separate models: update only current mode
const modelIdField = currentMode === "plan" ? "planModeOpenRouterModelId" : "actModeOpenRouterModelId"
const modelInfoField = currentMode === "plan" ? "planModeOpenRouterModelInfo" : "actModeOpenRouterModelInfo"
const modelId = apiConfiguration[modelIdField]
if (modelId && response.models[modelId]) {
await updateGlobalState(controller.context, modelInfoField, response.models[modelId])
await controller.postStateToWebview()
}
} else {
// Shared models: update both plan and act modes
const planModelId = apiConfiguration.planModeOpenRouterModelId
const actModelId = apiConfiguration.actModeOpenRouterModelId
// Update plan mode model info if we have a model ID
if (planModelId && response.models[planModelId]) {
await updateGlobalState(controller.context, "planModeOpenRouterModelInfo", response.models[planModelId])
}
// Update act mode model info if we have a model ID
if (actModelId && response.models[actModelId]) {
await updateGlobalState(controller.context, "actModeOpenRouterModelInfo", response.models[actModelId])
}
// Post state update if we updated any model info
if ((planModelId && response.models[planModelId]) || (actModelId && response.models[actModelId])) {
await controller.postStateToWebview()
}
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()
}
}
})
handleModelsServiceRequest(controller, "refreshGroqModels", EmptyRequest.create()).then(async (response) => {
if (response && response.models) {
// Update model info in state for Groq (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, planActSeparateModelsSetting } = await getAllExtensionState(controller.context)
const currentMode = await controller.getCurrentMode()
if (planActSeparateModelsSetting) {
// Separate models: update only current mode
const modelIdField = currentMode === "plan" ? "planModeGroqModelId" : "actModeGroqModelId"
const modelInfoField = currentMode === "plan" ? "planModeGroqModelInfo" : "actModeGroqModelInfo"
const modelId = apiConfiguration[modelIdField]
if (modelId && response.models[modelId]) {
await updateGlobalState(controller.context, modelInfoField, response.models[modelId])
await controller.postStateToWebview()
}
} else {
// Shared models: update both plan and act modes
const planModelId = apiConfiguration.planModeGroqModelId
const actModelId = apiConfiguration.actModeGroqModelId
// Update plan mode model info if we have a model ID
if (planModelId && response.models[planModelId]) {
await updateGlobalState(controller.context, "planModeGroqModelInfo", response.models[planModelId])
}
// Update act mode model info if we have a model ID
if (actModelId && response.models[actModelId]) {
await updateGlobalState(controller.context, "actModeGroqModelInfo", response.models[actModelId])
}
// Post state update if we updated any model info
if ((planModelId && response.models[planModelId]) || (actModelId && response.models[actModelId])) {
await controller.postStateToWebview()
}
// update model info in state for Groq
const { apiConfiguration } = await getAllExtensionState(controller.context)
if (apiConfiguration.groqModelId && response.models[apiConfiguration.groqModelId]) {
await updateGlobalState(controller.context, "groqModelInfo", response.models[apiConfiguration.groqModelId])
await controller.postStateToWebview()
}
}
})
-22
View File
@@ -1,22 +0,0 @@
import * as vscode from "vscode"
import type { Controller } from "../index"
import type { EmptyRequest } from "../../../shared/proto/common"
import { Empty } from "../../../shared/proto/common"
import { telemetryService } from "../../../services/posthog/telemetry/TelemetryService"
/**
* Opens the Cline walkthrough in VSCode
* @param controller The controller instance
* @param request Empty request
* @returns Empty response
*/
export async function openWalkthrough(controller: Controller, request: EmptyRequest): Promise<Empty> {
try {
await vscode.commands.executeCommand("workbench.action.openWalkthrough", "saoudrizwan.claude-dev#ClineWalkthrough")
telemetryService.captureButtonClick("webview_openWalkthrough")
return Empty.create({})
} catch (error) {
console.error(`Failed to open walkthrough: ${error}`)
throw error
}
}
+12 -8
View File
@@ -78,10 +78,12 @@ export async function parseMentions(
await urlContentFetcher.launchBrowser()
} catch (error) {
launchBrowserError = error
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.ERROR,
message: `Error fetching content for ${urlMention}: ${error.message}`,
})
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.ERROR,
message: `Error fetching content for ${urlMention}: ${error.message}`,
}),
)
}
}
@@ -98,10 +100,12 @@ export async function parseMentions(
const markdown = await urlContentFetcher.urlToMarkdown(mention)
result = markdown
} catch (error) {
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.ERROR,
message: `Error fetching content for ${mention}: ${error.message}`,
})
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.ERROR,
message: `Error fetching content for ${mention}: ${error.message}`,
}),
)
result = `Error fetching content: ${error.message}`
}
}
+1 -2
View File
@@ -2,7 +2,6 @@ import { Anthropic } from "@anthropic-ai/sdk"
import * as diff from "diff"
import * as path from "path"
import { ClineIgnoreController, LOCK_TEXT_SYMBOL } from "../ignore/ClineIgnoreController"
import { Mode } from "@/shared/ChatSettings"
export const formatResponse = {
duplicateFileReadNotice: () =>
@@ -148,7 +147,7 @@ Otherwise, if you have not completed the task and do not need additional informa
},
taskResumption: (
mode: Mode,
mode: "plan" | "act",
agoText: string,
cwd: string,
wasRecent: boolean | 0 | undefined,
+36 -51
View File
@@ -21,7 +21,6 @@ export type SecretKey =
| "asksageApiKey"
| "xaiApiKey"
| "moonshotApiKey"
| "huggingFaceApiKey"
| "nebiusApiKey"
| "sambanovaApiKey"
| "cerebrasApiKey"
@@ -43,9 +42,12 @@ export type GlobalStateKey =
| "lastShownAnnouncementId"
| "taskHistory"
| "openAiBaseUrl"
| "openAiModelId"
| "openAiModelInfo"
| "openAiHeaders"
| "ollamaBaseUrl"
| "ollamaApiOptionsCtxNum"
| "lmStudioModelId"
| "lmStudioBaseUrl"
| "anthropicBaseUrl"
| "geminiBaseUrl"
@@ -85,55 +87,38 @@ export type GlobalStateKey =
// Settings around plan/act and ephemeral model configuration
| "chatSettings"
| "mode"
// Plan mode configurations
| "planModeApiProvider"
| "planModeApiModelId"
| "planModeThinkingBudgetTokens"
| "planModeReasoningEffort"
| "planModeVsCodeLmModelSelector"
| "planModeAwsBedrockCustomSelected"
| "planModeAwsBedrockCustomModelBaseId"
| "planModeOpenRouterModelId"
| "planModeOpenRouterModelInfo"
| "planModeOpenAiModelId"
| "planModeOpenAiModelInfo"
| "planModeOllamaModelId"
| "planModeLmStudioModelId"
| "planModeLiteLlmModelId"
| "planModeLiteLlmModelInfo"
| "planModeRequestyModelId"
| "planModeRequestyModelInfo"
| "planModeTogetherModelId"
| "planModeFireworksModelId"
| "planModeSapAiCoreModelId"
| "planModeGroqModelId"
| "planModeGroqModelInfo"
| "planModeHuggingFaceModelId"
| "planModeHuggingFaceModelInfo"
// Act mode configurations
| "actModeApiProvider"
| "actModeApiModelId"
| "actModeThinkingBudgetTokens"
| "actModeReasoningEffort"
| "actModeVsCodeLmModelSelector"
| "actModeAwsBedrockCustomSelected"
| "actModeAwsBedrockCustomModelBaseId"
| "actModeOpenRouterModelId"
| "actModeOpenRouterModelInfo"
| "actModeOpenAiModelId"
| "actModeOpenAiModelInfo"
| "actModeOllamaModelId"
| "actModeLmStudioModelId"
| "actModeLiteLlmModelId"
| "actModeLiteLlmModelInfo"
| "actModeRequestyModelId"
| "actModeRequestyModelInfo"
| "actModeTogetherModelId"
| "actModeFireworksModelId"
| "actModeSapAiCoreModelId"
| "actModeGroqModelId"
| "actModeGroqModelInfo"
| "actModeHuggingFaceModelId"
| "actModeHuggingFaceModelInfo"
// Current active model configuration (per workspace)
| "apiProvider"
| "apiModelId"
| "thinkingBudgetTokens"
| "reasoningEffort"
| "vsCodeLmModelSelector"
| "awsBedrockCustomSelected"
| "awsBedrockCustomModelBaseId"
| "openRouterModelId"
| "openRouterModelInfo"
| "openAiModelId"
| "openAiModelInfo"
| "ollamaModelId"
| "lmStudioModelId"
| "liteLlmModelId"
| "liteLlmModelInfo"
| "requestyModelId"
| "requestyModelInfo"
| "togetherModelId"
| "fireworksModelId"
| "sapAiCoreModelId"
// Previous mode saved configurations (per workspace)
| "previousModeApiProvider"
| "previousModeModelId"
| "previousModeModelInfo"
| "previousModeVsCodeLmModelSelector"
| "previousModeThinkingBudgetTokens"
| "previousModeReasoningEffort"
| "previousModeAwsBedrockCustomSelected"
| "previousModeAwsBedrockCustomModelBaseId"
| "previousModeSapAiCoreModelId"
| "groqModelId"
| "groqModelInfo"
export type LocalStateKey = "localClineRulesToggles" | "localCursorRulesToggles" | "localWindsurfRulesToggles" | "workflowToggles"
+5 -381
View File
@@ -32,10 +32,6 @@ export async function migrateWorkspaceToGlobalStorage(context: vscode.ExtensionC
"togetherModelId",
"fireworksModelId",
"sapAiCoreModelId",
"groqModelId",
"groqModelInfo",
"huggingFaceModelId",
"huggingFaceModelInfo",
// Previous mode settings
"previousModeApiProvider",
@@ -57,8 +53,8 @@ export async function migrateWorkspaceToGlobalStorage(context: vscode.ExtensionC
if (workspaceValue !== undefined && globalValue === undefined) {
console.log(`[Storage Migration] migrating key: ${key} to global storage. Current value: ${workspaceValue}`)
// Move to global storage using raw VSCode method to avoid type errors
await context.globalState.update(key, workspaceValue)
// Move to global storage
await updateGlobalState(context, key as GlobalStateKey, workspaceValue)
// Remove from workspace storage
await context.workspaceState.update(key, undefined)
const newWorkspaceValue = await context.workspaceState.get(key)
@@ -173,375 +169,6 @@ export async function migrateModeFromWorkspaceStorageToControllerState(context:
}
}
export async function migrateLegacyApiConfigurationToModeSpecific(context: vscode.ExtensionContext) {
try {
// Check if migration is needed - if planModeApiProvider already exists, skip migration
const planModeApiProvider = await context.globalState.get("planModeApiProvider")
if (planModeApiProvider !== undefined) {
console.log("Legacy API configuration migration already completed, skipping...")
return
}
console.log("Starting legacy API configuration migration to mode-specific keys...")
// Get the planActSeparateModelsSetting to determine migration strategy
const planActSeparateModelsSetting = (await context.globalState.get("planActSeparateModelsSetting")) as
| boolean
| undefined
// Read legacy values directly
const apiProvider = await context.globalState.get("apiProvider")
const apiModelId = await context.globalState.get("apiModelId")
const thinkingBudgetTokens = await context.globalState.get("thinkingBudgetTokens")
const reasoningEffort = await context.globalState.get("reasoningEffort")
const vsCodeLmModelSelector = await context.globalState.get("vsCodeLmModelSelector")
const awsBedrockCustomSelected = await context.globalState.get("awsBedrockCustomSelected")
const awsBedrockCustomModelBaseId = await context.globalState.get("awsBedrockCustomModelBaseId")
const openRouterModelId = await context.globalState.get("openRouterModelId")
const openRouterModelInfo = await context.globalState.get("openRouterModelInfo")
const openAiModelId = await context.globalState.get("openAiModelId")
const openAiModelInfo = await context.globalState.get("openAiModelInfo")
const ollamaModelId = await context.globalState.get("ollamaModelId")
const lmStudioModelId = await context.globalState.get("lmStudioModelId")
const liteLlmModelId = await context.globalState.get("liteLlmModelId")
const liteLlmModelInfo = await context.globalState.get("liteLlmModelInfo")
const requestyModelId = await context.globalState.get("requestyModelId")
const requestyModelInfo = await context.globalState.get("requestyModelInfo")
const togetherModelId = await context.globalState.get("togetherModelId")
const fireworksModelId = await context.globalState.get("fireworksModelId")
const sapAiCoreModelId = await context.globalState.get("sapAiCoreModelId")
const groqModelId = await context.globalState.get("groqModelId")
const groqModelInfo = await context.globalState.get("groqModelInfo")
const huggingFaceModelId = await context.globalState.get("huggingFaceModelId")
const huggingFaceModelInfo = await context.globalState.get("huggingFaceModelInfo")
// Read previous mode values
const previousModeApiProvider = await context.globalState.get("previousModeApiProvider")
const previousModeModelId = await context.globalState.get("previousModeModelId")
const previousModeModelInfo = await context.globalState.get("previousModeModelInfo")
const previousModeVsCodeLmModelSelector = await context.globalState.get("previousModeVsCodeLmModelSelector")
const previousModeThinkingBudgetTokens = await context.globalState.get("previousModeThinkingBudgetTokens")
const previousModeReasoningEffort = await context.globalState.get("previousModeReasoningEffort")
const previousModeAwsBedrockCustomSelected = await context.globalState.get("previousModeAwsBedrockCustomSelected")
const previousModeAwsBedrockCustomModelBaseId = await context.globalState.get("previousModeAwsBedrockCustomModelBaseId")
const previousModeSapAiCoreModelId = await context.globalState.get("previousModeSapAiCoreModelId")
// Migrate based on planActSeparateModelsSetting
if (planActSeparateModelsSetting === false) {
console.log("Migrating with separate models DISABLED - using current values for both modes")
// Use current values for both plan and act modes
if (apiProvider !== undefined) {
await context.globalState.update("planModeApiProvider", apiProvider)
await context.globalState.update("actModeApiProvider", apiProvider)
}
if (apiModelId !== undefined) {
await context.globalState.update("planModeApiModelId", apiModelId)
await context.globalState.update("actModeApiModelId", apiModelId)
}
if (thinkingBudgetTokens !== undefined) {
await context.globalState.update("planModeThinkingBudgetTokens", thinkingBudgetTokens)
await context.globalState.update("actModeThinkingBudgetTokens", thinkingBudgetTokens)
}
if (reasoningEffort !== undefined) {
await context.globalState.update("planModeReasoningEffort", reasoningEffort)
await context.globalState.update("actModeReasoningEffort", reasoningEffort)
}
if (vsCodeLmModelSelector !== undefined) {
await context.globalState.update("planModeVsCodeLmModelSelector", vsCodeLmModelSelector)
await context.globalState.update("actModeVsCodeLmModelSelector", vsCodeLmModelSelector)
}
if (awsBedrockCustomSelected !== undefined) {
await context.globalState.update("planModeAwsBedrockCustomSelected", awsBedrockCustomSelected)
await context.globalState.update("actModeAwsBedrockCustomSelected", awsBedrockCustomSelected)
}
if (awsBedrockCustomModelBaseId !== undefined) {
await context.globalState.update("planModeAwsBedrockCustomModelBaseId", awsBedrockCustomModelBaseId)
await context.globalState.update("actModeAwsBedrockCustomModelBaseId", awsBedrockCustomModelBaseId)
}
if (openRouterModelId !== undefined) {
await context.globalState.update("planModeOpenRouterModelId", openRouterModelId)
await context.globalState.update("actModeOpenRouterModelId", openRouterModelId)
}
if (openRouterModelInfo !== undefined) {
await context.globalState.update("planModeOpenRouterModelInfo", openRouterModelInfo)
await context.globalState.update("actModeOpenRouterModelInfo", openRouterModelInfo)
}
if (openAiModelId !== undefined) {
await context.globalState.update("planModeOpenAiModelId", openAiModelId)
await context.globalState.update("actModeOpenAiModelId", openAiModelId)
}
if (openAiModelInfo !== undefined) {
await context.globalState.update("planModeOpenAiModelInfo", openAiModelInfo)
await context.globalState.update("actModeOpenAiModelInfo", openAiModelInfo)
}
if (ollamaModelId !== undefined) {
await context.globalState.update("planModeOllamaModelId", ollamaModelId)
await context.globalState.update("actModeOllamaModelId", ollamaModelId)
}
if (lmStudioModelId !== undefined) {
await context.globalState.update("planModeLmStudioModelId", lmStudioModelId)
await context.globalState.update("actModeLmStudioModelId", lmStudioModelId)
}
if (liteLlmModelId !== undefined) {
await context.globalState.update("planModeLiteLlmModelId", liteLlmModelId)
await context.globalState.update("actModeLiteLlmModelId", liteLlmModelId)
}
if (liteLlmModelInfo !== undefined) {
await context.globalState.update("planModeLiteLlmModelInfo", liteLlmModelInfo)
await context.globalState.update("actModeLiteLlmModelInfo", liteLlmModelInfo)
}
if (requestyModelId !== undefined) {
await context.globalState.update("planModeRequestyModelId", requestyModelId)
await context.globalState.update("actModeRequestyModelId", requestyModelId)
}
if (requestyModelInfo !== undefined) {
await context.globalState.update("planModeRequestyModelInfo", requestyModelInfo)
await context.globalState.update("actModeRequestyModelInfo", requestyModelInfo)
}
if (togetherModelId !== undefined) {
await context.globalState.update("planModeTogetherModelId", togetherModelId)
await context.globalState.update("actModeTogetherModelId", togetherModelId)
}
if (fireworksModelId !== undefined) {
await context.globalState.update("planModeFireworksModelId", fireworksModelId)
await context.globalState.update("actModeFireworksModelId", fireworksModelId)
}
if (sapAiCoreModelId !== undefined) {
await context.globalState.update("planModeSapAiCoreModelId", sapAiCoreModelId)
await context.globalState.update("actModeSapAiCoreModelId", sapAiCoreModelId)
}
if (groqModelId !== undefined) {
await context.globalState.update("planModeGroqModelId", groqModelId)
await context.globalState.update("actModeGroqModelId", groqModelId)
}
if (groqModelInfo !== undefined) {
await context.globalState.update("planModeGroqModelInfo", groqModelInfo)
await context.globalState.update("actModeGroqModelInfo", groqModelInfo)
}
if (huggingFaceModelId !== undefined) {
await context.globalState.update("planModeHuggingFaceModelId", huggingFaceModelId)
await context.globalState.update("actModeHuggingFaceModelId", huggingFaceModelId)
}
if (huggingFaceModelInfo !== undefined) {
await context.globalState.update("planModeHuggingFaceModelInfo", huggingFaceModelInfo)
await context.globalState.update("actModeHuggingFaceModelInfo", huggingFaceModelInfo)
}
} else {
console.log("Migrating with separate models ENABLED - using current->plan, previous->act")
// Use current values for plan mode
if (apiProvider !== undefined) {
await context.globalState.update("planModeApiProvider", apiProvider)
}
if (apiModelId !== undefined) {
await context.globalState.update("planModeApiModelId", apiModelId)
}
if (thinkingBudgetTokens !== undefined) {
await context.globalState.update("planModeThinkingBudgetTokens", thinkingBudgetTokens)
}
if (reasoningEffort !== undefined) {
await context.globalState.update("planModeReasoningEffort", reasoningEffort)
}
if (vsCodeLmModelSelector !== undefined) {
await context.globalState.update("planModeVsCodeLmModelSelector", vsCodeLmModelSelector)
}
if (awsBedrockCustomSelected !== undefined) {
await context.globalState.update("planModeAwsBedrockCustomSelected", awsBedrockCustomSelected)
}
if (awsBedrockCustomModelBaseId !== undefined) {
await context.globalState.update("planModeAwsBedrockCustomModelBaseId", awsBedrockCustomModelBaseId)
}
if (openRouterModelId !== undefined) {
await context.globalState.update("planModeOpenRouterModelId", openRouterModelId)
}
if (openRouterModelInfo !== undefined) {
await context.globalState.update("planModeOpenRouterModelInfo", openRouterModelInfo)
}
if (openAiModelId !== undefined) {
await context.globalState.update("planModeOpenAiModelId", openAiModelId)
}
if (openAiModelInfo !== undefined) {
await context.globalState.update("planModeOpenAiModelInfo", openAiModelInfo)
}
if (ollamaModelId !== undefined) {
await context.globalState.update("planModeOllamaModelId", ollamaModelId)
}
if (lmStudioModelId !== undefined) {
await context.globalState.update("planModeLmStudioModelId", lmStudioModelId)
}
if (liteLlmModelId !== undefined) {
await context.globalState.update("planModeLiteLlmModelId", liteLlmModelId)
}
if (liteLlmModelInfo !== undefined) {
await context.globalState.update("planModeLiteLlmModelInfo", liteLlmModelInfo)
}
if (requestyModelId !== undefined) {
await context.globalState.update("planModeRequestyModelId", requestyModelId)
}
if (requestyModelInfo !== undefined) {
await context.globalState.update("planModeRequestyModelInfo", requestyModelInfo)
}
if (togetherModelId !== undefined) {
await context.globalState.update("planModeTogetherModelId", togetherModelId)
}
if (fireworksModelId !== undefined) {
await context.globalState.update("planModeFireworksModelId", fireworksModelId)
}
if (sapAiCoreModelId !== undefined) {
await context.globalState.update("planModeSapAiCoreModelId", sapAiCoreModelId)
}
if (groqModelId !== undefined) {
await context.globalState.update("planModeGroqModelId", groqModelId)
}
if (groqModelInfo !== undefined) {
await context.globalState.update("planModeGroqModelInfo", groqModelInfo)
}
if (huggingFaceModelId !== undefined) {
await context.globalState.update("planModeHuggingFaceModelId", huggingFaceModelId)
}
if (huggingFaceModelInfo !== undefined) {
await context.globalState.update("planModeHuggingFaceModelInfo", huggingFaceModelInfo)
}
// Use previous values for act mode (with fallback to current values)
if (previousModeApiProvider !== undefined) {
await context.globalState.update("actModeApiProvider", previousModeApiProvider)
} else if (apiProvider !== undefined) {
await context.globalState.update("actModeApiProvider", apiProvider)
}
if (previousModeModelId !== undefined) {
await context.globalState.update("actModeApiModelId", previousModeModelId)
} else if (apiModelId !== undefined) {
await context.globalState.update("actModeApiModelId", apiModelId)
}
if (previousModeThinkingBudgetTokens !== undefined) {
await context.globalState.update("actModeThinkingBudgetTokens", previousModeThinkingBudgetTokens)
} else if (thinkingBudgetTokens !== undefined) {
await context.globalState.update("actModeThinkingBudgetTokens", thinkingBudgetTokens)
}
if (previousModeReasoningEffort !== undefined) {
await context.globalState.update("actModeReasoningEffort", previousModeReasoningEffort)
} else if (reasoningEffort !== undefined) {
await context.globalState.update("actModeReasoningEffort", reasoningEffort)
}
if (previousModeVsCodeLmModelSelector !== undefined) {
await context.globalState.update("actModeVsCodeLmModelSelector", previousModeVsCodeLmModelSelector)
} else if (vsCodeLmModelSelector !== undefined) {
await context.globalState.update("actModeVsCodeLmModelSelector", vsCodeLmModelSelector)
}
if (previousModeAwsBedrockCustomSelected !== undefined) {
await context.globalState.update("actModeAwsBedrockCustomSelected", previousModeAwsBedrockCustomSelected)
} else if (awsBedrockCustomSelected !== undefined) {
await context.globalState.update("actModeAwsBedrockCustomSelected", awsBedrockCustomSelected)
}
if (previousModeAwsBedrockCustomModelBaseId !== undefined) {
await context.globalState.update("actModeAwsBedrockCustomModelBaseId", previousModeAwsBedrockCustomModelBaseId)
} else if (awsBedrockCustomModelBaseId !== undefined) {
await context.globalState.update("actModeAwsBedrockCustomModelBaseId", awsBedrockCustomModelBaseId)
}
if (previousModeSapAiCoreModelId !== undefined) {
await context.globalState.update("actModeSapAiCoreModelId", previousModeSapAiCoreModelId)
} else if (sapAiCoreModelId !== undefined) {
await context.globalState.update("actModeSapAiCoreModelId", sapAiCoreModelId)
}
// For fields without previous variants, use current values for act mode
if (previousModeModelInfo !== undefined) {
await context.globalState.update("actModeOpenRouterModelInfo", previousModeModelInfo)
} else if (openRouterModelInfo !== undefined) {
await context.globalState.update("actModeOpenRouterModelInfo", openRouterModelInfo)
}
if (openRouterModelId !== undefined) {
await context.globalState.update("actModeOpenRouterModelId", openRouterModelId)
}
if (openAiModelId !== undefined) {
await context.globalState.update("actModeOpenAiModelId", openAiModelId)
}
if (openAiModelInfo !== undefined) {
await context.globalState.update("actModeOpenAiModelInfo", openAiModelInfo)
}
if (ollamaModelId !== undefined) {
await context.globalState.update("actModeOllamaModelId", ollamaModelId)
}
if (lmStudioModelId !== undefined) {
await context.globalState.update("actModeLmStudioModelId", lmStudioModelId)
}
if (liteLlmModelId !== undefined) {
await context.globalState.update("actModeLiteLlmModelId", liteLlmModelId)
}
if (liteLlmModelInfo !== undefined) {
await context.globalState.update("actModeLiteLlmModelInfo", liteLlmModelInfo)
}
if (requestyModelId !== undefined) {
await context.globalState.update("actModeRequestyModelId", requestyModelId)
}
if (requestyModelInfo !== undefined) {
await context.globalState.update("actModeRequestyModelInfo", requestyModelInfo)
}
if (togetherModelId !== undefined) {
await context.globalState.update("actModeTogetherModelId", togetherModelId)
}
if (fireworksModelId !== undefined) {
await context.globalState.update("actModeFireworksModelId", fireworksModelId)
}
if (groqModelId !== undefined) {
await context.globalState.update("actModeGroqModelId", groqModelId)
}
if (groqModelInfo !== undefined) {
await context.globalState.update("actModeGroqModelInfo", groqModelInfo)
}
if (huggingFaceModelId !== undefined) {
await context.globalState.update("actModeHuggingFaceModelId", huggingFaceModelId)
}
if (huggingFaceModelInfo !== undefined) {
await context.globalState.update("actModeHuggingFaceModelInfo", huggingFaceModelInfo)
}
}
// Clean up legacy keys after successful migration
console.log("Cleaning up legacy keys...")
await context.globalState.update("apiProvider", undefined)
await context.globalState.update("apiModelId", undefined)
await context.globalState.update("thinkingBudgetTokens", undefined)
await context.globalState.update("reasoningEffort", undefined)
await context.globalState.update("vsCodeLmModelSelector", undefined)
await context.globalState.update("awsBedrockCustomSelected", undefined)
await context.globalState.update("awsBedrockCustomModelBaseId", undefined)
await context.globalState.update("openRouterModelId", undefined)
await context.globalState.update("openRouterModelInfo", undefined)
await context.globalState.update("openAiModelId", undefined)
await context.globalState.update("openAiModelInfo", undefined)
await context.globalState.update("ollamaModelId", undefined)
await context.globalState.update("lmStudioModelId", undefined)
await context.globalState.update("liteLlmModelId", undefined)
await context.globalState.update("liteLlmModelInfo", undefined)
await context.globalState.update("requestyModelId", undefined)
await context.globalState.update("requestyModelInfo", undefined)
await context.globalState.update("togetherModelId", undefined)
await context.globalState.update("fireworksModelId", undefined)
await context.globalState.update("sapAiCoreModelId", undefined)
await context.globalState.update("groqModelId", undefined)
await context.globalState.update("groqModelInfo", undefined)
await context.globalState.update("huggingFaceModelId", undefined)
await context.globalState.update("huggingFaceModelInfo", undefined)
await context.globalState.update("previousModeApiProvider", undefined)
await context.globalState.update("previousModeModelId", undefined)
await context.globalState.update("previousModeModelInfo", undefined)
await context.globalState.update("previousModeVsCodeLmModelSelector", undefined)
await context.globalState.update("previousModeThinkingBudgetTokens", undefined)
await context.globalState.update("previousModeReasoningEffort", undefined)
await context.globalState.update("previousModeAwsBedrockCustomSelected", undefined)
await context.globalState.update("previousModeAwsBedrockCustomModelBaseId", undefined)
await context.globalState.update("previousModeSapAiCoreModelId", undefined)
console.log("Successfully migrated legacy API configuration to mode-specific keys")
} catch (error) {
console.error("Failed to migrate legacy API configuration to mode-specific keys:", error)
// Continue execution - migration failure shouldn't break extension startup
}
}
export async function migrateWelcomeViewCompleted(context: vscode.ExtensionContext) {
try {
// Check if welcomeViewCompleted is already set
@@ -563,10 +190,8 @@ export async function migrateWelcomeViewCompleted(context: vscode.ExtensionConte
config.awsRegion,
config.vertexProjectId,
config.openAiApiKey,
config.planModeOllamaModelId,
config.planModeLmStudioModelId,
config.actModeOllamaModelId,
config.actModeLmStudioModelId,
config.ollamaModelId,
config.lmStudioModelId,
config.liteLlmApiKey,
config.geminiApiKey,
config.openAiNativeApiKey,
@@ -576,8 +201,7 @@ export async function migrateWelcomeViewCompleted(context: vscode.ExtensionConte
config.qwenApiKey,
config.doubaoApiKey,
config.mistralApiKey,
config.planModeVsCodeLmModelSelector,
config.actModeVsCodeLmModelSelector,
config.vsCodeLmModelSelector,
config.clineAccountId,
config.asksageApiKey,
config.xaiApiKey,
+144 -260
View File
@@ -1,5 +1,5 @@
import * as vscode from "vscode"
import { DEFAULT_CHAT_SETTINGS, Mode } from "@shared/ChatSettings"
import { DEFAULT_CHAT_SETTINGS } from "@shared/ChatSettings"
import { DEFAULT_BROWSER_SETTINGS } from "@shared/BrowserSettings"
import { DEFAULT_AUTO_APPROVAL_SETTINGS } from "@shared/AutoApprovalSettings"
import { GlobalStateKey, LocalStateKey, SecretKey } from "./state-keys"
@@ -170,7 +170,6 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
groqApiKey,
moonshotApiKey,
nebiusApiKey,
huggingFaceApiKey,
planActSeparateModelsSettingRaw,
favoritedModelIds,
globalClineRulesToggles,
@@ -190,6 +189,8 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
sapAiCoreTokenUrl,
sapAiResourceGroup,
claudeCodePath,
groqModelId,
groqModelInfo,
] = await Promise.all([
getGlobalState(context, "isNewUser") as Promise<boolean | undefined>,
getGlobalState(context, "welcomeViewCompleted") as Promise<boolean | undefined>,
@@ -249,7 +250,6 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
getSecret(context, "groqApiKey") as Promise<string | undefined>,
getSecret(context, "moonshotApiKey") as Promise<string | undefined>,
getSecret(context, "nebiusApiKey") as Promise<string | undefined>,
getSecret(context, "huggingFaceApiKey") as Promise<string | undefined>,
getGlobalState(context, "planActSeparateModelsSetting") as Promise<boolean | undefined>,
getGlobalState(context, "favoritedModelIds") as Promise<string[] | undefined>,
getGlobalState(context, "globalClineRulesToggles") as Promise<ClineRulesToggles | undefined>,
@@ -269,6 +269,8 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
getGlobalState(context, "sapAiCoreTokenUrl") as Promise<string | undefined>,
getGlobalState(context, "sapAiResourceGroup") as Promise<string | undefined>,
getGlobalState(context, "claudeCodePath") as Promise<string | undefined>,
getGlobalState(context, "groqModelId") as Promise<string | undefined>,
getGlobalState(context, "groqModelInfo") as Promise<ModelInfo | undefined>,
])
const localClineRulesToggles = (await getWorkspaceState(context, "localClineRulesToggles")) as ClineRulesToggles
@@ -277,115 +279,74 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
const [
chatSettings,
currentMode,
// Plan mode configurations
planModeApiProvider,
planModeApiModelId,
planModeThinkingBudgetTokens,
planModeReasoningEffort,
planModeVsCodeLmModelSelector,
planModeAwsBedrockCustomSelected,
planModeAwsBedrockCustomModelBaseId,
planModeOpenRouterModelId,
planModeOpenRouterModelInfo,
planModeOpenAiModelId,
planModeOpenAiModelInfo,
planModeOllamaModelId,
planModeLmStudioModelId,
planModeLiteLlmModelId,
planModeLiteLlmModelInfo,
planModeRequestyModelId,
planModeRequestyModelInfo,
planModeTogetherModelId,
planModeFireworksModelId,
planModeSapAiCoreModelId,
planModeGroqModelId,
planModeGroqModelInfo,
planModeHuggingFaceModelId,
planModeHuggingFaceModelInfo,
// Act mode configurations
actModeApiProvider,
actModeApiModelId,
actModeThinkingBudgetTokens,
actModeReasoningEffort,
actModeVsCodeLmModelSelector,
actModeAwsBedrockCustomSelected,
actModeAwsBedrockCustomModelBaseId,
actModeOpenRouterModelId,
actModeOpenRouterModelInfo,
actModeOpenAiModelId,
actModeOpenAiModelInfo,
actModeOllamaModelId,
actModeLmStudioModelId,
actModeLiteLlmModelId,
actModeLiteLlmModelInfo,
actModeRequestyModelId,
actModeRequestyModelInfo,
actModeTogetherModelId,
actModeFireworksModelId,
actModeSapAiCoreModelId,
actModeGroqModelId,
actModeGroqModelInfo,
actModeHuggingFaceModelId,
actModeHuggingFaceModelInfo,
storedApiProvider,
apiModelId,
thinkingBudgetTokens,
reasoningEffort,
vsCodeLmModelSelector,
awsBedrockCustomSelected,
awsBedrockCustomModelBaseId,
openRouterModelId,
openRouterModelInfo,
openAiModelId,
openAiModelInfo,
ollamaModelId,
lmStudioModelId,
liteLlmModelId,
liteLlmModelInfo,
requestyModelId,
requestyModelInfo,
togetherModelId,
fireworksModelId,
previousModeApiProvider,
previousModeModelId,
previousModeModelInfo,
previousModeVsCodeLmModelSelector,
previousModeThinkingBudgetTokens,
previousModeReasoningEffort,
previousModeAwsBedrockCustomSelected,
previousModeAwsBedrockCustomModelBaseId,
previousModeSapAiCoreModelId,
sapAiCoreModelId,
] = await Promise.all([
getGlobalState(context, "chatSettings") as Promise<StoredChatSettings | undefined>,
getGlobalState(context, "mode") as Promise<Mode | undefined>,
// Plan mode configurations
getGlobalState(context, "planModeApiProvider") as Promise<ApiProvider | undefined>,
getGlobalState(context, "planModeApiModelId") as Promise<string | undefined>,
getGlobalState(context, "planModeThinkingBudgetTokens") as Promise<number | undefined>,
getGlobalState(context, "planModeReasoningEffort") as Promise<string | undefined>,
getGlobalState(context, "planModeVsCodeLmModelSelector") as Promise<vscode.LanguageModelChatSelector | undefined>,
getGlobalState(context, "planModeAwsBedrockCustomSelected") as Promise<boolean | undefined>,
getGlobalState(context, "planModeAwsBedrockCustomModelBaseId") as Promise<BedrockModelId | undefined>,
getGlobalState(context, "planModeOpenRouterModelId") as Promise<string | undefined>,
getGlobalState(context, "planModeOpenRouterModelInfo") as Promise<ModelInfo | undefined>,
getGlobalState(context, "planModeOpenAiModelId") as Promise<string | undefined>,
getGlobalState(context, "planModeOpenAiModelInfo") as Promise<ModelInfo | undefined>,
getGlobalState(context, "planModeOllamaModelId") as Promise<string | undefined>,
getGlobalState(context, "planModeLmStudioModelId") as Promise<string | undefined>,
getGlobalState(context, "planModeLiteLlmModelId") as Promise<string | undefined>,
getGlobalState(context, "planModeLiteLlmModelInfo") as Promise<ModelInfo | undefined>,
getGlobalState(context, "planModeRequestyModelId") as Promise<string | undefined>,
getGlobalState(context, "planModeRequestyModelInfo") as Promise<ModelInfo | undefined>,
getGlobalState(context, "planModeTogetherModelId") as Promise<string | undefined>,
getGlobalState(context, "planModeFireworksModelId") as Promise<string | undefined>,
getGlobalState(context, "planModeSapAiCoreModelId") as Promise<string | undefined>,
getGlobalState(context, "planModeGroqModelId") as Promise<string | undefined>,
getGlobalState(context, "planModeGroqModelInfo") as Promise<ModelInfo | undefined>,
getGlobalState(context, "planModeHuggingFaceModelId") as Promise<string | undefined>,
getGlobalState(context, "planModeHuggingFaceModelInfo") as Promise<ModelInfo | undefined>,
// Act mode configurations
getGlobalState(context, "actModeApiProvider") as Promise<ApiProvider | undefined>,
getGlobalState(context, "actModeApiModelId") as Promise<string | undefined>,
getGlobalState(context, "actModeThinkingBudgetTokens") as Promise<number | undefined>,
getGlobalState(context, "actModeReasoningEffort") as Promise<string | undefined>,
getGlobalState(context, "actModeVsCodeLmModelSelector") as Promise<vscode.LanguageModelChatSelector | undefined>,
getGlobalState(context, "actModeAwsBedrockCustomSelected") as Promise<boolean | undefined>,
getGlobalState(context, "actModeAwsBedrockCustomModelBaseId") as Promise<BedrockModelId | undefined>,
getGlobalState(context, "actModeOpenRouterModelId") as Promise<string | undefined>,
getGlobalState(context, "actModeOpenRouterModelInfo") as Promise<ModelInfo | undefined>,
getGlobalState(context, "actModeOpenAiModelId") as Promise<string | undefined>,
getGlobalState(context, "actModeOpenAiModelInfo") as Promise<ModelInfo | undefined>,
getGlobalState(context, "actModeOllamaModelId") as Promise<string | undefined>,
getGlobalState(context, "actModeLmStudioModelId") as Promise<string | undefined>,
getGlobalState(context, "actModeLiteLlmModelId") as Promise<string | undefined>,
getGlobalState(context, "actModeLiteLlmModelInfo") as Promise<ModelInfo | undefined>,
getGlobalState(context, "actModeRequestyModelId") as Promise<string | undefined>,
getGlobalState(context, "actModeRequestyModelInfo") as Promise<ModelInfo | undefined>,
getGlobalState(context, "actModeTogetherModelId") as Promise<string | undefined>,
getGlobalState(context, "actModeFireworksModelId") as Promise<string | undefined>,
getGlobalState(context, "actModeSapAiCoreModelId") as Promise<string | undefined>,
getGlobalState(context, "actModeGroqModelId") as Promise<string | undefined>,
getGlobalState(context, "actModeGroqModelInfo") as Promise<ModelInfo | undefined>,
getGlobalState(context, "actModeHuggingFaceModelId") as Promise<string | undefined>,
getGlobalState(context, "actModeHuggingFaceModelInfo") as Promise<ModelInfo | undefined>,
getGlobalState(context, "mode") as Promise<"plan" | "act" | undefined>,
getGlobalState(context, "apiProvider") as Promise<ApiProvider | undefined>,
getGlobalState(context, "apiModelId") as Promise<string | undefined>,
getGlobalState(context, "thinkingBudgetTokens") as Promise<number | undefined>,
getGlobalState(context, "reasoningEffort") as Promise<string | undefined>,
getGlobalState(context, "vsCodeLmModelSelector") as Promise<vscode.LanguageModelChatSelector | undefined>,
getGlobalState(context, "awsBedrockCustomSelected") as Promise<boolean | undefined>,
getGlobalState(context, "awsBedrockCustomModelBaseId") as Promise<BedrockModelId | undefined>,
getGlobalState(context, "openRouterModelId") as Promise<string | undefined>,
getGlobalState(context, "openRouterModelInfo") as Promise<ModelInfo | undefined>,
getGlobalState(context, "openAiModelId") as Promise<string | undefined>,
getGlobalState(context, "openAiModelInfo") as Promise<ModelInfo | undefined>,
getGlobalState(context, "ollamaModelId") as Promise<string | undefined>,
getGlobalState(context, "lmStudioModelId") as Promise<string | undefined>,
getGlobalState(context, "liteLlmModelId") as Promise<string | undefined>,
getGlobalState(context, "liteLlmModelInfo") as Promise<ModelInfo | undefined>,
getGlobalState(context, "requestyModelId") as Promise<string | undefined>,
getGlobalState(context, "requestyModelInfo") as Promise<ModelInfo | undefined>,
getGlobalState(context, "togetherModelId") as Promise<string | undefined>,
getGlobalState(context, "fireworksModelId") as Promise<string | undefined>,
getGlobalState(context, "previousModeApiProvider") as Promise<ApiProvider | undefined>,
getGlobalState(context, "previousModeModelId") as Promise<string | undefined>,
getGlobalState(context, "previousModeModelInfo") as Promise<ModelInfo | undefined>,
getGlobalState(context, "previousModeVsCodeLmModelSelector") as Promise<vscode.LanguageModelChatSelector | undefined>,
getGlobalState(context, "previousModeThinkingBudgetTokens") as Promise<number | undefined>,
getGlobalState(context, "previousModeReasoningEffort") as Promise<string | undefined>,
getGlobalState(context, "previousModeAwsBedrockCustomSelected") as Promise<boolean | undefined>,
getGlobalState(context, "previousModeAwsBedrockCustomModelBaseId") as Promise<BedrockModelId | undefined>,
getGlobalState(context, "previousModeSapAiCoreModelId") as Promise<string | undefined>,
getGlobalState(context, "sapAiCoreModelId") as Promise<string | undefined>,
])
const processingStart = performance.now()
let apiProvider: ApiProvider
if (planModeApiProvider) {
apiProvider = planModeApiProvider
if (storedApiProvider) {
// Use the explicitly stored provider - this respects user's selection
apiProvider = storedApiProvider
} else {
// Either new user or legacy user that doesn't have the apiProvider stored in state
// (If they're using OpenRouter or Bedrock, then apiProvider state will exist)
@@ -408,7 +369,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
planActSeparateModelsSetting = planActSeparateModelsSettingRaw
} else {
// default to true for existing users
if (planModeApiProvider) {
if (storedApiProvider) {
planActSeparateModelsSetting = true
} else {
// default to false for new users
@@ -421,6 +382,8 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
return {
apiConfiguration: {
apiProvider,
apiModelId,
apiKey,
openRouterApiKey,
clineAccountId,
@@ -436,13 +399,19 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
awsBedrockApiKey,
awsUseProfile,
awsAuthentication,
awsBedrockCustomSelected,
awsBedrockCustomModelBaseId,
vertexProjectId,
vertexRegion,
openAiBaseUrl,
openAiApiKey,
openAiModelId,
openAiModelInfo,
openAiHeaders: openAiHeaders || {},
ollamaModelId,
ollamaBaseUrl,
ollamaApiOptionsCtxNum,
lmStudioModelId,
lmStudioBaseUrl,
anthropicBaseUrl,
geminiApiKey,
@@ -450,18 +419,29 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
openAiNativeApiKey,
deepSeekApiKey,
requestyApiKey,
requestyModelId,
requestyModelInfo,
togetherApiKey,
togetherModelId,
qwenApiKey,
qwenApiLine,
moonshotApiLine,
doubaoApiKey,
mistralApiKey,
azureApiVersion,
openRouterModelId,
openRouterModelInfo,
openRouterProviderSorting,
vsCodeLmModelSelector,
thinkingBudgetTokens,
reasoningEffort,
liteLlmBaseUrl,
liteLlmModelId,
liteLlmModelInfo,
liteLlmApiKey,
liteLlmUsePromptCache,
fireworksApiKey,
fireworksModelId,
fireworksModelMaxCompletionTokens,
fireworksModelMaxTokens,
asksageApiKey,
@@ -470,6 +450,8 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
sambanovaApiKey,
cerebrasApiKey,
groqApiKey,
groqModelId,
groqModelInfo,
moonshotApiKey,
nebiusApiKey,
favoritedModelIds,
@@ -479,55 +461,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
sapAiCoreBaseUrl,
sapAiCoreTokenUrl,
sapAiResourceGroup,
huggingFaceApiKey,
// Plan mode configurations
planModeApiProvider: planModeApiProvider || apiProvider,
planModeApiModelId,
planModeThinkingBudgetTokens,
planModeReasoningEffort,
planModeVsCodeLmModelSelector,
planModeAwsBedrockCustomSelected,
planModeAwsBedrockCustomModelBaseId,
planModeOpenRouterModelId,
planModeOpenRouterModelInfo,
planModeOpenAiModelId,
planModeOpenAiModelInfo,
planModeOllamaModelId,
planModeLmStudioModelId,
planModeLiteLlmModelId,
planModeLiteLlmModelInfo,
planModeRequestyModelId,
planModeRequestyModelInfo,
planModeTogetherModelId,
planModeFireworksModelId,
planModeSapAiCoreModelId,
planModeGroqModelId,
planModeGroqModelInfo,
planModeHuggingFaceModelId,
planModeHuggingFaceModelInfo,
// Act mode configurations
actModeApiProvider: actModeApiProvider || apiProvider,
actModeApiModelId,
actModeThinkingBudgetTokens,
actModeReasoningEffort,
actModeVsCodeLmModelSelector,
actModeAwsBedrockCustomSelected,
actModeAwsBedrockCustomModelBaseId,
actModeOpenRouterModelId,
actModeOpenRouterModelInfo,
actModeOpenAiModelId,
actModeOpenAiModelInfo,
actModeOllamaModelId,
actModeLmStudioModelId,
actModeLiteLlmModelId,
actModeLiteLlmModelInfo,
actModeRequestyModelId,
actModeRequestyModelInfo,
actModeTogetherModelId,
actModeFireworksModelId,
actModeSapAiCoreModelId,
actModeGroqModelId,
actModeGroqModelInfo,
sapAiCoreModelId,
},
isNewUser: isNewUser ?? true,
welcomeViewCompleted,
@@ -543,6 +477,15 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
mode: currentMode || "act", // Merge mode from global state
},
userInfo,
previousModeApiProvider,
previousModeModelId,
previousModeModelInfo,
previousModeVsCodeLmModelSelector,
previousModeThinkingBudgetTokens,
previousModeReasoningEffort,
previousModeAwsBedrockCustomSelected,
previousModeAwsBedrockCustomModelBaseId,
previousModeSapAiCoreModelId,
mcpMarketplaceEnabled: mcpMarketplaceEnabled,
mcpDisplayMode: mcpDisplayMode ?? DEFAULT_MCP_DISPLAY_MODE,
mcpResponsesCollapsed: mcpResponsesCollapsed,
@@ -559,6 +502,8 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
export async function updateApiConfiguration(context: vscode.ExtensionContext, apiConfiguration: ApiConfiguration) {
const {
apiProvider,
apiModelId,
apiKey,
openRouterApiKey,
awsAccessKey,
@@ -572,13 +517,19 @@ export async function updateApiConfiguration(context: vscode.ExtensionContext, a
awsProfile,
awsUseProfile,
awsAuthentication,
awsBedrockCustomSelected,
awsBedrockCustomModelBaseId,
vertexProjectId,
vertexRegion,
openAiBaseUrl,
openAiApiKey,
openAiModelId,
openAiModelInfo,
openAiHeaders,
ollamaModelId,
ollamaBaseUrl,
ollamaApiOptionsCtxNum,
lmStudioModelId,
lmStudioBaseUrl,
anthropicBaseUrl,
geminiApiKey,
@@ -586,13 +537,21 @@ export async function updateApiConfiguration(context: vscode.ExtensionContext, a
openAiNativeApiKey,
deepSeekApiKey,
requestyApiKey,
requestyModelId,
requestyModelInfo,
togetherApiKey,
togetherModelId,
qwenApiKey,
doubaoApiKey,
mistralApiKey,
azureApiVersion,
openRouterModelId,
openRouterModelInfo,
openRouterProviderSorting,
vsCodeLmModelSelector,
liteLlmBaseUrl,
liteLlmModelId,
liteLlmModelInfo,
liteLlmApiKey,
liteLlmUsePromptCache,
qwenApiLine,
@@ -600,14 +559,19 @@ export async function updateApiConfiguration(context: vscode.ExtensionContext, a
asksageApiKey,
asksageApiUrl,
xaiApiKey,
thinkingBudgetTokens,
reasoningEffort,
clineAccountId,
sambanovaApiKey,
cerebrasApiKey,
groqApiKey,
groqModelId,
groqModelInfo,
moonshotApiKey,
nebiusApiKey,
favoritedModelIds,
fireworksApiKey,
fireworksModelId,
fireworksModelMaxCompletionTokens,
fireworksModelMaxTokens,
sapAiCoreClientId,
@@ -615,113 +579,35 @@ export async function updateApiConfiguration(context: vscode.ExtensionContext, a
sapAiCoreBaseUrl,
sapAiCoreTokenUrl,
sapAiResourceGroup,
sapAiCoreModelId,
claudeCodePath,
huggingFaceApiKey,
// Plan mode configurations
planModeApiProvider,
planModeApiModelId,
planModeThinkingBudgetTokens,
planModeReasoningEffort,
planModeVsCodeLmModelSelector,
planModeAwsBedrockCustomSelected,
planModeAwsBedrockCustomModelBaseId,
planModeOpenRouterModelId,
planModeOpenRouterModelInfo,
planModeOpenAiModelId,
planModeOpenAiModelInfo,
planModeOllamaModelId,
planModeLmStudioModelId,
planModeLiteLlmModelId,
planModeLiteLlmModelInfo,
planModeRequestyModelId,
planModeRequestyModelInfo,
planModeTogetherModelId,
planModeFireworksModelId,
planModeSapAiCoreModelId,
planModeGroqModelId,
planModeGroqModelInfo,
planModeHuggingFaceModelId,
planModeHuggingFaceModelInfo,
// Act mode configurations
actModeApiProvider,
actModeApiModelId,
actModeThinkingBudgetTokens,
actModeReasoningEffort,
actModeVsCodeLmModelSelector,
actModeAwsBedrockCustomSelected,
actModeAwsBedrockCustomModelBaseId,
actModeOpenRouterModelId,
actModeOpenRouterModelInfo,
actModeOpenAiModelId,
actModeOpenAiModelInfo,
actModeOllamaModelId,
actModeLmStudioModelId,
actModeLiteLlmModelId,
actModeLiteLlmModelInfo,
actModeRequestyModelId,
actModeRequestyModelInfo,
actModeTogetherModelId,
actModeFireworksModelId,
actModeSapAiCoreModelId,
actModeGroqModelId,
actModeGroqModelInfo,
actModeHuggingFaceModelId,
actModeHuggingFaceModelInfo,
} = apiConfiguration
// OPTIMIZED: Batch all global state updates into 2 operations instead of 47
const batchedGlobalUpdates = {
// Plan mode configuration updates
planModeApiProvider,
planModeApiModelId,
planModeThinkingBudgetTokens,
planModeReasoningEffort,
planModeVsCodeLmModelSelector,
planModeAwsBedrockCustomSelected,
planModeAwsBedrockCustomModelBaseId,
planModeOpenRouterModelId,
planModeOpenRouterModelInfo,
planModeOpenAiModelId,
planModeOpenAiModelInfo,
planModeOllamaModelId,
planModeLmStudioModelId,
planModeLiteLlmModelId,
planModeLiteLlmModelInfo,
planModeRequestyModelId,
planModeRequestyModelInfo,
planModeTogetherModelId,
planModeFireworksModelId,
planModeSapAiCoreModelId,
planModeGroqModelId,
planModeGroqModelInfo,
planModeHuggingFaceModelId,
planModeHuggingFaceModelInfo,
// Act mode configuration updates
actModeApiProvider,
actModeApiModelId,
actModeThinkingBudgetTokens,
actModeReasoningEffort,
actModeVsCodeLmModelSelector,
actModeAwsBedrockCustomSelected,
actModeAwsBedrockCustomModelBaseId,
actModeOpenRouterModelId,
actModeOpenRouterModelInfo,
actModeOpenAiModelId,
actModeOpenAiModelInfo,
actModeOllamaModelId,
actModeLmStudioModelId,
actModeLiteLlmModelId,
actModeLiteLlmModelInfo,
actModeRequestyModelId,
actModeRequestyModelInfo,
actModeTogetherModelId,
actModeFireworksModelId,
actModeSapAiCoreModelId,
actModeGroqModelId,
actModeGroqModelInfo,
actModeHuggingFaceModelId,
actModeHuggingFaceModelInfo,
// Ephemeral model config updates (20 keys)
apiProvider,
apiModelId,
thinkingBudgetTokens,
reasoningEffort,
vsCodeLmModelSelector,
awsBedrockCustomSelected,
awsBedrockCustomModelBaseId,
openRouterModelId,
openRouterModelInfo,
openAiModelId,
openAiModelInfo,
ollamaModelId,
lmStudioModelId,
liteLlmModelId,
liteLlmModelInfo,
requestyModelId,
requestyModelInfo,
togetherModelId,
fireworksModelId,
groqModelId,
groqModelInfo,
sapAiCoreModelId,
// Global state updates (27 keys)
awsRegion,
@@ -786,7 +672,6 @@ export async function updateApiConfiguration(context: vscode.ExtensionContext, a
nebiusApiKey,
sapAiCoreClientId,
sapAiCoreClientSecret,
huggingFaceApiKey,
}
// Execute batched operations in parallel for maximum performance
@@ -830,7 +715,6 @@ export async function resetGlobalState(context: vscode.ExtensionContext) {
"groqApiKey",
"moonshotApiKey",
"nebiusApiKey",
"huggingFaceApiKey",
]
for (const key of secretKeys) {
await storeSecret(context, key, undefined)
+2 -9
View File
@@ -54,7 +54,6 @@ import { TaskState } from "./TaskState"
import { MessageStateHandler } from "./message-state"
import { AutoApprove } from "./tools/autoApprove"
import { showNotificationForApprovalIfAutoApprovalEnabled } from "./utils"
import { ChatSettings } from "@/shared/ChatSettings"
export class ToolExecutor {
private autoApprover: AutoApprove
@@ -91,7 +90,6 @@ export class ToolExecutor {
private browserSettings: BrowserSettings,
private cwd: string,
private taskId: string,
private chatSettings: ChatSettings,
// Callbacks to the Task (Entity)
private say: (
@@ -636,7 +634,7 @@ export class ToolExecutor {
}
await this.diffViewProvider.update(newContent, true)
await setTimeoutPromise(300) // wait for diff view to update
await this.diffViewProvider.scrollToFirstDiff()
this.diffViewProvider.scrollToFirstDiff()
// showOmissionWarning(this.diffViewProvider.originalContent || "", newContent)
const completeMessage = JSON.stringify({
@@ -1919,12 +1917,7 @@ export class ToolExecutor {
const clineVersion =
vscode.extensions.getExtension("saoudrizwan.claude-dev")?.packageJSON.version || "Unknown"
const systemInfo = `VSCode: ${vscode.version}, Node.js: ${process.version}, Architecture: ${os.arch()}`
const currentMode = this.chatSettings.mode
const apiProvider =
currentMode === "plan"
? await getGlobalState(this.context, "planModeApiProvider")
: await getGlobalState(this.context, "actModeApiProvider")
const providerAndModel = `${apiProvider} / ${this.api.getModel().id}`
const providerAndModel = `${await getGlobalState(this.context, "apiProvider")} / ${this.api.getModel().id}`
// Ask user for confirmation
const bugReportData = JSON.stringify({
+51 -77
View File
@@ -81,10 +81,9 @@ import { refreshWorkflowToggles } from "../context/instructions/user-instruction
import { MessageStateHandler } from "./message-state"
import { TaskState } from "./TaskState"
import { ToolExecutor } from "./ToolExecutor"
import { updateApiReqMsg } from "./utils"
import { createDiffViewProvider } from "@/hosts/host-providers"
import { ErrorService } from "@/services/error/ErrorService"
import { ClineErrorType } from "@/services/error/ClineError"
import { extractErrorDetails, formatErrorWithStatusCode, updateApiReqMsg } from "./utils"
import { createDiffViewProvider, getHostBridgeProvider } from "@/hosts/host-providers"
export const USE_EXPERIMENTAL_CLAUDE4_FEATURES = false
export type ToolResponse = string | Array<Anthropic.TextBlockParam | Anthropic.ImageBlockParam>
@@ -166,19 +165,8 @@ export class Task {
this.reinitExistingTaskFromId = reinitExistingTaskFromId
this.cancelTask = cancelTask
this.clineIgnoreController = new ClineIgnoreController(cwd)
// TODO(ae) this is a hack to replace the terminal manager for standalone,
// until we have proper host bridge support for terminal execution. The
// standaloneTerminalManager is defined in the vscode-impls and injected
// during compilation of the standalone manager only, so this variable only
// exists in that case
if ((global as any).standaloneTerminalManager) {
console.log("[DEBUG] Using vscode-impls.js terminal manager")
this.terminalManager = (global as any).standaloneTerminalManager
} else {
console.log("[DEBUG] Using built in terminal manager")
this.terminalManager = new TerminalManager()
}
// Initialization moved to startTask/resumeTaskFromHistory
this.terminalManager = new TerminalManager()
this.terminalManager.setShellIntegrationTimeout(shellIntegrationTimeout)
this.terminalManager.setTerminalReuseEnabled(terminalReuseEnabled ?? true)
this.terminalManager.setTerminalOutputLineLimit(terminalOutputLineLimit)
@@ -261,19 +249,12 @@ export class Task {
},
}
const currentProvider =
chatSettings.mode === "plan" ? apiConfiguration.planModeApiProvider : apiConfiguration.actModeApiProvider
if (currentProvider === "openai" || currentProvider === "openai-native") {
if (chatSettings.mode === "plan") {
effectiveApiConfiguration.planModeReasoningEffort = chatSettings.openAIReasoningEffort
} else {
effectiveApiConfiguration.actModeReasoningEffort = chatSettings.openAIReasoningEffort
}
if (apiConfiguration.apiProvider === "openai" || apiConfiguration.apiProvider === "openai-native") {
effectiveApiConfiguration.reasoningEffort = chatSettings.openAIReasoningEffort
}
// Now that taskId is initialized, we can build the API handler
this.api = buildApiHandler(effectiveApiConfiguration, chatSettings.mode)
this.api = buildApiHandler(effectiveApiConfiguration)
// Set taskId on browserSession for telemetry tracking
this.browserSession.setTaskId(this.taskId)
@@ -288,10 +269,10 @@ export class Task {
// initialize telemetry
if (historyItem) {
// Open task from history
telemetryService.captureTaskRestarted(this.taskId, currentProvider)
telemetryService.captureTaskRestarted(this.taskId, apiConfiguration.apiProvider)
} else {
// New task started
telemetryService.captureTaskCreated(this.taskId, currentProvider)
telemetryService.captureTaskCreated(this.taskId, apiConfiguration.apiProvider)
}
this.toolExecutor = new ToolExecutor(
@@ -311,7 +292,6 @@ export class Task {
this.browserSettings,
cwd,
this.taskId,
this.chatSettings,
this.say.bind(this),
this.ask.bind(this),
this.saveCheckpoint.bind(this),
@@ -1597,15 +1577,6 @@ export class Task {
}
}
private async getCurrentProviderInfo(): Promise<{ modelId: string; providerId: string }> {
const modelId = this.api.getModel()?.id
const providerId =
this.chatSettings.mode === "plan"
? ((await getGlobalState(this.getContext(), "planModeApiProvider")) as string)
: ((await getGlobalState(this.getContext(), "actModeApiProvider")) as string)
return { modelId, providerId }
}
async *attemptApiRequest(previousApiReqIndex: number): ApiStream {
// Wait for MCP servers to be connected before generating system prompt
await pWaitFor(() => this.mcpHub.isConnecting !== true, { timeout: 10_000 }).catch(() => {
@@ -1700,17 +1671,16 @@ export class Task {
const isAnthropic = this.api instanceof AnthropicHandler
const isOpenRouterContextWindowError = checkIsOpenRouterContextWindowError(error) && isOpenRouter
const isAnthropicContextWindowError = checkIsAnthropicContextWindowError(error) && isAnthropic
const { modelId, providerId } = await this.getCurrentProviderInfo()
const clineError = ErrorService.toClineError(error, modelId, providerId)
// Capture provider failure telemetry using clineError
// TODO: Move into ErrorService
const { statusCode, message, requestId } = extractErrorDetails(error)
// Capture provider failure telemetry
telemetryService.captureProviderApiError({
taskId: this.taskId,
model: modelInfo.id,
errorMessage: clineError.message,
errorStatus: clineError._error?.status,
requestId: clineError._error?.request_id,
errorMessage: message,
errorStatus: statusCode,
requestId,
})
if (isAnthropic && isAnthropicContextWindowError && !this.taskState.didAutomaticallyRetryFailedApiRequest) {
@@ -1756,12 +1726,12 @@ export class Task {
// If the conversation has more than 3 messages, we can truncate again. If not, then the conversation is bricked.
// ToDo: Allow the user to change their input if this is the case.
if (truncatedConversationHistory.length > 3) {
clineError.message = "Context window exceeded. Click retry to truncate the conversation and try again."
error = new Error("Context window exceeded. Click retry to truncate the conversation and try again.")
this.taskState.didAutomaticallyRetryFailedApiRequest = false
}
}
const streamingFailedMessage = clineError.serialize()
const errorMessage = formatErrorWithStatusCode(error)
// Update the 'api_req_started' message to reflect final failure before asking user to manually retry
const lastApiReqStartedIndex = findLastIndex(
@@ -1777,24 +1747,19 @@ export class Task {
text: JSON.stringify({
...currentApiReqInfo, // Spread the modified info (with retryStatus removed)
// cancelReason: "retries_exhausted", // Indicate that automatic retries failed
streamingFailedMessage,
streamingFailedMessage: errorMessage,
} satisfies ClineApiReqInfo),
})
// this.ask will trigger postStateToWebview, so this change should be picked up.
}
const { response } = await this.ask("api_req_failed", streamingFailedMessage)
const { response } = await this.ask("api_req_failed", errorMessage)
if (response !== "yesButtonClicked") {
// this will never happen since if noButtonClicked, we will clear current task, aborting this instance
throw new Error("API request failed")
}
// Do not retry automatically again if currently unauthenticated
if (clineError.isErrorType(ClineErrorType.Auth)) {
return
}
await this.say("api_req_retried")
}
// delegate generator output from the recursive call
@@ -1930,10 +1895,10 @@ export class Task {
}
// Used to know what models were used in the task if user wants to export metadata for error reporting purposes
const { modelId, providerId } = await this.getCurrentProviderInfo()
if (providerId && modelId) {
const currentProviderId = (await getGlobalState(this.getContext(), "apiProvider")) as string
if (currentProviderId && this.api.getModel().id) {
try {
await this.modelContextTracker.recordModelUsage(providerId, modelId, this.chatSettings.mode)
await this.modelContextTracker.recordModelUsage(currentProviderId, this.api.getModel().id, this.chatSettings.mode)
} catch {}
}
@@ -2111,7 +2076,7 @@ export class Task {
content: userContent,
})
telemetryService.captureConversationTurnEvent(this.taskId, providerId, modelId, "user")
telemetryService.captureConversationTurnEvent(this.taskId, currentProviderId, this.api.getModel().id, "user")
// since we sent off a placeholder api_req_started message to update the webview while waiting to actually start the API request (to load potential details for example), we need to update the text of that message
const lastApiReqIndex = findLastIndex(this.messageStateHandler.getClineMessages(), (m) => m.say === "api_req_started")
@@ -2176,13 +2141,19 @@ export class Task {
})
await this.messageStateHandler.saveClineMessagesAndUpdateHistory()
telemetryService.captureConversationTurnEvent(this.taskId, providerId, this.api.getModel().id, "assistant", {
tokensIn: inputTokens,
tokensOut: outputTokens,
cacheWriteTokens,
cacheReadTokens,
totalCost,
})
telemetryService.captureConversationTurnEvent(
this.taskId,
currentProviderId,
this.api.getModel().id,
"assistant",
{
tokensIn: inputTokens,
tokensOut: outputTokens,
cacheWriteTokens,
cacheReadTokens,
totalCost,
},
)
// signals to provider that it can retrieve the saved messages from disk, as abortTask can not be awaited on in nature
this.taskState.didFinishAbortingStream = true
@@ -2279,8 +2250,7 @@ export class Task {
// abandoned happens when extension is no longer waiting for the cline instance to finish aborting (error is thrown here when any function in the for loop throws due to this.abort)
if (!this.taskState.abandoned) {
this.abortTask() // if the stream failed, there's various states the task could be in (i.e. could have streamed some tools the user may have executed), so we just resort to replicating a cancel task
const clineError = ErrorService.toClineError(error, this.api.getModel().id)
const errorMessage = clineError.serialize()
const errorMessage = formatErrorWithStatusCode(error)
await abortStream("streaming_failed", errorMessage)
await this.reinitExistingTaskFromId(this.taskId)
@@ -2350,13 +2320,19 @@ export class Task {
// need to save assistant responses to file before proceeding to tool use since user can exit at any moment and we wouldn't be able to save the assistant's response
let didEndLoop = false
if (assistantMessage.length > 0) {
telemetryService.captureConversationTurnEvent(this.taskId, providerId, modelId, "assistant", {
tokensIn: inputTokens,
tokensOut: outputTokens,
cacheWriteTokens,
cacheReadTokens,
totalCost,
})
telemetryService.captureConversationTurnEvent(
this.taskId,
currentProviderId,
this.api.getModel().id,
"assistant",
{
tokensIn: inputTokens,
tokensOut: outputTokens,
cacheWriteTokens,
cacheReadTokens,
totalCost,
},
)
await this.messageStateHandler.addToApiConversationHistory({
role: "assistant",
@@ -2402,8 +2378,6 @@ export class Task {
},
],
})
// Returns early to avoid retry since no assistant message was received
return true
}
return didEndLoop // will always be false for now
+16
View File
@@ -1,9 +1,25 @@
import { showSystemNotification } from "@/integrations/notifications"
import { ClineApiReqCancelReason, ClineApiReqInfo } from "@/shared/ExtensionMessage"
import { serializeError } from "serialize-error"
import { MessageStateHandler } from "./message-state"
import { calculateApiCostAnthropic } from "@/utils/cost"
import { ApiHandler } from "@/api"
export function formatErrorWithStatusCode(error: any): string {
const { statusCode, message } = extractErrorDetails(error)
// Only prepend the statusCode if it's not already part of the message
return statusCode && !message.includes(statusCode.toString()) ? `${statusCode} - ${message}` : message
}
export function extractErrorDetails(error: any): { message: string; statusCode?: number; requestId?: string } {
const statusCode = error.status || error.statusCode || (error.response && error.response?.status)
const message = error.message ?? JSON.stringify(serializeError(error), null, 2)
const requestId = error.request_id || error.response?.request_id || undefined
return { message, statusCode, requestId }
}
export const showNotificationForApprovalIfAutoApprovalEnabled = (
message: string,
autoApprovalSettingsEnabled: boolean,
+7 -5
View File
@@ -264,11 +264,13 @@ export abstract class WebviewProvider {
} catch (error) {
// Only show the error message if not in development mode.
if (!process.env.IS_DEV) {
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.ERROR,
message:
"Cline: Local webview dev server is not running, HMR will not work. Please run 'npm run dev:webview' before launching the extension to enable HMR. Using bundled assets.",
})
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.ERROR,
message:
"Cline: Local webview dev server is not running, HMR will not work. Please run 'npm run dev:webview' before launching the extension to enable HMR. Using bundled assets.",
}),
)
}
return this.getHtmlContent()
+6 -4
View File
@@ -99,10 +99,12 @@ export function registerTaskCommands(context: vscode.ExtensionContext, controlle
await controller.postStateToWebview()
const message = `Created ${tasksCount} test tasks`
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.INFORMATION,
message,
})
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.INFORMATION,
message,
}),
)
},
)
}),
+32 -35
View File
@@ -27,7 +27,6 @@ import {
migrateCustomInstructionsToGlobalRules,
migrateModeFromWorkspaceStorageToControllerState,
migrateWelcomeViewCompleted,
migrateLegacyApiConfigurationToModeSpecific,
} from "./core/storage/state-migrations"
import { sendFocusChatInputEvent } from "./core/controller/ui/subscribeToFocusChatInput"
@@ -76,9 +75,6 @@ export async function activate(context: vscode.ExtensionContext) {
// Migrate workspace storage values back to global storage (reverting previous migration)
await migrateWorkspaceToGlobalStorage(context)
// Migrate legacy API configuration to mode-specific keys (one-time migration)
await migrateLegacyApiConfigurationToModeSpecific(context)
// Clean up orphaned file context warnings (startup cleanup)
await FileContextTracker.cleanupOrphanedWarnings(context)
@@ -110,10 +106,12 @@ export async function activate(context: vscode.ExtensionContext) {
const message = `Cline has been updated to v${currentVersion}`
await vscode.commands.executeCommand("claude-dev.SidebarProvider.focus")
await new Promise((resolve) => setTimeout(resolve, 200))
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.INFORMATION,
message,
})
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.INFORMATION,
message,
}),
)
// Record that we've shown the popup for this version.
await context.globalState.update("clineLastPopupNotificationVersion", currentVersion)
}
@@ -415,10 +413,12 @@ export async function activate(context: vscode.ExtensionContext) {
// Ensure clipboard is restored even if an error occurs
await writeTextToClipboard(tempCopyBuffer)
console.error("Error getting terminal contents:", error)
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.ERROR,
message: "Failed to get terminal contents",
})
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.ERROR,
message: "Failed to get terminal contents",
}),
)
}
}),
)
@@ -554,10 +554,12 @@ export async function activate(context: vscode.ExtensionContext) {
}
const selectedText = editor.document.getText(range)
if (!selectedText.trim()) {
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.INFORMATION,
message: "Please select some code to explain.",
})
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.INFORMATION,
message: "Please select some code to explain.",
}),
)
return
}
const filePath = editor.document.uri.fsPath
@@ -579,10 +581,12 @@ export async function activate(context: vscode.ExtensionContext) {
}
const selectedText = editor.document.getText(range)
if (!selectedText.trim()) {
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.INFORMATION,
message: "Please select some code to improve.",
})
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.INFORMATION,
message: "Please select some code to improve.",
}),
)
return
}
const filePath = editor.document.uri.fsPath
@@ -647,10 +651,12 @@ export async function activate(context: vscode.ExtensionContext) {
sendFocusChatInputEvent(clientId)
} else {
console.error("FocusChatInput: Could not find or activate a Cline webview to focus.")
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.ERROR,
message: "Could not activate Cline view. Please try opening it manually from the Activity Bar.",
})
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.ERROR,
message: "Could not activate Cline view. Please try opening it manually from the Activity Bar.",
}),
)
}
telemetryService.captureButtonClick("command_focusChatInput", activeWebviewProvider?.controller.task?.taskId)
}),
@@ -685,18 +691,9 @@ export async function activate(context: vscode.ExtensionContext) {
)
context.subscriptions.push(
context.secrets.onDidChange(async (event) => {
context.secrets.onDidChange((event) => {
if (event.key === "clineAccountId") {
// Check if the secret was removed (logout) or added/updated (login)
const secretValue = await context.secrets.get("clineAccountId")
const authService = AuthService.getInstance(context)
if (secretValue) {
// Secret was added or updated - restore auth info (login from another window)
authService?.restoreRefreshTokenAndRetrieveAuthInfo()
} else {
// Secret was removed - handle logout for all windows
authService?.handleDeauth()
}
AuthService.getInstance(context)?.restoreRefreshTokenAndRetrieveAuthInfo()
}
}),
)
-82
View File
@@ -1,82 +0,0 @@
import { getHostBridgeProvider } from "@/hosts/host-providers"
import { DiffViewProvider } from "@/integrations/editor/DiffViewProvider"
export class ExternalDiffViewProvider extends DiffViewProvider {
private activeDiffEditorId: string | undefined
override async openDiffEditor(): Promise<void> {
if (!this.absolutePath) {
return
}
const response = await getHostBridgeProvider().diffClient.openDiff({
path: this.absolutePath,
content: this.originalContent ?? "",
})
this.activeDiffEditorId = response.diffId
}
override async replaceText(
content: string,
rangeToReplace: { startLine: number; endLine: number },
_currentLine: number | undefined,
): Promise<void> {
if (!this.activeDiffEditorId) {
return
}
await getHostBridgeProvider().diffClient.replaceText({
diffId: this.activeDiffEditorId,
content: content,
startLine: rangeToReplace.startLine,
endLine: rangeToReplace.endLine,
})
}
protected override async truncateDocument(lineNumber: number): Promise<void> {
if (!this.activeDiffEditorId) {
return
}
await getHostBridgeProvider().diffClient.truncateDocument({
diffId: this.activeDiffEditorId,
endLine: lineNumber,
})
}
protected async saveDocument(): Promise<void> {
if (!this.activeDiffEditorId) {
return
}
await getHostBridgeProvider().diffClient.saveDocument({ diffId: this.activeDiffEditorId })
}
protected override async scrollEditorToLine(line: number): Promise<void> {
console.log(`Called ExternalDiffViewProvider.scrollEditorToLine(${line}) stub`)
}
override async scrollAnimation(startLine: number, endLine: number): Promise<void> {
console.log(`Called ExternalDiffViewProvider.scrollAnimation(${startLine}, ${endLine}) stub`)
}
protected override async getDocumentText(): Promise<string | undefined> {
if (!this.activeDiffEditorId) {
return undefined
}
return (await getHostBridgeProvider().diffClient.getDocumentText({ diffId: this.activeDiffEditorId })).content
}
protected override async getNewDiagnosticProblems(): Promise<string> {
console.log(`Called ExternalDiffViewProvider.getNewDiagnosticProblems() stub`)
return ""
}
protected override async closeDiffView(): Promise<void> {
if (!this.activeDiffEditorId) {
return
}
await getHostBridgeProvider().diffClient.closeDiff({ diffId: this.activeDiffEditorId })
this.activeDiffEditorId = undefined
}
protected override async resetDiffView(): Promise<void> {
this.activeDiffEditorId = undefined
}
}
+7 -104
View File
@@ -1,17 +1,10 @@
import { arePathsEqual } from "@/utils/path"
import * as path from "path"
import * as vscode from "vscode"
import { DecorationController } from "@/hosts/vscode/DecorationController"
import { DecorationController } from "@integrations/editor/DecorationController"
import { DIFF_VIEW_URI_SCHEME, DiffViewProvider } from "@integrations/editor/DiffViewProvider"
import { diagnosticsToProblemsString, getNewDiagnostics } from "@/integrations/diagnostics"
export class VscodeDiffViewProvider extends DiffViewProvider {
private activeDiffEditor?: vscode.TextEditor
private preDiagnostics: [vscode.Uri, vscode.Diagnostic[]][] = []
private fadedOverlayController?: DecorationController
private activeLineController?: DecorationController
override async openDiffEditor(): Promise<void> {
if (!this.absolutePath) {
throw new Error("No file path set")
@@ -88,110 +81,20 @@ export class VscodeDiffViewProvider extends DiffViewProvider {
override async replaceText(
content: string,
rangeToReplace: { startLine: number; endLine: number },
currentLine: number | undefined,
currentLine: number,
): Promise<void> {
if (!this.activeDiffEditor || !this.activeDiffEditor.document) {
const document = this.activeDiffEditor?.document
if (!document) {
throw new Error("User closed text editor, unable to edit file...")
}
// Place cursor at the beginning of the diff editor to keep it out of the way of the stream animation
const beginningOfDocument = new vscode.Position(0, 0)
this.activeDiffEditor.selection = new vscode.Selection(beginningOfDocument, beginningOfDocument)
// Replace the text in the diff editor document.
const document = this.activeDiffEditor?.document
const edit = new vscode.WorkspaceEdit()
const range = new vscode.Range(rangeToReplace.startLine, 0, rangeToReplace.endLine, 0)
edit.replace(document.uri, range, content)
await vscode.workspace.applyEdit(edit)
if (currentLine !== undefined) {
// Update decorations for the entire changed section
this.activeLineController?.setActiveLine(currentLine)
this.fadedOverlayController?.updateOverlayAfterLine(currentLine, document.lineCount)
}
}
override async scrollEditorToLine(line: number): Promise<void> {
if (!this.activeDiffEditor) {
return
}
const scrollLine = line + 4
this.activeDiffEditor.revealRange(new vscode.Range(scrollLine, 0, scrollLine, 0), vscode.TextEditorRevealType.InCenter)
}
override async scrollAnimation(startLine: number, endLine: number): Promise<void> {
if (!this.activeDiffEditor) {
return
}
const totalLines = endLine - startLine
const numSteps = 10 // Adjust this number to control animation speed
const stepSize = Math.max(1, Math.floor(totalLines / numSteps))
// Create and await the smooth scrolling animation
for (let line = startLine; line <= endLine; line += stepSize) {
this.activeDiffEditor.revealRange(new vscode.Range(line, 0, line, 0), vscode.TextEditorRevealType.InCenter)
await new Promise((resolve) => setTimeout(resolve, 16)) // ~60fps
}
}
override async truncateDocument(lineNumber: number): Promise<void> {
if (!this.activeDiffEditor) {
return
}
const document = this.activeDiffEditor.document
if (lineNumber < document.lineCount) {
const edit = new vscode.WorkspaceEdit()
edit.delete(document.uri, new vscode.Range(lineNumber, 0, document.lineCount, 0))
await vscode.workspace.applyEdit(edit)
}
// Clear all decorations at the end (before applying final edit)
this.fadedOverlayController?.clear()
this.activeLineController?.clear()
}
protected override async getDocumentText(): Promise<string | undefined> {
if (!this.activeDiffEditor || !this.activeDiffEditor.document) {
return undefined
}
return this.activeDiffEditor.document.getText()
}
protected override async getNewDiagnosticProblems(): Promise<string> {
// Get the diagnostics after changing the document.
const postDiagnostics = vscode.languages.getDiagnostics()
const newProblems = getNewDiagnostics(this.preDiagnostics, postDiagnostics)
// Only including errors since warnings can be distracting (if user wants to fix warnings they can use the @problems mention)
// will be empty string if no errors
const problems = await diagnosticsToProblemsString(newProblems, [vscode.DiagnosticSeverity.Error])
return problems
}
protected override async saveDocument(): Promise<void> {
if (!this.activeDiffEditor) {
return
}
if (this.activeDiffEditor.document.isDirty) {
await this.activeDiffEditor.document.save()
}
}
protected async closeDiffView(): Promise<void> {
// Close all the cline diff views.
const tabs = vscode.window.tabGroups.all
.flatMap((tg) => tg.tabs)
.filter((tab) => tab.input instanceof vscode.TabInputTextDiff && tab.input?.original?.scheme === DIFF_VIEW_URI_SCHEME)
for (const tab of tabs) {
// trying to close dirty views results in save popup
if (!tab.isDirty) {
await vscode.window.tabGroups.close(tab)
}
}
}
protected override async resetDiffView(): Promise<void> {
this.activeDiffEditor = undefined
this.fadedOverlayController = undefined
this.activeLineController = undefined
this.preDiagnostics = []
// Update decorations for the entire changed section
this.activeLineController?.setActiveLine(currentLine)
this.fadedOverlayController?.updateOverlayAfterLine(currentLine, document.lineCount)
}
}
@@ -1,5 +0,0 @@
import { CloseDiffRequest, CloseDiffResponse } from "@/shared/proto/index.host"
export async function closeDiff(_request: CloseDiffRequest): Promise<CloseDiffResponse> {
throw new Error("diffService is not supported. Use the VscodeDiffViewProvider.")
}
@@ -1,5 +0,0 @@
import { GetDocumentTextRequest, GetDocumentTextResponse } from "@/shared/proto/index.host"
export async function getDocumentText(_request: GetDocumentTextRequest): Promise<GetDocumentTextResponse> {
throw new Error("diffService is not supported. Use the VscodeDiffViewProvider.")
}
+1 -1
View File
@@ -1,5 +1,5 @@
import { OpenDiffRequest, OpenDiffResponse } from "@/shared/proto/index.host"
export async function openDiff(_request: OpenDiffRequest): Promise<OpenDiffResponse> {
throw new Error("diffService is not supported. Use the VscodeDiffViewProvider.")
throw new Error("diffService.openDiff is not supported. Use the VscodeDiffViewProvider.")
}
@@ -1,5 +1,5 @@
import { ReplaceTextRequest, ReplaceTextResponse } from "@/shared/proto/index.host"
export async function replaceText(_request: ReplaceTextRequest): Promise<ReplaceTextResponse> {
throw new Error("diffService is not supported. Use the VscodeDiffViewProvider.")
throw new Error("diffService.replaceText is not supported. Use the VscodeDiffViewProvider.")
}
@@ -1,5 +0,0 @@
import { SaveDocumentRequest, SaveDocumentResponse } from "@/shared/proto/index.host"
export async function saveDocument(_request: SaveDocumentRequest): Promise<SaveDocumentResponse> {
throw new Error("diffService is not supported. Use the VscodeDiffViewProvider.")
}
@@ -1,5 +0,0 @@
import { TruncateDocumentRequest, TruncateDocumentResponse } from "@/shared/proto/index.host"
export async function truncateDocument(_request: TruncateDocumentRequest): Promise<TruncateDocumentResponse> {
throw new Error("diffService is not supported. Use the VscodeDiffViewProvider.")
}
@@ -3,7 +3,7 @@ import { SelectedResponse, ShowMessageRequest, ShowMessageType } from "@/shared/
const DEFAULT_OPTIONS = { modal: false, items: [] } as const
export async function showMessage(request: ShowMessageRequest): Promise<SelectedResponse> {
export async function showMessage(request: ShowMessageRequest): Promise<SelectedResponse | undefined> {
const { message, type, options } = request
const { modal, detail, items } = { ...DEFAULT_OPTIONS, ...options }
const option = { modal, detail }
@@ -0,0 +1,75 @@
import * as vscode from "vscode"
interface DebugSession {
id: string
name: string
output: string[]
lastRetrievedIndex: number
}
export class DebugConsoleManager {
private sessions: Map<string, DebugSession> = new Map()
private disposables: vscode.Disposable[] = []
constructor() {
// Listen for debug session start events
this.disposables.push(
vscode.debug.onDidStartDebugSession((session) => {
this.sessions.set(session.id, {
id: session.id,
name: session.name,
output: [],
lastRetrievedIndex: -1,
})
}),
)
// Listen for debug session end events
this.disposables.push(
vscode.debug.onDidTerminateDebugSession((session) => {
this.sessions.delete(session.id)
}),
)
// Listen for debug console output
this.disposables.push(
vscode.debug.onDidReceiveDebugSessionCustomEvent((e: vscode.DebugSessionCustomEvent) => {
if (e.event === "output" && e.body?.output) {
const session = this.sessions.get(e.session.id)
if (session) {
session.output.push(e.body.output)
}
}
}),
)
}
/**
* Get all active debug sessions
*/
getActiveSessions(): { id: string; name: string }[] {
return Array.from(this.sessions.values()).map(({ id, name }) => ({ id, name }))
}
/**
* Get any new output since the last retrieval for a specific debug session
*/
getUnretrievedOutput(sessionId: string): string | undefined {
const session = this.sessions.get(sessionId)
if (!session) {
return undefined
}
const newOutput = session.output.slice(session.lastRetrievedIndex + 1).join("")
session.lastRetrievedIndex = session.output.length - 1
return newOutput || undefined
}
/**
* Clean up resources
*/
dispose() {
this.disposables.forEach((d) => d.dispose())
this.sessions.clear()
}
}
@@ -0,0 +1,123 @@
/*
import * as vscode from "vscode"
import deepEqual from "fast-deep-equal"
type FileDiagnostics = [vscode.Uri, vscode.Diagnostic[]][]
About Diagnostics:
The Problems tab shows diagnostics that have been reported for your project. These diagnostics are categorized into:
Errors: Critical issues that usually prevent your code from compiling or running correctly.
Warnings: Potential problems in the code that may not prevent it from running but could cause issues (e.g., bad practices, unused variables).
Information: Non-critical suggestions or tips (e.g., formatting issues or notes from linters).
The Problems tab displays diagnostics from various sources:
1. Language Servers:
- TypeScript: Type errors, missing imports, syntax issues
- Python: Syntax errors, invalid type hints, undefined variables
- JavaScript/Node.js: Parsing and execution errors
2. Linters:
- ESLint: Code style, best practices, potential bugs
- Pylint: Unused imports, naming conventions
- TSLint: Style and correctness issues in TypeScript
3. Build Tools:
- Webpack: Module resolution failures, build errors
- Gulp: Build errors during task execution
4. Custom Validators:
- Extensions can generate custom diagnostics for specific languages or tools
Each problem typically indicates its source (e.g., language server, linter, build tool).
Diagnostics update in real-time as you edit code, helping identify issues quickly. For example, if you introduce a syntax error in a TypeScript file, the Problems tab will immediately display the new error.
Notes on diagnostics:
- linter diagnostics are only captured for open editors
- this works great for us since when cline edits/creates files its through vscode's textedit api's and we get those diagnostics for free
- some tools might require you to save the file or manually refresh to clear the problem from the list.
System Prompt
- You will automatically receive workspace error diagnostics in environment_details. Be mindful that this may include issues beyond the scope of your task or the user's request. Only address errors relevant to your work, and avoid fixing pre-existing or unrelated issues unless the user specifically instructs you to do so.
- If you are unable to resolve errors provided in environment_details after two attempts, consider using ask_followup_question to ask the user for additional information, such as the latest documentation related to a problematic framework, to help you make progress on the task. If the error remains unresolved after this step, proceed with your task while disregarding the error.
class DiagnosticsMonitor {
private diagnosticsChangeEmitter: vscode.EventEmitter<void> = new vscode.EventEmitter<void>()
private disposables: vscode.Disposable[] = []
private lastDiagnostics: FileDiagnostics = []
constructor() {
this.disposables.push(
vscode.languages.onDidChangeDiagnostics(() => {
this.diagnosticsChangeEmitter.fire()
})
)
}
public async getCurrentDiagnostics(shouldWaitForChanges: boolean): Promise<FileDiagnostics> {
const currentDiagnostics = this.getDiagnostics()
if (!shouldWaitForChanges) {
this.lastDiagnostics = currentDiagnostics
return currentDiagnostics
}
if (!deepEqual(this.lastDiagnostics, currentDiagnostics)) {
this.lastDiagnostics = currentDiagnostics
return currentDiagnostics
}
let timeout = 300 // only way this happens is if there's no errors
// if diagnostics contain existing errors (since the check above didn't trigger) then it's likely cline just did something that should have fixed the error, so we'll give a longer grace period for diagnostics to catch up
const hasErrors = currentDiagnostics.some(([_, diagnostics]) =>
diagnostics.some((d) => d.severity === vscode.DiagnosticSeverity.Error)
)
if (hasErrors) {
console.log("Existing errors detected, extending timeout", currentDiagnostics)
timeout = 10_000
}
return this.waitForUpdatedDiagnostics(timeout)
}
private async waitForUpdatedDiagnostics(timeout: number): Promise<FileDiagnostics> {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
cleanup()
const finalDiagnostics = this.getDiagnostics()
this.lastDiagnostics = finalDiagnostics
resolve(finalDiagnostics)
}, timeout)
const disposable = this.diagnosticsChangeEmitter.event(() => {
const updatedDiagnostics = this.getDiagnostics() // I thought this would only trigger when diagnostics changed, but that's not the case.
if (deepEqual(this.lastDiagnostics, updatedDiagnostics)) {
// diagnostics have not changed, ignoring...
return
}
cleanup()
this.lastDiagnostics = updatedDiagnostics
resolve(updatedDiagnostics)
})
const cleanup = () => {
clearTimeout(timer)
disposable.dispose()
}
})
}
private getDiagnostics(): FileDiagnostics {
const allDiagnostics = vscode.languages.getDiagnostics()
return allDiagnostics
.filter(([_, diagnostics]) => diagnostics.some((d) => d.severity === vscode.DiagnosticSeverity.Error))
.map(([uri, diagnostics]) => [
uri,
diagnostics.filter((d) => d.severity === vscode.DiagnosticSeverity.Error),
])
}
public dispose() {
this.disposables.forEach((d) => d.dispose())
this.disposables = []
this.diagnosticsChangeEmitter.dispose()
}
}
export default DiagnosticsMonitor
*/
+148 -119
View File
@@ -4,10 +4,13 @@ import * as fs from "fs/promises"
import { createDirectoriesForFile } from "@utils/fs"
import { arePathsEqual, getCwd } from "@utils/path"
import { formatResponse } from "@core/prompts/responses"
import { DecorationController } from "./DecorationController"
import * as diff from "diff"
import { diagnosticsToProblemsString, getNewDiagnostics } from "../diagnostics"
import { detectEncoding } from "../misc/extract-text"
import * as iconv from "iconv-lite"
import { getHostBridgeProvider } from "@/hosts/host-providers"
import { ShowTextDocumentRequest, ShowTextDocumentOptions } from "@/shared/proto/host/window"
export const DIFF_VIEW_URI_SCHEME = "cline-diff"
@@ -23,6 +26,11 @@ export abstract class DiffViewProvider {
private streamedLines: string[] = []
private newContent?: string
protected activeDiffEditor?: vscode.TextEditor
protected fadedOverlayController?: DecorationController
protected activeLineController?: DecorationController
protected preDiagnostics: [vscode.Uri, vscode.Diagnostic[]][] = []
constructor() {}
public async open(relPath: string): Promise<void> {
@@ -54,13 +62,17 @@ export abstract class DiffViewProvider {
await fs.writeFile(this.absolutePath, "")
}
await this.openDiffEditor()
await this.scrollEditorToLine(0)
this.scrollEditorToLine(0) // will this crash for new files?
this.streamedLines = []
}
/**
* Opens a diff editor or viewer for the current file.
*
* This abstract method must be implemented by subclasses to create and display
* a diff editor or viewer that shows the difference between the original and
* modified content.
*
* Called automatically by the `open` method after ensuring the file exists and
* creating any necessary directories.
*
@@ -68,82 +80,13 @@ export abstract class DiffViewProvider {
*/
protected abstract openDiffEditor(): Promise<void>
/**
* Scrolls the diff editor to reveal a specific line.
*
* It's used during streaming updates to keep the user's view focused on the changing content.
*
* @param line The 0-based line number to scroll to
*/
protected abstract scrollEditorToLine(line: number): Promise<void>
/**
* Creates a smooth scrolling animation between two lines in the diff editor.
*
* It's typically used when updates contain many lines, to help the user visually track the flow
* of significant changes in the document.
*
* @param startLine The 0-based line number to begin the animation from
* @param endLine The 0-based line number to animate to
*/
protected abstract scrollAnimation(startLine: number, endLine: number): Promise<void>
/**
* Removes content from the specified line to the end of the document.
* Called after the final update is received.
*/
protected abstract truncateDocument(lineNumber: number): Promise<void>
/**
* Get the contents of the diff editor document.
*
* Returns undefined if the diff editor was closed.
*/
protected abstract getDocumentText(): Promise<string | undefined>
/**
* Get any new diagnostic problems that appeared after applying the diff.
*
* Getting diagnostics before and after the file edit is a better approach than
* automatically tracking problems in real-time. This method ensures we only
* report new problems that are a direct result of this specific edit.
* Since these are new problems resulting from Cline's edit, we know they're
* directly related to the work he's doing. This eliminates the risk of Cline
* going off-task or getting distracted by unrelated issues, which was a problem
* with the previous auto-debug approach. Some users' machines may be slow to
* update diagnostics, so this approach provides a good balance between automation
* and avoiding potential issues where Cline might get stuck in loops due to
* outdated problem information. If no new problems show up by the time the user
* accepts the changes, they can always debug later using the '@problems' mention.
* This way, Cline only becomes aware of new problems resulting from his edits
* and can address them accordingly. If problems don't change immediately after
* applying a fix, Cline won't be notified, which is generally fine since the
* initial fix is usually correct and it may just take time for linters to catch up.
*/
protected abstract getNewDiagnosticProblems(): Promise<string>
/**
* Save the contents of the diff editor UI to the file.
*/
protected abstract saveDocument(): Promise<void>
/**
* Closes the diff editor tab or window.
*/
protected abstract closeDiffView(): Promise<void>
/**
* Cleans up the diff view resources and resets internal state.
*/
protected abstract resetDiffView(): Promise<void>
async update(
accumulatedContent: string,
isFinal: boolean,
changeLocation?: { startLine: number; endLine: number; startChar: number; endChar: number },
) {
if (!this.isEditing) {
throw new Error("Not editing any file")
if (!this.relPath || !this.activeLineController || !this.fadedOverlayController) {
throw new Error("Required values not set")
}
// --- Fix to prevent duplicate BOM ---
@@ -161,6 +104,16 @@ export abstract class DiffViewProvider {
}
const diffLines = accumulatedLines.slice(this.streamedLines.length)
const diffEditor = this.activeDiffEditor
const document = diffEditor?.document
if (!diffEditor || !document) {
throw new Error("User closed text editor, unable to edit file...")
}
// Place cursor at the beginning of the diff editor to keep it out of the way of the stream animation
const beginningOfDocument = new vscode.Position(0, 0)
diffEditor.selection = new vscode.Selection(beginningOfDocument, beginningOfDocument)
// Instead of animating each line, we'll update in larger chunks
const currentLine = this.streamedLines.length + diffLines.length - 1
if (currentLine >= 0) {
@@ -176,19 +129,30 @@ export abstract class DiffViewProvider {
if (changeLocation) {
// We have the actual location of the change, scroll to it
const targetLine = changeLocation.startLine
await this.scrollEditorToLine(targetLine)
this.scrollEditorToLine(targetLine)
} else {
// Fallback to the old logic for non-replacement updates
if (diffLines.length <= 5) {
// For small changes, just jump directly to the line
await this.scrollEditorToLine(currentLine)
this.scrollEditorToLine(currentLine)
} else {
// For larger changes, create a quick scrolling animation
const startLine = this.streamedLines.length
const endLine = currentLine
await this.scrollAnimation(startLine, endLine)
const totalLines = endLine - startLine
const numSteps = 10 // Adjust this number to control animation speed
const stepSize = Math.max(1, Math.floor(totalLines / numSteps))
// Create and await the smooth scrolling animation
for (let line = startLine; line <= endLine; line += stepSize) {
this.activeDiffEditor?.revealRange(
new vscode.Range(line, 0, line, 0),
vscode.TextEditorRevealType.InCenter,
)
await new Promise((resolve) => setTimeout(resolve, 16)) // ~60fps
}
// Ensure we end at the final line
await this.scrollEditorToLine(currentLine)
this.scrollEditorToLine(currentLine)
}
}
}
@@ -197,8 +161,11 @@ export abstract class DiffViewProvider {
this.streamedLines = accumulatedLines
if (isFinal) {
// Handle any remaining lines if the new content is shorter than the original
await this.truncateDocument(this.streamedLines.length)
if (this.streamedLines.length < document.lineCount) {
const edit = new vscode.WorkspaceEdit()
edit.delete(document.uri, new vscode.Range(this.streamedLines.length, 0, document.lineCount, 0))
await vscode.workspace.applyEdit(edit)
}
// Add empty last line if original content had one
const hasEmptyLastLine = this.originalContent?.endsWith("\n")
if (hasEmptyLastLine) {
@@ -207,6 +174,9 @@ export abstract class DiffViewProvider {
accumulatedContent += "\n"
}
}
// Clear all decorations at the end (before applying final edit)
this.fadedOverlayController.clear()
this.activeLineController.clear()
}
}
@@ -225,7 +195,7 @@ export abstract class DiffViewProvider {
abstract replaceText(
content: string,
rangeToReplace: { startLine: number; endLine: number },
currentLine: number | undefined,
currentLine: number,
): Promise<void>
async saveChanges(): Promise<{
@@ -234,10 +204,7 @@ export abstract class DiffViewProvider {
autoFormattingEdits: string | undefined
finalContent: string | undefined
}> {
// get the contents before save operation which may do auto-formatting
const preSaveContent = await this.getDocumentText()
if (!this.relPath || !this.absolutePath || !this.newContent || preSaveContent === undefined) {
if (!this.relPath || !this.newContent || !this.activeDiffEditor) {
return {
newProblemsMessage: undefined,
userEdits: undefined,
@@ -245,21 +212,50 @@ export abstract class DiffViewProvider {
finalContent: undefined,
}
}
const updatedDocument = this.activeDiffEditor.document
// get the contents before save operation which may do auto-formatting
const preSaveContent = updatedDocument.getText()
if (updatedDocument.isDirty) {
await updatedDocument.save()
}
await this.saveDocument()
// get text after save in case there is any auto-formatting done by the editor
const postSaveContent = (await this.getDocumentText()) || ""
const postSaveContent = updatedDocument.getText()
await getHostBridgeProvider().windowClient.showTextDocument({
path: this.absolutePath,
options: {
preview: false,
preserveFocus: true,
},
})
await this.closeDiffView()
await getHostBridgeProvider().windowClient.showTextDocument(
ShowTextDocumentRequest.create({
path: this.absolutePath,
options: ShowTextDocumentOptions.create({
preview: false,
preserveFocus: true,
}),
}),
)
await this.closeAllDiffViews()
const newProblems = await this.getNewDiagnosticProblems()
/*
Getting diagnostics before and after the file edit is a better approach than
automatically tracking problems in real-time. This method ensures we only
report new problems that are a direct result of this specific edit.
Since these are new problems resulting from Cline's edit, we know they're
directly related to the work he's doing. This eliminates the risk of Cline
going off-task or getting distracted by unrelated issues, which was a problem
with the previous auto-debug approach. Some users' machines may be slow to
update diagnostics, so this approach provides a good balance between automation
and avoiding potential issues where Cline might get stuck in loops due to
outdated problem information. If no new problems show up by the time the user
accepts the changes, they can always debug later using the '@problems' mention.
This way, Cline only becomes aware of new problems resulting from his edits
and can address them accordingly. If problems don't change immediately after
applying a fix, Cline won't be notified, which is generally fine since the
initial fix is usually correct and it may just take time for linters to catch up.
*/
const postDiagnostics = vscode.languages.getDiagnostics()
const newProblems = await diagnosticsToProblemsString(getNewDiagnostics(this.preDiagnostics, postDiagnostics), [
vscode.DiagnosticSeverity.Error, // only including errors since warnings can be distracting (if user wants to fix warnings they can use the @problems mention)
]) // will be empty string if no errors
const newProblemsMessage =
newProblems.length > 0 ? `\n\nNew problems detected after saving the file:\n${newProblems}` : ""
@@ -299,14 +295,16 @@ export abstract class DiffViewProvider {
}
async revertChanges(): Promise<void> {
if (!this.absolutePath) {
if (!this.absolutePath || !this.activeDiffEditor) {
return
}
const fileExists = this.editType === "modify"
const updatedDocument = this.activeDiffEditor.document
if (!fileExists) {
await this.saveDocument()
await this.closeDiffView()
if (updatedDocument.isDirty) {
await updatedDocument.save()
}
await this.closeAllDiffViews()
await fs.unlink(this.absolutePath)
// Remove only the directories we created, in reverse order
for (let i = this.createdDirs.length - 1; i >= 0; i--) {
@@ -316,41 +314,70 @@ export abstract class DiffViewProvider {
console.log(`File ${this.absolutePath} has been deleted.`)
} else {
// revert document
// Apply the edit and save, since contents shouldn't have changed this won't show in local history unless of
// course the user made changes and saved during the edit.
const contents = (await this.getDocumentText()) || ""
const lineCount = (contents.match(/\n/g) || []).length + 1
await this.replaceText(this.originalContent ?? "", { startLine: 0, endLine: lineCount }, undefined)
await this.saveDocument()
const edit = new vscode.WorkspaceEdit()
const fullRange = new vscode.Range(
updatedDocument.positionAt(0),
updatedDocument.positionAt(updatedDocument.getText().length),
)
edit.replace(updatedDocument.uri, fullRange, this.originalContent ?? "")
// Apply the edit and save, since contents shouldn't have changed this won't show in local history unless of course the user made changes and saved during the edit
await vscode.workspace.applyEdit(edit)
await updatedDocument.save()
console.log(`File ${this.absolutePath} has been reverted to its original content.`)
if (this.documentWasOpen) {
await getHostBridgeProvider().windowClient.showTextDocument({
path: this.absolutePath,
options: {
preview: false,
preserveFocus: true,
},
})
await getHostBridgeProvider().windowClient.showTextDocument(
ShowTextDocumentRequest.create({
path: this.absolutePath,
options: ShowTextDocumentOptions.create({
preview: false,
preserveFocus: true,
}),
}),
)
}
await this.closeDiffView()
await this.closeAllDiffViews()
}
// edit is done
await this.reset()
}
async scrollToFirstDiff() {
if (!this.isEditing) {
private async closeAllDiffViews() {
const tabs = vscode.window.tabGroups.all
.flatMap((tg) => tg.tabs)
.filter((tab) => tab.input instanceof vscode.TabInputTextDiff && tab.input?.original?.scheme === DIFF_VIEW_URI_SCHEME)
for (const tab of tabs) {
// trying to close dirty views results in save popup
if (!tab.isDirty) {
await vscode.window.tabGroups.close(tab)
}
}
}
private scrollEditorToLine(line: number) {
if (this.activeDiffEditor) {
const scrollLine = line + 4
this.activeDiffEditor.revealRange(
new vscode.Range(scrollLine, 0, scrollLine, 0),
vscode.TextEditorRevealType.InCenter,
)
}
}
scrollToFirstDiff() {
if (!this.activeDiffEditor) {
return
}
const currentContent = (await this.getDocumentText()) || ""
const currentContent = this.activeDiffEditor.document.getText()
const diffs = diff.diffLines(this.originalContent || "", currentContent)
let lineCount = 0
for (const part of diffs) {
if (part.added || part.removed) {
// Found the first diff, scroll to it
this.scrollEditorToLine(lineCount)
this.activeDiffEditor.revealRange(
new vscode.Range(lineCount, 0, lineCount, 0),
vscode.TextEditorRevealType.InCenter,
)
return
}
if (!part.removed) {
@@ -366,8 +393,10 @@ export abstract class DiffViewProvider {
this.originalContent = undefined
this.createdDirs = []
this.documentWasOpen = false
this.activeDiffEditor = undefined
this.fadedOverlayController = undefined
this.activeLineController = undefined
this.streamedLines = []
await this.resetDiffView()
this.preDiagnostics = []
}
}
@@ -59,10 +59,12 @@ export function extractCommitMessage(aiResponse: string): string {
*/
export async function copyCommitMessageToClipboard(message: string): Promise<void> {
await writeTextToClipboard(message)
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.INFORMATION,
message: "Commit message copied to clipboard",
})
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.INFORMATION,
message: "Commit message copied to clipboard",
}),
)
}
/**
@@ -75,16 +77,18 @@ export async function showCommitMessageOptions(message: string): Promise<void> {
const editAction = "Edit Message"
const selectedAction = (
await getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.INFORMATION,
message: "Commit message generated",
options: {
modal: false,
detail: message,
items: [copyAction, applyAction, editAction],
},
})
).selectedOption
await getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.INFORMATION,
message: "Commit message generated",
options: {
modal: false,
detail: message,
items: [copyAction, applyAction, editAction],
},
}),
)
)?.selectedOption
// Handle user dismissing the dialog (selectedAction is undefined)
if (!selectedAction) {
@@ -116,22 +120,28 @@ async function applyCommitMessageToGitInput(message: string): Promise<void> {
if (api && api.repositories.length > 0) {
const repo = api.repositories[0]
repo.inputBox.value = message
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.INFORMATION,
message: "Commit message applied to Git input",
})
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.INFORMATION,
message: "Commit message applied to Git input",
}),
)
} else {
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.ERROR,
message: "No Git repositories found",
})
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.ERROR,
message: "No Git repositories found",
}),
)
await copyCommitMessageToClipboard(message)
}
} else {
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.ERROR,
message: "Git extension not found",
})
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.ERROR,
message: "Git extension not found",
}),
)
await copyCommitMessageToClipboard(message)
}
}
@@ -151,8 +161,10 @@ async function editCommitMessage(message: string): Promise<void> {
path: document.uri.fsPath,
}),
)
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.INFORMATION,
message: "Edit the commit message and copy when ready",
})
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.INFORMATION,
message: "Edit the commit message and copy when ready",
}),
)
}
+6 -4
View File
@@ -48,10 +48,12 @@ export async function downloadTask(dateTs: number, conversationHistory: Anthropi
}),
)
} catch (error) {
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.ERROR,
message: `Failed to save markdown file: ${error instanceof Error ? error.message : String(error)}`,
})
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.ERROR,
message: `Failed to save markdown file: ${error instanceof Error ? error.message : String(error)}`,
}),
)
}
}
}
+13 -9
View File
@@ -3,16 +3,18 @@ import * as os from "os"
import * as vscode from "vscode"
import { arePathsEqual } from "@utils/path"
import { getHostBridgeProvider } from "@/hosts/host-providers"
import { ShowMessageRequest, ShowMessageType } from "@/shared/proto/host/window"
import { ShowTextDocumentRequest, ShowTextDocumentOptions, ShowMessageRequest, ShowMessageType } from "@/shared/proto/host/window"
import { writeFile } from "@utils/fs"
export async function openImage(dataUri: string) {
const matches = dataUri.match(/^data:image\/([a-zA-Z]+);base64,(.+)$/)
if (!matches) {
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.ERROR,
message: "Invalid data URI format",
})
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.ERROR,
message: "Invalid data URI format",
}),
)
return
}
const [, format, base64Data] = matches
@@ -22,10 +24,12 @@ export async function openImage(dataUri: string) {
await writeFile(tempFilePath, new Uint8Array(imageBuffer))
await vscode.commands.executeCommand("vscode.open", vscode.Uri.file(tempFilePath))
} catch (error) {
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.ERROR,
message: `Error opening image: ${error}`,
})
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.ERROR,
message: `Error opening image: ${error}`,
}),
)
}
}
+24 -16
View File
@@ -46,18 +46,22 @@ export async function selectFiles(imagesAllowed: boolean): Promise<{ images: str
const dimensions = sizeOf(uint8Array) // Get dimensions from Uint8Array
if (dimensions.width! > 7500 || dimensions.height! > 7500) {
console.warn(`Image dimensions exceed 7500px, skipping: ${filePath}`)
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.ERROR,
message: `Image too large: ${path.basename(filePath)} was skipped (dimensions exceed 7500px).`,
})
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.ERROR,
message: `Image too large: ${path.basename(filePath)} was skipped (dimensions exceed 7500px).`,
}),
)
return null
}
} catch (error) {
console.error(`Error reading file or getting dimensions for ${filePath}:`, error)
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.ERROR,
message: `Could not read dimensions for ${path.basename(filePath)}, skipping.`,
})
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.ERROR,
message: `Could not read dimensions for ${path.basename(filePath)}, skipping.`,
}),
)
return null
}
@@ -72,18 +76,22 @@ export async function selectFiles(imagesAllowed: boolean): Promise<{ images: str
const stats = await fs.stat(filePath)
if (stats.size > 20 * 1000 * 1024) {
console.warn(`File too large, skipping: ${filePath}`)
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.ERROR,
message: `File too large: ${path.basename(filePath)} was skipped (size exceeds 20MB).`,
})
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.ERROR,
message: `File too large: ${path.basename(filePath)} was skipped (size exceeds 20MB).`,
}),
)
return null
}
} catch (error) {
console.error(`Error checking file size for ${filePath}:`, error)
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.ERROR,
message: `Could not check file size for ${path.basename(filePath)}, skipping.`,
})
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.ERROR,
message: `Could not check file size for ${path.basename(filePath)}, skipping.`,
}),
)
return null
}
return { type: "file", data: filePath }
-157
View File
@@ -1,157 +0,0 @@
import { CLINE_ACCOUNT_AUTH_ERROR_MESSAGE } from "../../shared/ClineAccount"
import { serializeError } from "serialize-error"
export enum ClineErrorType {
Auth = "auth",
Network = "network",
RateLimit = "rateLimit",
Balance = "balance",
}
interface ErrorDetails {
/**
* The HTTP status code of the error, if applicable.
*/
status?: number
/**
* The request ID associated with the error, if available.
* This can be useful for debugging and support.
*/
request_id?: string
/**
* Specific error code provided by the API or service.
*/
code?: string
/**
* The model ID associated with the error, if applicable.
* This is useful for identifying which model the error relates to.
*/
modelId?: string
/**
* The provider ID associated with the error, if applicable.
* This is useful for identifying which provider the error relates to.
*/
providerId?: string
/**
* The error message associated with the error, if applicable.
*/
message?: string
// Additional details that might be present in the error
// This can include things like current balance, error messages, etc.
details?: any
}
const RATE_LIMIT_PATTERNS = [/status code 429/i, /rate limit/i, /too many requests/i, /quota exceeded/i, /resource exhausted/i]
export class ClineError extends Error {
readonly title = "ClineError"
readonly _error: ErrorDetails
// Error details per providers:
// Cline: error?.error
// Ollama: error?.cause
// tbc
constructor(
raw: any,
public readonly modelId?: string,
public readonly providerId?: string,
) {
const error = serializeError(raw)
const message = error.message || String(error) || error?.cause?.means
super(message)
// Extract status from multiple possible locations
const status = error.status || error.statusCode || error.response?.status
// Construct the error details object to includes relevant information
// And ensure it has a consistent structure
this._error = {
message: raw.message || message,
status,
request_id: error.request_id || error.response?.request_id,
code: error.code || error?.cause?.code,
modelId,
providerId,
details: error.details || error.error, // Additional details provided by the server
...error,
stack: undefined, // Avoid serializing stack trace to keep the error object clean
}
}
/**
* Serializes the error to a JSON string that allows for easy transmission and storage.
* This is useful for logging or sending error details to a webviews.
*/
public serialize(): string {
return JSON.stringify({
message: this.message,
status: this._error.status,
request_id: this._error.request_id,
code: this._error.code,
modelId: this.modelId,
providerId: this.providerId,
details: this._error.details,
})
}
/**
* Parses a stringified error into a ClineError instance.
*/
static parse(errorStr?: string, modelId?: string): ClineError | undefined {
if (!errorStr || typeof errorStr !== "string") {
return undefined
}
return ClineError.transform(errorStr, modelId)
}
/**
* Transforms any object into a ClineError instance.
* Always returns a ClineError, even if the input is not a valid error object.
*/
static transform(error: any, modelId?: string, providerId?: string): ClineError {
try {
return new ClineError(JSON.parse(error), modelId, providerId)
} catch {
return new ClineError(error, modelId, providerId)
}
}
public isErrorType(type: ClineErrorType): boolean {
return ClineError.getErrorType(this) === type
}
/**
* Is known error type based on the error code, status, and details.
* This is useful for determining how to handle the error in the UI or logic.
*/
static getErrorType(err: ClineError): ClineErrorType | undefined {
const { code, status, details } = err._error
// Check balance error first (most specific)
if (code === "insufficient_credits" && typeof details?.current_balance === "number") {
return ClineErrorType.Balance
}
// Check auth errors
if (code === "ERR_BAD_REQUEST" || status === 401) {
return ClineErrorType.Auth
}
// Check for auth message (only if message exists)
const message = err.message
if (message?.includes(CLINE_ACCOUNT_AUTH_ERROR_MESSAGE)) {
return ClineErrorType.Auth
}
// Check rate limit patterns
if (message) {
const lowerMessage = message.toLowerCase()
if (RATE_LIMIT_PATTERNS.some((pattern) => pattern.test(lowerMessage))) {
return ClineErrorType.RateLimit
}
}
return undefined
}
}
+3 -10
View File
@@ -1,8 +1,7 @@
import * as Sentry from "@sentry/browser"
import * as vscode from "vscode"
import { telemetryService } from "../posthog/telemetry/TelemetryService"
import { telemetryService } from "@/services/posthog/telemetry/TelemetryService"
import * as pkg from "../../../package.json"
import { ClineError } from "./ClineError"
let telemetryLevel = vscode.workspace.getConfiguration("telemetry").get<string>("telemetryLevel", "all")
let isTelemetryEnabled = ["all", "error"].includes(telemetryLevel)
@@ -16,8 +15,6 @@ vscode.workspace.onDidChangeConfiguration(() => {
}
})
const isDev = process.env.IS_DEV === "true"
export class ErrorService {
private static serviceEnabled: boolean
private static serviceLevel: string
@@ -32,7 +29,7 @@ export class ErrorService {
beforeSend(event) {
// TelemetryService keeps track of whether the user has opted in to telemetry/error reporting
const isUserManuallyOptedIn = telemetryService.isTelemetryEnabled()
if (isUserManuallyOptedIn && ErrorService.isEnabled() && !isDev) {
if (isUserManuallyOptedIn && ErrorService.isEnabled()) {
return event
}
return null
@@ -68,7 +65,7 @@ export class ErrorService {
}
}
static logException(error: Error | ClineError): void {
static logException(error: Error): void {
// Don't log if telemetry is off
const isUserManuallyOptedIn = telemetryService.isTelemetryEnabled()
if (!isUserManuallyOptedIn || !ErrorService.isEnabled()) {
@@ -96,8 +93,4 @@ export class ErrorService {
static isEnabled(): boolean {
return ErrorService.serviceEnabled
}
static toClineError(rawError: any, modelId?: string, providerId?: string): ClineError {
return ClineError.transform(rawError, modelId, providerId)
}
}
+71 -90
View File
@@ -35,10 +35,10 @@ import { secondsToMs } from "@utils/time"
import { GlobalFileNames } from "@core/storage/disk"
import { ExtensionMessage } from "@shared/ExtensionMessage"
import { DEFAULT_REQUEST_TIMEOUT_MS } from "./constants"
import { McpConnection, McpServerConfig, Transport } from "./types"
import { McpConnection, McpServerConfig } from "./types"
import { BaseConfigSchema, ServerConfigSchema, McpSettingsSchema } from "./schemas"
import { getHostBridgeProvider } from "@/hosts/host-providers"
import { ShowMessageType } from "@/shared/proto/host/window"
import { ShowMessageRequest, ShowMessageType } from "@/shared/proto/host/window"
export class McpHub {
getMcpServersPath: () => Promise<string>
private getSettingsDirectoryPath: () => Promise<string>
@@ -109,20 +109,24 @@ export class McpHub {
try {
config = JSON.parse(content)
} catch (error) {
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.ERROR,
message: "Invalid MCP settings format. Please ensure your settings follow the correct JSON format.",
})
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.ERROR,
message: "Invalid MCP settings format. Please ensure your settings follow the correct JSON format.",
}),
)
return undefined
}
// Validate against schema
const result = McpSettingsSchema.safeParse(config)
if (!result.success) {
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.ERROR,
message: "Invalid MCP settings schema.",
})
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.ERROR,
message: "Invalid MCP settings schema.",
}),
)
return undefined
}
@@ -154,6 +158,12 @@ export class McpHub {
if (settings) {
try {
await this.updateServerConnections(settings.mcpServers)
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.INFORMATION,
message: "MCP servers updated",
}),
)
} catch (error) {
console.error("Failed to process MCP settings change:", error)
}
@@ -192,23 +202,6 @@ export class McpHub {
// Remove existing connection if it exists (should never happen, the connection should be deleted beforehand)
this.connections = this.connections.filter((conn) => conn.server.name !== name)
if (config.disabled) {
console.log(`[MCP Debug] Creating disabled connection object for server "${name}"`)
// Create a connection object for disabled server so it appears in UI
const disabledConnection: McpConnection = {
server: {
name,
config: JSON.stringify(config),
status: "disconnected",
disabled: true,
},
client: null as unknown as Client,
transport: null as unknown as Transport,
}
this.connections.push(disabledConnection)
return
}
try {
// Each MCP server requires its own transport connection and has unique capabilities, configurations, and error handling. Having separate clients also allows proper scoping of resources/tools and independent server management like reconnection.
const client = new Client(
@@ -420,10 +413,12 @@ export class McpHub {
console.log(`[MCP Fallback Notification] ${name}:`, JSON.stringify(notification, null, 2))
// Show in VS Code for visibility
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.INFORMATION,
message: `MCP ${name}: ${notification.method || "unknown"} - ${JSON.stringify(notification.params || {})}`,
})
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.INFORMATION,
message: `MCP ${name}: ${notification.method || "unknown"} - ${JSON.stringify(notification.params || {})}`,
}),
)
}
console.log(`[MCP Debug] Successfully set fallback notification handler for ${name}`)
} catch (error) {
@@ -458,11 +453,6 @@ export class McpHub {
throw new Error(`No connection found for server: ${serverName}`)
}
// Disabled servers don't have clients, so return empty tools list
if (connection.server.disabled || !connection.client) {
return []
}
const response = await connection.client.request({ method: "tools/list" }, ListToolsResultSchema, {
timeout: DEFAULT_REQUEST_TIMEOUT_MS,
})
@@ -488,16 +478,9 @@ export class McpHub {
private async fetchResourcesList(serverName: string): Promise<McpResource[]> {
try {
const connection = this.connections.find((conn) => conn.server.name === serverName)
// Disabled servers don't have clients, so return empty resources list
if (!connection || connection.server.disabled || !connection.client) {
return []
}
const response = await connection.client.request({ method: "resources/list" }, ListResourcesResultSchema, {
timeout: DEFAULT_REQUEST_TIMEOUT_MS,
})
const response = await this.connections
.find((conn) => conn.server.name === serverName)
?.client.request({ method: "resources/list" }, ListResourcesResultSchema, { timeout: DEFAULT_REQUEST_TIMEOUT_MS })
return response?.resources || []
} catch (error) {
// console.error(`Failed to fetch resources for ${serverName}:`, error)
@@ -507,20 +490,11 @@ export class McpHub {
private async fetchResourceTemplatesList(serverName: string): Promise<McpResourceTemplate[]> {
try {
const connection = this.connections.find((conn) => conn.server.name === serverName)
// Disabled servers don't have clients, so return empty resource templates list
if (!connection || connection.server.disabled || !connection.client) {
return []
}
const response = await connection.client.request(
{ method: "resources/templates/list" },
ListResourceTemplatesResultSchema,
{
const response = await this.connections
.find((conn) => conn.server.name === serverName)
?.client.request({ method: "resources/templates/list" }, ListResourceTemplatesResultSchema, {
timeout: DEFAULT_REQUEST_TIMEOUT_MS,
},
)
})
return response?.resourceTemplates || []
} catch (error) {
@@ -533,13 +507,8 @@ export class McpHub {
const connection = this.connections.find((conn) => conn.server.name === name)
if (connection) {
try {
// Only close transport and client if they exist (disabled servers don't have them)
if (connection.transport) {
await connection.transport.close()
}
if (connection.client) {
await connection.client.close()
}
await connection.transport.close()
await connection.client.close()
} catch (error) {
console.error(`Failed to close transport for ${name}:`, error)
}
@@ -702,10 +671,12 @@ export class McpHub {
const connection = this.connections.find((conn) => conn.server.name === serverName)
const config = connection?.server.config
if (config) {
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.INFORMATION,
message: `Restarting ${serverName} MCP server...`,
})
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.INFORMATION,
message: `Restarting ${serverName} MCP server...`,
}),
)
connection.server.status = "connecting"
connection.server.error = ""
await this.notifyWebviewOfServerChanges()
@@ -714,16 +685,20 @@ export class McpHub {
await this.deleteConnection(serverName)
// Try to connect again using existing config
await this.connectToServer(serverName, JSON.parse(config), "internal")
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.INFORMATION,
message: `${serverName} MCP server connected`,
})
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.INFORMATION,
message: `${serverName} MCP server connected`,
}),
)
} catch (error) {
console.error(`Failed to restart connection for ${serverName}:`, error)
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.ERROR,
message: `Failed to connect to ${serverName} MCP server`,
})
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.ERROR,
message: `Failed to connect to ${serverName} MCP server`,
}),
)
}
}
@@ -809,10 +784,12 @@ export class McpHub {
if (error instanceof Error) {
console.error("Error details:", error.message, error.stack)
}
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.ERROR,
message: `Failed to update server state: ${error instanceof Error ? error.message : String(error)}`,
})
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.ERROR,
message: `Failed to update server state: ${error instanceof Error ? error.message : String(error)}`,
}),
)
throw error
}
}
@@ -969,10 +946,12 @@ export class McpHub {
}
} catch (error) {
console.error("Failed to update autoApprove settings:", error)
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.ERROR,
message: "Failed to update autoApprove settings",
})
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.ERROR,
message: "Failed to update autoApprove settings",
}),
)
throw error // Re-throw to ensure the error is properly handled
}
}
@@ -1090,10 +1069,12 @@ export class McpHub {
if (error instanceof Error) {
console.error("Error details:", error.message, error.stack)
}
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.ERROR,
message: `Failed to update server timeout: ${error instanceof Error ? error.message : String(error)}`,
})
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.ERROR,
message: `Failed to update server timeout: ${error instanceof Error ? error.message : String(error)}`,
}),
)
throw error
}
}
@@ -1,5 +1,5 @@
import { PostHog } from "posthog-node"
import { posthogConfig } from "../../shared/services/config/posthog-config"
import { posthogConfig } from "@/shared/services/config/posthog-config"
class PostHogClientProvider {
private static instance: PostHogClientProvider
@@ -5,7 +5,6 @@ import { version as extensionVersion } from "../../../../package.json"
import type { TaskFeedbackType } from "@shared/WebviewMessage"
import type { BrowserSettings } from "@shared/BrowserSettings"
import { posthogClientProvider } from "../PostHogClientProvider"
import { Mode } from "@/shared/ChatSettings"
/**
* TelemetryService handles telemetry event tracking for the Cline extension
@@ -313,7 +312,7 @@ class TelemetryService {
* @param taskId Unique identifier for the task
* @param mode The mode being switched to (plan or act)
*/
public captureModeSwitch(taskId: string, mode: Mode) {
public captureModeSwitch(taskId: string, mode: "plan" | "act") {
this.capture({
event: TelemetryService.EVENTS.TASK.MODE_SWITCH,
properties: {
@@ -535,7 +534,7 @@ class TelemetryService {
* @param qty The quantity of options that were presented
* @param mode The mode in which the option was selected ("plan" or "act")
*/
public captureOptionSelected(taskId: string, qty: number, mode: Mode) {
public captureOptionSelected(taskId: string, qty: number, mode: "plan" | "act") {
this.capture({
event: TelemetryService.EVENTS.TASK.OPTION_SELECTED,
properties: {
@@ -552,7 +551,7 @@ class TelemetryService {
* @param qty The quantity of options that were presented
* @param mode The mode in which the custom response was provided ("plan" or "act")
*/
public captureOptionsIgnored(taskId: string, qty: number, mode: Mode) {
public captureOptionsIgnored(taskId: string, qty: number, mode: "plan" | "act") {
this.capture({
event: TelemetryService.EVENTS.TASK.OPTIONS_IGNORED,
properties: {
+1 -2
View File
@@ -274,8 +274,7 @@ export function createTestServer(webviewProvider?: WebviewProvider): http.Server
await updateApiConfiguration(visibleWebview.controller.context, updatedConfig)
// Update global state to use cline provider
await updateGlobalState(visibleWebview.controller.context, "planModeApiProvider", "cline")
await updateGlobalState(visibleWebview.controller.context, "actModeApiProvider", "cline")
await updateGlobalState(visibleWebview.controller.context, "apiProvider", "cline" as ApiProvider)
// Post state to webview to reflect changes
await visibleWebview.controller.postStateToWebview()
+1 -3
View File
@@ -1,9 +1,7 @@
export type OpenAIReasoningEffort = "low" | "medium" | "high"
export type Mode = "plan" | "act"
export interface ChatSettings {
mode: Mode
mode: "plan" | "act"
preferredLanguage?: string
openAIReasoningEffort?: OpenAIReasoningEffort
}
+1 -1
View File
@@ -7,7 +7,7 @@ import { HistoryItem } from "./HistoryItem"
import { TelemetrySetting } from "./TelemetrySetting"
import { ClineRulesToggles } from "./cline-rules"
import { UserInfo } from "./UserInfo"
import { McpDisplayMode } from "./McpDisplayMode"
import { McpDisplayMode, DEFAULT_MCP_DISPLAY_MODE } from "./McpDisplayMode"
// webview will hold state
export interface ExtensionMessage {
+22 -97
View File
@@ -29,19 +29,22 @@ export type ApiProvider =
| "cerebras"
| "sapaicore"
| "groq"
| "huggingface"
export interface ApiHandlerOptions {
// Global configuration (not mode-specific)
apiModelId?: string
apiKey?: string // anthropic
clineAccountId?: string
taskId?: string // Used to identify the task in API requests
liteLlmBaseUrl?: string
liteLlmModelId?: string
liteLlmApiKey?: string
liteLlmUsePromptCache?: boolean
openAiHeaders?: Record<string, string> // Custom headers for OpenAI requests
liteLlmModelInfo?: LiteLLMModelInfo
anthropicBaseUrl?: string
openRouterApiKey?: string
openRouterModelId?: string
openRouterModelInfo?: ModelInfo
openRouterProviderSorting?: string
awsAccessKey?: string
awsSecretKey?: string
@@ -54,99 +57,64 @@ export interface ApiHandlerOptions {
awsProfile?: string
awsBedrockApiKey?: string
awsBedrockEndpoint?: string
awsBedrockCustomSelected?: boolean
awsBedrockCustomModelBaseId?: BedrockModelId
claudeCodePath?: string
vertexProjectId?: string
vertexRegion?: string
openAiBaseUrl?: string
openAiApiKey?: string
openAiModelId?: string
openAiModelInfo?: OpenAiCompatibleModelInfo
ollamaModelId?: string
ollamaBaseUrl?: string
ollamaApiOptionsCtxNum?: string
lmStudioModelId?: string
lmStudioBaseUrl?: string
geminiApiKey?: string
geminiBaseUrl?: string
openAiNativeApiKey?: string
deepSeekApiKey?: string
requestyApiKey?: string
requestyModelId?: string
requestyModelInfo?: ModelInfo
togetherApiKey?: string
togetherModelId?: string
fireworksApiKey?: string
fireworksModelId?: string
fireworksModelMaxCompletionTokens?: number
fireworksModelMaxTokens?: number
qwenApiKey?: string
doubaoApiKey?: string
mistralApiKey?: string
azureApiVersion?: string
vsCodeLmModelSelector?: LanguageModelChatSelector
qwenApiLine?: string
moonshotApiLine?: string
moonshotApiKey?: string
huggingFaceApiKey?: string
nebiusApiKey?: string
asksageApiUrl?: string
asksageApiKey?: string
xaiApiKey?: string
thinkingBudgetTokens?: number
reasoningEffort?: string
sambanovaApiKey?: string
cerebrasApiKey?: string
groqApiKey?: string
groqModelId?: string
groqModelInfo?: ModelInfo
requestTimeoutMs?: number
sapAiCoreClientId?: string
sapAiCoreClientSecret?: string
sapAiResourceGroup?: string
sapAiCoreTokenUrl?: string
sapAiCoreBaseUrl?: string
sapAiCoreModelId?: string
onRetryAttempt?: (attempt: number, maxRetries: number, delay: number, error: any) => void
// Plan mode configurations
planModeApiModelId?: string
planModeThinkingBudgetTokens?: number
planModeReasoningEffort?: string
planModeVsCodeLmModelSelector?: LanguageModelChatSelector
planModeAwsBedrockCustomSelected?: boolean
planModeAwsBedrockCustomModelBaseId?: BedrockModelId
planModeOpenRouterModelId?: string
planModeOpenRouterModelInfo?: ModelInfo
planModeOpenAiModelId?: string
planModeOpenAiModelInfo?: OpenAiCompatibleModelInfo
planModeOllamaModelId?: string
planModeLmStudioModelId?: string
planModeLiteLlmModelId?: string
planModeLiteLlmModelInfo?: LiteLLMModelInfo
planModeRequestyModelId?: string
planModeRequestyModelInfo?: ModelInfo
planModeTogetherModelId?: string
planModeFireworksModelId?: string
planModeSapAiCoreModelId?: string
planModeGroqModelId?: string
planModeGroqModelInfo?: ModelInfo
planModeHuggingFaceModelId?: string
planModeHuggingFaceModelInfo?: ModelInfo
// Act mode configurations
actModeApiModelId?: string
actModeThinkingBudgetTokens?: number
actModeReasoningEffort?: string
actModeVsCodeLmModelSelector?: LanguageModelChatSelector
actModeAwsBedrockCustomSelected?: boolean
actModeAwsBedrockCustomModelBaseId?: BedrockModelId
actModeOpenRouterModelId?: string
actModeOpenRouterModelInfo?: ModelInfo
actModeOpenAiModelId?: string
actModeOpenAiModelInfo?: OpenAiCompatibleModelInfo
actModeOllamaModelId?: string
actModeLmStudioModelId?: string
actModeLiteLlmModelId?: string
actModeLiteLlmModelInfo?: LiteLLMModelInfo
actModeRequestyModelId?: string
actModeRequestyModelInfo?: ModelInfo
actModeTogetherModelId?: string
actModeFireworksModelId?: string
actModeSapAiCoreModelId?: string
actModeGroqModelId?: string
actModeGroqModelInfo?: ModelInfo
actModeHuggingFaceModelId?: string
actModeHuggingFaceModelInfo?: ModelInfo
}
export type ApiConfiguration = ApiHandlerOptions & {
planModeApiProvider?: ApiProvider
actModeApiProvider?: ApiProvider
apiProvider?: ApiProvider
favoritedModelIds?: string[]
}
@@ -1106,49 +1074,6 @@ export const deepSeekModels = {
},
} as const satisfies Record<string, ModelInfo>
// Hugging Face Inference Providers
// https://huggingface.co/docs/inference-providers/en/index
export type HuggingFaceModelId = keyof typeof huggingFaceModels
export const huggingFaceDefaultModelId: HuggingFaceModelId = "moonshotai/Kimi-K2-Instruct"
export const huggingFaceModels = {
"moonshotai/Kimi-K2-Instruct": {
maxTokens: 131_072,
contextWindow: 131_072,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
description: "Advanced reasoning model with superior performance across coding, math, and general capabilities.",
},
"deepseek-ai/DeepSeek-V3-0324": {
maxTokens: 8192,
contextWindow: 64_000,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
description: "Advanced reasoning model with superior performance across coding, math, and general capabilities.",
},
"deepseek-ai/DeepSeek-R1": {
maxTokens: 8192,
contextWindow: 64_000,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
description: "DeepSeek's reasoning model with step-by-step thinking capabilities.",
},
"meta-llama/Llama-3.1-8B-Instruct": {
maxTokens: 8192,
contextWindow: 128_000,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
description: "Efficient 8B parameter Llama model for general-purpose tasks.",
},
} as const satisfies Record<string, ModelInfo>
// Qwen
// https://bailian.console.aliyun.com/
export type MainlandQwenModelId = keyof typeof mainlandQwenModels
@@ -224,8 +224,6 @@ function convertApiProviderToProto(provider: string | undefined): ProtoApiProvid
return ProtoApiProvider.LITELLM
case "moonshot":
return ProtoApiProvider.MOONSHOT
case "huggingface":
return ProtoApiProvider.HUGGINGFACE
case "nebius":
return ProtoApiProvider.NEBIUS
case "fireworks":
@@ -290,8 +288,6 @@ function convertProtoToApiProvider(provider: ProtoApiProvider): ApiProvider {
return "litellm"
case ProtoApiProvider.MOONSHOT:
return "moonshot"
case ProtoApiProvider.HUGGINGFACE:
return "huggingface"
case ProtoApiProvider.NEBIUS:
return "nebius"
case ProtoApiProvider.FIREWORKS:
@@ -318,16 +314,20 @@ function convertProtoToApiProvider(provider: ProtoApiProvider): ApiProvider {
// Converts application ApiConfiguration to proto ApiConfiguration
export function convertApiConfigurationToProto(config: ApiConfiguration): ProtoApiConfiguration {
return {
// Global configuration fields
apiModelId: config.apiModelId,
apiKey: config.apiKey,
clineAccountId: config.clineAccountId,
taskId: config.taskId,
liteLlmBaseUrl: config.liteLlmBaseUrl,
liteLlmModelId: config.liteLlmModelId,
liteLlmApiKey: config.liteLlmApiKey,
liteLlmUsePromptCache: config.liteLlmUsePromptCache,
openAiHeaders: config.openAiHeaders || {},
liteLlmModelInfo: convertLiteLLMModelInfoToProto(config.liteLlmModelInfo),
anthropicBaseUrl: config.anthropicBaseUrl,
openRouterApiKey: config.openRouterApiKey,
openRouterModelId: config.openRouterModelId,
openRouterModelInfo: convertModelInfoToProtoOpenRouter(config.openRouterModelInfo),
openRouterProviderSorting: config.openRouterProviderSorting,
awsAccessKey: config.awsAccessKey,
awsSecretKey: config.awsSecretKey,
@@ -340,115 +340,80 @@ export function convertApiConfigurationToProto(config: ApiConfiguration): ProtoA
awsProfile: config.awsProfile,
awsBedrockApiKey: config.awsBedrockApiKey,
awsBedrockEndpoint: config.awsBedrockEndpoint,
claudeCodePath: config.claudeCodePath,
awsBedrockCustomSelected: config.awsBedrockCustomSelected,
awsBedrockCustomModelBaseId: config.awsBedrockCustomModelBaseId as string | undefined,
vertexProjectId: config.vertexProjectId,
vertexRegion: config.vertexRegion,
openAiBaseUrl: config.openAiBaseUrl,
openAiApiKey: config.openAiApiKey,
openAiModelId: config.openAiModelId,
openAiModelInfo: convertOpenAiCompatibleModelInfoToProto(config.openAiModelInfo),
ollamaModelId: config.ollamaModelId,
ollamaBaseUrl: config.ollamaBaseUrl,
ollamaApiOptionsCtxNum: config.ollamaApiOptionsCtxNum,
lmStudioModelId: config.lmStudioModelId,
lmStudioBaseUrl: config.lmStudioBaseUrl,
geminiApiKey: config.geminiApiKey,
geminiBaseUrl: config.geminiBaseUrl,
openAiNativeApiKey: config.openAiNativeApiKey,
deepSeekApiKey: config.deepSeekApiKey,
requestyApiKey: config.requestyApiKey,
requestyModelId: config.requestyModelId,
requestyModelInfo: convertModelInfoToProtoOpenRouter(config.requestyModelInfo),
togetherApiKey: config.togetherApiKey,
togetherModelId: config.togetherModelId,
fireworksApiKey: config.fireworksApiKey,
fireworksModelId: config.fireworksModelId,
fireworksModelMaxCompletionTokens: config.fireworksModelMaxCompletionTokens,
fireworksModelMaxTokens: config.fireworksModelMaxTokens,
qwenApiKey: config.qwenApiKey,
doubaoApiKey: config.doubaoApiKey,
mistralApiKey: config.mistralApiKey,
azureApiVersion: config.azureApiVersion,
vsCodeLmModelSelector: config.vsCodeLmModelSelector,
qwenApiLine: config.qwenApiLine,
moonshotApiLine: config.moonshotApiLine,
moonshotApiKey: config.moonshotApiKey,
huggingFaceApiKey: config.huggingFaceApiKey,
nebiusApiKey: config.nebiusApiKey,
asksageApiUrl: config.asksageApiUrl,
asksageApiKey: config.asksageApiKey,
xaiApiKey: config.xaiApiKey,
thinkingBudgetTokens: config.thinkingBudgetTokens,
reasoningEffort: config.reasoningEffort,
sambanovaApiKey: config.sambanovaApiKey,
cerebrasApiKey: config.cerebrasApiKey,
groqApiKey: config.groqApiKey,
groqModelId: config.groqModelId,
groqModelInfo: convertModelInfoToProtoOpenRouter(config.groqModelInfo),
requestTimeoutMs: config.requestTimeoutMs,
apiProvider: config.apiProvider ? convertApiProviderToProto(config.apiProvider) : undefined,
favoritedModelIds: config.favoritedModelIds || [],
sapAiCoreClientId: config.sapAiCoreClientId,
sapAiCoreClientSecret: config.sapAiCoreClientSecret,
sapAiResourceGroup: config.sapAiResourceGroup,
sapAiCoreTokenUrl: config.sapAiCoreTokenUrl,
sapAiCoreBaseUrl: config.sapAiCoreBaseUrl,
// Plan mode configurations
planModeApiProvider: config.planModeApiProvider ? convertApiProviderToProto(config.planModeApiProvider) : undefined,
planModeApiModelId: config.planModeApiModelId,
planModeThinkingBudgetTokens: config.planModeThinkingBudgetTokens,
planModeReasoningEffort: config.planModeReasoningEffort,
planModeVsCodeLmModelSelector: config.planModeVsCodeLmModelSelector,
planModeAwsBedrockCustomSelected: config.planModeAwsBedrockCustomSelected,
planModeAwsBedrockCustomModelBaseId: config.planModeAwsBedrockCustomModelBaseId as string | undefined,
planModeOpenRouterModelId: config.planModeOpenRouterModelId,
planModeOpenRouterModelInfo: convertModelInfoToProtoOpenRouter(config.planModeOpenRouterModelInfo),
planModeOpenAiModelId: config.planModeOpenAiModelId,
planModeOpenAiModelInfo: convertOpenAiCompatibleModelInfoToProto(config.planModeOpenAiModelInfo),
planModeOllamaModelId: config.planModeOllamaModelId,
planModeLmStudioModelId: config.planModeLmStudioModelId,
planModeLiteLlmModelId: config.planModeLiteLlmModelId,
planModeLiteLlmModelInfo: convertLiteLLMModelInfoToProto(config.planModeLiteLlmModelInfo),
planModeRequestyModelId: config.planModeRequestyModelId,
planModeRequestyModelInfo: convertModelInfoToProtoOpenRouter(config.planModeRequestyModelInfo),
planModeTogetherModelId: config.planModeTogetherModelId,
planModeFireworksModelId: config.planModeFireworksModelId,
planModeGroqModelId: config.planModeGroqModelId,
planModeGroqModelInfo: convertModelInfoToProtoOpenRouter(config.planModeGroqModelInfo),
planModeHuggingFaceModelId: config.planModeHuggingFaceModelId,
planModeHuggingFaceModelInfo: convertModelInfoToProtoOpenRouter(config.planModeHuggingFaceModelInfo),
planModeSapAiCoreModelId: config.planModeSapAiCoreModelId,
// Act mode configurations
actModeApiProvider: config.actModeApiProvider ? convertApiProviderToProto(config.actModeApiProvider) : undefined,
actModeApiModelId: config.actModeApiModelId,
actModeThinkingBudgetTokens: config.actModeThinkingBudgetTokens,
actModeReasoningEffort: config.actModeReasoningEffort,
actModeVsCodeLmModelSelector: config.actModeVsCodeLmModelSelector,
actModeAwsBedrockCustomSelected: config.actModeAwsBedrockCustomSelected,
actModeAwsBedrockCustomModelBaseId: config.actModeAwsBedrockCustomModelBaseId as string | undefined,
actModeOpenRouterModelId: config.actModeOpenRouterModelId,
actModeOpenRouterModelInfo: convertModelInfoToProtoOpenRouter(config.actModeOpenRouterModelInfo),
actModeOpenAiModelId: config.actModeOpenAiModelId,
actModeOpenAiModelInfo: convertOpenAiCompatibleModelInfoToProto(config.actModeOpenAiModelInfo),
actModeOllamaModelId: config.actModeOllamaModelId,
actModeLmStudioModelId: config.actModeLmStudioModelId,
actModeLiteLlmModelId: config.actModeLiteLlmModelId,
actModeLiteLlmModelInfo: convertLiteLLMModelInfoToProto(config.actModeLiteLlmModelInfo),
actModeRequestyModelId: config.actModeRequestyModelId,
actModeRequestyModelInfo: convertModelInfoToProtoOpenRouter(config.actModeRequestyModelInfo),
actModeTogetherModelId: config.actModeTogetherModelId,
actModeFireworksModelId: config.actModeFireworksModelId,
actModeGroqModelId: config.actModeGroqModelId,
actModeGroqModelInfo: convertModelInfoToProtoOpenRouter(config.actModeGroqModelInfo),
actModeHuggingFaceModelId: config.actModeHuggingFaceModelId,
actModeHuggingFaceModelInfo: convertModelInfoToProtoOpenRouter(config.actModeHuggingFaceModelInfo),
actModeSapAiCoreModelId: config.actModeSapAiCoreModelId,
// Favorited model IDs
favoritedModelIds: config.favoritedModelIds || [],
claudeCodePath: config.claudeCodePath,
}
}
// Converts proto ApiConfiguration to application ApiConfiguration
export function convertProtoToApiConfiguration(protoConfig: ProtoApiConfiguration): ApiConfiguration {
return {
// Global configuration fields
apiModelId: protoConfig.apiModelId,
apiKey: protoConfig.apiKey,
clineAccountId: protoConfig.clineAccountId,
taskId: protoConfig.taskId,
liteLlmBaseUrl: protoConfig.liteLlmBaseUrl,
liteLlmModelId: protoConfig.liteLlmModelId,
liteLlmApiKey: protoConfig.liteLlmApiKey,
liteLlmUsePromptCache: protoConfig.liteLlmUsePromptCache,
openAiHeaders: Object.keys(protoConfig.openAiHeaders || {}).length > 0 ? protoConfig.openAiHeaders : undefined,
openAiHeaders: Object.keys(protoConfig.openAiHeaders).length > 0 ? protoConfig.openAiHeaders : undefined,
liteLlmModelInfo: convertProtoToLiteLLMModelInfo(protoConfig.liteLlmModelInfo),
anthropicBaseUrl: protoConfig.anthropicBaseUrl,
openRouterApiKey: protoConfig.openRouterApiKey,
openRouterModelId: protoConfig.openRouterModelId,
openRouterModelInfo: convertProtoToModelInfo(protoConfig.openRouterModelInfo),
openRouterProviderSorting: protoConfig.openRouterProviderSorting,
awsAccessKey: protoConfig.awsAccessKey,
awsSecretKey: protoConfig.awsSecretKey,
@@ -461,103 +426,59 @@ export function convertProtoToApiConfiguration(protoConfig: ProtoApiConfiguratio
awsProfile: protoConfig.awsProfile,
awsBedrockApiKey: protoConfig.awsBedrockApiKey,
awsBedrockEndpoint: protoConfig.awsBedrockEndpoint,
claudeCodePath: protoConfig.claudeCodePath,
awsBedrockCustomSelected: protoConfig.awsBedrockCustomSelected,
awsBedrockCustomModelBaseId: protoConfig.awsBedrockCustomModelBaseId as BedrockModelId | undefined,
vertexProjectId: protoConfig.vertexProjectId,
vertexRegion: protoConfig.vertexRegion,
openAiBaseUrl: protoConfig.openAiBaseUrl,
openAiApiKey: protoConfig.openAiApiKey,
openAiModelId: protoConfig.openAiModelId,
openAiModelInfo: convertProtoToOpenAiCompatibleModelInfo(protoConfig.openAiModelInfo),
ollamaModelId: protoConfig.ollamaModelId,
ollamaBaseUrl: protoConfig.ollamaBaseUrl,
ollamaApiOptionsCtxNum: protoConfig.ollamaApiOptionsCtxNum,
lmStudioModelId: protoConfig.lmStudioModelId,
lmStudioBaseUrl: protoConfig.lmStudioBaseUrl,
geminiApiKey: protoConfig.geminiApiKey,
geminiBaseUrl: protoConfig.geminiBaseUrl,
openAiNativeApiKey: protoConfig.openAiNativeApiKey,
deepSeekApiKey: protoConfig.deepSeekApiKey,
requestyApiKey: protoConfig.requestyApiKey,
requestyModelId: protoConfig.requestyModelId,
requestyModelInfo: convertProtoToModelInfo(protoConfig.requestyModelInfo),
togetherApiKey: protoConfig.togetherApiKey,
togetherModelId: protoConfig.togetherModelId,
fireworksApiKey: protoConfig.fireworksApiKey,
fireworksModelId: protoConfig.fireworksModelId,
fireworksModelMaxCompletionTokens: protoConfig.fireworksModelMaxCompletionTokens,
fireworksModelMaxTokens: protoConfig.fireworksModelMaxTokens,
qwenApiKey: protoConfig.qwenApiKey,
doubaoApiKey: protoConfig.doubaoApiKey,
mistralApiKey: protoConfig.mistralApiKey,
azureApiVersion: protoConfig.azureApiVersion,
vsCodeLmModelSelector: protoConfig.vsCodeLmModelSelector,
qwenApiLine: protoConfig.qwenApiLine,
moonshotApiLine: protoConfig.moonshotApiLine,
moonshotApiKey: protoConfig.moonshotApiKey,
huggingFaceApiKey: protoConfig.huggingFaceApiKey,
nebiusApiKey: protoConfig.nebiusApiKey,
asksageApiUrl: protoConfig.asksageApiUrl,
asksageApiKey: protoConfig.asksageApiKey,
xaiApiKey: protoConfig.xaiApiKey,
thinkingBudgetTokens: protoConfig.thinkingBudgetTokens,
reasoningEffort: protoConfig.reasoningEffort,
sambanovaApiKey: protoConfig.sambanovaApiKey,
cerebrasApiKey: protoConfig.cerebrasApiKey,
groqApiKey: protoConfig.groqApiKey,
groqModelId: protoConfig.groqModelId,
groqModelInfo: convertProtoToModelInfo(protoConfig.groqModelInfo),
requestTimeoutMs: protoConfig.requestTimeoutMs,
apiProvider: protoConfig.apiProvider !== undefined ? convertProtoToApiProvider(protoConfig.apiProvider) : undefined,
favoritedModelIds: protoConfig.favoritedModelIds.length > 0 ? protoConfig.favoritedModelIds : undefined,
sapAiCoreClientId: protoConfig.sapAiCoreClientId,
sapAiCoreClientSecret: protoConfig.sapAiCoreClientSecret,
sapAiResourceGroup: protoConfig.sapAiResourceGroup,
sapAiCoreTokenUrl: protoConfig.sapAiCoreTokenUrl,
sapAiCoreBaseUrl: protoConfig.sapAiCoreBaseUrl,
// Plan mode configurations
planModeApiProvider:
protoConfig.planModeApiProvider !== undefined
? convertProtoToApiProvider(protoConfig.planModeApiProvider)
: undefined,
planModeApiModelId: protoConfig.planModeApiModelId,
planModeThinkingBudgetTokens: protoConfig.planModeThinkingBudgetTokens,
planModeReasoningEffort: protoConfig.planModeReasoningEffort,
planModeVsCodeLmModelSelector: protoConfig.planModeVsCodeLmModelSelector,
planModeAwsBedrockCustomSelected: protoConfig.planModeAwsBedrockCustomSelected,
planModeAwsBedrockCustomModelBaseId: protoConfig.planModeAwsBedrockCustomModelBaseId as BedrockModelId | undefined,
planModeOpenRouterModelId: protoConfig.planModeOpenRouterModelId,
planModeOpenRouterModelInfo: convertProtoToModelInfo(protoConfig.planModeOpenRouterModelInfo),
planModeOpenAiModelId: protoConfig.planModeOpenAiModelId,
planModeOpenAiModelInfo: convertProtoToOpenAiCompatibleModelInfo(protoConfig.planModeOpenAiModelInfo),
planModeOllamaModelId: protoConfig.planModeOllamaModelId,
planModeLmStudioModelId: protoConfig.planModeLmStudioModelId,
planModeLiteLlmModelId: protoConfig.planModeLiteLlmModelId,
planModeLiteLlmModelInfo: convertProtoToLiteLLMModelInfo(protoConfig.planModeLiteLlmModelInfo),
planModeRequestyModelId: protoConfig.planModeRequestyModelId,
planModeRequestyModelInfo: convertProtoToModelInfo(protoConfig.planModeRequestyModelInfo),
planModeTogetherModelId: protoConfig.planModeTogetherModelId,
planModeFireworksModelId: protoConfig.planModeFireworksModelId,
planModeGroqModelId: protoConfig.planModeGroqModelId,
planModeGroqModelInfo: convertProtoToModelInfo(protoConfig.planModeGroqModelInfo),
planModeHuggingFaceModelId: protoConfig.planModeHuggingFaceModelId,
planModeHuggingFaceModelInfo: convertProtoToModelInfo(protoConfig.planModeHuggingFaceModelInfo),
planModeSapAiCoreModelId: protoConfig.planModeSapAiCoreModelId,
// Act mode configurations
actModeApiProvider:
protoConfig.actModeApiProvider !== undefined ? convertProtoToApiProvider(protoConfig.actModeApiProvider) : undefined,
actModeApiModelId: protoConfig.actModeApiModelId,
actModeThinkingBudgetTokens: protoConfig.actModeThinkingBudgetTokens,
actModeReasoningEffort: protoConfig.actModeReasoningEffort,
actModeVsCodeLmModelSelector: protoConfig.actModeVsCodeLmModelSelector,
actModeAwsBedrockCustomSelected: protoConfig.actModeAwsBedrockCustomSelected,
actModeAwsBedrockCustomModelBaseId: protoConfig.actModeAwsBedrockCustomModelBaseId as BedrockModelId | undefined,
actModeOpenRouterModelId: protoConfig.actModeOpenRouterModelId,
actModeOpenRouterModelInfo: convertProtoToModelInfo(protoConfig.actModeOpenRouterModelInfo),
actModeOpenAiModelId: protoConfig.actModeOpenAiModelId,
actModeOpenAiModelInfo: convertProtoToOpenAiCompatibleModelInfo(protoConfig.actModeOpenAiModelInfo),
actModeOllamaModelId: protoConfig.actModeOllamaModelId,
actModeLmStudioModelId: protoConfig.actModeLmStudioModelId,
actModeLiteLlmModelId: protoConfig.actModeLiteLlmModelId,
actModeLiteLlmModelInfo: convertProtoToLiteLLMModelInfo(protoConfig.actModeLiteLlmModelInfo),
actModeRequestyModelId: protoConfig.actModeRequestyModelId,
actModeRequestyModelInfo: convertProtoToModelInfo(protoConfig.actModeRequestyModelInfo),
actModeTogetherModelId: protoConfig.actModeTogetherModelId,
actModeFireworksModelId: protoConfig.actModeFireworksModelId,
actModeGroqModelId: protoConfig.actModeGroqModelId,
actModeGroqModelInfo: convertProtoToModelInfo(protoConfig.actModeGroqModelInfo),
actModeHuggingFaceModelId: protoConfig.actModeHuggingFaceModelId,
actModeHuggingFaceModelInfo: convertProtoToModelInfo(protoConfig.actModeHuggingFaceModelInfo),
actModeSapAiCoreModelId: protoConfig.actModeSapAiCoreModelId,
// Favorited model IDs
favoritedModelIds:
protoConfig.favoritedModelIds && protoConfig.favoritedModelIds.length > 0 ? protoConfig.favoritedModelIds : undefined,
claudeCodePath: protoConfig.claudeCodePath,
}
}
@@ -11,17 +11,49 @@ import {
*/
export function convertApiConfigurationToProtoApiConfiguration(config: ApiConfiguration): ProtoApiConfiguration {
return ProtoApiConfiguration.create({
// Global configuration fields (not mode-specific)
// Core API fields
apiProvider: config.apiProvider,
apiModelId: config.apiModelId,
apiKey: config.apiKey,
// Provider-specific API keys
clineAccountId: config.clineAccountId,
taskId: config.taskId,
liteLlmBaseUrl: config.liteLlmBaseUrl,
liteLlmApiKey: config.liteLlmApiKey,
liteLlmUsePromptCache: config.liteLlmUsePromptCache,
openaiHeaders: config.openAiHeaders ? JSON.stringify(config.openAiHeaders) : undefined,
anthropicBaseUrl: config.anthropicBaseUrl,
openrouterApiKey: config.openRouterApiKey,
openrouterProviderSorting: config.openRouterProviderSorting,
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,
moonshotApiKey: config.moonshotApiKey,
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,
@@ -33,101 +65,68 @@ export function convertApiConfigurationToProtoApiConfiguration(config: ApiConfig
awsProfile: config.awsProfile,
awsBedrockApiKey: config.awsBedrockApiKey,
awsBedrockEndpoint: config.awsBedrockEndpoint,
claudeCodePath: config.claudeCodePath,
// Vertex AI fields
vertexProjectId: config.vertexProjectId,
vertexRegion: config.vertexRegion,
// Base URLs and endpoints
openaiBaseUrl: config.openAiBaseUrl,
openaiApiKey: config.openAiApiKey,
ollamaBaseUrl: config.ollamaBaseUrl,
ollamaApiOptionsCtxNum: config.ollamaApiOptionsCtxNum,
lmStudioBaseUrl: config.lmStudioBaseUrl,
geminiApiKey: config.geminiApiKey,
geminiBaseUrl: config.geminiBaseUrl,
openaiNativeApiKey: config.openAiNativeApiKey,
deepSeekApiKey: config.deepSeekApiKey,
requestyApiKey: config.requestyApiKey,
togetherApiKey: config.togetherApiKey,
fireworksApiKey: config.fireworksApiKey,
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,
qwenApiKey: config.qwenApiKey,
doubaoApiKey: config.doubaoApiKey,
mistralApiKey: config.mistralApiKey,
moonshotApiKey: config.moonshotApiKey,
// Azure specific
azureApiVersion: config.azureApiVersion,
// Ollama specific
ollamaApiOptionsCtxNum: config.ollamaApiOptionsCtxNum,
// Qwen specific
qwenApiLine: config.qwenApiLine,
nebiusApiKey: config.nebiusApiKey,
asksageApiUrl: config.asksageApiUrl,
asksageApiKey: config.asksageApiKey,
xaiApiKey: config.xaiApiKey,
sambanovaApiKey: config.sambanovaApiKey,
cerebrasApiKey: config.cerebrasApiKey,
requestTimeoutMs: config.requestTimeoutMs ? Number(config.requestTimeoutMs) : undefined,
// Moonshot specific
moonshotApiLine: config.moonshotApiLine,
// OpenRouter specific
openrouterProviderSorting: config.openRouterProviderSorting,
// SAP AI Core specific
sapAiCoreClientId: config.sapAiCoreClientId,
sapAiCoreClientSecret: config.sapAiCoreClientSecret,
sapAiResourceGroup: config.sapAiResourceGroup,
sapAiCoreTokenUrl: config.sapAiCoreTokenUrl,
sapAiCoreBaseUrl: config.sapAiCoreBaseUrl,
sapAiCoreTokenUrl: config.sapAiCoreTokenUrl,
sapAiResourceGroup: config.sapAiResourceGroup,
// Plan mode configurations
planModeApiProvider: config.planModeApiProvider,
planModeApiModelId: config.planModeApiModelId,
planModeThinkingBudgetTokens: config.planModeThinkingBudgetTokens
? Number(config.planModeThinkingBudgetTokens)
: undefined,
planModeReasoningEffort: config.planModeReasoningEffort,
planModeVscodeLmModelSelector: config.planModeVsCodeLmModelSelector
? JSON.stringify(config.planModeVsCodeLmModelSelector)
: undefined,
planModeAwsBedrockCustomSelected: config.planModeAwsBedrockCustomSelected,
planModeAwsBedrockCustomModelBaseId: config.planModeAwsBedrockCustomModelBaseId,
planModeOpenrouterModelId: config.planModeOpenRouterModelId,
planModeOpenrouterModelInfo: config.planModeOpenRouterModelInfo
? JSON.stringify(config.planModeOpenRouterModelInfo)
: undefined,
planModeOpenaiModelId: config.planModeOpenAiModelId,
planModeOpenaiModelInfo: config.planModeOpenAiModelInfo ? JSON.stringify(config.planModeOpenAiModelInfo) : undefined,
planModeOllamaModelId: config.planModeOllamaModelId,
planModeLmStudioModelId: config.planModeLmStudioModelId,
planModeLiteLlmModelId: config.planModeLiteLlmModelId,
planModeLiteLlmModelInfo: config.planModeLiteLlmModelInfo ? JSON.stringify(config.planModeLiteLlmModelInfo) : undefined,
planModeRequestyModelId: config.planModeRequestyModelId,
planModeRequestyModelInfo: config.planModeRequestyModelInfo
? JSON.stringify(config.planModeRequestyModelInfo)
: undefined,
planModeTogetherModelId: config.planModeTogetherModelId,
planModeFireworksModelId: config.planModeFireworksModelId,
planModeSapAiCoreModelId: config.planModeSapAiCoreModelId,
// 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,
// Act mode configurations
actModeApiProvider: config.actModeApiProvider,
actModeApiModelId: config.actModeApiModelId,
actModeThinkingBudgetTokens: config.actModeThinkingBudgetTokens ? Number(config.actModeThinkingBudgetTokens) : undefined,
actModeReasoningEffort: config.actModeReasoningEffort,
actModeVscodeLmModelSelector: config.actModeVsCodeLmModelSelector
? JSON.stringify(config.actModeVsCodeLmModelSelector)
: undefined,
actModeAwsBedrockCustomSelected: config.actModeAwsBedrockCustomSelected,
actModeAwsBedrockCustomModelBaseId: config.actModeAwsBedrockCustomModelBaseId,
actModeOpenrouterModelId: config.actModeOpenRouterModelId,
actModeOpenrouterModelInfo: config.actModeOpenRouterModelInfo
? JSON.stringify(config.actModeOpenRouterModelInfo)
: undefined,
actModeOpenaiModelId: config.actModeOpenAiModelId,
actModeOpenaiModelInfo: config.actModeOpenAiModelInfo ? JSON.stringify(config.actModeOpenAiModelInfo) : undefined,
actModeOllamaModelId: config.actModeOllamaModelId,
actModeLmStudioModelId: config.actModeLmStudioModelId,
actModeLiteLlmModelId: config.actModeLiteLlmModelId,
actModeLiteLlmModelInfo: config.actModeLiteLlmModelInfo ? JSON.stringify(config.actModeLiteLlmModelInfo) : undefined,
actModeRequestyModelId: config.actModeRequestyModelId,
actModeRequestyModelInfo: config.actModeRequestyModelInfo ? JSON.stringify(config.actModeRequestyModelInfo) : undefined,
actModeTogetherModelId: config.actModeTogetherModelId,
actModeFireworksModelId: config.actModeFireworksModelId,
actModeSapAiCoreModelId: config.actModeSapAiCoreModelId,
// Claude Code specific
claudeCodePath: config.claudeCodePath,
// Favorited model IDs
// Arrays
favoritedModelIds: config.favoritedModelIds || [],
})
}
@@ -138,16 +137,45 @@ export function convertApiConfigurationToProtoApiConfiguration(config: ApiConfig
export function convertProtoApiConfigurationToApiConfiguration(protoConfig: ProtoApiConfiguration): ApiConfiguration {
// eslint-disable-next-line eslint-rules/no-protobuf-object-literals
const config: ApiConfiguration = {
// Global configuration fields (not mode-specific)
// Core API fields
apiProvider: protoConfig.apiProvider as ApiProvider,
apiModelId: protoConfig.apiModelId,
apiKey: protoConfig.apiKey,
// Provider-specific API keys
clineAccountId: protoConfig.clineAccountId,
taskId: protoConfig.taskId,
liteLlmBaseUrl: protoConfig.liteLlmBaseUrl,
liteLlmApiKey: protoConfig.liteLlmApiKey,
liteLlmUsePromptCache: protoConfig.liteLlmUsePromptCache,
anthropicBaseUrl: protoConfig.anthropicBaseUrl,
openRouterApiKey: protoConfig.openrouterApiKey,
openRouterProviderSorting: protoConfig.openrouterProviderSorting,
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,
moonshotApiKey: protoConfig.moonshotApiKey,
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 BedrockModelId | undefined,
awsAccessKey: protoConfig.awsAccessKey,
awsSecretKey: protoConfig.awsSecretKey,
awsSessionToken: protoConfig.awsSessionToken,
@@ -159,121 +187,83 @@ export function convertProtoApiConfigurationToApiConfiguration(protoConfig: Prot
awsAuthentication: protoConfig.awsAuthentication,
awsBedrockApiKey: protoConfig.awsBedrockApiKey,
awsBedrockEndpoint: protoConfig.awsBedrockEndpoint,
claudeCodePath: protoConfig.claudeCodePath,
// Vertex AI fields
vertexProjectId: protoConfig.vertexProjectId,
vertexRegion: protoConfig.vertexRegion,
// Base URLs and endpoints
openAiBaseUrl: protoConfig.openaiBaseUrl,
openAiApiKey: protoConfig.openaiApiKey,
ollamaBaseUrl: protoConfig.ollamaBaseUrl,
ollamaApiOptionsCtxNum: protoConfig.ollamaApiOptionsCtxNum,
lmStudioBaseUrl: protoConfig.lmStudioBaseUrl,
geminiApiKey: protoConfig.geminiApiKey,
geminiBaseUrl: protoConfig.geminiBaseUrl,
openAiNativeApiKey: protoConfig.openaiNativeApiKey,
deepSeekApiKey: protoConfig.deepSeekApiKey,
requestyApiKey: protoConfig.requestyApiKey,
togetherApiKey: protoConfig.togetherApiKey,
fireworksApiKey: protoConfig.fireworksApiKey,
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,
qwenApiKey: protoConfig.qwenApiKey,
doubaoApiKey: protoConfig.doubaoApiKey,
mistralApiKey: protoConfig.mistralApiKey,
moonshotApiKey: protoConfig.moonshotApiKey,
// Azure specific
azureApiVersion: protoConfig.azureApiVersion,
// Ollama specific
ollamaApiOptionsCtxNum: protoConfig.ollamaApiOptionsCtxNum,
// Qwen specific
qwenApiLine: protoConfig.qwenApiLine,
nebiusApiKey: protoConfig.nebiusApiKey,
asksageApiUrl: protoConfig.asksageApiUrl,
asksageApiKey: protoConfig.asksageApiKey,
xaiApiKey: protoConfig.xaiApiKey,
sambanovaApiKey: protoConfig.sambanovaApiKey,
cerebrasApiKey: protoConfig.cerebrasApiKey,
requestTimeoutMs: protoConfig.requestTimeoutMs ? Number(protoConfig.requestTimeoutMs) : undefined,
// Moonshot specific
moonshotApiLine: protoConfig.moonshotApiLine,
// OpenRouter specific
openRouterProviderSorting: protoConfig.openrouterProviderSorting,
// SAP AI Core specific
sapAiCoreClientId: protoConfig.sapAiCoreClientId,
sapAiCoreClientSecret: protoConfig.sapAiCoreClientSecret,
sapAiResourceGroup: protoConfig.sapAiResourceGroup,
sapAiCoreTokenUrl: protoConfig.sapAiCoreTokenUrl,
sapAiCoreBaseUrl: protoConfig.sapAiCoreBaseUrl,
sapAiCoreTokenUrl: protoConfig.sapAiCoreTokenUrl,
sapAiResourceGroup: protoConfig.sapAiResourceGroup,
// Plan mode configurations
planModeApiProvider: protoConfig.planModeApiProvider as ApiProvider,
planModeApiModelId: protoConfig.planModeApiModelId,
planModeThinkingBudgetTokens: protoConfig.planModeThinkingBudgetTokens
? Number(protoConfig.planModeThinkingBudgetTokens)
: undefined,
planModeReasoningEffort: protoConfig.planModeReasoningEffort,
planModeAwsBedrockCustomSelected: protoConfig.planModeAwsBedrockCustomSelected,
planModeAwsBedrockCustomModelBaseId: protoConfig.planModeAwsBedrockCustomModelBaseId as BedrockModelId | undefined,
planModeOpenRouterModelId: protoConfig.planModeOpenrouterModelId,
planModeOpenAiModelId: protoConfig.planModeOpenaiModelId,
planModeOllamaModelId: protoConfig.planModeOllamaModelId,
planModeLmStudioModelId: protoConfig.planModeLmStudioModelId,
planModeLiteLlmModelId: protoConfig.planModeLiteLlmModelId,
planModeRequestyModelId: protoConfig.planModeRequestyModelId,
planModeTogetherModelId: protoConfig.planModeTogetherModelId,
planModeFireworksModelId: protoConfig.planModeFireworksModelId,
planModeSapAiCoreModelId: protoConfig.planModeSapAiCoreModelId,
// Claude Code specific
claudeCodePath: protoConfig.claudeCodePath,
// Act mode configurations
actModeApiProvider: protoConfig.actModeApiProvider as ApiProvider,
actModeApiModelId: protoConfig.actModeApiModelId,
actModeThinkingBudgetTokens: protoConfig.actModeThinkingBudgetTokens
? Number(protoConfig.actModeThinkingBudgetTokens)
: undefined,
actModeReasoningEffort: protoConfig.actModeReasoningEffort,
actModeAwsBedrockCustomSelected: protoConfig.actModeAwsBedrockCustomSelected,
actModeAwsBedrockCustomModelBaseId: protoConfig.actModeAwsBedrockCustomModelBaseId as BedrockModelId | undefined,
actModeOpenRouterModelId: protoConfig.actModeOpenrouterModelId,
actModeOpenAiModelId: protoConfig.actModeOpenaiModelId,
actModeOllamaModelId: protoConfig.actModeOllamaModelId,
actModeLmStudioModelId: protoConfig.actModeLmStudioModelId,
actModeLiteLlmModelId: protoConfig.actModeLiteLlmModelId,
actModeRequestyModelId: protoConfig.actModeRequestyModelId,
actModeTogetherModelId: protoConfig.actModeTogetherModelId,
actModeFireworksModelId: protoConfig.actModeFireworksModelId,
actModeSapAiCoreModelId: protoConfig.actModeSapAiCoreModelId,
// Favorited model IDs
// 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)
}
if (protoConfig.planModeVscodeLmModelSelector) {
config.planModeVsCodeLmModelSelector = JSON.parse(protoConfig.planModeVscodeLmModelSelector)
}
if (protoConfig.planModeOpenrouterModelInfo) {
config.planModeOpenRouterModelInfo = JSON.parse(protoConfig.planModeOpenrouterModelInfo)
}
if (protoConfig.planModeOpenaiModelInfo) {
config.planModeOpenAiModelInfo = JSON.parse(protoConfig.planModeOpenaiModelInfo)
}
if (protoConfig.planModeLiteLlmModelInfo) {
config.planModeLiteLlmModelInfo = JSON.parse(protoConfig.planModeLiteLlmModelInfo)
}
if (protoConfig.planModeRequestyModelInfo) {
config.planModeRequestyModelInfo = JSON.parse(protoConfig.planModeRequestyModelInfo)
}
if (protoConfig.actModeVscodeLmModelSelector) {
config.actModeVsCodeLmModelSelector = JSON.parse(protoConfig.actModeVscodeLmModelSelector)
}
if (protoConfig.actModeOpenrouterModelInfo) {
config.actModeOpenRouterModelInfo = JSON.parse(protoConfig.actModeOpenrouterModelInfo)
}
if (protoConfig.actModeOpenaiModelInfo) {
config.actModeOpenAiModelInfo = JSON.parse(protoConfig.actModeOpenaiModelInfo)
}
if (protoConfig.actModeLiteLlmModelInfo) {
config.actModeLiteLlmModelInfo = JSON.parse(protoConfig.actModeLiteLlmModelInfo)
}
if (protoConfig.actModeRequestyModelInfo) {
config.actModeRequestyModelInfo = JSON.parse(protoConfig.actModeRequestyModelInfo)
}
} catch (error) {
console.error("Failed to parse complex JSON objects in API configuration:", error)
}
@@ -0,0 +1,28 @@
import { getHostBridgeProvider } from "@/hosts/host-providers"
import { DiffViewProvider } from "@/integrations/editor/DiffViewProvider"
export class ExternalDiffViewProvider extends DiffViewProvider {
private activeDiffEditorId: string | undefined
override async openDiffEditor(): Promise<void> {
if (!this.absolutePath) {
return
}
const response = await getHostBridgeProvider().diffClient.openDiff({
path: this.absolutePath,
content: this.originalContent ?? "",
})
this.activeDiffEditorId = response.diffId
}
override async replaceText(
content: string,
rangeToReplace: { startLine: number; endLine: number },
_currentLine: number,
): Promise<void> {
await getHostBridgeProvider().diffClient.replaceText({
diffId: this.activeDiffEditorId,
content: content,
startLine: rangeToReplace.startLine,
endLine: rangeToReplace.endLine,
})
}
}
@@ -4,6 +4,11 @@ import * as vscode from "vscode"
import { URI } from "vscode-uri"
import { WebviewProvider } from "@core/webview"
/*
https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
https://github.com/KumarVariable/vscode-extension-sidebar-html/blob/master/src/customSidebarViewProvider.ts
*/
export class ExternalWebviewProvider extends WebviewProvider {
// This hostname cannot be changed without updating the external webview handler.
private RESOURCE_HOSTNAME: string = "internal.resources"
+8 -8
View File
@@ -2,18 +2,18 @@ import * as grpc from "@grpc/grpc-js"
import { ReflectionService } from "@grpc/reflection"
import * as health from "grpc-health-check"
import * as hostProviders from "@hosts/host-providers"
import { activate } from "@/extension"
import { Controller } from "@core/controller"
import { activate } from "../extension"
import { Controller } from "../core/controller"
import { extensionContext, outputChannel, postMessage } from "./vscode-context"
import { getPackageDefinition, log } from "./utils"
import { GrpcHandler, GrpcStreamingResponseHandler } from "@hosts/external/grpc-types"
import { GrpcHandler, GrpcStreamingResponseHandler } from "./grpc-types"
import { addProtobusServices } from "@generated/standalone/server-setup"
import { StreamingResponseHandler } from "@core/controller/grpc-handler"
import { ExternalHostBridgeClientManager } from "@hosts/external/host-bridge-client-manager"
import { ExternalWebviewProvider } from "@hosts/external/ExternalWebviewProvider"
import { WebviewProviderType } from "@shared/webview/types"
import { StreamingResponseHandler } from "@/core/controller/grpc-handler"
import { ExternalHostBridgeClientManager } from "./host-bridge-client-manager"
import { ExternalWebviewProvider } from "./ExternalWebviewProvider"
import { WebviewProviderType } from "@/shared/webview/types"
import { v4 as uuidv4 } from "uuid"
import { ExternalDiffViewProvider } from "@hosts/external/ExternalDiffviewProvider"
import { ExternalDiffViewProvider } from "./ExternalDiffviewProvider"
export const PROTOBUS_PORT = 26040
export const HOSTBRIDGE_PORT = 26041
@@ -1,5 +1,5 @@
import * as grpc from "@grpc/grpc-js"
import { Controller } from "@core/controller"
import { Controller } from "../core/controller"
/**
* Type definition for a gRPC handler function.
@@ -14,7 +14,7 @@ import {
DiffServiceClientInterface,
} from "@generated/hosts/host-bridge-client-types"
import { HostBridgeClientProvider } from "@/hosts/host-provider-types"
import { HOSTBRIDGE_PORT } from "../../standalone/cline-core"
import { HOSTBRIDGE_PORT } from "./cline-core"
/**
* Manager to hold the gRPC clients for the host bridge. The clients should be re-used to avoid
+655 -19
View File
@@ -39,9 +39,210 @@ vscode.window = {
console.log("Stubbed showSaveDialog:", options)
return undefined
},
showTextDocument: async (...args) => {
console.log("Stubbed showTextDocument:", ...args)
return {}
showTextDocument: async (uri, options) => {
console.log("Stubbed showTextDocument:", uri, options)
// Extract file path from URI
let filePath = uri.path || uri.fsPath || uri
if (typeof filePath !== "string") {
filePath = uri.toString()
}
// Remove file:// prefix if present
if (filePath.startsWith("file://")) {
filePath = filePath.substring(7)
}
// Create a function that always reads the current file content
const getCurrentFileContent = async () => {
try {
const content = await fs.promises.readFile(filePath, "utf8")
console.log(`getCurrentFileContent: Read file ${filePath} (${content.length} chars)`)
return content
} catch (error) {
console.log(`getCurrentFileContent: Could not read file ${filePath}:`, error.message)
return ""
}
}
// Try to read the initial file content
let fileContent = await getCurrentFileContent()
let lineCount = fileContent.split("\n").length
// Check if we already have an active editor for this file path
const existingEditor = vscode.window._documentEditors && vscode.window._documentEditors[filePath]
if (existingEditor) {
console.log(`showTextDocument: Updating existing editor for ${filePath}`)
// Update the existing editor's content
fileContent = await getCurrentFileContent()
lineCount = fileContent.split("\n").length
// Update the document's getText method to return current content
existingEditor.document.getText = (range) => {
// Always read fresh content for getText calls
const currentContent = require("fs").readFileSync(filePath, "utf8")
if (!range) {
return currentContent
}
// Handle range-based getText with current content
const lines = currentContent.split("\n")
const startLine = Math.max(0, range.start.line)
const endLine = Math.min(lines.length - 1, range.end.line)
if (startLine === endLine) {
// Single line
const line = lines[startLine] || ""
const startChar = Math.max(0, range.start.character)
const endChar = Math.min(line.length, range.end.character)
return line.substring(startChar, endChar)
} else {
// Multiple lines
const result = []
for (let i = startLine; i <= endLine; i++) {
const line = lines[i] || ""
if (i === startLine) {
result.push(line.substring(range.start.character))
} else if (i === endLine) {
result.push(line.substring(0, range.end.character))
} else {
result.push(line)
}
}
return result.join("\n")
}
}
// Update other properties
existingEditor.document.lineCount = lineCount
existingEditor.document.fileName = filePath
// Update the active text editor reference
vscode.window.activeTextEditor = existingEditor
return existingEditor
}
// Create a new mock text editor that always reads current file content
const mockEditor = {
document: {
uri: uri,
fileName: filePath,
isDirty: false,
lineCount: lineCount,
getText: (range) => {
// Always read fresh content for getText calls
try {
const currentContent = require("fs").readFileSync(filePath, "utf8")
console.log(`document.getText: Read fresh content (${currentContent.length} chars)`)
if (!range) {
return currentContent
}
// Handle range-based getText with current content
const lines = currentContent.split("\n")
const startLine = Math.max(0, range.start.line)
const endLine = Math.min(lines.length - 1, range.end.line)
if (startLine === endLine) {
// Single line
const line = lines[startLine] || ""
const startChar = Math.max(0, range.start.character)
const endChar = Math.min(line.length, range.end.character)
return line.substring(startChar, endChar)
} else {
// Multiple lines
const result = []
for (let i = startLine; i <= endLine; i++) {
const line = lines[i] || ""
if (i === startLine) {
result.push(line.substring(range.start.character))
} else if (i === endLine) {
result.push(line.substring(0, range.end.character))
} else {
result.push(line)
}
}
return result.join("\n")
}
} catch (error) {
console.error(`Error reading file in getText: ${error.message}`)
return ""
}
},
save: async () => {
console.log("Called mock textDocument.save")
return true
},
positionAt: (offset) => {
try {
const currentContent = require("fs").readFileSync(filePath, "utf8")
const lines = currentContent.split("\n")
let currentOffset = 0
for (let line = 0; line < lines.length; line++) {
const lineLength = lines[line].length + 1 // +1 for newline
if (currentOffset + lineLength > offset) {
return { line: line, character: offset - currentOffset }
}
currentOffset += lineLength
}
return { line: lines.length - 1, character: lines[lines.length - 1]?.length || 0 }
} catch (error) {
return { line: 0, character: 0 }
}
},
offsetAt: (position) => {
try {
const currentContent = require("fs").readFileSync(filePath, "utf8")
const lines = currentContent.split("\n")
let offset = 0
for (let i = 0; i < position.line && i < lines.length; i++) {
offset += lines[i].length + 1 // +1 for newline
}
offset += Math.min(position.character, lines[position.line]?.length || 0)
return offset
} catch (error) {
return 0
}
},
},
selection: { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } },
selections: [],
visibleRanges: [{ start: { line: 0, character: 0 }, end: { line: 0, character: 0 } }],
options: {},
viewColumn: 1,
edit: async (callback) => {
console.log("Called mock textEditor.edit")
return true
},
insertSnippet: async () => true,
setDecorations: () => {},
revealRange: () => {},
show: () => {},
hide: () => {},
}
// Store the editor by file path for future reference
if (!vscode.window._documentEditors) {
vscode.window._documentEditors = {}
}
vscode.window._documentEditors[filePath] = mockEditor
// Update the active text editor
vscode.window.activeTextEditor = mockEditor
// Trigger onDidChangeActiveTextEditor listeners
if (vscode.window._activeTextEditorListeners) {
setTimeout(() => {
vscode.window._activeTextEditorListeners.forEach((listener) => {
try {
listener(mockEditor)
} catch (error) {
console.error("Error calling onDidChangeActiveTextEditor listener:", error)
}
})
}, 10) // Small delay to simulate async behavior
}
return mockEditor
},
createOutputChannel: (name) => {
console.log("Stubbed createOutputChannel:", name)
@@ -89,10 +290,23 @@ vscode.window = {
activeTextEditor: undefined,
visibleTextEditors: [],
tabGroups: {
all: [],
close: async () => {},
onDidChangeTabs: createStub("vscode.env.tabGroups.onDidChangeTabs"),
activeTabGroup: { tabs: [] },
all: [
{
tabs: [],
isActive: true,
viewColumn: 1,
},
],
activeTabGroup: {
tabs: [],
isActive: true,
viewColumn: 1,
},
close: async (tab) => {
console.log("Stubbed tabGroups.close:", tab)
return true
},
onDidChangeTabs: createStub("vscode.window.tabGroups.onDidChangeTabs"),
},
withProgress: async (_options, task) => {
console.log("Stubbed withProgress")
@@ -100,14 +314,56 @@ vscode.window = {
},
registerUriHandler: () => ({ dispose: () => {} }),
registerWebviewViewProvider: () => ({ dispose: () => {} }),
onDidChangeActiveTextEditor: () => ({ dispose: () => {} }),
createTextEditorDecorationType: () => ({ dispose: () => {} }),
createWebviewPanel: (..._args) => {
throw new Error("WebviewPanel is not supported in standalone app.")
onDidChangeActiveTextEditor: (listener) => {
console.log("Called vscode.window.onDidChangeActiveTextEditor")
// Store the listener so we can call it when showTextDocument is called
vscode.window._activeTextEditorListeners = vscode.window._activeTextEditorListeners || []
vscode.window._activeTextEditorListeners.push(listener)
return {
dispose: () => {
console.log("Disposed onDidChangeActiveTextEditor listener")
const index = vscode.window._activeTextEditorListeners.indexOf(listener)
if (index > -1) {
vscode.window._activeTextEditorListeners.splice(index, 1)
}
},
}
},
createTextEditorDecorationType: () => ({ dispose: () => {} }),
createWebviewPanel: (...args) => {
console.log("Stubbed createWebviewPanel:", ...args)
return {
webview: {},
reveal: () => {},
dispose: () => {},
}
},
onDidChangeTerminalState: (listener) => {
console.log("Called vscode.window.onDidChangeTerminalState")
return {
dispose: () => {
console.log("Disposed onDidChangeTerminalState listener")
},
}
},
onDidChangeTextEditorVisibleRanges: (listener) => {
console.log("Called vscode.window.onDidChangeTextEditorVisibleRanges")
return {
dispose: () => {
console.log("Disposed onDidChangeTextEditorVisibleRanges listener")
},
}
},
terminals: [],
activeTerminal: null,
}
vscode.env = {
// Initialize env object if it doesn't exist, then extend it
if (!vscode.env) {
vscode.env = {}
}
Object.assign(vscode.env, {
uriScheme: "vscode",
appName: "Visual Studio Code",
appRoot: "/tmp/vscode/appRoot",
@@ -117,17 +373,26 @@ vscode.env = {
sessionId: "stub-session-id",
shell: "/bin/bash",
// Add the stub functions that were missing
clipboard: createStub("vscode.env.clipboard"),
openExternal: createStub("vscode.env.openExternal"),
getQueryParameter: createStub("vscode.env.getQueryParameter"),
onDidChangeTelemetryEnabled: createStub("vscode.env.onDidChangeTelemetryEnabled"),
isTelemetryEnabled: createStub("vscode.env.isTelemetryEnabled"),
telemetryConfiguration: createStub("vscode.env.telemetryConfiguration"),
onDidChangeTelemetryConfiguration: createStub("vscode.env.onDidChangeTelemetryConfiguration"),
createTelemetryLogger: createStub("vscode.env.createTelemetryLogger"),
})
// Override the openExternal function with actual implementation
vscode.env.openExternal = async (uri) => {
const url = typeof uri === "string" ? uri : (uri.toString?.() ?? "")
console.log("Opening browser:", url)
await open(url)
return true
}
vscode.Uri = {
// Extend Uri object with improved implementations
Object.assign(vscode.Uri, {
parse: (uriString) => {
const url = new URL(uriString)
return {
@@ -172,16 +437,387 @@ vscode.Uri = {
const joined = segments.map((s) => (typeof s === "string" ? s : s.path)).join("/")
return vscode.Uri.file("/" + joined.replace(/\/+/g, "/"))
},
})
// Extend workspace object with file system operations
Object.assign(vscode.workspace, {
fs: {
readFile: async function (uri) {
console.log(`Called vscode.workspace.fs.readFile with uri:`, uri)
try {
// Extract file path from URI
let filePath = uri.path || uri.fsPath || uri
if (typeof filePath !== "string") {
filePath = uri.toString()
}
// Remove file:// prefix if present
if (filePath.startsWith("file://")) {
filePath = filePath.substring(7)
}
console.log(`Reading file: ${filePath}`)
const content = await fs.promises.readFile(filePath, "utf8")
console.log(
`File content read (${content.length} chars):`,
content.substring(0, 100) + (content.length > 100 ? "..." : ""),
)
return new Uint8Array(Buffer.from(content, "utf8"))
} catch (error) {
console.error(`Error reading file:`, error)
throw error
}
},
writeFile: async function (uri, content) {
console.log(`Called vscode.workspace.fs.writeFile with uri:`, uri)
try {
// Extract file path from URI
let filePath = uri.path || uri.fsPath || uri
if (typeof filePath !== "string") {
filePath = uri.toString()
}
// Remove file:// prefix if present
if (filePath.startsWith("file://")) {
filePath = filePath.substring(7)
}
console.log(`Writing file: ${filePath}`)
// Ensure directory exists
const dir = path.dirname(filePath)
await fs.promises.mkdir(dir, { recursive: true })
// Write the file
await fs.promises.writeFile(filePath, content)
} catch (error) {
console.error(`Error writing file:`, error)
throw error
}
},
},
rootPath: process.cwd(),
name: path.basename(process.cwd()),
// Add other workspace methods as stubs
getConfiguration: () => ({
get: () => undefined,
update: () => Promise.resolve(),
has: () => false,
}),
createFileSystemWatcher: () => ({
onDidChange: () => ({ dispose: () => {} }),
onDidCreate: () => ({ dispose: () => {} }),
onDidDelete: () => ({ dispose: () => {} }),
dispose: () => {},
}),
onDidChangeConfiguration: () => ({ dispose: () => {} }),
onDidCreateFiles: createStub("vscode.workspace.onDidCreateFiles"),
onDidDeleteFiles: createStub("vscode.workspace.onDidDeleteFiles"),
onDidRenameFiles: createStub("vscode.workspace.onDidRenameFiles"),
onWillCreateFiles: createStub("vscode.workspace.onWillCreateFiles"),
onWillDeleteFiles: createStub("vscode.workspace.onWillDeleteFiles"),
onWillRenameFiles: createStub("vscode.workspace.onWillRenameFiles"),
textDocuments: {
find: (predicate) => {
console.log("Called vscode.workspace.textDocuments.find")
// Return a mock text document that behaves like VSCode expects
return {
uri: { fsPath: "/tmp/mock-document" },
fileName: "/tmp/mock-document",
isDirty: false,
save: async () => {
console.log("Called mock textDocument.save")
return true
},
getText: () => "",
lineCount: 0,
}
},
forEach: (callback) => {
console.log("Called vscode.workspace.textDocuments.forEach")
// No documents to iterate over in standalone mode
},
length: 0,
[Symbol.iterator]: function* () {
// Empty iterator for standalone mode
},
},
// Add the crucial applyEdit method
applyEdit: async (workspaceEdit) => {
console.log("Called vscode.workspace.applyEdit", workspaceEdit)
// For standalone mode, we'll simulate applying the edit by actually writing to files
try {
// WorkspaceEdit can contain multiple types of edits
if (workspaceEdit._edits) {
for (const edit of workspaceEdit._edits) {
if (edit._type === 1) {
// TextEdit
const uri = edit._uri
const edits = edit._edits
let filePath = uri.path || uri.fsPath
if (filePath.startsWith("file://")) {
filePath = filePath.substring(7)
}
console.log(`Applying text edits to: ${filePath}`)
// Read current content if file exists
let currentContent = ""
try {
currentContent = await fs.promises.readFile(filePath, "utf8")
} catch (e) {
// File doesn't exist, start with empty content
console.log(`File ${filePath} doesn't exist, starting with empty content`)
}
// Apply edits in reverse order (from end to beginning) to maintain positions
const sortedEdits = edits.sort((a, b) => {
const aStart = a.range.start.line * 1000000 + a.range.start.character
const bStart = b.range.start.line * 1000000 + b.range.start.character
return bStart - aStart
})
let lines = currentContent.split("\n")
for (const edit of sortedEdits) {
const startLine = edit.range.start.line
const startChar = edit.range.start.character
const endLine = edit.range.end.line
const endChar = edit.range.end.character
const newText = edit.newText
console.log(`Applying edit: ${startLine}:${startChar} - ${endLine}:${endChar} -> "${newText}"`)
// Handle the edit
if (startLine === endLine) {
// Single line edit
const line = lines[startLine] || ""
lines[startLine] = line.substring(0, startChar) + newText + line.substring(endChar)
} else {
// Multi-line edit
const firstLine = lines[startLine] || ""
const lastLine = lines[endLine] || ""
const newFirstLine = firstLine.substring(0, startChar) + newText + lastLine.substring(endChar)
// Replace the range with the new content
lines.splice(startLine, endLine - startLine + 1, newFirstLine)
}
}
const newContent = lines.join("\n")
// Ensure directory exists
const dir = path.dirname(filePath)
await fs.promises.mkdir(dir, { recursive: true })
// Write the updated content
await fs.promises.writeFile(filePath, newContent, "utf8")
console.log(`Successfully applied edits to: ${filePath}`)
}
}
}
return true
} catch (error) {
console.error("Error applying workspace edit:", error)
return false
}
},
})
// Fix CodeActionKind to have static properties instead of being a class
vscode.CodeActionKind = {
Empty: "",
QuickFix: "quickfix",
Refactor: "refactor",
RefactorExtract: "refactor.extract",
RefactorInline: "refactor.inline",
RefactorRewrite: "refactor.rewrite",
Source: "source",
SourceOrganizeImports: "source.organizeImports",
SourceFixAll: "source.fixAll",
}
vscode.env.openExternal = async (uri) => {
const url = typeof uri === "string" ? uri : (uri.toString?.() ?? "")
console.log("Opening browser:", url)
await open(url)
return true
// Add missing commands implementation
if (!vscode.commands) {
vscode.commands = {}
}
Object.assign(vscode.commands, {
executeCommand: async (command, ...args) => {
console.log(`Called vscode.commands.executeCommand: ${command}`, args)
// Handle the vscode.diff command specifically
if (command === "vscode.diff") {
const [originalUri, modifiedUri, title, options] = args
console.log("Opening diff view:", { originalUri, modifiedUri, title })
// For standalone mode, just open the modified file directly
// since we can't show a proper diff view
const editor = await vscode.window.showTextDocument(modifiedUri, {
preserveFocus: options?.preserveFocus || false,
preview: false,
})
// Ensure the onDidChangeActiveTextEditor event fires with a slight delay
// This is crucial for DiffViewProvider.openDiffEditor() to work properly
setTimeout(() => {
if (vscode.window._activeTextEditorListeners) {
vscode.window._activeTextEditorListeners.forEach((listener) => {
try {
listener(editor)
} catch (error) {
console.error("Error calling onDidChangeActiveTextEditor listener in vscode.diff:", error)
}
})
}
}, 50) // Slightly longer delay to ensure proper event ordering
return editor
}
// For other commands, just return a resolved promise
return Promise.resolve()
},
registerCommand: (command, callback) => {
console.log(`Registered command: ${command}`)
return { dispose: () => {} }
},
getCommands: async () => {
return []
},
})
// Add missing TabInput classes
vscode.TabInputText = class TabInputText {
constructor(uri) {
this.uri = uri
}
}
vscode.TabInputTextDiff = class TabInputTextDiff {
constructor(original, modified) {
this.original = original
this.modified = modified
}
}
// Add missing WorkspaceEdit and related classes
vscode.WorkspaceEdit = class WorkspaceEdit {
constructor() {
this._edits = []
}
replace(uri, range, newText) {
console.log("WorkspaceEdit.replace:", uri, range, newText)
this._edits.push({
_type: 1, // TextEdit
_uri: uri,
_edits: [
{
range: range,
newText: newText,
},
],
})
}
insert(uri, position, newText) {
console.log("WorkspaceEdit.insert:", uri, position, newText)
this.replace(uri, new vscode.Range(position, position), newText)
}
delete(uri, range) {
console.log("WorkspaceEdit.delete:", uri, range)
this.replace(uri, range, "")
}
}
vscode.Range = class Range {
constructor(startLine, startCharacter, endLine, endCharacter) {
if (typeof startLine === "object") {
// Called with Position objects
this.start = startLine
this.end = startCharacter
} else {
// Called with line/character numbers
this.start = new vscode.Position(startLine, startCharacter)
this.end = new vscode.Position(endLine, endCharacter)
}
}
}
vscode.Position = class Position {
constructor(line, character) {
this.line = line
this.character = character
}
}
vscode.Selection = class Selection extends vscode.Range {
constructor(anchorLine, anchorCharacter, activeLine, activeCharacter) {
if (typeof anchorLine === "object") {
// Called with Position objects
super(anchorLine, anchorCharacter)
this.anchor = anchorLine
this.active = anchorCharacter
} else {
// Called with line/character numbers
super(anchorLine, anchorCharacter, activeLine, activeCharacter)
this.anchor = new vscode.Position(anchorLine, anchorCharacter)
this.active = new vscode.Position(activeLine, activeCharacter)
}
}
}
// Add TextEditorRevealType enum
vscode.TextEditorRevealType = {
Default: 0,
InCenter: 1,
InCenterIfOutsideViewport: 2,
AtTop: 3,
}
// Add missing languages API
if (!vscode.languages) {
vscode.languages = {}
}
Object.assign(vscode.languages, {
getDiagnostics: (uri) => {
console.log("Called vscode.languages.getDiagnostics")
// Return empty diagnostics for standalone mode
if (uri) {
return []
} else {
// Return all diagnostics as empty array
return []
}
},
registerCodeActionsProvider: () => ({ dispose: () => {} }),
createDiagnosticCollection: () => ({
set: () => {},
delete: () => {},
clear: () => {},
dispose: () => {},
}),
})
// Export the terminal manager globally for Cline core to use
global.standaloneTerminalManager = globalTerminalManager
// Override the TerminalManager to use our standalone implementation
if (typeof global !== "undefined") {
// Replace the TerminalManager class with our standalone implementation
global.StandaloneTerminalManagerClass = require("./enhanced-terminal").StandaloneTerminalManager
}
module.exports = vscode
console.log("Finished loading stub impls...")
+1 -1
View File
@@ -4,7 +4,7 @@
"experimentalDecorators": true,
"forceConsistentCasingInFileNames": true,
"isolatedModules": true,
"lib": ["es2022", "DOM"],
"lib": ["es2022", "esnext.disposable", "DOM"],
"module": "esnext",
"moduleResolution": "Bundler",
"noFallthroughCasesInSwitch": true,
+331 -21
View File
@@ -1,9 +1,10 @@
import { VSCodeBadge, VSCodeProgressRing } from "@vscode/webview-ui-toolkit/react"
import { VSCodeBadge, VSCodeButton, VSCodeProgressRing } from "@vscode/webview-ui-toolkit/react"
import deepEqual from "fast-deep-equal"
import React, { memo, MouseEvent, useCallback, useEffect, useMemo, useRef, useState } from "react"
import styled from "styled-components"
import { useSize } from "react-use"
import CreditLimitError from "@/components/chat/CreditLimitError"
import { OptionsButtons } from "@/components/chat/OptionsButtons"
import TaskFeedbackButtons from "@/components/chat/TaskFeedbackButtons"
import { CheckmarkControl } from "@/components/common/CheckmarkControl"
@@ -35,8 +36,8 @@ import NewTaskPreview from "./NewTaskPreview"
import ReportBugPreview from "./ReportBugPreview"
import UserMessage from "./UserMessage"
import QuoteButton from "./QuoteButton"
import ErrorRow from "./ErrorRow"
import { ErrorBlockTitle } from "./ErrorBlockTitle"
import { useClineAuth } from "@/context/ClineAuthContext"
import { CLINE_ACCOUNT_AUTH_ERROR_MESSAGE } from "@shared/ClineAccount"
const normalColor = "var(--vscode-foreground)"
const errorColor = "var(--vscode-errorForeground)"
@@ -103,6 +104,42 @@ const Markdown = memo(({ markdown }: { markdown?: string }) => {
)
})
const RetryMessage = memo(
({ seconds, attempt, retryOperations }: { retryOperations: number; attempt: number; seconds?: number }) => {
const [remainingSeconds, setRemainingSeconds] = useState(seconds || 0)
useEffect(() => {
if (seconds && seconds > 0) {
setRemainingSeconds(seconds)
const interval = setInterval(() => {
setRemainingSeconds((prev) => {
if (prev <= 1) {
clearInterval(interval)
return 0
}
return prev - 1
})
}, 1000)
return () => clearInterval(interval)
}
}, [seconds])
return (
<span
style={{
color: normalColor,
fontWeight: "bold",
}}>
{`API Request (Retrying failed attempt ${attempt}/${retryOperations}`}
{remainingSeconds > 0 && ` in ${remainingSeconds} seconds`}
)...
</span>
)
},
)
const ChatRow = memo(
(props: ChatRowProps) => {
const { isLast, onHeightChange, message, lastModifiedMessage, inputValue } = props
@@ -149,6 +186,7 @@ export const ChatRowContent = memo(
sendMessageFromChatRow,
onSetQuote,
}: ChatRowContentProps) => {
const { handleSignIn, clineUser } = useClineAuth()
const { mcpServers, mcpMarketplaceCatalog, onRelinquishControl, apiConfiguration } = useExtensionState()
const [seeNewChangesDisabled, setSeeNewChangesDisabled] = useState(false)
const [quoteButtonState, setQuoteButtonState] = useState<QuoteButtonState>({
@@ -163,7 +201,7 @@ export const ChatRowContent = memo(
const info: ClineApiReqInfo = JSON.parse(message.text)
return [info.cost, info.cancelReason, info.streamingFailedMessage, info.retryStatus]
}
return [undefined, undefined, undefined, undefined, undefined]
return [undefined, undefined, undefined, undefined]
}, [message.text, message.say])
// when resuming task last won't be api_req_failed but a resume_task message so api_req_started will show loading spinner. that's why we just remove the last api_req_started that failed without streaming anything
@@ -343,12 +381,73 @@ export const ChatRowContent = memo(
<span style={{ color: successColor, fontWeight: "bold" }}>Task Completed</span>,
]
case "api_req_started":
return ErrorBlockTitle({
cost,
apiReqCancelReason,
apiRequestFailedMessage,
retryStatus,
})
const getIconSpan = (iconName: string, color: string) => (
<div
style={{
width: 16,
height: 16,
display: "flex",
alignItems: "center",
justifyContent: "center",
}}>
<span
className={`codicon codicon-${iconName}`}
style={{
color,
fontSize: 16,
marginBottom: "-1.5px",
}}></span>
</div>
)
return [
apiReqCancelReason != null ? (
apiReqCancelReason === "user_cancelled" ? (
getIconSpan("error", cancelledColor)
) : (
getIconSpan("error", errorColor)
)
) : cost != null ? (
getIconSpan("check", successColor)
) : apiRequestFailedMessage ? (
getIconSpan("error", errorColor)
) : (
<ProgressIndicator />
),
(() => {
if (apiReqCancelReason != null) {
return apiReqCancelReason === "user_cancelled" ? (
<span style={{ color: normalColor, fontWeight: "bold" }}>API Request Cancelled</span>
) : (
<span style={{ color: errorColor, fontWeight: "bold" }}>API Streaming Failed</span>
)
}
if (cost != null) {
return <span style={{ color: normalColor, fontWeight: "bold" }}>API Request</span>
}
if (apiRequestFailedMessage) {
const errorData = parseErrorText(apiRequestFailedMessage)
if (errorData?.code === "insufficient_credits") {
return <span style={{ color: errorColor, fontWeight: "bold" }}>Credit Limit Reached</span>
}
return <span style={{ color: errorColor, fontWeight: "bold" }}>API Request Failed</span>
}
// New: Check for retryStatus to modify the title
if (retryStatus && cost == null && !apiReqCancelReason) {
const retryOperations = retryStatus.maxAttempts > 0 ? retryStatus.maxAttempts - 1 : 0
return (
<RetryMessage
seconds={retryStatus.delaySec}
attempt={retryStatus.attempt}
retryOperations={retryOperations}
/>
)
}
return <span style={{ color: normalColor, fontWeight: "bold" }}>API Request...</span>
})(),
]
case "followup":
return [
<span
@@ -847,12 +946,92 @@ export const ChatRowContent = memo(
<span className={`codicon codicon-chevron-${isExpanded ? "up" : "down"}`}></span>
</div>
{((cost == null && apiRequestFailedMessage) || apiReqStreamingFailedMessage) && (
<ErrorRow
message={message}
errorType="error"
apiRequestFailedMessage={apiRequestFailedMessage}
apiReqStreamingFailedMessage={apiReqStreamingFailedMessage}
/>
<>
{(() => {
// Try to parse the error message as JSON for credit limit error
const errorData = parseErrorText(
apiRequestFailedMessage || apiReqStreamingFailedMessage,
)
if (errorData) {
if (
errorData.code === "insufficient_credits" &&
typeof errorData.current_balance === "number"
) {
return (
<CreditLimitError
currentBalance={errorData.current_balance}
totalSpent={errorData.total_spent}
totalPromotions={errorData.total_promotions}
message={errorData.message}
buyCreditsUrl={errorData.buy_credits_url}
/>
)
}
}
// Check for rate limit errors (status code 429)
const isRateLimitError =
apiRequestFailedMessage?.includes("status code 429") ||
apiRequestFailedMessage?.toLowerCase().includes("rate limit") ||
apiRequestFailedMessage?.toLowerCase().includes("too many requests") ||
apiRequestFailedMessage?.toLowerCase().includes("quota exceeded") ||
apiRequestFailedMessage?.toLowerCase().includes("resource exhausted")
if (isRateLimitError) {
return (
<p
style={{
...pStyle,
color: "var(--vscode-errorForeground)",
}}>
{apiRequestFailedMessage || apiReqStreamingFailedMessage}
</p>
)
}
// Default error display
return (
<p
style={{
...pStyle,
color: "var(--vscode-errorForeground)",
}}>
{apiRequestFailedMessage || apiReqStreamingFailedMessage}
{apiRequestFailedMessage?.toLowerCase().includes("powershell") && (
<>
<br />
<br />
It seems like you're having Windows PowerShell issues, please see this{" "}
<a
href="https://github.com/cline/cline/wiki/TroubleShooting-%E2%80%90-%22PowerShell-is-not-recognized-as-an-internal-or-external-command%22"
style={{
color: "inherit",
textDecoration: "underline",
}}>
troubleshooting guide
</a>
.
</>
)}
{apiRequestFailedMessage?.includes(CLINE_ACCOUNT_AUTH_ERROR_MESSAGE) && (
<>
<br />
<br />
{clineUser ? (
<span style={{ color: "var(--vscode-descriptionForeground)" }}>
(Click "Retry" below)
</span>
) : (
<VSCodeButton onClick={handleSignIn} className="w-full mb-4">
Sign in to Cline
</VSCodeButton>
)}
</>
)}
</p>
)
})()}
</>
)}
{isExpanded && (
@@ -1004,11 +1183,97 @@ export const ChatRowContent = memo(
</div>
)
case "error":
return <ErrorRow message={message} errorType="error" />
return (
<>
{title && (
<div style={headerStyle}>
{icon}
{title}
</div>
)}
<p
style={{
...pStyle,
color: "var(--vscode-errorForeground)",
}}>
{message.text}
</p>
</>
)
case "diff_error":
return <ErrorRow message={message} errorType="diff_error" />
return (
<>
<div
style={{
display: "flex",
flexDirection: "column",
backgroundColor: "var(--vscode-textBlockQuote-background)",
padding: 8,
borderRadius: 3,
fontSize: 12,
color: "var(--vscode-foreground)",
opacity: 0.8,
}}>
<div
style={{
display: "flex",
alignItems: "center",
marginBottom: 4,
}}>
<i
className="codicon codicon-warning"
style={{
marginRight: 8,
fontSize: 14,
color: "var(--vscode-descriptionForeground)",
}}></i>
<span style={{ fontWeight: 500 }}>Diff Edit Mismatch</span>
</div>
<div>The model used search patterns that don't match anything in the file. Retrying...</div>
</div>
</>
)
case "clineignore_error":
return <ErrorRow message={message} errorType="clineignore_error" />
return (
<>
<div
style={{
display: "flex",
flexDirection: "column",
backgroundColor: "rgba(255, 191, 0, 0.1)",
padding: 8,
borderRadius: 3,
fontSize: 12,
}}>
<div
style={{
display: "flex",
alignItems: "center",
marginBottom: 4,
}}>
<i
className="codicon codicon-error"
style={{
marginRight: 8,
fontSize: 18,
color: "#FFA500",
}}></i>
<span
style={{
fontWeight: 500,
color: "#FFA500",
}}>
Access Denied
</span>
</div>
<div>
Cline tried to access <code>{message.text}</code> which is blocked by the{" "}
<code>.clineignore</code>
file.
</div>
</div>
</>
)
case "checkpoint_created":
return (
<>
@@ -1168,9 +1433,37 @@ export const ChatRowContent = memo(
case "ask":
switch (message.ask) {
case "mistake_limit_reached":
return <ErrorRow message={message} errorType="mistake_limit_reached" />
return (
<>
<div style={headerStyle}>
{icon}
{title}
</div>
<p
style={{
...pStyle,
color: "var(--vscode-errorForeground)",
}}>
{message.text}
</p>
</>
)
case "auto_approval_max_req_reached":
return <ErrorRow message={message} errorType="auto_approval_max_req_reached" />
return (
<>
<div style={headerStyle}>
{icon}
{title}
</div>
<p
style={{
...pStyle,
color: "var(--vscode-errorForeground)",
}}>
{message.text}
</p>
</>
)
case "completion_result":
if (message.text) {
const hasChanges = message.text.endsWith(COMPLETION_RESULT_CHANGES_FLAG) ?? false
@@ -1393,3 +1686,20 @@ export const ChatRowContent = memo(
}
},
)
function parseErrorText(text: string | undefined) {
if (!text) {
return undefined
}
try {
const startIndex = text.indexOf("{")
const endIndex = text.lastIndexOf("}")
if (startIndex !== -1 && endIndex !== -1) {
const jsonStr = text.substring(startIndex, endIndex + 1)
const errorObject = JSON.parse(jsonStr)
return errorObject
}
} catch (e) {
// Not JSON or missing required fields
}
}
+13 -28
View File
@@ -7,7 +7,7 @@ import { CODE_BLOCK_BG_COLOR } from "@/components/common/CodeBlock"
import Thumbnails from "@/components/common/Thumbnails"
import Tooltip from "@/components/common/Tooltip"
import ApiOptions from "@/components/settings/ApiOptions"
import { normalizeApiConfiguration, getModeSpecificFields } from "@/components/settings/utils/providerUtils"
import { normalizeApiConfiguration } from "@/components/settings/utils/providerUtils"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { FileServiceClient, StateServiceClient, ModelsServiceClient } from "@/services/grpc-client"
import {
@@ -966,8 +966,8 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
// Separate the API config submission logic
const submitApiConfig = useCallback(async () => {
const apiValidationResult = validateApiConfiguration(chatSettings.mode, apiConfiguration)
const modelIdValidationResult = validateModelId(chatSettings.mode, apiConfiguration, openRouterModels)
const apiValidationResult = validateApiConfiguration(apiConfiguration)
const modelIdValidationResult = validateModelId(apiConfiguration, openRouterModels)
if (!apiValidationResult && !modelIdValidationResult && apiConfiguration) {
try {
@@ -1089,16 +1089,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
// Get model display name
const modelDisplayName = useMemo(() => {
const { selectedProvider, selectedModelId } = normalizeApiConfiguration(apiConfiguration, chatSettings.mode)
const {
vsCodeLmModelSelector,
togetherModelId,
fireworksModelId,
lmStudioModelId,
ollamaModelId,
liteLlmModelId,
requestyModelId,
} = getModeSpecificFields(apiConfiguration, chatSettings.mode)
const { selectedProvider, selectedModelId } = normalizeApiConfiguration(apiConfiguration)
const unknownModel = "unknown"
if (!apiConfiguration) return unknownModel
switch (selectedProvider) {
@@ -1107,25 +1098,25 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
case "openai":
return `openai-compat:${selectedModelId}`
case "vscode-lm":
return `vscode-lm:${vsCodeLmModelSelector ? `${vsCodeLmModelSelector.vendor ?? ""}/${vsCodeLmModelSelector.family ?? ""}` : unknownModel}`
return `vscode-lm:${apiConfiguration.vsCodeLmModelSelector ? `${apiConfiguration.vsCodeLmModelSelector.vendor ?? ""}/${apiConfiguration.vsCodeLmModelSelector.family ?? ""}` : unknownModel}`
case "together":
return `${selectedProvider}:${togetherModelId}`
return `${selectedProvider}:${apiConfiguration.togetherModelId}`
case "fireworks":
return `fireworks:${fireworksModelId}`
return `fireworks:${apiConfiguration.fireworksModelId}`
case "lmstudio":
return `${selectedProvider}:${lmStudioModelId}`
return `${selectedProvider}:${apiConfiguration.lmStudioModelId}`
case "ollama":
return `${selectedProvider}:${ollamaModelId}`
return `${selectedProvider}:${apiConfiguration.ollamaModelId}`
case "litellm":
return `${selectedProvider}:${liteLlmModelId}`
return `${selectedProvider}:${apiConfiguration.liteLlmModelId}`
case "requesty":
return `${selectedProvider}:${requestyModelId}`
return `${selectedProvider}:${apiConfiguration.requestyModelId}`
case "anthropic":
case "openrouter":
default:
return `${selectedProvider}:${selectedModelId}`
}
}, [apiConfiguration, chatSettings.mode])
}, [apiConfiguration])
// Calculate arrow position and menu position based on button location
useEffect(() => {
@@ -1729,13 +1720,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
style={{
bottom: `calc(100vh - ${menuPosition}px + 6px)`,
}}>
<ApiOptions
showModelOptions={true}
apiErrorMessage={undefined}
modelIdErrorMessage={undefined}
isPopup={true}
currentMode={chatSettings.mode}
/>
<ApiOptions showModelOptions={true} modelIdErrorMessage={undefined} isPopup={true} />
</ModelSelectorTooltip>
)}
</ModelContainer>
+2 -3
View File
@@ -47,7 +47,6 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
apiConfiguration,
telemetrySetting,
navigateToChat,
chatSettings,
} = useExtensionState()
const shouldShowQuickWins = false // !taskHistory || taskHistory.length < QUICK_WINS_HISTORY_THRESHOLD
//const task = messages.length > 0 ? (messages[0].say === "task" ? messages[0] : undefined) : undefined) : undefined
@@ -200,8 +199,8 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
messageHandlers
const { selectedModelInfo } = useMemo(() => {
return normalizeApiConfiguration(apiConfiguration, chatSettings.mode)
}, [apiConfiguration, chatSettings.mode])
return normalizeApiConfiguration(apiConfiguration)
}, [apiConfiguration])
const selectFilesAndImages = useCallback(async () => {
try {
@@ -1,106 +0,0 @@
import React from "react"
import { ClineError, ClineErrorType } from "../../../../src/services/error/ClineError"
import { ProgressIndicator } from "./ChatRow"
const RetryMessage = React.memo(
({ seconds, attempt, retryOperations }: { retryOperations: number; attempt: number; seconds?: number }) => {
const [remainingSeconds, setRemainingSeconds] = React.useState(seconds || 0)
React.useEffect(() => {
if (seconds && seconds > 0) {
setRemainingSeconds(seconds)
const interval = setInterval(() => {
setRemainingSeconds((prev) => {
if (prev <= 1) {
clearInterval(interval)
return 0
}
return prev - 1
})
}, 1000)
return () => clearInterval(interval)
}
}, [seconds])
return (
<span className="font-bold text-[var(--vscode-foreground)]">
{`API Request (Retrying failed attempt ${attempt}/${retryOperations}`}
{remainingSeconds > 0 && ` in ${remainingSeconds} seconds`}
)...
</span>
)
},
)
interface ErrorBlockTitleProps {
cost?: number
apiReqCancelReason?: string
apiRequestFailedMessage?: string
retryStatus?: {
attempt: number
maxAttempts: number
delaySec?: number
errorSnippet?: string
}
}
export const ErrorBlockTitle = ({
cost,
apiReqCancelReason,
apiRequestFailedMessage,
retryStatus,
}: ErrorBlockTitleProps): [React.ReactElement, React.ReactElement] => {
const getIconSpan = (iconName: string, colorClass: string) => (
<div className="w-4 h-4 flex items-center justify-center">
<span className={`codicon codicon-${iconName} text-base -mb-0.5 ${colorClass}`}></span>
</div>
)
const icon =
apiReqCancelReason != null ? (
apiReqCancelReason === "user_cancelled" ? (
getIconSpan("error", "text-[var(--vscode-descriptionForeground)]")
) : (
getIconSpan("error", "text-[var(--vscode-errorForeground)]")
)
) : cost != null ? (
getIconSpan("check", "text-[var(--vscode-charts-green)]")
) : apiRequestFailedMessage ? (
getIconSpan("error", "text-[var(--vscode-errorForeground)]")
) : (
<ProgressIndicator />
)
const title = (() => {
// Default loading state
const details = { title: "API Request...", classNames: ["font-bold"] }
// Handle cancellation states first
if (apiReqCancelReason === "user_cancelled") {
details.title = "API Request Cancelled"
details.classNames.push("text-[var(--vscode-foreground)]")
} else if (apiReqCancelReason != null) {
details.title = "API Streaming Failed"
details.classNames.push("text-[var(--vscode-errorForeground)]")
} else if (cost != null) {
// Handle completed request
details.title = "API Request"
details.classNames.push("text-[var(--vscode-foreground)]")
} else if (apiRequestFailedMessage) {
// Handle failed request
const clineError = ClineError.parse(apiRequestFailedMessage)
const titleText = clineError?.isErrorType(ClineErrorType.Balance) ? "Credit Limit Reached" : "API Request Failed"
details.title = titleText
details.classNames.push("font-bold text-[var(--vscode-errorForeground)]")
} else if (retryStatus) {
// Handle retry state
const retryOperations = Math.max(0, retryStatus.maxAttempts - 1)
return <RetryMessage seconds={retryStatus.delaySec} attempt={retryStatus.attempt} retryOperations={retryOperations} />
}
return <span className={details.classNames.join(" ")}>{details.title}</span>
})()
return [icon, title]
}
@@ -1,200 +0,0 @@
import { render, screen } from "@testing-library/react"
import { describe, it, expect, vi } from "vitest"
import ErrorRow from "./ErrorRow"
import { ClineMessage } from "@shared/ExtensionMessage"
// Mock the auth context
const mockHandleSignIn = vi.fn()
vi.mock("@/context/ClineAuthContext", () => ({
useClineAuth: () => ({
handleSignIn: mockHandleSignIn,
clineUser: null,
}),
}))
// Mock CreditLimitError component
vi.mock("@/components/chat/CreditLimitError", () => ({
default: ({ message }: { message: string }) => <div data-testid="credit-limit-error">{message}</div>,
}))
// Mock ClineError
vi.mock("../../../../src/services/error/ClineError", () => ({
ClineError: {
parse: vi.fn(),
},
ClineErrorType: {
Balance: "balance",
RateLimit: "rateLimit",
Auth: "auth",
},
}))
describe("ErrorRow", () => {
const mockMessage: ClineMessage = {
ts: 123456789,
type: "say",
say: "error",
text: "Test error message",
}
beforeEach(() => {
vi.clearAllMocks()
})
it("renders basic error message", () => {
render(<ErrorRow message={mockMessage} errorType="error" />)
expect(screen.getByText("Test error message")).toBeInTheDocument()
})
it("renders mistake limit reached error", () => {
const mistakeMessage = { ...mockMessage, text: "Mistake limit reached" }
render(<ErrorRow message={mistakeMessage} errorType="mistake_limit_reached" />)
expect(screen.getByText("Mistake limit reached")).toBeInTheDocument()
})
it("renders auto approval max requests error", () => {
const maxReqMessage = { ...mockMessage, text: "Max requests reached" }
render(<ErrorRow message={maxReqMessage} errorType="auto_approval_max_req_reached" />)
expect(screen.getByText("Max requests reached")).toBeInTheDocument()
})
it("renders diff error", () => {
render(<ErrorRow message={mockMessage} errorType="diff_error" />)
expect(
screen.getByText("The model used search patterns that don't match anything in the file. Retrying..."),
).toBeInTheDocument()
})
it("renders clineignore error", () => {
const clineignoreMessage = { ...mockMessage, text: "/path/to/file.txt" }
render(<ErrorRow message={clineignoreMessage} errorType="clineignore_error" />)
expect(screen.getByText(/Cline tried to access/)).toBeInTheDocument()
expect(screen.getByText("/path/to/file.txt")).toBeInTheDocument()
})
describe("API error handling", () => {
it("renders credit limit error when balance error is detected", async () => {
const mockClineError = {
message: "Insufficient credits",
isErrorType: vi.fn((type) => type === "balance"),
_error: {
details: {
current_balance: 0,
total_spent: 10.5,
total_promotions: 5.0,
message: "You have run out of credit.",
buy_credits_url: "https://app.cline.bot/dashboard",
},
},
}
const { ClineError } = await import("../../../../src/services/error/ClineError")
vi.mocked(ClineError.parse).mockReturnValue(mockClineError as any)
render(<ErrorRow message={mockMessage} errorType="error" apiRequestFailedMessage="Insufficient credits error" />)
expect(screen.getByTestId("credit-limit-error")).toBeInTheDocument()
expect(screen.getByText("You have run out of credit.")).toBeInTheDocument()
})
it("renders rate limit error with request ID", async () => {
const mockClineError = {
message: "Rate limit exceeded",
isErrorType: vi.fn((type) => type === "rateLimit"),
_error: {
request_id: "req_123456",
},
}
const { ClineError } = await import("../../../../src/services/error/ClineError")
vi.mocked(ClineError.parse).mockReturnValue(mockClineError as any)
render(<ErrorRow message={mockMessage} errorType="error" apiRequestFailedMessage="Rate limit exceeded" />)
expect(screen.getByText("Rate limit exceeded")).toBeInTheDocument()
expect(screen.getByText("Request ID: req_123456")).toBeInTheDocument()
})
it("renders auth error with sign in button when user is not signed in", async () => {
const mockClineError = {
message: "Authentication failed",
isErrorType: vi.fn((type) => type === "auth"),
providerId: "cline",
_error: {},
}
const { ClineError } = await import("../../../../src/services/error/ClineError")
vi.mocked(ClineError.parse).mockReturnValue(mockClineError as any)
render(<ErrorRow message={mockMessage} errorType="error" apiRequestFailedMessage="Authentication failed" />)
expect(screen.getByText("Authentication failed")).toBeInTheDocument()
expect(screen.getByText("Sign in to Cline")).toBeInTheDocument()
})
it("renders PowerShell troubleshooting link when error mentions PowerShell", async () => {
const mockClineError = {
message: "PowerShell is not recognized as an internal or external command",
isErrorType: vi.fn(() => false),
_error: {},
}
const { ClineError } = await import("../../../../src/services/error/ClineError")
vi.mocked(ClineError.parse).mockReturnValue(mockClineError as any)
render(
<ErrorRow
message={mockMessage}
errorType="error"
apiRequestFailedMessage="PowerShell is not recognized as an internal or external command"
/>,
)
expect(screen.getByText(/PowerShell is not recognized/)).toBeInTheDocument()
expect(screen.getByText("troubleshooting guide")).toBeInTheDocument()
expect(screen.getByRole("link", { name: "troubleshooting guide" })).toHaveAttribute(
"href",
"https://github.com/cline/cline/wiki/TroubleShooting-%E2%80%90-%22PowerShell-is-not-recognized-as-an-internal-or-external-command%22",
)
})
it("handles apiReqStreamingFailedMessage instead of apiRequestFailedMessage", async () => {
const mockClineError = {
message: "Streaming failed",
isErrorType: vi.fn(() => false),
_error: {},
}
const { ClineError } = await import("../../../../src/services/error/ClineError")
vi.mocked(ClineError.parse).mockReturnValue(mockClineError as any)
render(<ErrorRow message={mockMessage} errorType="error" apiReqStreamingFailedMessage="Streaming failed" />)
expect(screen.getByText("Streaming failed")).toBeInTheDocument()
})
it("falls back to regular error message when ClineError.parse returns null", async () => {
const { ClineError } = await import("../../../../src/services/error/ClineError")
vi.mocked(ClineError.parse).mockReturnValue(undefined)
render(<ErrorRow message={mockMessage} errorType="error" apiRequestFailedMessage="Some API error" />)
// When ClineError.parse returns null, clineErrorMessage is undefined, so it renders an empty paragraph
// The fallback to message.text only happens when there's no apiRequestFailedMessage at all
const paragraph = screen.getByRole("paragraph")
expect(paragraph).toBeInTheDocument()
expect(paragraph).toBeEmptyDOMElement()
})
it("renders regular error message when no API error messages are provided", () => {
render(<ErrorRow message={mockMessage} errorType="error" />)
expect(screen.getByText("Test error message")).toBeInTheDocument()
})
})
})
-130
View File
@@ -1,130 +0,0 @@
import { memo } from "react"
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
import { ClineMessage } from "@shared/ExtensionMessage"
import { ClineError, ClineErrorType } from "../../../../src/services/error/ClineError"
import CreditLimitError from "@/components/chat/CreditLimitError"
import { useClineAuth } from "@/context/ClineAuthContext"
const errorColor = "var(--vscode-errorForeground)"
interface ErrorRowProps {
message: ClineMessage
errorType: "error" | "mistake_limit_reached" | "auto_approval_max_req_reached" | "diff_error" | "clineignore_error"
apiRequestFailedMessage?: string
apiReqStreamingFailedMessage?: string
}
const ErrorRow = memo(({ message, errorType, apiRequestFailedMessage, apiReqStreamingFailedMessage }: ErrorRowProps) => {
const { handleSignIn, clineUser } = useClineAuth()
const renderErrorContent = () => {
switch (errorType) {
case "error":
case "mistake_limit_reached":
case "auto_approval_max_req_reached":
// Handle API request errors with special error parsing
if (apiRequestFailedMessage || apiReqStreamingFailedMessage) {
const clineError = ClineError.parse(apiRequestFailedMessage || apiReqStreamingFailedMessage)
const clineErrorMessage = clineError?.message
const requestId = clineError?._error?.request_id
const isClineProvider = clineError?.providerId === "cline"
if (clineError) {
if (clineError.isErrorType(ClineErrorType.Balance)) {
const errorDetails = clineError._error?.details
return (
<CreditLimitError
currentBalance={errorDetails?.current_balance}
totalSpent={errorDetails?.total_spent}
totalPromotions={errorDetails?.total_promotions}
message={errorDetails?.message}
buyCreditsUrl={errorDetails?.buy_credits_url}
/>
)
}
}
if (clineError?.isErrorType(ClineErrorType.RateLimit)) {
return (
<p className="m-0 whitespace-pre-wrap text-[var(--vscode-errorForeground)] wrap-anywhere">
{clineErrorMessage}
{requestId && <div>Request ID: {requestId}</div>}
</p>
)
}
// Default error display
return (
<p className="m-0 whitespace-pre-wrap text-[var(--vscode-errorForeground)] wrap-anywhere">
{clineErrorMessage}
{requestId && <div>Request ID: {requestId}</div>}
{clineErrorMessage?.toLowerCase()?.includes("powershell") && (
<>
<br />
<br />
It seems like you're having Windows PowerShell issues, please see this{" "}
<a
href="https://github.com/cline/cline/wiki/TroubleShooting-%E2%80%90-%22PowerShell-is-not-recognized-as-an-internal-or-external-command%22"
className="underline text-inherit">
troubleshooting guide
</a>
.
</>
)}
{clineError?.isErrorType(ClineErrorType.Auth) && (
<>
<br />
<br />
{/* The user is signed in or not using cline provider */}
{clineUser && !isClineProvider ? (
<span className="mb-4 text-[var(--vscode-descriptionForeground)]">
(Click "Retry" below)
</span>
) : (
<VSCodeButton onClick={handleSignIn} className="w-full mb-4">
Sign in to Cline
</VSCodeButton>
)}
</>
)}
</p>
)
}
// Regular error message
return (
<p className="m-0 whitespace-pre-wrap text-[var(--vscode-errorForeground)] wrap-anywhere">{message.text}</p>
)
case "diff_error":
return (
<div className="flex flex-col p-2 rounded text-xs opacity-80 bg-[var(--vscode-textBlockQuote-background)] text-[var(--vscode-foreground)]">
<div>The model used search patterns that don't match anything in the file. Retrying...</div>
</div>
)
case "clineignore_error":
return (
<div className="flex flex-col p-2 rounded text-xs bg-[var(--vscode-textBlockQuote-background)] text-[var(--vscode-foreground)] opacity-80">
<div>
Cline tried to access <code>{message.text}</code> which is blocked by the <code>.clineignore</code>
file.
</div>
</div>
)
default:
return null
}
}
// For diff_error and clineignore_error, we don't show the header separately
if (errorType === "diff_error" || errorType === "clineignore_error") {
return <>{renderErrorContent()}</>
}
// For other error types, show header + content
return <>{renderErrorContent()}</>
})
export default ErrorRow
@@ -1,51 +0,0 @@
import { describe, it, expect } from "vitest"
import { ErrorBlockTitle } from "../ErrorBlockTitle"
describe("ErrorBlockTitle", () => {
it("should return icon and title for API request cancelled", () => {
const [icon, title] = ErrorBlockTitle({
apiReqCancelReason: "user_cancelled",
})
expect(icon).toBeDefined()
expect(title).toBeDefined()
})
it("should return icon and title for completed API request", () => {
const [icon, title] = ErrorBlockTitle({
cost: 0.001,
})
expect(icon).toBeDefined()
expect(title).toBeDefined()
})
it("should return icon and title for failed API request", () => {
const [icon, title] = ErrorBlockTitle({
apiRequestFailedMessage: "Request failed",
})
expect(icon).toBeDefined()
expect(title).toBeDefined()
})
it("should return icon and title for retry status", () => {
const [icon, title] = ErrorBlockTitle({
retryStatus: {
attempt: 2,
maxAttempts: 3,
delaySec: 5,
},
})
expect(icon).toBeDefined()
expect(title).toBeDefined()
})
it("should return icon and title for default API request", () => {
const [icon, title] = ErrorBlockTitle({})
expect(icon).toBeDefined()
expect(title).toBeDefined()
})
})
@@ -1,6 +1,6 @@
import HeroTooltip from "@/components/common/HeroTooltip"
import Thumbnails from "@/components/common/Thumbnails"
import { normalizeApiConfiguration, getModeSpecificFields } from "@/components/settings/utils/providerUtils"
import { normalizeApiConfiguration } from "@/components/settings/utils/providerUtils"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { FileServiceClient, TaskServiceClient, UiServiceClient } from "@/services/grpc-client"
import { formatLargeNumber, formatSize } from "@/utils/format"
@@ -43,7 +43,7 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
onClose,
onScrollToMessage,
}) => {
const { apiConfiguration, currentTaskItem, checkpointTrackerErrorMessage, clineMessages, navigateToSettings, chatSettings } =
const { apiConfiguration, currentTaskItem, checkpointTrackerErrorMessage, clineMessages, navigateToSettings } =
useExtensionState()
const [isTaskExpanded, setIsTaskExpanded] = useState(true)
const [isTextExpanded, setIsTextExpanded] = useState(false)
@@ -51,10 +51,7 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
const textContainerRef = useRef<HTMLDivElement>(null)
const textRef = useRef<HTMLDivElement>(null)
const { selectedModelInfo } = useMemo(
() => normalizeApiConfiguration(apiConfiguration, chatSettings.mode),
[apiConfiguration, chatSettings.mode],
)
const { selectedModelInfo } = useMemo(() => normalizeApiConfiguration(apiConfiguration), [apiConfiguration])
const contextWindow = selectedModelInfo?.contextWindow
// Open task header when checkpoint tracker error message is set
@@ -133,18 +130,19 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
}, [task.text, windowWidth, isTaskExpanded])
const isCostAvailable = useMemo(() => {
const modeFields = getModeSpecificFields(apiConfiguration, chatSettings.mode)
const openAiCompatHasPricing =
modeFields.apiProvider === "openai" &&
modeFields.openAiModelInfo?.inputPrice &&
modeFields.openAiModelInfo?.outputPrice
apiConfiguration?.apiProvider === "openai" &&
apiConfiguration?.openAiModelInfo?.inputPrice &&
apiConfiguration?.openAiModelInfo?.outputPrice
if (openAiCompatHasPricing) {
return true
}
return (
modeFields.apiProvider !== "vscode-lm" && modeFields.apiProvider !== "ollama" && modeFields.apiProvider !== "lmstudio"
apiConfiguration?.apiProvider !== "vscode-lm" &&
apiConfiguration?.apiProvider !== "ollama" &&
apiConfiguration?.apiProvider !== "lmstudio"
)
}, [apiConfiguration, chatSettings.mode])
}, [apiConfiguration?.apiProvider, apiConfiguration?.openAiModelInfo])
const shouldShowPromptCacheInfo = () => {
// Hybrid logic: Show cache info if we have actual cache data,
@@ -1,12 +1,12 @@
import { useExtensionState } from "@/context/ExtensionStateContext"
import { ModelsServiceClient } from "@/services/grpc-client"
import { StringRequest } from "@shared/proto/common"
import { VSCodeDropdown, VSCodeOption } from "@vscode/webview-ui-toolkit/react"
import { ModelsServiceClient, StateServiceClient } from "@/services/grpc-client"
import { BooleanRequest, StringRequest } from "@shared/proto/common"
import { VSCodeButton, VSCodeDropdown, VSCodeOption } from "@vscode/webview-ui-toolkit/react"
import { useCallback, useEffect, useState } from "react"
import { useInterval } from "react-use"
import styled from "styled-components"
import { OPENROUTER_MODEL_PICKER_Z_INDEX } from "./OpenRouterModelPicker"
import { normalizeApiConfiguration } from "@/components/settings/utils/providerUtils"
import { ClineProvider } from "./providers/ClineProvider"
import { OpenRouterProvider } from "./providers/OpenRouterProvider"
import { MistralProvider } from "./providers/MistralProvider"
@@ -30,21 +30,19 @@ import { ClaudeCodeProvider } from "./providers/ClaudeCodeProvider"
import { SapAiCoreProvider } from "./providers/SapAiCoreProvider"
import { BedrockProvider } from "./providers/BedrockProvider"
import { MoonshotProvider } from "./providers/MoonshotProvider"
import { HuggingFaceProvider } from "./providers/HuggingFaceProvider"
import { NebiusProvider } from "./providers/NebiusProvider"
import { LiteLlmProvider } from "./providers/LiteLlmProvider"
import { VSCodeLmProvider } from "./providers/VSCodeLmProvider"
import { LMStudioProvider } from "./providers/LMStudioProvider"
import { useApiConfigurationHandlers } from "./utils/useApiConfigurationHandlers"
import { GroqProvider } from "./providers/GroqProvider"
import { Mode } from "@shared/ChatSettings"
interface ApiOptionsProps {
showSubmitButton?: boolean
showModelOptions: boolean
apiErrorMessage?: string
modelIdErrorMessage?: string
isPopup?: boolean
currentMode: Mode
}
// This is necessary to ensure dropdown opens downward, important for when this is used in popup
@@ -71,16 +69,24 @@ declare module "vscode" {
}
}
const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, isPopup, currentMode }: ApiOptionsProps) => {
const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, isPopup, showSubmitButton }: ApiOptionsProps) => {
// Use full context state for immediate save payload
const { apiConfiguration } = useExtensionState()
const { apiConfiguration, uriScheme } = useExtensionState()
const { selectedProvider } = normalizeApiConfiguration(apiConfiguration, currentMode)
const selectedProvider = apiConfiguration?.apiProvider
const { handleModeFieldChange } = useApiConfigurationHandlers()
const { handleFieldChange } = useApiConfigurationHandlers()
const [ollamaModels, setOllamaModels] = useState<string[]>([])
const handleSubmit = async () => {
try {
await StateServiceClient.setWelcomeViewCompleted(BooleanRequest.create({ value: true }))
} catch (error) {
console.error("Failed to update API configuration or complete welcome view:", error)
}
}
// Poll ollama/vscode-lm models
const requestLocalModels = useCallback(async () => {
if (selectedProvider === "ollama") {
@@ -125,11 +131,7 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
id="api-provider"
value={selectedProvider}
onChange={(e: any) => {
handleModeFieldChange(
{ plan: "planModeApiProvider", act: "actModeApiProvider" },
e.target.value,
currentMode,
)
handleFieldChange("apiProvider", e.target.value)
}}
style={{
minWidth: 130,
@@ -140,13 +142,13 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
<VSCodeOption value="anthropic">Anthropic</VSCodeOption>
<VSCodeOption value="claude-code">Claude Code</VSCodeOption>
<VSCodeOption value="bedrock">Amazon Bedrock</VSCodeOption>
<VSCodeOption value="openai">OpenAI Compatible</VSCodeOption>
<VSCodeOption value="openai-native">OpenAI</VSCodeOption>
<VSCodeOption value="vertex">GCP Vertex AI</VSCodeOption>
<VSCodeOption value="gemini">Google Gemini</VSCodeOption>
<VSCodeOption value="groq">Groq</VSCodeOption>
<VSCodeOption value="deepseek">DeepSeek</VSCodeOption>
<VSCodeOption value="openai">OpenAI Compatible</VSCodeOption>
<VSCodeOption value="mistral">Mistral</VSCodeOption>
<VSCodeOption value="openai-native">OpenAI</VSCodeOption>
<VSCodeOption value="vscode-lm">VS Code LM API</VSCodeOption>
<VSCodeOption value="requesty">Requesty</VSCodeOption>
<VSCodeOption value="fireworks">Fireworks</VSCodeOption>
@@ -157,7 +159,6 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
<VSCodeOption value="ollama">Ollama</VSCodeOption>
<VSCodeOption value="litellm">LiteLLM</VSCodeOption>
<VSCodeOption value="moonshot">Moonshot AI</VSCodeOption>
<VSCodeOption value="huggingface">Hugging Face</VSCodeOption>
<VSCodeOption value="nebius">Nebius AI Studio</VSCodeOption>
<VSCodeOption value="asksage">AskSage</VSCodeOption>
<VSCodeOption value="xai">xAI</VSCodeOption>
@@ -168,116 +169,112 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
</DropdownContainer>
{apiConfiguration && selectedProvider === "cline" && (
<ClineProvider showModelOptions={showModelOptions} isPopup={isPopup} currentMode={currentMode} />
<ClineProvider showModelOptions={showModelOptions} isPopup={isPopup} />
)}
{apiConfiguration && selectedProvider === "asksage" && (
<AskSageProvider showModelOptions={showModelOptions} isPopup={isPopup} currentMode={currentMode} />
<AskSageProvider showModelOptions={showModelOptions} isPopup={isPopup} />
)}
{apiConfiguration && selectedProvider === "anthropic" && (
<AnthropicProvider showModelOptions={showModelOptions} isPopup={isPopup} currentMode={currentMode} />
<AnthropicProvider showModelOptions={showModelOptions} isPopup={isPopup} />
)}
{apiConfiguration && selectedProvider === "claude-code" && (
<ClaudeCodeProvider showModelOptions={showModelOptions} isPopup={isPopup} currentMode={currentMode} />
<ClaudeCodeProvider showModelOptions={showModelOptions} isPopup={isPopup} />
)}
{apiConfiguration && selectedProvider === "openai-native" && (
<OpenAINativeProvider showModelOptions={showModelOptions} isPopup={isPopup} currentMode={currentMode} />
<OpenAINativeProvider showModelOptions={showModelOptions} isPopup={isPopup} />
)}
{apiConfiguration && selectedProvider === "qwen" && (
<QwenProvider showModelOptions={showModelOptions} isPopup={isPopup} currentMode={currentMode} />
<QwenProvider showModelOptions={showModelOptions} isPopup={isPopup} />
)}
{apiConfiguration && selectedProvider === "doubao" && (
<DoubaoProvider showModelOptions={showModelOptions} isPopup={isPopup} currentMode={currentMode} />
<DoubaoProvider showModelOptions={showModelOptions} isPopup={isPopup} />
)}
{apiConfiguration && selectedProvider === "mistral" && (
<MistralProvider showModelOptions={showModelOptions} isPopup={isPopup} currentMode={currentMode} />
<MistralProvider showModelOptions={showModelOptions} isPopup={isPopup} />
)}
{apiConfiguration && selectedProvider === "openrouter" && (
<OpenRouterProvider showModelOptions={showModelOptions} isPopup={isPopup} currentMode={currentMode} />
<OpenRouterProvider showModelOptions={showModelOptions} isPopup={isPopup} uriScheme={uriScheme} />
)}
{apiConfiguration && selectedProvider === "deepseek" && (
<DeepSeekProvider showModelOptions={showModelOptions} isPopup={isPopup} currentMode={currentMode} />
<DeepSeekProvider showModelOptions={showModelOptions} isPopup={isPopup} />
)}
{apiConfiguration && selectedProvider === "together" && (
<TogetherProvider showModelOptions={showModelOptions} isPopup={isPopup} currentMode={currentMode} />
<TogetherProvider showModelOptions={showModelOptions} isPopup={isPopup} />
)}
{apiConfiguration && selectedProvider === "openai" && (
<OpenAICompatibleProvider showModelOptions={showModelOptions} isPopup={isPopup} currentMode={currentMode} />
<OpenAICompatibleProvider showModelOptions={showModelOptions} isPopup={isPopup} />
)}
{apiConfiguration && selectedProvider === "sambanova" && (
<SambanovaProvider showModelOptions={showModelOptions} isPopup={isPopup} currentMode={currentMode} />
<SambanovaProvider showModelOptions={showModelOptions} isPopup={isPopup} />
)}
{apiConfiguration && selectedProvider === "bedrock" && (
<BedrockProvider showModelOptions={showModelOptions} isPopup={isPopup} currentMode={currentMode} />
<BedrockProvider showModelOptions={showModelOptions} isPopup={isPopup} />
)}
{apiConfiguration && selectedProvider === "vertex" && (
<VertexProvider showModelOptions={showModelOptions} isPopup={isPopup} currentMode={currentMode} />
<VertexProvider showModelOptions={showModelOptions} isPopup={isPopup} />
)}
{apiConfiguration && selectedProvider === "gemini" && (
<GeminiProvider showModelOptions={showModelOptions} isPopup={isPopup} currentMode={currentMode} />
<GeminiProvider showModelOptions={showModelOptions} isPopup={isPopup} />
)}
{apiConfiguration && selectedProvider === "requesty" && (
<RequestyProvider showModelOptions={showModelOptions} isPopup={isPopup} currentMode={currentMode} />
<RequestyProvider showModelOptions={showModelOptions} isPopup={isPopup} />
)}
{apiConfiguration && selectedProvider === "fireworks" && (
<FireworksProvider showModelOptions={showModelOptions} isPopup={isPopup} currentMode={currentMode} />
<FireworksProvider showModelOptions={showModelOptions} isPopup={isPopup} />
)}
{apiConfiguration && selectedProvider === "vscode-lm" && <VSCodeLmProvider currentMode={currentMode} />}
{apiConfiguration && selectedProvider === "vscode-lm" && <VSCodeLmProvider />}
{apiConfiguration && selectedProvider === "groq" && (
<GroqProvider showModelOptions={showModelOptions} isPopup={isPopup} currentMode={currentMode} />
<GroqProvider showModelOptions={showModelOptions} isPopup={isPopup} />
)}
{apiConfiguration && selectedProvider === "litellm" && (
<LiteLlmProvider showModelOptions={showModelOptions} isPopup={isPopup} currentMode={currentMode} />
<LiteLlmProvider showModelOptions={showModelOptions} isPopup={isPopup} />
)}
{apiConfiguration && selectedProvider === "lmstudio" && (
<LMStudioProvider showModelOptions={showModelOptions} isPopup={isPopup} currentMode={currentMode} />
<LMStudioProvider showModelOptions={showModelOptions} isPopup={isPopup} />
)}
{apiConfiguration && selectedProvider === "ollama" && (
<OllamaProvider showModelOptions={showModelOptions} isPopup={isPopup} currentMode={currentMode} />
<OllamaProvider showModelOptions={showModelOptions} isPopup={isPopup} />
)}
{apiConfiguration && selectedProvider === "moonshot" && (
<MoonshotProvider showModelOptions={showModelOptions} isPopup={isPopup} currentMode={currentMode} />
)}
{apiConfiguration && selectedProvider === "huggingface" && (
<HuggingFaceProvider showModelOptions={showModelOptions} isPopup={isPopup} currentMode={currentMode} />
<MoonshotProvider showModelOptions={showModelOptions} isPopup={isPopup} />
)}
{apiConfiguration && selectedProvider === "nebius" && (
<NebiusProvider showModelOptions={showModelOptions} isPopup={isPopup} currentMode={currentMode} />
<NebiusProvider showModelOptions={showModelOptions} isPopup={isPopup} />
)}
{apiConfiguration && selectedProvider === "xai" && (
<XaiProvider showModelOptions={showModelOptions} isPopup={isPopup} currentMode={currentMode} />
<XaiProvider showModelOptions={showModelOptions} isPopup={isPopup} />
)}
{apiConfiguration && selectedProvider === "cerebras" && (
<CerebrasProvider showModelOptions={showModelOptions} isPopup={isPopup} currentMode={currentMode} />
<CerebrasProvider showModelOptions={showModelOptions} isPopup={isPopup} />
)}
{apiConfiguration && selectedProvider === "sapaicore" && (
<SapAiCoreProvider showModelOptions={showModelOptions} isPopup={isPopup} currentMode={currentMode} />
<SapAiCoreProvider showModelOptions={showModelOptions} isPopup={isPopup} />
)}
{apiErrorMessage && (
@@ -300,6 +297,12 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
{modelIdErrorMessage}
</p>
)}
{showSubmitButton && (
<VSCodeButton onClick={handleSubmit} disabled={apiErrorMessage != null} className="mt-0.75" title="Submit">
Let's go!
</VSCodeButton>
)}
</div>
)
}
@@ -12,19 +12,15 @@ import { highlight } from "../history/HistoryView"
import { ModelInfoView } from "./common/ModelInfoView"
import { normalizeApiConfiguration } from "./utils/providerUtils"
import { useApiConfigurationHandlers } from "./utils/useApiConfigurationHandlers"
import { getModeSpecificFields } from "./utils/providerUtils"
import { Mode } from "@shared/ChatSettings"
export interface GroqModelPickerProps {
isPopup?: boolean
currentMode: Mode
}
const GroqModelPicker: React.FC<GroqModelPickerProps> = ({ isPopup, currentMode }) => {
const GroqModelPicker: React.FC<GroqModelPickerProps> = ({ isPopup }) => {
const { apiConfiguration, groqModels: dynamicGroqModels, setGroqModels } = useExtensionState()
const { handleModeFieldsChange } = useApiConfigurationHandlers()
const modeFields = getModeSpecificFields(apiConfiguration, currentMode)
const [searchTerm, setSearchTerm] = useState(modeFields.groqModelId || groqDefaultModelId)
const { handleFieldsChange } = useApiConfigurationHandlers()
const [searchTerm, setSearchTerm] = useState(apiConfiguration?.groqModelId || groqDefaultModelId)
const [debouncedSearchTerm, setDebouncedSearchTerm] = useState(searchTerm)
const [isDropdownVisible, setIsDropdownVisible] = useState(false)
const [selectedIndex, setSelectedIndex] = useState(-1)
@@ -36,23 +32,16 @@ const GroqModelPicker: React.FC<GroqModelPickerProps> = ({ isPopup, currentMode
// Use dynamic models if available, otherwise fall back to static models
const modelInfo = dynamicGroqModels?.[newModelId] || groqModels[newModelId as keyof typeof groqModels]
handleModeFieldsChange(
{
groqModelId: { plan: "planModeGroqModelId", act: "actModeGroqModelId" },
groqModelInfo: { plan: "planModeGroqModelInfo", act: "actModeGroqModelInfo" },
},
{
groqModelId: newModelId,
groqModelInfo: modelInfo,
},
currentMode,
)
handleFieldsChange({
groqModelId: newModelId,
groqModelInfo: modelInfo,
})
setSearchTerm(newModelId)
}
const { selectedModelId, selectedModelInfo } = useMemo(() => {
return normalizeApiConfiguration(apiConfiguration, currentMode)
}, [apiConfiguration, currentMode])
return normalizeApiConfiguration(apiConfiguration)
}, [apiConfiguration])
useMount(() => {
ModelsServiceClient.refreshGroqModels(EmptyRequest.create({}))
@@ -1,212 +0,0 @@
import { EmptyRequest } from "@shared/proto/common"
import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import Fuse from "fuse.js"
import React, { KeyboardEvent, useEffect, useMemo, useRef, useState } from "react"
import { useMount } from "react-use"
import { huggingFaceDefaultModelId, huggingFaceModels } from "@shared/api"
import { Mode } from "@shared/ChatSettings"
import { useExtensionState } from "../../context/ExtensionStateContext"
import { ModelsServiceClient } from "../../services/grpc-client"
import { highlight } from "../history/HistoryView"
import { ModelInfoView } from "./common/ModelInfoView"
import { normalizeApiConfiguration, getModeSpecificFields } from "./utils/providerUtils"
import { useApiConfigurationHandlers } from "./utils/useApiConfigurationHandlers"
export interface HuggingFaceModelPickerProps {
isPopup?: boolean
currentMode: Mode
}
const HuggingFaceModelPicker: React.FC<HuggingFaceModelPickerProps> = ({ isPopup, currentMode }) => {
const { apiConfiguration, huggingFaceModels: dynamicModels, setHuggingFaceModels } = useExtensionState()
const { handleModeFieldsChange } = useApiConfigurationHandlers()
const modeFields = getModeSpecificFields(apiConfiguration, currentMode)
const [searchTerm, setSearchTerm] = useState(modeFields.huggingFaceModelId || huggingFaceDefaultModelId)
const [isDropdownVisible, setIsDropdownVisible] = useState(false)
const [selectedIndex, setSelectedIndex] = useState(-1)
const dropdownRef = useRef<HTMLDivElement>(null)
const itemRefs = useRef<(HTMLDivElement | null)[]>([])
const dropdownListRef = useRef<HTMLDivElement>(null)
const handleModelChange = (newModelId: string) => {
const allModels = { ...huggingFaceModels, ...dynamicModels }
const modelInfo = allModels[newModelId as keyof typeof allModels]
handleModeFieldsChange(
{
huggingFaceModelId: { plan: "planModeHuggingFaceModelId", act: "actModeHuggingFaceModelId" },
huggingFaceModelInfo: { plan: "planModeHuggingFaceModelInfo", act: "actModeHuggingFaceModelInfo" },
},
{
huggingFaceModelId: newModelId,
huggingFaceModelInfo: modelInfo,
},
currentMode,
)
setSearchTerm(newModelId)
}
const { selectedModelId, selectedModelInfo } = useMemo(() => {
return normalizeApiConfiguration(apiConfiguration, currentMode)
}, [apiConfiguration, currentMode])
useMount(() => {
ModelsServiceClient.refreshHuggingFaceModels(EmptyRequest.create({}))
.then((response) => {
setHuggingFaceModels({
[huggingFaceDefaultModelId]: huggingFaceModels[huggingFaceDefaultModelId],
...response.models,
})
})
.catch((err) => {
console.error("Failed to refresh Hugging Face models:", err)
})
})
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
setIsDropdownVisible(false)
}
}
document.addEventListener("mousedown", handleClickOutside)
return () => {
document.removeEventListener("mousedown", handleClickOutside)
}
}, [])
const allModels = useMemo(() => {
return { ...huggingFaceModels, ...dynamicModels }
}, [dynamicModels])
const modelIds = useMemo(() => {
return Object.keys(allModels).sort((a, b) => a.localeCompare(b))
}, [allModels])
const searchableItems = useMemo(() => {
return modelIds.map((id) => ({
id,
html: id,
}))
}, [modelIds])
const fuse = useMemo(() => {
return new Fuse(searchableItems, {
keys: ["html"],
threshold: 0.6,
shouldSort: true,
isCaseSensitive: false,
ignoreLocation: false,
includeMatches: true,
minMatchCharLength: 1,
})
}, [searchableItems])
const modelSearchResults = useMemo(() => {
let results: { id: string; html: string }[] = searchTerm
? highlight(fuse.search(searchTerm), "model-item-highlight")
: searchableItems
return results
}, [searchTerm, fuse, searchableItems])
const handleKeyDown = (e: KeyboardEvent<HTMLElement>) => {
if (!isDropdownVisible) return
switch (e.key) {
case "ArrowDown":
e.preventDefault()
setSelectedIndex((prev) => (prev < modelSearchResults.length - 1 ? prev + 1 : 0))
break
case "ArrowUp":
e.preventDefault()
setSelectedIndex((prev) => (prev > 0 ? prev - 1 : modelSearchResults.length - 1))
break
case "Enter":
e.preventDefault()
if (selectedIndex >= 0 && selectedIndex < modelSearchResults.length) {
const selectedModelId = modelSearchResults[selectedIndex].id
handleModelChange(selectedModelId)
setIsDropdownVisible(false)
}
break
case "Escape":
e.preventDefault()
setIsDropdownVisible(false)
break
}
}
useEffect(() => {
if (selectedIndex >= 0 && itemRefs.current[selectedIndex] && dropdownListRef.current) {
const selectedItem = itemRefs.current[selectedIndex]
const dropdown = dropdownListRef.current
const itemOffsetTop = selectedItem.offsetTop
const itemHeight = selectedItem.offsetHeight
const dropdownScrollTop = dropdown.scrollTop
const dropdownHeight = dropdown.offsetHeight
if (itemOffsetTop < dropdownScrollTop) {
dropdown.scrollTop = itemOffsetTop
} else if (itemOffsetTop + itemHeight > dropdownScrollTop + dropdownHeight) {
dropdown.scrollTop = itemOffsetTop + itemHeight - dropdownHeight
}
}
}, [selectedIndex])
return (
<div className="w-full">
<div className="flex flex-col">
<label htmlFor="hf-model-search">
<span className="font-medium">Model</span>
</label>
<div ref={dropdownRef} className="relative w-full">
<VSCodeTextField
id="hf-model-search"
placeholder="Search models..."
value={searchTerm}
onInput={(e: any) => {
setSearchTerm(e.target.value)
setIsDropdownVisible(true)
setSelectedIndex(-1)
}}
onFocus={() => setIsDropdownVisible(true)}
onKeyDown={handleKeyDown}
className="w-full relative z-[1000]"
/>
{isDropdownVisible && (
<div
ref={dropdownListRef}
className={`absolute top-[calc(100%-3px)] left-0 w-[calc(100%-2px)] ${
isPopup ? "max-h-[90px]" : "max-h-[200px]"
} overflow-y-auto bg-[var(--vscode-dropdown-background)] border border-[var(--vscode-list-activeSelectionBackground)] z-[999] rounded-b-[3px]`}>
{modelSearchResults.map((result, index) => (
<div
key={result.id}
ref={(el: HTMLDivElement | null) => (itemRefs.current[index] = el)}
className={`p-[5px_10px] cursor-pointer break-all whitespace-normal ${
index === selectedIndex ? "bg-[var(--vscode-list-activeSelectionBackground)]" : ""
} hover:bg-[var(--vscode-list-activeSelectionBackground)]`}
onMouseEnter={() => setSelectedIndex(index)}
onClick={() => {
handleModelChange(result.id)
setIsDropdownVisible(false)
}}>
<div
dangerouslySetInnerHTML={{ __html: result.html }}
className="[&_.model-item-highlight]:bg-[var(--vscode-editor-findMatchHighlightBackground)] [&_.model-item-highlight]:text-inherit"
/>
</div>
))}
</div>
)}
</div>
</div>
<ModelInfoView selectedModelId={selectedModelId} modelInfo={selectedModelInfo} isPopup={isPopup} />
</div>
)
}
export { HuggingFaceModelPicker }

Some files were not shown because too many files have changed in this diff Show More