Compare commits

...
Author SHA1 Message Date
abeatrix 3572772df2 Merge branch 'main' into bee/storybook 2025-07-18 17:55:37 -07:00
abeatrix 3e0e541332 merge 2025-07-17 19:47:32 -07:00
abeatrix a8823ab048 Merge branch 'bee/storybook' into separate-plan-act-model-settings 2025-07-17 10:36:04 -07:00
abeatrix 1cf48eca36 remove "esnext.disposable" from lib 2025-07-16 20:32:02 -07:00
celestial-vault 28022f56fd fix code after merge conflicts; switch groq provider to plan/act paradigm 2025-07-16 19:27:22 -07:00
celestial-vault 28a71ecd85 merge conflicts 2025-07-16 18:29:26 -07:00
abeatrix 85245ca18f Merge branch 'main' into bee/storybook 2025-07-16 18:20:24 -07:00
abeatrix 8b49b5ef9f revert accidental changes 2025-07-16 15:03:55 -07:00
abeatrix d29e4e07f8 revert workspace 2025-07-16 14:55:24 -07:00
abeatrix 7a8ab90a49 clean up 2025-07-16 14:29:39 -07:00
abeatrix 09032188c1 fix api options 2025-07-16 13:50:43 -07:00
abeatrix d18492ca37 Fix ESLint errors: rename Boolean import and fix React Hook usage 2025-07-16 11:51:42 -07:00
abeatrix 5f78128cd0 Support theme switching 2025-07-16 11:51:15 -07:00
abeatrix b8bd3af2f1 update 2025-07-16 01:44:07 -07:00
abeatrix d56a3ee1bc Merge branch 'main' into bee/storybook 2025-07-16 01:24:35 -07:00
celestial-vault 34e2f0dc41 fix frontend types 2025-07-15 13:53:09 -07:00
celestial-vault 4b1429c667 fix type errors 2025-07-15 13:32:31 -07:00
celestial-vault a6eb8765c5 add in moonshotApiKey to settings-conversion after main merge 2025-07-15 11:28:52 -07:00
celestial-vault 0bb13b1a36 merge conflicts 2025-07-15 11:22:04 -07:00
celestial-vault 574f82a171 post merge issues fixed: bedrock and cline api build handlers; current mode reading in controller/index.ts; adding a couple fields to welcomeView migration 2025-07-15 10:07:14 -07:00
abeatrix 0f7904e52b Add Storybook for webview-ui
- Adds Storybook to the webview-ui project for improved component development and testing.
- Includes necessary dependencies such as `@storybook/react-vite`, `eslint-plugin-storybook`, and `storybook`.
- Updates the `.eslintrc.json` file to include the `plugin:storybook/recommended` ESLint plugin.
- Adds a mock `ExtensionState` and `ExtensionStateProvider` for use in Storybook stories, allowing components to be developed in isolation.
- Updates `.gitignore` to exclude Storybook log files and the static Storybook build directory.
2025-07-14 19:33:55 -07:00
celestial-vault 21eaa754ac merge conflicts 2025-07-14 19:23:36 -07:00
celestial-vault 5d47aa5f8f use separate fields for plan and act mode for ephemeral model settings 2025-07-10 23:34:52 -07:00
celestial-vault f2c9b4f42b add provider handler options types and only pass those fields when building 2025-07-08 10:57:23 -05:00
88 changed files with 7484 additions and 2333 deletions
+3 -1
View File
@@ -36,6 +36,8 @@ webview-ui/src/services/grpc-client.ts
# Host bridge
src/hosts/vscode/client/host-grpc-client.ts
src/standalone/server-setup.ts
# Storybook
*storybook.log
storybook-static
# E2E Tests
test-results
+1337 -668
View File
File diff suppressed because it is too large Load Diff
+2 -1
View File
@@ -359,7 +359,8 @@
"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"
"report-issue": "node scripts/report-issue.js",
"storybook": "cd webview-ui && npm run storybook"
},
"lint-staged": {
"*": [
+115 -84
View File
@@ -168,88 +168,119 @@ message LiteLLMModelInfo {
// Main ApiConfiguration message
message ModelsApiConfiguration {
// 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;
optional string hugging_face_api_key = 81;
optional string hugging_face_model_id = 82;
optional OpenRouterModelInfo hugging_face_model_info = 83;
// 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;
}
+102 -115
View File
@@ -118,126 +118,113 @@ message UpdateSettingsRequest {
// Complete API Configuration message
message ApiConfiguration {
// Core API fields
optional string api_provider = 1;
optional string api_model_id = 2;
optional string api_key = 3; // anthropic
optional string api_base_url = 4;
// 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;
// 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;
// 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;
// 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;
// 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;
// Favorited model IDs
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;
repeated string favorited_model_ids = 300;
// Extension fields for Bedrock Api Keys
optional string aws_authentication = 78;
optional string aws_bedrock_api_key = 79;
optional string aws_authentication = 301;
optional string aws_bedrock_api_key = 302;
// Moonshot
optional string moonshot_api_key = 80;
optional string moonshot_api_line = 81;
optional string cline_account_id = 303;
}
+89 -63
View File
@@ -41,27 +41,33 @@ export interface SingleCompletionHandler {
completePrompt(prompt: string): Promise<string>
}
function createHandlerForProvider(apiProvider: string | undefined, options: Omit<ApiConfiguration, "apiProvider">): ApiHandler {
function createHandlerForProvider(
apiProvider: string | undefined,
options: Omit<ApiConfiguration, "apiProvider">,
mode: "plan" | "act",
): ApiHandler {
switch (apiProvider) {
case "anthropic":
return new AnthropicHandler({
apiKey: options.apiKey,
anthropicBaseUrl: options.anthropicBaseUrl,
apiModelId: options.apiModelId,
thinkingBudgetTokens: options.thinkingBudgetTokens,
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
thinkingBudgetTokens:
mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens,
})
case "openrouter":
return new OpenRouterHandler({
openRouterApiKey: options.openRouterApiKey,
openRouterModelId: options.openRouterModelId,
openRouterModelInfo: options.openRouterModelInfo,
openRouterModelId: mode === "plan" ? options.planModeOpenRouterModelId : options.actModeOpenRouterModelId,
openRouterModelInfo: mode === "plan" ? options.planModeOpenRouterModelInfo : options.actModeOpenRouterModelInfo,
openRouterProviderSorting: options.openRouterProviderSorting,
reasoningEffort: options.reasoningEffort,
thinkingBudgetTokens: options.thinkingBudgetTokens,
reasoningEffort: mode === "plan" ? options.planModeReasoningEffort : options.actModeReasoningEffort,
thinkingBudgetTokens:
mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens,
})
case "bedrock":
return new AwsBedrockHandler({
apiModelId: options.apiModelId,
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
awsAccessKey: options.awsAccessKey,
awsSecretKey: options.awsSecretKey,
awsSessionToken: options.awsSessionToken,
@@ -73,16 +79,20 @@ function createHandlerForProvider(apiProvider: string | undefined, options: Omit
awsUseProfile: options.awsUseProfile,
awsProfile: options.awsProfile,
awsBedrockEndpoint: options.awsBedrockEndpoint,
awsBedrockCustomSelected: options.awsBedrockCustomSelected,
awsBedrockCustomModelBaseId: options.awsBedrockCustomModelBaseId,
thinkingBudgetTokens: options.thinkingBudgetTokens,
awsBedrockCustomSelected:
mode === "plan" ? options.planModeAwsBedrockCustomSelected : options.actModeAwsBedrockCustomSelected,
awsBedrockCustomModelBaseId:
mode === "plan" ? options.planModeAwsBedrockCustomModelBaseId : options.actModeAwsBedrockCustomModelBaseId,
thinkingBudgetTokens:
mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens,
})
case "vertex":
return new VertexHandler({
vertexProjectId: options.vertexProjectId,
vertexRegion: options.vertexRegion,
apiModelId: options.apiModelId,
thinkingBudgetTokens: options.thinkingBudgetTokens,
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
thinkingBudgetTokens:
mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens,
geminiApiKey: options.geminiApiKey,
geminiBaseUrl: options.geminiBaseUrl,
taskId: options.taskId,
@@ -93,21 +103,21 @@ function createHandlerForProvider(apiProvider: string | undefined, options: Omit
openAiBaseUrl: options.openAiBaseUrl,
azureApiVersion: options.azureApiVersion,
openAiHeaders: options.openAiHeaders,
openAiModelId: options.openAiModelId,
openAiModelInfo: options.openAiModelInfo,
reasoningEffort: options.reasoningEffort,
openAiModelId: mode === "plan" ? options.planModeOpenAiModelId : options.actModeOpenAiModelId,
openAiModelInfo: mode === "plan" ? options.planModeOpenAiModelInfo : options.actModeOpenAiModelInfo,
reasoningEffort: mode === "plan" ? options.planModeReasoningEffort : options.actModeReasoningEffort,
})
case "ollama":
return new OllamaHandler({
ollamaBaseUrl: options.ollamaBaseUrl,
ollamaModelId: options.ollamaModelId,
ollamaModelId: mode === "plan" ? options.planModeOllamaModelId : options.actModeOllamaModelId,
ollamaApiOptionsCtxNum: options.ollamaApiOptionsCtxNum,
requestTimeoutMs: options.requestTimeoutMs,
})
case "lmstudio":
return new LmStudioHandler({
lmStudioBaseUrl: options.lmStudioBaseUrl,
lmStudioModelId: options.lmStudioModelId,
lmStudioModelId: mode === "plan" ? options.planModeLmStudioModelId : options.actModeLmStudioModelId,
})
case "gemini":
return new GeminiHandler({
@@ -115,78 +125,85 @@ function createHandlerForProvider(apiProvider: string | undefined, options: Omit
vertexRegion: options.vertexRegion,
geminiApiKey: options.geminiApiKey,
geminiBaseUrl: options.geminiBaseUrl,
thinkingBudgetTokens: options.thinkingBudgetTokens,
apiModelId: options.apiModelId,
thinkingBudgetTokens:
mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens,
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
taskId: options.taskId,
})
case "openai-native":
return new OpenAiNativeHandler({
openAiNativeApiKey: options.openAiNativeApiKey,
reasoningEffort: options.reasoningEffort,
apiModelId: options.apiModelId,
reasoningEffort: mode === "plan" ? options.planModeReasoningEffort : options.actModeReasoningEffort,
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
})
case "deepseek":
return new DeepSeekHandler({
deepSeekApiKey: options.deepSeekApiKey,
apiModelId: options.apiModelId,
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
})
case "requesty":
return new RequestyHandler({
requestyApiKey: options.requestyApiKey,
reasoningEffort: options.reasoningEffort,
thinkingBudgetTokens: options.thinkingBudgetTokens,
requestyModelId: options.requestyModelId,
requestyModelInfo: options.requestyModelInfo,
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,
})
case "fireworks":
return new FireworksHandler({
fireworksApiKey: options.fireworksApiKey,
fireworksModelId: options.fireworksModelId,
fireworksModelId: mode === "plan" ? options.planModeFireworksModelId : options.actModeFireworksModelId,
fireworksModelMaxCompletionTokens: options.fireworksModelMaxCompletionTokens,
fireworksModelMaxTokens: options.fireworksModelMaxTokens,
})
case "together":
return new TogetherHandler({
togetherApiKey: options.togetherApiKey,
togetherModelId: options.togetherModelId,
togetherModelId: mode === "plan" ? options.planModeTogetherModelId : options.actModeTogetherModelId,
})
case "qwen":
return new QwenHandler({
qwenApiKey: options.qwenApiKey,
qwenApiLine: options.qwenApiLine,
apiModelId: options.apiModelId,
thinkingBudgetTokens: options.thinkingBudgetTokens,
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
thinkingBudgetTokens:
mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens,
})
case "doubao":
return new DoubaoHandler({
doubaoApiKey: options.doubaoApiKey,
apiModelId: options.apiModelId,
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
})
case "mistral":
return new MistralHandler({
mistralApiKey: options.mistralApiKey,
apiModelId: options.apiModelId,
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
})
case "vscode-lm":
return new VsCodeLmHandler({
vsCodeLmModelSelector: options.vsCodeLmModelSelector,
vsCodeLmModelSelector:
mode === "plan" ? options.planModeVsCodeLmModelSelector : options.actModeVsCodeLmModelSelector,
})
case "cline":
return new ClineHandler({
clineAccountId: options.clineAccountId,
taskId: options.taskId,
reasoningEffort: options.reasoningEffort,
thinkingBudgetTokens: options.thinkingBudgetTokens,
reasoningEffort: mode === "plan" ? options.planModeReasoningEffort : options.actModeReasoningEffort,
thinkingBudgetTokens:
mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens,
openRouterProviderSorting: options.openRouterProviderSorting,
openRouterModelId: options.openRouterModelId,
openRouterModelInfo: options.openRouterModelInfo,
openRouterModelId: mode === "plan" ? options.planModeOpenRouterModelId : options.actModeOpenRouterModelId,
openRouterModelInfo: mode === "plan" ? options.planModeOpenRouterModelInfo : options.actModeOpenRouterModelInfo,
})
case "litellm":
return new LiteLlmHandler({
liteLlmApiKey: options.liteLlmApiKey,
liteLlmBaseUrl: options.liteLlmBaseUrl,
liteLlmModelId: options.liteLlmModelId,
liteLlmModelInfo: options.liteLlmModelInfo,
thinkingBudgetTokens: options.thinkingBudgetTokens,
liteLlmModelId: mode === "plan" ? options.planModeLiteLlmModelId : options.actModeLiteLlmModelId,
liteLlmModelInfo: mode === "plan" ? options.planModeLiteLlmModelInfo : options.actModeLiteLlmModelInfo,
thinkingBudgetTokens:
mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens,
liteLlmUsePromptCache: options.liteLlmUsePromptCache,
taskId: options.taskId,
})
@@ -194,7 +211,7 @@ function createHandlerForProvider(apiProvider: string | undefined, options: Omit
return new MoonshotHandler({
moonshotApiKey: options.moonshotApiKey,
moonshotApiLine: options.moonshotApiLine,
apiModelId: options.apiModelId,
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
})
case "huggingface":
return new HuggingFaceHandler({
@@ -204,36 +221,36 @@ function createHandlerForProvider(apiProvider: string | undefined, options: Omit
case "nebius":
return new NebiusHandler({
nebiusApiKey: options.nebiusApiKey,
apiModelId: options.apiModelId,
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
})
case "asksage":
return new AskSageHandler({
asksageApiKey: options.asksageApiKey,
asksageApiUrl: options.asksageApiUrl,
apiModelId: options.apiModelId,
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
})
case "xai":
return new XAIHandler({
xaiApiKey: options.xaiApiKey,
reasoningEffort: options.reasoningEffort,
apiModelId: options.apiModelId,
reasoningEffort: mode === "plan" ? options.planModeReasoningEffort : options.actModeReasoningEffort,
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
})
case "sambanova":
return new SambanovaHandler({
sambanovaApiKey: options.sambanovaApiKey,
apiModelId: options.apiModelId,
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
})
case "cerebras":
return new CerebrasHandler({
cerebrasApiKey: options.cerebrasApiKey,
apiModelId: options.apiModelId,
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
})
case "groq":
return new GroqHandler({
groqApiKey: options.groqApiKey,
groqModelId: options.groqModelId,
groqModelInfo: options.groqModelInfo,
apiModelId: options.apiModelId,
groqModelId: mode === "plan" ? options.planModeGroqModelId : options.actModeGroqModelId,
groqModelInfo: mode === "plan" ? options.planModeGroqModelInfo : options.actModeGroqModelInfo,
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
})
case "sapaicore":
return new SapAiCoreHandler({
@@ -242,37 +259,46 @@ function createHandlerForProvider(apiProvider: string | undefined, options: Omit
sapAiCoreTokenUrl: options.sapAiCoreTokenUrl,
sapAiResourceGroup: options.sapAiResourceGroup,
sapAiCoreBaseUrl: options.sapAiCoreBaseUrl,
apiModelId: options.apiModelId,
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
})
case "claude-code":
return new ClaudeCodeHandler({
claudeCodePath: options.claudeCodePath,
apiModelId: options.apiModelId,
thinkingBudgetTokens: options.thinkingBudgetTokens,
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
thinkingBudgetTokens:
mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens,
})
default:
return new AnthropicHandler({
apiKey: options.apiKey,
anthropicBaseUrl: options.anthropicBaseUrl,
apiModelId: options.apiModelId,
thinkingBudgetTokens: options.thinkingBudgetTokens,
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
thinkingBudgetTokens:
mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens,
})
}
}
export function buildApiHandler(configuration: ApiConfiguration): ApiHandler {
const { apiProvider, ...options } = configuration
export function buildApiHandler(configuration: ApiConfiguration, mode: "plan" | "act"): ApiHandler {
const { planModeApiProvider, actModeApiProvider, ...options } = configuration
const apiProvider = mode === "plan" ? planModeApiProvider : actModeApiProvider
// 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 {
if (options.thinkingBudgetTokens && options.thinkingBudgetTokens > 0) {
const handler = createHandlerForProvider(apiProvider, options)
const thinkingBudgetTokens = mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens
if (thinkingBudgetTokens && thinkingBudgetTokens > 0) {
const handler = createHandlerForProvider(apiProvider, options, mode)
const modelInfo = handler.getModel().info
if (modelInfo.maxTokens && options.thinkingBudgetTokens > modelInfo.maxTokens) {
if (modelInfo.maxTokens && thinkingBudgetTokens > modelInfo.maxTokens) {
const clippedValue = modelInfo.maxTokens - 1
options.thinkingBudgetTokens = clippedValue
if (mode === "plan") {
options.planModeThinkingBudgetTokens = clippedValue
} else {
options.actModeThinkingBudgetTokens = clippedValue
}
} else {
return handler // don't rebuild unless its necessary
}
@@ -281,5 +307,5 @@ export function buildApiHandler(configuration: ApiConfiguration): ApiHandler {
console.error("buildApiHandler error:", error)
}
return createHandlerForProvider(apiProvider, options)
return createHandlerForProvider(apiProvider, options, mode)
}
+13 -13
View File
@@ -203,7 +203,7 @@ describe("AwsBedrockHandler", () => {
})
const mockOptions: ApiHandlerOptions = {
apiModelId: "anthropic.claude-3-7-sonnet-20250219-v1:0",
actModeApiModelId: "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: "",
awsBedrockCustomSelected: false,
awsBedrockCustomModelBaseId: undefined,
thinkingBudgetTokens: 1600,
actModeAwsBedrockCustomSelected: false,
actModeAwsBedrockCustomModelBaseId: undefined,
actModeThinkingBudgetTokens: 1600,
}
const mockModelInfo = {
@@ -616,8 +616,8 @@ describe("AwsBedrockHandler", () => {
it("should return raw model ID for custom models", async () => {
const customOptions: ApiHandlerOptions = {
...mockOptions,
awsBedrockCustomSelected: true,
apiModelId:
actModeAwsBedrockCustomSelected: true,
actModeApiModelId:
"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,
awsBedrockCustomSelected: true,
apiModelId: "my-namespace/my-custom-model",
actModeAwsBedrockCustomSelected: true,
actModeApiModelId: "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,
awsBedrockCustomSelected: true,
apiModelId: "arn:aws:bedrock:us-west-2:123456789012:custom-model/my-model",
actModeAwsBedrockCustomSelected: true,
actModeApiModelId: "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,
awsBedrockCustomSelected: true,
apiModelId:
actModeAwsBedrockCustomSelected: true,
actModeApiModelId:
"arn:aws:bedrock:us-west-2:123456789012:custom-model/anthropic.claude-3-5-sonnet-20241022-v2:0/Qk8MMyLmRd",
awsBedrockCustomModelBaseId: "anthropic.claude-3-5-sonnet-20241022-v2:0",
actModeAwsBedrockCustomModelBaseId: "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 = {
ollamaModelId: "llama2",
actModeOllamaModelId: "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: ApiHandlerOptions
private options: AnthropicHandlerOptions
private client: Anthropic | undefined
constructor(options: AnthropicHandlerOptions) {
+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: ApiHandlerOptions
private options: GeminiHandlerOptions
private client: GoogleGenAI | undefined
constructor(options: GeminiHandlerOptions) {
+9
View File
@@ -19,6 +19,15 @@ 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,6 +14,12 @@ interface XAIHandlerOptions {
apiModelId?: string
}
interface XAIHandlerOptions {
xaiApiKey?: string
reasoningEffort?: string
apiModelId?: string
}
export class XAIHandler implements ApiHandler {
private options: XAIHandlerOptions
private client: OpenAI | undefined
+46 -158
View File
@@ -50,6 +50,7 @@ export class Controller {
private disposables: vscode.Disposable[] = []
task?: Task
workspaceTracker: WorkspaceTracker
mcpHub: McpHub
accountService: ClineAccountService
@@ -83,7 +84,7 @@ export class Controller {
})
}
private async getCurrentMode(): Promise<"plan" | "act"> {
async getCurrentMode(): Promise<"plan" | "act"> {
return ((await getGlobalState(this.context, "mode")) as "plan" | "act" | undefined) || "act"
}
@@ -112,7 +113,10 @@ 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 updateGlobalState(this.context, "apiProvider", "openrouter")
await Promise.all([
updateGlobalState(this.context, "planModeApiProvider", "openrouter"),
updateGlobalState(this.context, "actModeApiProvider", "openrouter"),
])
await this.postStateToWebview()
getHostBridgeProvider().windowClient.showMessage({
type: ShowMessageType.INFORMATION,
@@ -253,153 +257,10 @@ export class Controller {
// Capture mode switch telemetry | Capture regardless of if we know the taskId
telemetryService.captureModeSwitch(this.task?.taskId ?? "0", 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)
}
}
// 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)
}
// Save only non-mode properties to global storage
@@ -463,19 +324,38 @@ export class Controller {
await this.authService.handleAuthCallback(customToken, provider ? provider : "google")
const clineProvider: ApiProvider = "cline"
await updateGlobalState(this.context, "apiProvider", clineProvider)
// Mark welcome view as completed since user has successfully logged in
await updateGlobalState(this.context, "welcomeViewCompleted", true)
// Get current settings to determine how to update providers
const { planActSeparateModelsSetting } = await getAllExtensionState(this.context)
const currentMode = await this.getCurrentMode()
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)
this.task.api = buildApiHandler(updatedConfig, currentMode)
}
await this.postStateToWebview()
@@ -630,14 +510,21 @@ export class Controller {
}
const openrouter: ApiProvider = "openrouter"
await updateGlobalState(this.context, "apiProvider", openrouter)
const currentMode = await this.getCurrentMode()
await Promise.all([
updateGlobalState(this.context, "planModeApiProvider", openrouter),
updateGlobalState(this.context, "actModeApiProvider", openrouter),
])
await storeSecret(this.context, "openRouterApiKey", apiKey)
await this.postStateToWebview()
if (this.task) {
this.task.api = buildApiHandler({
apiProvider: openrouter,
// Get the updated API configuration (now includes the updated providers)
const { apiConfiguration } = await getAllExtensionState(this.context)
const updatedConfig = {
...apiConfiguration,
openRouterApiKey: apiKey,
})
}
this.task.api = buildApiHandler(updatedConfig, currentMode)
}
// await this.postMessageToWebview({ type: "action", action: "settingsButtonClicked" }) // bad ux if user is on welcome
}
@@ -1013,9 +900,10 @@ 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)
const apiHandler = buildApiHandler(apiConfiguration, currentMode)
// Create a system prompt
const systemPrompt =
@@ -29,7 +29,8 @@ export async function updateApiConfigurationProto(
// Update the task's API handler if there's an active task
if (controller.task) {
controller.task.api = buildApiHandler(appApiConfiguration)
const currentMode = await controller.getCurrentMode()
controller.task.api = buildApiHandler(appApiConfiguration, currentMode)
}
// Post updated state to webview
+2 -1
View File
@@ -21,7 +21,8 @@ export async function updateSettings(controller: Controller, request: UpdateSett
await updateApiConfiguration(controller.context, apiConfiguration)
if (controller.task) {
controller.task.api = buildApiHandler(apiConfiguration)
const currentMode = await controller.getCurrentMode()
controller.task.api = buildApiHandler(apiConfiguration, currentMode)
}
}
+65 -13
View File
@@ -30,25 +30,77 @@ 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 } = await getAllExtensionState(controller.context)
if (apiConfiguration.openRouterModelId && response.models[apiConfiguration.openRouterModelId]) {
await updateGlobalState(
controller.context,
"openRouterModelInfo",
response.models[apiConfiguration.openRouterModelId],
)
await controller.postStateToWebview()
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()
}
}
}
})
handleModelsServiceRequest(controller, "refreshGroqModels", EmptyRequest.create()).then(async (response) => {
if (response && response.models) {
// 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()
// 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()
}
}
}
})
+50 -38
View File
@@ -43,12 +43,9 @@ export type GlobalStateKey =
| "lastShownAnnouncementId"
| "taskHistory"
| "openAiBaseUrl"
| "openAiModelId"
| "openAiModelInfo"
| "openAiHeaders"
| "ollamaBaseUrl"
| "ollamaApiOptionsCtxNum"
| "lmStudioModelId"
| "lmStudioBaseUrl"
| "anthropicBaseUrl"
| "geminiBaseUrl"
@@ -88,40 +85,55 @@ export type GlobalStateKey =
// Settings around plan/act and ephemeral model configuration
| "chatSettings"
| "mode"
// 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"
| "huggingFaceModelId"
| "huggingFaceModelInfo"
| "sapAiCoreModelId"
// Previous mode saved configurations (per workspace)
| "previousModeApiProvider"
| "previousModeModelId"
| "previousModeModelInfo"
| "previousModeVsCodeLmModelSelector"
| "previousModeThinkingBudgetTokens"
| "previousModeReasoningEffort"
| "previousModeAwsBedrockCustomSelected"
| "previousModeAwsBedrockCustomModelBaseId"
| "previousModeSapAiCoreModelId"
| "groqModelId"
| "groqModelInfo"
// 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"
export type LocalStateKey = "localClineRulesToggles" | "localCursorRulesToggles" | "localWindsurfRulesToggles" | "workflowToggles"
+329 -5
View File
@@ -53,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
await updateGlobalState(context, key as GlobalStateKey, workspaceValue)
// Move to global storage using raw VSCode method to avoid type errors
await context.globalState.update(key, workspaceValue)
// Remove from workspace storage
await context.workspaceState.update(key, undefined)
const newWorkspaceValue = await context.workspaceState.get(key)
@@ -169,6 +169,327 @@ 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")
// 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)
}
} 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)
}
// 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)
}
}
// 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("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
@@ -190,8 +511,10 @@ export async function migrateWelcomeViewCompleted(context: vscode.ExtensionConte
config.awsRegion,
config.vertexProjectId,
config.openAiApiKey,
config.ollamaModelId,
config.lmStudioModelId,
config.planModeOllamaModelId,
config.planModeLmStudioModelId,
config.actModeOllamaModelId,
config.actModeLmStudioModelId,
config.liteLlmApiKey,
config.geminiApiKey,
config.openAiNativeApiKey,
@@ -201,7 +524,8 @@ export async function migrateWelcomeViewCompleted(context: vscode.ExtensionConte
config.qwenApiKey,
config.doubaoApiKey,
config.mistralApiKey,
config.vsCodeLmModelSelector,
config.planModeVsCodeLmModelSelector,
config.actModeVsCodeLmModelSelector,
config.clineAccountId,
config.asksageApiKey,
config.xaiApiKey,
+254 -154
View File
@@ -1,5 +1,5 @@
import * as vscode from "vscode"
import { DEFAULT_CHAT_SETTINGS } from "@shared/ChatSettings"
import { DEFAULT_CHAT_SETTINGS, Mode } 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"
@@ -190,8 +190,6 @@ 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>,
@@ -271,8 +269,6 @@ 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
@@ -281,78 +277,115 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
const [
chatSettings,
currentMode,
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,
huggingFaceModelId,
huggingFaceModelInfo,
// 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,
] = await Promise.all([
getGlobalState(context, "chatSettings") as Promise<StoredChatSettings | 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>,
getGlobalState(context, "huggingFaceModelId") as Promise<string | undefined>,
getGlobalState(context, "huggingFaceModelInfo") as Promise<ModelInfo | 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>,
])
const processingStart = performance.now()
let apiProvider: ApiProvider
if (storedApiProvider) {
// Use the explicitly stored provider - this respects user's selection
apiProvider = storedApiProvider
if (planModeApiProvider) {
apiProvider = planModeApiProvider
} 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)
@@ -375,7 +408,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
planActSeparateModelsSetting = planActSeparateModelsSettingRaw
} else {
// default to true for existing users
if (storedApiProvider) {
if (planModeApiProvider) {
planActSeparateModelsSetting = true
} else {
// default to false for new users
@@ -388,8 +421,6 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
return {
apiConfiguration: {
apiProvider,
apiModelId,
apiKey,
openRouterApiKey,
clineAccountId,
@@ -405,19 +436,13 @@ 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,
@@ -425,29 +450,18 @@ 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,
@@ -456,8 +470,6 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
sambanovaApiKey,
cerebrasApiKey,
groqApiKey,
groqModelId,
groqModelInfo,
moonshotApiKey,
nebiusApiKey,
favoritedModelIds,
@@ -467,10 +479,55 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
sapAiCoreBaseUrl,
sapAiCoreTokenUrl,
sapAiResourceGroup,
sapAiCoreModelId,
huggingFaceApiKey,
huggingFaceModelId,
huggingFaceModelInfo,
// 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,
},
isNewUser: isNewUser ?? true,
welcomeViewCompleted,
@@ -486,15 +543,6 @@ 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,
@@ -511,8 +559,6 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
export async function updateApiConfiguration(context: vscode.ExtensionContext, apiConfiguration: ApiConfiguration) {
const {
apiProvider,
apiModelId,
apiKey,
openRouterApiKey,
awsAccessKey,
@@ -526,19 +572,13 @@ 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,
@@ -546,21 +586,13 @@ 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,
@@ -568,19 +600,14 @@ 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,
@@ -588,40 +615,113 @@ export async function updateApiConfiguration(context: vscode.ExtensionContext, a
sapAiCoreBaseUrl,
sapAiCoreTokenUrl,
sapAiResourceGroup,
sapAiCoreModelId,
claudeCodePath,
huggingFaceApiKey,
huggingFaceModelId,
huggingFaceModelInfo,
// 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 = {
// 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,
huggingFaceModelId,
huggingFaceModelInfo,
// 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,
// Global state updates (27 keys)
awsRegion,
+8 -1
View File
@@ -54,6 +54,7 @@ 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
@@ -90,6 +91,7 @@ export class ToolExecutor {
private browserSettings: BrowserSettings,
private cwd: string,
private taskId: string,
private chatSettings: ChatSettings,
// Callbacks to the Task (Entity)
private say: (
@@ -1917,7 +1919,12 @@ 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 providerAndModel = `${await getGlobalState(this.context, "apiProvider")} / ${this.api.getModel().id}`
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}`
// Ask user for confirmation
const bugReportData = JSON.stringify({
+16 -6
View File
@@ -261,12 +261,19 @@ export class Task {
},
}
if (apiConfiguration.apiProvider === "openai" || apiConfiguration.apiProvider === "openai-native") {
effectiveApiConfiguration.reasoningEffort = chatSettings.openAIReasoningEffort
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
}
}
// Now that taskId is initialized, we can build the API handler
this.api = buildApiHandler(effectiveApiConfiguration)
this.api = buildApiHandler(effectiveApiConfiguration, chatSettings.mode)
// Set taskId on browserSession for telemetry tracking
this.browserSession.setTaskId(this.taskId)
@@ -281,10 +288,10 @@ export class Task {
// initialize telemetry
if (historyItem) {
// Open task from history
telemetryService.captureTaskRestarted(this.taskId, apiConfiguration.apiProvider)
telemetryService.captureTaskRestarted(this.taskId, currentProvider)
} else {
// New task started
telemetryService.captureTaskCreated(this.taskId, apiConfiguration.apiProvider)
telemetryService.captureTaskCreated(this.taskId, currentProvider)
}
this.toolExecutor = new ToolExecutor(
@@ -304,6 +311,7 @@ export class Task {
this.browserSettings,
cwd,
this.taskId,
this.chatSettings,
this.say.bind(this),
this.ask.bind(this),
this.saveCheckpoint.bind(this),
@@ -1591,7 +1599,9 @@ export class Task {
private async getCurrentProviderInfo(): Promise<{ modelId: string; providerId: string }> {
const modelId = this.api.getModel()?.id
const providerId = (await getGlobalState(this.getContext(), "apiProvider")) as string
const context = this.getContext()
const providerKey = this.chatSettings.mode === "plan" ? "planModeApiProvider" : "actModeApiProvider"
const providerId: string = (await getGlobalState(context, providerKey)) ?? "cline"
return { modelId, providerId }
}
+4
View File
@@ -27,6 +27,7 @@ import {
migrateCustomInstructionsToGlobalRules,
migrateModeFromWorkspaceStorageToControllerState,
migrateWelcomeViewCompleted,
migrateLegacyApiConfigurationToModeSpecific,
} from "./core/storage/state-migrations"
import { sendFocusChatInputEvent } from "./core/controller/ui/subscribeToFocusChatInput"
@@ -75,6 +76,9 @@ 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)
+2 -1
View File
@@ -274,7 +274,8 @@ 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, "apiProvider", "cline" as ApiProvider)
await updateGlobalState(visibleWebview.controller.context, "planModeApiProvider", "cline")
await updateGlobalState(visibleWebview.controller.context, "actModeApiProvider", "cline")
// Post state to webview to reflect changes
await visibleWebview.controller.postStateToWebview()
+48 -22
View File
@@ -32,20 +32,16 @@ export type ApiProvider =
| "huggingface"
export interface ApiHandlerOptions {
apiModelId?: string
// Global configuration (not mode-specific)
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
@@ -58,38 +54,27 @@ 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
@@ -100,25 +85,66 @@ export interface ApiHandlerOptions {
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
// 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
}
export type ApiConfiguration = ApiHandlerOptions & {
apiProvider?: ApiProvider
planModeApiProvider?: ApiProvider
actModeApiProvider?: ApiProvider
favoritedModelIds?: string[]
}
@@ -318,20 +318,16 @@ function convertProtoToApiProvider(provider: ProtoApiProvider): ApiProvider {
// Converts application ApiConfiguration to proto ApiConfiguration
export function convertApiConfigurationToProto(config: ApiConfiguration): ProtoApiConfiguration {
return {
apiModelId: config.apiModelId,
// Global configuration fields
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,
@@ -344,37 +340,27 @@ export function convertApiConfigurationToProto(config: ApiConfiguration): ProtoA
awsProfile: config.awsProfile,
awsBedrockApiKey: config.awsBedrockApiKey,
awsBedrockEndpoint: config.awsBedrockEndpoint,
awsBedrockCustomSelected: config.awsBedrockCustomSelected,
awsBedrockCustomModelBaseId: config.awsBedrockCustomModelBaseId as string | undefined,
claudeCodePath: config.claudeCodePath,
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,
@@ -385,42 +371,82 @@ export function convertApiConfigurationToProto(config: ApiConfiguration): ProtoA
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,
claudeCodePath: config.claudeCodePath,
// 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),
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),
actModeSapAiCoreModelId: config.actModeSapAiCoreModelId,
// Favorited model IDs
favoritedModelIds: config.favoritedModelIds || [],
}
}
// Converts proto ApiConfiguration to application ApiConfiguration
export function convertProtoToApiConfiguration(protoConfig: ProtoApiConfiguration): ApiConfiguration {
return {
apiModelId: protoConfig.apiModelId,
// Global configuration fields
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,
liteLlmModelInfo: convertProtoToLiteLLMModelInfo(protoConfig.liteLlmModelInfo),
openAiHeaders: Object.keys(protoConfig.openAiHeaders || {}).length > 0 ? protoConfig.openAiHeaders : undefined,
anthropicBaseUrl: protoConfig.anthropicBaseUrl,
openRouterApiKey: protoConfig.openRouterApiKey,
openRouterModelId: protoConfig.openRouterModelId,
openRouterModelInfo: convertProtoToModelInfo(protoConfig.openRouterModelInfo),
openRouterProviderSorting: protoConfig.openRouterProviderSorting,
awsAccessKey: protoConfig.awsAccessKey,
awsSecretKey: protoConfig.awsSecretKey,
@@ -433,37 +459,27 @@ export function convertProtoToApiConfiguration(protoConfig: ProtoApiConfiguratio
awsProfile: protoConfig.awsProfile,
awsBedrockApiKey: protoConfig.awsBedrockApiKey,
awsBedrockEndpoint: protoConfig.awsBedrockEndpoint,
awsBedrockCustomSelected: protoConfig.awsBedrockCustomSelected,
awsBedrockCustomModelBaseId: protoConfig.awsBedrockCustomModelBaseId as BedrockModelId | undefined,
claudeCodePath: protoConfig.claudeCodePath,
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,
@@ -474,21 +490,70 @@ export function convertProtoToApiConfiguration(protoConfig: ProtoApiConfiguratio
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,
claudeCodePath: protoConfig.claudeCodePath,
// 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),
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),
actModeSapAiCoreModelId: protoConfig.actModeSapAiCoreModelId,
// Favorited model IDs
favoritedModelIds:
protoConfig.favoritedModelIds && protoConfig.favoritedModelIds.length > 0 ? protoConfig.favoritedModelIds : undefined,
}
}
@@ -11,49 +11,17 @@ import {
*/
export function convertApiConfigurationToProtoApiConfiguration(config: ApiConfiguration): ProtoApiConfiguration {
return ProtoApiConfiguration.create({
// Core API fields
apiProvider: config.apiProvider,
apiModelId: config.apiModelId,
// Global configuration fields (not mode-specific)
apiKey: config.apiKey,
// Provider-specific API keys
clineAccountId: config.clineAccountId,
openrouterApiKey: config.openRouterApiKey,
taskId: config.taskId,
liteLlmBaseUrl: config.liteLlmBaseUrl,
liteLlmApiKey: config.liteLlmApiKey,
liteLlmUsePromptCache: config.liteLlmUsePromptCache,
openaiHeaders: config.openAiHeaders ? JSON.stringify(config.openAiHeaders) : undefined,
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,
openrouterApiKey: config.openRouterApiKey,
openrouterProviderSorting: config.openRouterProviderSorting,
awsAccessKey: config.awsAccessKey,
awsSecretKey: config.awsSecretKey,
awsSessionToken: config.awsSessionToken,
@@ -65,68 +33,101 @@ export function convertApiConfigurationToProtoApiConfiguration(config: ApiConfig
awsProfile: config.awsProfile,
awsBedrockApiKey: config.awsBedrockApiKey,
awsBedrockEndpoint: config.awsBedrockEndpoint,
// Vertex AI fields
claudeCodePath: config.claudeCodePath,
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,
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
openaiNativeApiKey: config.openAiNativeApiKey,
deepSeekApiKey: config.deepSeekApiKey,
requestyApiKey: config.requestyApiKey,
togetherApiKey: config.togetherApiKey,
fireworksApiKey: config.fireworksApiKey,
fireworksModelMaxCompletionTokens: config.fireworksModelMaxCompletionTokens
? Number(config.fireworksModelMaxCompletionTokens)
: undefined,
fireworksModelMaxTokens: config.fireworksModelMaxTokens ? Number(config.fireworksModelMaxTokens) : undefined,
// Azure specific
qwenApiKey: config.qwenApiKey,
doubaoApiKey: config.doubaoApiKey,
mistralApiKey: config.mistralApiKey,
moonshotApiKey: config.moonshotApiKey,
azureApiVersion: config.azureApiVersion,
// Ollama specific
ollamaApiOptionsCtxNum: config.ollamaApiOptionsCtxNum,
// Qwen specific
qwenApiLine: config.qwenApiLine,
// Moonshot specific
moonshotApiLine: config.moonshotApiLine,
// OpenRouter specific
openrouterProviderSorting: config.openRouterProviderSorting,
// SAP AI Core specific
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,
sapAiCoreClientId: config.sapAiCoreClientId,
sapAiCoreClientSecret: config.sapAiCoreClientSecret,
sapAiCoreBaseUrl: config.sapAiCoreBaseUrl,
sapAiCoreTokenUrl: config.sapAiCoreTokenUrl,
sapAiResourceGroup: config.sapAiResourceGroup,
sapAiCoreTokenUrl: config.sapAiCoreTokenUrl,
sapAiCoreBaseUrl: config.sapAiCoreBaseUrl,
// 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,
// 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,
// Claude Code specific
claudeCodePath: config.claudeCodePath,
// 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,
// Arrays
// Favorited model IDs
favoritedModelIds: config.favoritedModelIds || [],
})
}
@@ -137,45 +138,16 @@ export function convertApiConfigurationToProtoApiConfiguration(config: ApiConfig
export function convertProtoApiConfigurationToApiConfiguration(protoConfig: ProtoApiConfiguration): ApiConfiguration {
// eslint-disable-next-line eslint-rules/no-protobuf-object-literals
const config: ApiConfiguration = {
// Core API fields
apiProvider: protoConfig.apiProvider as ApiProvider,
apiModelId: protoConfig.apiModelId,
// Global configuration fields (not mode-specific)
apiKey: protoConfig.apiKey,
// Provider-specific API keys
clineAccountId: protoConfig.clineAccountId,
openRouterApiKey: protoConfig.openrouterApiKey,
taskId: protoConfig.taskId,
liteLlmBaseUrl: protoConfig.liteLlmBaseUrl,
liteLlmApiKey: protoConfig.liteLlmApiKey,
liteLlmUsePromptCache: protoConfig.liteLlmUsePromptCache,
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,
openRouterApiKey: protoConfig.openrouterApiKey,
openRouterProviderSorting: protoConfig.openrouterProviderSorting,
awsAccessKey: protoConfig.awsAccessKey,
awsSecretKey: protoConfig.awsSecretKey,
awsSessionToken: protoConfig.awsSessionToken,
@@ -187,83 +159,121 @@ export function convertProtoApiConfigurationToApiConfiguration(protoConfig: Prot
awsAuthentication: protoConfig.awsAuthentication,
awsBedrockApiKey: protoConfig.awsBedrockApiKey,
awsBedrockEndpoint: protoConfig.awsBedrockEndpoint,
// Vertex AI fields
claudeCodePath: protoConfig.claudeCodePath,
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,
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
openAiNativeApiKey: protoConfig.openaiNativeApiKey,
deepSeekApiKey: protoConfig.deepSeekApiKey,
requestyApiKey: protoConfig.requestyApiKey,
togetherApiKey: protoConfig.togetherApiKey,
fireworksApiKey: protoConfig.fireworksApiKey,
fireworksModelMaxCompletionTokens: protoConfig.fireworksModelMaxCompletionTokens
? Number(protoConfig.fireworksModelMaxCompletionTokens)
: undefined,
fireworksModelMaxTokens: protoConfig.fireworksModelMaxTokens ? Number(protoConfig.fireworksModelMaxTokens) : undefined,
// Azure specific
qwenApiKey: protoConfig.qwenApiKey,
doubaoApiKey: protoConfig.doubaoApiKey,
mistralApiKey: protoConfig.mistralApiKey,
moonshotApiKey: protoConfig.moonshotApiKey,
azureApiVersion: protoConfig.azureApiVersion,
// Ollama specific
ollamaApiOptionsCtxNum: protoConfig.ollamaApiOptionsCtxNum,
// Qwen specific
qwenApiLine: protoConfig.qwenApiLine,
// Moonshot specific
moonshotApiLine: protoConfig.moonshotApiLine,
// OpenRouter specific
openRouterProviderSorting: protoConfig.openrouterProviderSorting,
// SAP AI Core specific
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,
sapAiCoreClientId: protoConfig.sapAiCoreClientId,
sapAiCoreClientSecret: protoConfig.sapAiCoreClientSecret,
sapAiCoreBaseUrl: protoConfig.sapAiCoreBaseUrl,
sapAiCoreTokenUrl: protoConfig.sapAiCoreTokenUrl,
sapAiResourceGroup: protoConfig.sapAiResourceGroup,
sapAiCoreTokenUrl: protoConfig.sapAiCoreTokenUrl,
sapAiCoreBaseUrl: protoConfig.sapAiCoreBaseUrl,
// Claude Code specific
claudeCodePath: protoConfig.claudeCodePath,
// 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,
// Arrays
// 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
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)
}
+1 -1
View File
@@ -4,7 +4,7 @@
"experimentalDecorators": true,
"forceConsistentCasingInFileNames": true,
"isolatedModules": true,
"lib": ["es2022", "esnext.disposable", "DOM"],
"lib": ["es2022", "DOM"],
"module": "esnext",
"moduleResolution": "Bundler",
"noFallthroughCasesInSwitch": true,
+1 -1
View File
@@ -1,6 +1,6 @@
{
"root": true,
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended", "plugin:storybook/recommended"],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 2020,
+30
View File
@@ -0,0 +1,30 @@
import type { StorybookConfig } from "@storybook/react-vite"
const config: StorybookConfig = {
stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
addons: [],
framework: "@storybook/react-vite",
viteFinal: async (config) => {
// Define environment variables for Storybook
config.define = {
...config.define,
"process.env": {
...process.env,
IS_DEV: JSON.stringify(true),
IS_TEST: JSON.stringify(false),
TEMP_PROFILE: JSON.stringify(true),
},
}
return config
},
typescript: {
check: false,
reactDocgen: "react-docgen-typescript",
reactDocgenTypescriptOptions: {
shouldExtractLiteralValuesFromEnum: true,
propFilter: (prop) => (prop.parent ? !/node_modules/.test(prop.parent.fileName) : true),
},
},
}
export default config
+53
View File
@@ -0,0 +1,53 @@
import type { Preview } from "@storybook/react-vite"
import "../src/index.css" // Import Tailwind CSS and other global styles
import { VSCodeWebview } from "../src/components/common/StorybookDecorator"
const preview: Preview = {
parameters: {
viewport: {
viewports: {
"Editor Sidebar": {
name: "Editor Sidebar",
styles: { width: "700px", height: "800px" },
type: "desktop",
},
},
defaultViewport: "Editor Sidebar",
},
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
docs: {
theme: {
base: "dark",
colorPrimary: "#3794ff",
colorSecondary: "#0e639c",
appBg: "#1e1e1e",
appContentBg: "#252526",
textColor: "#d4d4d4",
},
},
layout: "padded",
},
decorators: [VSCodeWebview],
globalTypes: {
theme: {
description: "Color Themes",
defaultValue: "vs_dark",
toolbar: {
dynamicTitle: true,
icon: "sun",
title: "Themes",
items: [
{ value: "vs_dark", title: "VS Code Dark" },
{ value: "vs_light", title: "VS Code Light" },
],
},
},
},
}
export default preview
+63
View File
@@ -0,0 +1,63 @@
const mockVSCodeDarkTheme = {
"--vscode-editor-background": "#1E1E1E",
"--vscode-editor-foreground": "#D4D4D4",
"--vscode-sideBar-background": "#252526",
"--vscode-editorGroup-border": "#454545",
"--vscode-descriptionForeground": "#CCCCCC99",
"--vscode-button-background": "#007ACC",
"--vscode-button-foreground": "#FFFFFF",
"--vscode-button-secondaryBackground": "#383B3D",
"--vscode-textLink-foreground": "#007ACC",
"--vscode-errorForeground": "#f48771",
"--vscode-editorWidget-border": "#303031",
"--vscode-editorError-foreground": "#f14c4c",
"--vscode-diffEditor-removedTextBackground": "#9c353520",
"--vscode-diffEditor-insertedTextBackground": "#9ccc6520",
"--vscode-editor-font-family": 'Menlo, Monaco, "Courier New", monospace',
"--vscode-editor-font-size": "12px",
"--vscode-font-family": '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
"--vscode-font-size": "13px",
"--vscode-focusBorder": "#007ACC",
"--vscode-input-border": "#6B6B6B",
"--vscode-banner-background": "#007ACC",
"--vscode-titleBar-inactiveForeground": "#BBBBBB",
"--vscode-input-foreground": "#D4D4D4",
"--vscode-input-background": "#383B3D",
"--vscode-editor-line-height": "20px",
"--vscode-input-placeholderForeground": "#A6A6A6",
}
const mockVSCodeLightTheme = {
"--vscode-editor-background": "#FFFFFF",
"--vscode-editor-foreground": "#000000",
"--vscode-sideBar-background": "#F3F3F3",
"--vscode-editorGroup-border": "#D4D4D4",
"--vscode-descriptionForeground": "#6F6F6F",
"--vscode-button-background": "#007ACC",
"--vscode-button-foreground": "#FFFFFF",
"--vscode-button-secondaryBackground": "#E8E8E8",
"--vscode-textLink-foreground": "#007ACC",
"--vscode-errorForeground": "#c72e0f",
"--vscode-editorWidget-border": "#D4D4D4",
"--vscode-editorError-foreground": "#c72e0f",
"--vscode-diffEditor-removedTextBackground": "#c72e0f20",
"--vscode-diffEditor-insertedTextBackground": "#36943220",
"--vscode-editor-font-family": 'Menlo, Monaco, "Courier New", monospace',
"--vscode-editor-font-size": "12px",
"--vscode-font-family": '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
"--vscode-font-size": "13px",
"--vscode-focusBorder": "#007ACC",
"--vscode-input-border": "#CECECE",
"--vscode-banner-background": "#007ACC",
"--vscode-titleBar-inactiveForeground": "#919191",
"--vscode-input-foreground": "#000000",
"--vscode-input-background": "#FFFFFF",
"--vscode-editor-line-height": "20px",
"--vscode-input-placeholderForeground": "#767676",
}
// Mock VSCode theme variables for Storybook
export const StorybookThemes = {
dark: mockVSCodeDarkTheme,
light: mockVSCodeLightTheme,
}
+1012 -259
View File
File diff suppressed because it is too large Load Diff
+5 -1
View File
@@ -12,15 +12,18 @@
"test": "vitest run",
"test:watch": "vitest dev",
"test:coverage": "vitest run --coverage",
"devtools": "react-devtools"
"devtools": "react-devtools",
"storybook": "storybook build && storybook dev -p 6006"
},
"dependencies": {
"@floating-ui/react": "^0.27.4",
"@fontsource/azeret-mono": "^5.2.9",
"@heroui/react": "^2.8.0-beta.2",
"@storybook/react-vite": "^9.0.16",
"@vscode/webview-ui-toolkit": "^1.4.0",
"debounce": "^2.1.1",
"dompurify": "^3.2.4",
"eslint-plugin-storybook": "^9.0.16",
"fast-deep-equal": "^3.1.3",
"firebase": "^11.3.0",
"framer-motion": "^12.7.4",
@@ -40,6 +43,7 @@
"rehype-parse": "^9.0.1",
"rehype-remark": "^10.0.1",
"remark-stringify": "^11.0.0",
"storybook": "^9.0.16",
"styled-components": "^6.1.15",
"unified": "^11.0.5",
"uuid": "^9.0.1"
+28 -13
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 } from "@/components/settings/utils/providerUtils"
import { normalizeApiConfiguration, getModeSpecificFields } 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(apiConfiguration)
const modelIdValidationResult = validateModelId(apiConfiguration, openRouterModels)
const apiValidationResult = validateApiConfiguration(chatSettings.mode, apiConfiguration)
const modelIdValidationResult = validateModelId(chatSettings.mode, apiConfiguration, openRouterModels)
if (!apiValidationResult && !modelIdValidationResult && apiConfiguration) {
try {
@@ -1089,7 +1089,16 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
// Get model display name
const modelDisplayName = useMemo(() => {
const { selectedProvider, selectedModelId } = normalizeApiConfiguration(apiConfiguration)
const { selectedProvider, selectedModelId } = normalizeApiConfiguration(apiConfiguration, chatSettings.mode)
const {
vsCodeLmModelSelector,
togetherModelId,
fireworksModelId,
lmStudioModelId,
ollamaModelId,
liteLlmModelId,
requestyModelId,
} = getModeSpecificFields(apiConfiguration, chatSettings.mode)
const unknownModel = "unknown"
if (!apiConfiguration) return unknownModel
switch (selectedProvider) {
@@ -1098,25 +1107,25 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
case "openai":
return `openai-compat:${selectedModelId}`
case "vscode-lm":
return `vscode-lm:${apiConfiguration.vsCodeLmModelSelector ? `${apiConfiguration.vsCodeLmModelSelector.vendor ?? ""}/${apiConfiguration.vsCodeLmModelSelector.family ?? ""}` : unknownModel}`
return `vscode-lm:${vsCodeLmModelSelector ? `${vsCodeLmModelSelector.vendor ?? ""}/${vsCodeLmModelSelector.family ?? ""}` : unknownModel}`
case "together":
return `${selectedProvider}:${apiConfiguration.togetherModelId}`
return `${selectedProvider}:${togetherModelId}`
case "fireworks":
return `fireworks:${apiConfiguration.fireworksModelId}`
return `fireworks:${fireworksModelId}`
case "lmstudio":
return `${selectedProvider}:${apiConfiguration.lmStudioModelId}`
return `${selectedProvider}:${lmStudioModelId}`
case "ollama":
return `${selectedProvider}:${apiConfiguration.ollamaModelId}`
return `${selectedProvider}:${ollamaModelId}`
case "litellm":
return `${selectedProvider}:${apiConfiguration.liteLlmModelId}`
return `${selectedProvider}:${liteLlmModelId}`
case "requesty":
return `${selectedProvider}:${apiConfiguration.requestyModelId}`
return `${selectedProvider}:${requestyModelId}`
case "anthropic":
case "openrouter":
default:
return `${selectedProvider}:${selectedModelId}`
}
}, [apiConfiguration])
}, [apiConfiguration, chatSettings.mode])
// Calculate arrow position and menu position based on button location
useEffect(() => {
@@ -1720,7 +1729,13 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
style={{
bottom: `calc(100vh - ${menuPosition}px + 6px)`,
}}>
<ApiOptions showModelOptions={true} modelIdErrorMessage={undefined} isPopup={true} />
<ApiOptions
showModelOptions={true}
apiErrorMessage={undefined}
modelIdErrorMessage={undefined}
isPopup={true}
currentMode={chatSettings.mode}
/>
</ModelSelectorTooltip>
)}
</ModelContainer>
@@ -0,0 +1,673 @@
import type { Meta, StoryObj } from "@storybook/react-vite"
import { ClineMessage } from "@shared/ExtensionMessage"
import { HistoryItem } from "@shared/HistoryItem"
import { DEFAULT_AUTO_APPROVAL_SETTINGS } from "@shared/AutoApprovalSettings"
import { DEFAULT_CHAT_SETTINGS } from "@shared/ChatSettings"
import { ApiConfiguration } from "@shared/api"
import ChatView from "./ChatView"
import { StorybookProvider, VSCodeWebview } from "../common/StorybookDecorator"
const sidebarViewClassNames = "w-[80px]"
const meta: Meta<typeof ChatView> = {
title: "Views/ChatView",
component: ChatView,
decorators: [VSCodeWebview],
}
export default meta
type Story = StoryObj<typeof ChatView>
export const Default: StoryObj<typeof meta> = {}
// Mock API configuration
const mockApiConfiguration: ApiConfiguration = {
actModeApiProvider: "anthropic",
actModeApiModelId: "claude-3-5-sonnet-20241022",
apiKey: "mock-key",
}
// Mock task history
const mockTaskHistory: HistoryItem[] = [
{
id: "task-1",
ts: Date.now() - 3600000, // 1 hour ago
task: "Create a React component for displaying user profiles",
tokensIn: 1500,
tokensOut: 800,
cacheWrites: 200,
cacheReads: 100,
totalCost: 0.05,
},
{
id: "task-2",
ts: Date.now() - 7200000, // 2 hours ago
task: "Debug the authentication flow in the login system",
tokensIn: 2200,
tokensOut: 1200,
cacheWrites: 300,
cacheReads: 150,
totalCost: 0.08,
},
{
id: "task-3",
ts: Date.now() - 86400000, // 1 day ago
task: "Optimize database queries for better performance",
tokensIn: 3000,
tokensOut: 1800,
cacheWrites: 500,
cacheReads: 250,
totalCost: 0.12,
},
]
// Mock messages for active conversation
const mockActiveMessages: ClineMessage[] = [
{
ts: Date.now() - 300000, // 5 minutes ago
type: "say",
say: "task",
text: "Help me create a responsive navigation component for a React application",
},
{
ts: Date.now() - 280000,
type: "say",
say: "text",
text: "I'll help you create a responsive navigation component for your React application. Let me start by examining your current project structure and then create a modern, accessible navigation component.",
},
{
ts: Date.now() - 260000,
type: "say",
say: "tool",
text: JSON.stringify({
tool: "listFilesTopLevel",
path: "src/components",
}),
},
{
ts: Date.now() - 240000,
type: "say",
say: "text",
text: "Based on your project structure, I'll create a responsive navigation component with the following features:\n\n- Mobile-first responsive design\n- Accessible keyboard navigation\n- Smooth animations\n- Support for nested menu items\n- Dark/light theme support",
},
{
ts: Date.now() - 220000,
type: "say",
say: "tool",
text: JSON.stringify({
tool: "newFileCreated",
path: "src/components/Navigation/Navigation.tsx",
content: `import React, { useState } from 'react'
import './Navigation.css'
interface NavigationProps {
items: NavigationItem[]
theme?: 'light' | 'dark'
}
interface NavigationItem {
label: string
href: string
children?: NavigationItem[]
}
export const Navigation: React.FC<NavigationProps> = ({ items, theme = 'light' }) => {
const [isOpen, setIsOpen] = useState(false)
return (
<nav className={\`navigation navigation--\${theme}\`}>
<div className="navigation__container">
<button
className="navigation__toggle"
onClick={() => setIsOpen(!isOpen)}
aria-expanded={isOpen}
>
Menu
</button>
<ul className={\`navigation__list \${isOpen ? 'navigation__list--open' : ''}\`}>
{items.map((item, index) => (
<li key={index} className="navigation__item">
<a href={item.href} className="navigation__link">
{item.label}
</a>
</li>
))}
</ul>
</div>
</nav>
)
}`,
}),
},
{
ts: Date.now() - 200000,
type: "say",
say: "text",
text: "I've created a responsive navigation component with TypeScript support. The component includes:\n\n✅ Mobile-first responsive design\n✅ Accessible ARIA attributes\n✅ Toggle functionality for mobile\n✅ TypeScript interfaces for type safety\n✅ Theme support\n\nWould you like me to also create the CSS styles for this component?",
},
]
// Mock streaming message
const mockStreamingMessages: ClineMessage[] = [
...mockActiveMessages,
{
ts: Date.now() - 10000,
type: "say",
say: "text",
text: "Now I'll create the CSS styles for the navigation component. This will include responsive breakpoints, smooth animations, and accessibility features...",
partial: true,
},
]
export const WelcomeScreen: Story = {
decorators: [
(Story) => {
const mockState = {
welcomeViewCompleted: false,
showWelcome: true,
clineMessages: [],
taskHistory: mockTaskHistory,
apiConfiguration: mockApiConfiguration,
}
return (
<StorybookProvider mockState={mockState}>
<Story />
</StorybookProvider>
)
},
],
}
export const ActiveConversation: Story = {
decorators: [
(Story) => (
<StorybookProvider
mockState={{
welcomeViewCompleted: true,
clineMessages: mockActiveMessages,
taskHistory: mockTaskHistory,
apiConfiguration: mockApiConfiguration,
}}>
<Story />
</StorybookProvider>
),
],
}
// Streaming response
export const StreamingResponse: Story = {
decorators: [
(Story) => {
const mockState = {
welcomeViewCompleted: true,
showWelcome: false,
clineMessages: mockStreamingMessages,
taskHistory: mockTaskHistory,
apiConfiguration: mockApiConfiguration,
}
return (
<StorybookProvider mockState={mockState}>
<Story />
</StorybookProvider>
)
},
],
parameters: {
docs: {
description: {
story: "Shows a streaming response in progress, demonstrating real-time AI response rendering.",
},
},
},
}
// Long conversation
export const LongConversation: Story = {
decorators: [
(Story) => {
const longMessages: ClineMessage[] = [
{
ts: Date.now() - 1800000, // 30 minutes ago
type: "say",
say: "task",
text: "Help me build a complete e-commerce application with React, Node.js, and MongoDB",
},
{
ts: Date.now() - 1780000,
type: "say",
say: "text",
text: "I'll help you build a complete e-commerce application. Let's start by setting up the project structure and implementing the core features step by step.",
},
{
ts: Date.now() - 1760000,
type: "say",
say: "tool",
text: JSON.stringify({
tool: "newFileCreated",
path: "package.json",
content: `{
"name": "ecommerce-app",
"version": "1.0.0",
"description": "Full-stack e-commerce application",
"main": "server.js",
"scripts": {
"dev": "concurrently \\"npm run server\\" \\"npm run client\\"",
"server": "nodemon server.js",
"client": "cd client && npm start"
}
}`,
}),
},
{
ts: Date.now() - 1740000,
type: "say",
say: "text",
text: "Great! I've set up the initial package.json. Now let's create the backend server with Express and MongoDB integration.",
},
{
ts: Date.now() - 1720000,
type: "say",
say: "tool",
text: JSON.stringify({
tool: "newFileCreated",
path: "server.js",
content: `const express = require('express')
const mongoose = require('mongoose')
const cors = require('cors')
const productRoutes = require('./routes/products')
const userRoutes = require('./routes/users')
const orderRoutes = require('./routes/orders')
const app = express()
const PORT = process.env.PORT || 5000
// Middleware
app.use(cors())
app.use(express.json())
// Routes
app.use('/api/products', productRoutes)
app.use('/api/users', userRoutes)
app.use('/api/orders', orderRoutes)
// MongoDB connection
mongoose.connect('mongodb://localhost:27017/ecommerce', {
useNewUrlParser: true,
useUnifiedTopology: true
})
app.listen(PORT, () => {
console.log(\`Server running on port \${PORT}\`)
})`,
}),
},
{
ts: Date.now() - 1700000,
type: "say",
say: "text",
text: "Perfect! The backend server is set up. Now let's create the product model and routes for handling product operations.",
},
{
ts: Date.now() - 1680000,
type: "say",
say: "tool",
text: JSON.stringify({
tool: "newFileCreated",
path: "models/Product.js",
content: `const mongoose = require('mongoose')
const productSchema = new mongoose.Schema({
name: {
type: String,
required: true,
trim: true
},
description: {
type: String,
required: true
},
price: {
type: Number,
required: true,
min: 0
},
category: {
type: String,
required: true
},
stock: {
type: Number,
required: true,
min: 0
},
images: [{
type: String
}],
ratings: {
average: {
type: Number,
default: 0
},
count: {
type: Number,
default: 0
}
}
}, {
timestamps: true
})
module.exports = mongoose.model('Product', productSchema)`,
}),
},
{
ts: Date.now() - 1660000,
type: "say",
say: "text",
text: "Excellent! The Product model is ready with all necessary fields. Now let's create the React frontend with a modern component structure.",
},
{
ts: Date.now() - 1640000,
type: "say",
say: "command",
text: "cd client && npx create-react-app . --template typescript",
},
{
ts: Date.now() - 1620000,
type: "say",
say: "command_output",
text: "Creating a new React app in /path/to/project/client...\n\nInstalling packages. This might take a couple of minutes.\nInstalling react, react-dom, and react-scripts with cra-template-typescript...\n\nSuccess! Created client at /path/to/project/client",
},
{
ts: Date.now() - 1600000,
type: "say",
say: "text",
text: "Great! The React frontend is set up with TypeScript. Now let's create the main components for our e-commerce application.",
},
]
const mockState = {
welcomeViewCompleted: true,
showWelcome: false,
clineMessages: longMessages,
taskHistory: mockTaskHistory,
apiConfiguration: mockApiConfiguration,
}
return (
<StorybookProvider mockState={mockState}>
<div className={sidebarViewClassNames}>
<Story />
</div>
</StorybookProvider>
)
},
],
parameters: {
docs: {
description: {
story: "A longer conversation showing multiple tool uses, file creation, and command execution in a complex development task.",
},
},
},
}
// Error state
export const ErrorState: Story = {
decorators: [
(Story) => {
const errorMessages: ClineMessage[] = [
{
ts: Date.now() - 300000,
type: "say",
say: "task",
text: "Help me fix the build errors in my React application",
},
{
ts: Date.now() - 280000,
type: "say",
say: "text",
text: "I'll help you fix the build errors. Let me first examine the current state of your application.",
},
{
ts: Date.now() - 260000,
type: "say",
say: "command",
text: "npm run build",
},
{
ts: Date.now() - 240000,
type: "say",
say: "error",
text: "Build failed with the following errors:\n\nTypeScript error in src/components/UserProfile.tsx(15,23):\nProperty 'username' does not exist on type 'User'.\n\nTypeScript error in src/utils/api.ts(42,15):\nArgument of type 'string | undefined' is not assignable to parameter of type 'string'.",
},
{
ts: Date.now() - 220000,
type: "say",
say: "text",
text: "I can see there are TypeScript errors in your code. Let me examine the files and fix these issues.",
},
{
ts: Date.now() - 200000,
type: "say",
say: "tool",
text: JSON.stringify({
tool: "readFile",
path: "src/components/UserProfile.tsx",
}),
},
{
ts: Date.now() - 180000,
type: "say",
say: "text",
text: "I found the issue. The User type doesn't have a 'username' property. Let me fix this by updating the component to use the correct property name.",
},
]
const mockState = {
welcomeViewCompleted: true,
showWelcome: false,
clineMessages: errorMessages,
taskHistory: mockTaskHistory,
apiConfiguration: mockApiConfiguration,
}
return (
<StorybookProvider mockState={mockState}>
<div className={sidebarViewClassNames}>
<Story />
</div>
</StorybookProvider>
)
},
],
parameters: {
docs: {
description: {
story: "Shows how Cline handles and displays error messages, helping users understand and resolve issues.",
},
},
},
}
// Auto-approval enabled
export const AutoApprovalEnabled: Story = {
decorators: [
(Story) => {
const mockState = {
welcomeViewCompleted: true,
showWelcome: false,
clineMessages: mockActiveMessages,
taskHistory: mockTaskHistory,
apiConfiguration: mockApiConfiguration,
autoApprovalSettings: {
...DEFAULT_AUTO_APPROVAL_SETTINGS,
enabled: true,
maxRequestsPerTask: 10,
maxRequestsPerHour: 50,
},
}
return (
<StorybookProvider mockState={mockState}>
<div className={sidebarViewClassNames}>
<Story />
</div>
</StorybookProvider>
)
},
],
parameters: {
docs: {
description: {
story: "Shows the interface with auto-approval enabled, allowing Cline to execute certain actions automatically without user confirmation.",
},
},
},
}
// Plan mode
export const PlanMode: Story = {
decorators: [
(Story) => {
const planModeMessages: ClineMessage[] = [
{
ts: Date.now() - 300000,
type: "say",
say: "task",
text: "Help me refactor my React application to use TypeScript and improve performance",
},
{
ts: Date.now() - 280000,
type: "say",
say: "text",
text: "I'll help you refactor your React application to use TypeScript and improve performance. Let me create a detailed plan for this migration.",
},
{
ts: Date.now() - 260000,
type: "ask",
ask: "plan_mode_respond",
text: JSON.stringify({
response:
"Here's my comprehensive plan for refactoring your React application:\n\n## Phase 1: TypeScript Migration\n1. **Setup TypeScript configuration**\n - Install TypeScript and related dependencies\n - Configure tsconfig.json with strict settings\n - Set up ESLint with TypeScript rules\n\n2. **Convert JavaScript files to TypeScript**\n - Start with utility functions and constants\n - Convert React components one by one\n - Add proper type definitions for props and state\n\n## Phase 2: Performance Optimization\n1. **Code splitting and lazy loading**\n - Implement React.lazy() for route-based splitting\n - Add Suspense boundaries\n\n2. **Memoization and optimization**\n - Use React.memo for expensive components\n - Implement useMemo and useCallback where appropriate\n - Optimize re-renders with proper dependency arrays\n\n## Phase 3: Bundle Optimization\n1. **Webpack optimization**\n - Configure tree shaking\n - Optimize chunk splitting\n - Implement proper caching strategies\n\nWould you like me to proceed with this plan, or would you like to modify any part of it?",
}),
},
]
const mockState = {
welcomeViewCompleted: true,
showWelcome: false,
clineMessages: planModeMessages,
taskHistory: mockTaskHistory,
apiConfiguration: mockApiConfiguration,
chatSettings: {
...DEFAULT_CHAT_SETTINGS,
mode: "plan" as const,
},
}
return (
<StorybookProvider mockState={mockState}>
<Story />
</StorybookProvider>
)
},
],
parameters: {
docs: {
description: {
story: "Shows Cline in Plan mode, where it focuses on creating detailed plans and discussing approaches before implementation.",
},
},
},
}
// Browser automation
export const BrowserAutomation: Story = {
decorators: [
(Story) => {
const browserMessages: ClineMessage[] = [
{
ts: Date.now() - 300000,
type: "say",
say: "task",
text: "Help me test the login functionality on my web application",
},
{
ts: Date.now() - 280000,
type: "say",
say: "text",
text: "I'll help you test the login functionality. Let me launch a browser and navigate to your application.",
},
{
ts: Date.now() - 260000,
type: "say",
say: "browser_action_launch",
text: JSON.stringify({
action: "launch",
url: "http://localhost:3000/login",
}),
},
{
ts: Date.now() - 240000,
type: "say",
say: "browser_action_result",
text: JSON.stringify({
screenshot:
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==",
currentUrl: "http://localhost:3000/login",
logs: "Page loaded successfully",
}),
},
{
ts: Date.now() - 220000,
type: "say",
say: "text",
text: "Great! The browser has launched and navigated to your login page. I can see the login form is displayed. Now let me test the login functionality by filling in the form.",
},
{
ts: Date.now() - 200000,
type: "say",
say: "browser_action",
text: JSON.stringify({
action: "click",
coordinate: "400,200",
}),
},
{
ts: Date.now() - 180000,
type: "say",
say: "browser_action",
text: JSON.stringify({
action: "type",
text: "test@example.com",
}),
},
]
const mockState = {
welcomeViewCompleted: true,
showWelcome: false,
clineMessages: browserMessages,
taskHistory: mockTaskHistory,
apiConfiguration: mockApiConfiguration,
}
return (
<StorybookProvider mockState={mockState}>
<Story />
</StorybookProvider>
)
},
],
parameters: {
docs: {
description: {
story: "Shows Cline performing browser automation tasks, including launching browsers, clicking elements, and testing web applications.",
},
},
},
}
+3 -2
View File
@@ -47,6 +47,7 @@ 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
@@ -199,8 +200,8 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
messageHandlers
const { selectedModelInfo } = useMemo(() => {
return normalizeApiConfiguration(apiConfiguration)
}, [apiConfiguration])
return normalizeApiConfiguration(apiConfiguration, chatSettings.mode)
}, [apiConfiguration, chatSettings.mode])
const selectFilesAndImages = useCallback(async () => {
try {
@@ -0,0 +1,174 @@
import type { Meta, StoryObj } from "@storybook/react-vite"
import React from "react"
import CreditLimitError from "./CreditLimitError"
import { StorybookProvider } from "../common/StorybookDecorator"
const meta: Meta<typeof CreditLimitError> = {
title: "Component/CreditLimitError",
component: CreditLimitError,
decorators: [
(Story) => {
return (
<StorybookProvider>
<Story />
</StorybookProvider>
)
},
],
argTypes: {
currentBalance: {
control: { type: "number", min: 0, max: 10000000, step: 100000 },
description: "Current balance in microcredits (1 credit = 1,000,000 microcredits)",
},
totalSpent: {
control: { type: "number", min: 0, max: 100000000, step: 1000000 },
description: "Total amount spent in microcredits (optional, for future use)",
},
totalPromotions: {
control: { type: "number", min: 0, max: 10000000, step: 100000 },
description: "Total promotional credits in microcredits (optional, for future use)",
},
message: {
control: "text",
description: "Custom error message to display to the user",
},
buyCreditsUrl: {
control: "text",
description: "URL for the credit purchase page",
},
},
}
export default meta
type Story = StoryObj<typeof CreditLimitError>
// Default credit limit error
export const Default: Story = {
args: {
currentBalance: 0,
message: "You have run out of credit.",
buyCreditsUrl: "https://app.cline.bot/dashboard",
},
parameters: {
docs: {
description: {
story: "Default credit limit error with zero balance and standard message.",
},
},
},
}
// With usage statistics
export const WithUsageStats: Story = {
args: {
currentBalance: 500000, // $0.5000
totalSpent: 15000000, // $15.0000
totalPromotions: 2000000, // $2.0000
message: "Your current request would exceed your remaining credit balance.",
buyCreditsUrl: "https://app.cline.bot/dashboard",
},
parameters: {
docs: {
description: {
story: "Component showing usage statistics including total spent and promotional credits (for future implementation).",
},
},
},
}
// Interactive demo
export const InteractiveDemo: Story = {
args: {
currentBalance: 0,
totalSpent: 15000000, // $15.0000
totalPromotions: 0,
message: "You have run out of credit.",
buyCreditsUrl: "https://app.cline.bot/dashboard/account?tab=credits&redirect=true",
},
decorators: [
(Story) => {
const [retryCount, setRetryCount] = React.useState(0)
const [lastAction, setLastAction] = React.useState<string>("")
// Mock the TaskServiceClient for demo purposes
React.useEffect(() => {
const originalConsoleLog = console.log
console.log = (...args) => {
if (args[0]?.includes?.("TaskServiceClient")) {
setLastAction("Retry request sent to extension")
setRetryCount((prev) => prev + 1)
}
originalConsoleLog(...args)
}
return () => {
console.log = originalConsoleLog
}
}, [])
return (
<StorybookProvider>
<div className="max-w-md space-y-4">
<Story />
{(retryCount > 0 || lastAction) && (
<div className="p-3 bg-[var(--vscode-textBlockQuote-background)] rounded border text-sm">
<div className="font-semibold mb-1">Demo Actions:</div>
{retryCount > 0 && <div>Retry attempts: {retryCount}</div>}
{lastAction && <div>Last action: {lastAction}</div>}
</div>
)}
</div>
</StorybookProvider>
)
},
],
parameters: {
docs: {
description: {
story: "Interactive demo showing retry functionality with action tracking for demonstration purposes.",
},
},
},
}
// Long message handling
export const LongMessage: Story = {
args: {
currentBalance: 0,
message:
"You have exceeded your credit limit for this billing period. This can happen when making many API requests or using advanced AI models that consume more credits per request. To continue using Cline's AI-powered features, please purchase additional credits from your dashboard. Your current usage will be preserved and you can resume immediately after adding credits to your account.",
buyCreditsUrl: "https://app.cline.bot/dashboard",
},
parameters: {
docs: {
description: {
story: "Component handling long error messages, demonstrating text wrapping and layout behavior.",
},
},
},
}
// Compact layout
export const CompactLayout: Story = {
args: {
currentBalance: 1000000, // $1.0000
message: "Low credits",
buyCreditsUrl: "https://app.cline.bot/dashboard",
},
decorators: [
(Story) => {
return (
<StorybookProvider>
<Story />
</StorybookProvider>
)
},
],
parameters: {
docs: {
description: {
story: "Component in a compact layout with shorter message, suitable for narrow interfaces.",
},
},
},
}
@@ -0,0 +1,449 @@
import type { Meta, StoryObj } from "@storybook/react-vite"
import React from "react"
import UserMessage from "./UserMessage"
import { StorybookProvider } from "../common/StorybookDecorator"
const meta: Meta<typeof UserMessage> = {
title: "Component/UserMessage",
component: UserMessage,
parameters: {
layout: "padded",
docs: {
description: {
component: `
The UserMessage component displays user messages in the chat interface with editing and checkpoint restore capabilities.
**Features:**
- **Editable Messages**: Click to edit message text with auto-resizing textarea
- **Checkpoint Restore**: Restore chat or workspace to previous states when editing
- **File Attachments**: Display thumbnails for attached images and files
- **Keyboard Shortcuts**:
- Escape: Cancel editing
- Enter: Restore chat and send edited message
- Cmd/Ctrl+Enter: Restore all (chat + workspace) and send edited message
- **Focus Management**: Proper handling of focus states and blur events
- **Text Highlighting**: Supports text highlighting and formatting
- **Error Handling**: Graceful handling of checkpoint tracker errors
**Restore Types:**
- **Restore Chat**: Restores just the conversation history to the checkpoint
- **Restore All**: Restores both conversation and workspace files to the checkpoint
**Use Cases:**
- Displaying user messages in chat conversations
- Editing and resending messages with checkpoint restoration
- Showing attached files and images with messages
- Managing conversation history and workspace state
**Interaction Flow:**
1. Click message to enter edit mode
2. Modify text as needed
3. Use restore buttons or keyboard shortcuts to apply changes
4. System restores to checkpoint and sends edited message
`,
},
},
},
decorators: [
(Story) => {
return (
<StorybookProvider>
<Story />
</StorybookProvider>
)
},
],
argTypes: {
text: {
control: "text",
description: "The message text content",
},
images: {
control: "object",
description: "Array of image file paths or URLs",
},
files: {
control: "object",
description: "Array of file paths",
},
messageTs: {
control: "number",
description: "Timestamp for the message (used for checkpoint restore)",
},
sendMessageFromChatRow: {
action: "sendMessageFromChatRow",
description: "Callback function when message is resent after editing",
},
},
}
export default meta
type Story = StoryObj<typeof UserMessage>
// Basic text message
export const BasicText: Story = {
args: {
text: "Create a simple todo app with HTML, CSS, and JavaScript",
messageTs: Date.now(),
sendMessageFromChatRow: (text: string, images: string[], files: string[]) => {
console.log("Resending message:", { text, images, files })
},
},
parameters: {
docs: {
description: {
story: "A basic user message with just text content. Click to edit and see the restore options.",
},
},
},
}
// Message with images
export const WithImages: Story = {
args: {
text: "Please analyze these screenshots and help me fix the layout issues",
images: ["/path/to/screenshot1.png", "/path/to/screenshot2.png", "/path/to/design-mockup.jpg"],
messageTs: Date.now(),
sendMessageFromChatRow: (text: string, images: string[], files: string[]) => {
console.log("Resending message:", { text, images, files })
},
},
parameters: {
docs: {
description: {
story: "User message with attached images. Shows thumbnail display below the message text.",
},
},
},
}
// Message with files
export const WithFiles: Story = {
args: {
text: "Review these configuration files and suggest improvements",
files: ["/path/to/package.json", "/path/to/tsconfig.json", "/path/to/webpack.config.js", "/path/to/README.md"],
messageTs: Date.now(),
sendMessageFromChatRow: (text: string, images: string[], files: string[]) => {
console.log("Resending message:", { text, images, files })
},
},
parameters: {
docs: {
description: {
story: "User message with attached files. File thumbnails are displayed with appropriate icons.",
},
},
},
}
// Message with both images and files
export const WithImagesAndFiles: Story = {
args: {
text: "Here's the current state of the project with some design mockups and the source files",
images: ["/path/to/current-state.png", "/path/to/desired-design.jpg"],
files: ["/path/to/src/App.tsx", "/path/to/src/components/Header.tsx", "/path/to/styles/main.css"],
messageTs: Date.now(),
sendMessageFromChatRow: (text: string, images: string[], files: string[]) => {
console.log("Resending message:", { text, images, files })
},
},
parameters: {
docs: {
description: {
story: "User message with both images and files attached, demonstrating the full attachment display.",
},
},
},
}
// Long text message
export const LongText: Story = {
args: {
text: `I need help creating a comprehensive web application with the following requirements:
1. User authentication system with login/logout functionality
2. Dashboard with data visualization charts
3. CRUD operations for managing user profiles
4. Real-time notifications using WebSockets
5. Responsive design that works on mobile and desktop
6. Integration with external APIs for data fetching
7. Proper error handling and loading states
8. Unit tests for all components
9. Documentation for the API endpoints
10. Deployment configuration for production
The application should be built using React with TypeScript, and I'd like to use modern best practices for state management, routing, and styling. Please provide a detailed implementation plan and help me get started with the project structure.`,
messageTs: Date.now(),
sendMessageFromChatRow: (text: string, images: string[], files: string[]) => {
console.log("Resending message:", { text, images, files })
},
},
parameters: {
docs: {
description: {
story: "A longer user message demonstrating text wrapping and the editing experience with more content.",
},
},
},
}
// Empty message
export const EmptyMessage: Story = {
args: {
text: "",
messageTs: Date.now(),
sendMessageFromChatRow: (text: string, images: string[], files: string[]) => {
console.log("Resending message:", { text, images, files })
},
},
parameters: {
docs: {
description: {
story: "Handles empty message text gracefully. Still allows editing to add content.",
},
},
},
}
// Message with special characters and formatting
export const WithSpecialCharacters: Story = {
args: {
text: `Here's some code with special characters:
const greeting = "Hello, World! 👋";
const math = 2 + 2 = 4;
const symbols = !@#$%^&*()_+-=[]{}|;':",./<>?
And some Unicode: 🚀 ✨ 💻 🎉 ⚡ 🔥`,
messageTs: Date.now(),
sendMessageFromChatRow: (text: string, images: string[], files: string[]) => {
console.log("Resending message:", { text, images, files })
},
},
parameters: {
docs: {
description: {
story: "Message containing special characters, code snippets, and Unicode emojis.",
},
},
},
}
// Message with checkpoint tracker error
export const WithCheckpointError: Story = {
args: {
text: "This message demonstrates the state when checkpoint tracking has an error",
messageTs: Date.now(),
sendMessageFromChatRow: (text: string, images: string[], files: string[]) => {
console.log("Resending message:", { text, images, files })
},
},
decorators: [
(Story) => {
const mockState = {
checkpointTrackerErrorMessage: "Git repository not found. Checkpoint functionality is disabled.",
}
return (
<StorybookProvider mockState={mockState}>
<Story />
</StorybookProvider>
)
},
],
parameters: {
docs: {
description: {
story: "Message state when checkpoint tracker has an error. Only 'Restore Chat' button is available, and Cmd+Enter shortcut is disabled.",
},
},
},
}
// Simulated editing state
export const EditingState: Story = {
args: {
text: "Click this message to see the editing interface",
messageTs: Date.now(),
sendMessageFromChatRow: (text: string, images: string[], files: string[]) => {
console.log("Resending message:", { text, images, files })
},
},
decorators: [
(Story) => {
// Custom wrapper to demonstrate editing state
const EditingDemo = () => {
const [isEditing, setIsEditing] = React.useState(false)
const [editedText, setEditedText] = React.useState("Click this message to see the editing interface")
return (
<div style={{ maxWidth: "600px", margin: "0 auto", padding: "16px" }}>
<div
style={{
backgroundColor: isEditing ? "unset" : "var(--vscode-badge-background)",
color: "var(--vscode-badge-foreground)",
borderRadius: "3px",
padding: "9px",
whiteSpace: "pre-line",
wordWrap: "break-word",
cursor: isEditing ? "default" : "pointer",
}}
onClick={() => !isEditing && setIsEditing(true)}>
{isEditing ? (
<>
<textarea
value={editedText}
onChange={(e) => setEditedText(e.target.value)}
autoFocus
placeholder="Edit your message..."
aria-label="Edit message text"
title="Edit message text"
style={{
width: "100%",
backgroundColor: "var(--vscode-input-background)",
color: "var(--vscode-input-foreground)",
borderColor: "var(--vscode-input-border)",
border: "1px solid",
borderRadius: "2px",
padding: "6px",
fontFamily: "inherit",
fontSize: "inherit",
lineHeight: "inherit",
boxSizing: "border-box",
resize: "none",
minHeight: "60px",
}}
/>
<div style={{ display: "flex", gap: "8px", marginTop: "8px", justifyContent: "flex-end" }}>
<button
onClick={(e) => {
e.stopPropagation()
console.log("Restore All clicked")
setIsEditing(false)
}}
style={{
backgroundColor:
"var(--vscode-button-secondaryBackground, var(--vscode-descriptionForeground))",
color: "var(--vscode-button-secondaryForeground, var(--vscode-foreground))",
border: "none",
padding: "4px 8px",
borderRadius: "2px",
fontSize: "9px",
cursor: "pointer",
}}>
Restore All
</button>
<button
onClick={(e) => {
e.stopPropagation()
console.log("Restore Chat clicked")
setIsEditing(false)
}}
style={{
backgroundColor: "var(--vscode-button-background)",
color: "var(--vscode-button-foreground)",
border: "none",
padding: "4px 8px",
borderRadius: "2px",
fontSize: "9px",
cursor: "pointer",
}}>
Restore Chat
</button>
</div>
</>
) : (
<span className="ph-no-capture" style={{ display: "block" }}>
{editedText}
</span>
)}
</div>
</div>
)
}
return <EditingDemo />
},
],
parameters: {
docs: {
description: {
story: "Interactive demonstration of the editing state with restore buttons. Click the message to enter edit mode.",
},
},
},
}
// Message without timestamp (no checkpoint functionality)
export const WithoutTimestamp: Story = {
args: {
text: "This message has no timestamp, so checkpoint restore functionality is not available",
// messageTs is undefined
sendMessageFromChatRow: (text: string, images: string[], files: string[]) => {
console.log("Resending message:", { text, images, files })
},
},
parameters: {
docs: {
description: {
story: "Message without a timestamp. Editing is still possible but checkpoint restore buttons won't function.",
},
},
},
}
// Message with line breaks and formatting
export const WithLineBreaks: Story = {
args: {
text: `This message contains multiple lines:
Line 1: Introduction
Line 2: Details about the request
Line 3: Additional context
And some formatting:
- Bullet point 1
- Bullet point 2
- Bullet point 3
Final paragraph with conclusion.`,
messageTs: Date.now(),
sendMessageFromChatRow: (text: string, images: string[], files: string[]) => {
console.log("Resending message:", { text, images, files })
},
},
parameters: {
docs: {
description: {
story: "Message with line breaks and basic formatting to demonstrate text rendering with whitespace preservation.",
},
},
},
}
// Comprehensive example with all features
export const ComprehensiveExample: Story = {
args: {
text: `Please help me implement a new feature for the dashboard:
Requirements:
1. Add a new chart component for sales data
2. Include filtering options by date range
3. Make it responsive for mobile devices
I've attached the current dashboard screenshot and the design mockup for reference.`,
images: ["/path/to/current-dashboard.png", "/path/to/new-design-mockup.jpg"],
files: ["/path/to/src/Dashboard.tsx", "/path/to/src/components/Chart.tsx", "/path/to/styles/dashboard.css"],
messageTs: Date.now(),
sendMessageFromChatRow: (text: string, images: string[], files: string[]) => {
console.log("Resending message:", { text, images, files })
},
},
parameters: {
docs: {
description: {
story: "Comprehensive example showing a realistic user message with text, images, and files - demonstrating all component features together.",
},
},
},
}
@@ -1,6 +1,6 @@
import HeroTooltip from "@/components/common/HeroTooltip"
import Thumbnails from "@/components/common/Thumbnails"
import { normalizeApiConfiguration } from "@/components/settings/utils/providerUtils"
import { normalizeApiConfiguration, getModeSpecificFields } 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 } =
const { apiConfiguration, currentTaskItem, checkpointTrackerErrorMessage, clineMessages, navigateToSettings, chatSettings } =
useExtensionState()
const [isTaskExpanded, setIsTaskExpanded] = useState(true)
const [isTextExpanded, setIsTextExpanded] = useState(false)
@@ -51,7 +51,10 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
const textContainerRef = useRef<HTMLDivElement>(null)
const textRef = useRef<HTMLDivElement>(null)
const { selectedModelInfo } = useMemo(() => normalizeApiConfiguration(apiConfiguration), [apiConfiguration])
const { selectedModelInfo } = useMemo(
() => normalizeApiConfiguration(apiConfiguration, chatSettings.mode),
[apiConfiguration, chatSettings.mode],
)
const contextWindow = selectedModelInfo?.contextWindow
// Open task header when checkpoint tracker error message is set
@@ -130,19 +133,18 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
}, [task.text, windowWidth, isTaskExpanded])
const isCostAvailable = useMemo(() => {
const modeFields = getModeSpecificFields(apiConfiguration, chatSettings.mode)
const openAiCompatHasPricing =
apiConfiguration?.apiProvider === "openai" &&
apiConfiguration?.openAiModelInfo?.inputPrice &&
apiConfiguration?.openAiModelInfo?.outputPrice
modeFields.apiProvider === "openai" &&
modeFields.openAiModelInfo?.inputPrice &&
modeFields.openAiModelInfo?.outputPrice
if (openAiCompatHasPricing) {
return true
}
return (
apiConfiguration?.apiProvider !== "vscode-lm" &&
apiConfiguration?.apiProvider !== "ollama" &&
apiConfiguration?.apiProvider !== "lmstudio"
modeFields.apiProvider !== "vscode-lm" && modeFields.apiProvider !== "ollama" && modeFields.apiProvider !== "lmstudio"
)
}, [apiConfiguration?.apiProvider, apiConfiguration?.openAiModelInfo])
}, [apiConfiguration, chatSettings.mode])
const shouldShowPromptCacheInfo = () => {
// Hybrid logic: Show cache info if we have actual cache data,
@@ -0,0 +1,73 @@
import React from "react"
import { ClineAuthProvider } from "@/context/ClineAuthContext"
import { ExtensionStateContextProvider, useExtensionState } from "@/context/ExtensionStateContext"
import { ExtensionState } from "@shared/ExtensionMessage"
import { Decorator } from "@storybook/react-vite"
import { StorybookThemes } from "../../../.storybook/themes"
// Inner component that uses the context
const StorybookStateUpdater: React.FC<{
children: React.ReactNode
mockState?: Partial<ExtensionState>
}> = ({ mockState, children }) => {
const { setExtensionStateForTest } = useExtensionState()
// Set state when mockState changes
React.useEffect(() => {
if (mockState) {
setExtensionStateForTest(mockState)
}
}, [mockState, setExtensionStateForTest])
return <div className="container">{children}</div>
}
export const StorybookProvider: React.FC<{
children: React.ReactNode
mockState?: Partial<ExtensionState>
}> = ({ mockState, children }) => {
return <StorybookStateUpdater mockState={mockState}>{children}</StorybookStateUpdater>
}
// Component that handles theme switching
const ThemeHandler: React.FC<{ children: React.ReactNode; theme?: string }> = ({ children, theme }) => {
React.useEffect(() => {
const styles = theme?.includes("light") ? StorybookThemes.light : StorybookThemes.dark
// Apply CSS variables to the document root
const root = document.documentElement
Object.entries(styles).forEach(([property, value]) => {
root.style.setProperty(property, value)
})
document.body.style.backgroundColor = styles["--vscode-editor-background"]
document.body.style.color = styles["--vscode-editor-foreground"]
document.body.style.fontFamily = styles["--vscode-font-family"]
document.body.style.fontSize = styles["--vscode-font-size"]
return () => {
// Cleanup on unmount
Object.keys(styles).forEach((property) => {
root.style.removeProperty(property)
})
}
}, [theme])
return <>{children}</>
}
function VSCodeDecorator(className: string | undefined): Decorator {
return (story, parameters) => {
return (
<div className={className}>
<ExtensionStateContextProvider>
<ClineAuthProvider>
<ThemeHandler theme={parameters?.globals?.theme}>{React.createElement(story)}</ThemeHandler>
</ClineAuthProvider>
</ExtensionStateContextProvider>
</div>
)
}
}
export const VSCodeWebview = VSCodeDecorator("relative")
@@ -0,0 +1,198 @@
import type { Meta, StoryObj } from "@storybook/react-vite"
import McpResponseDisplay from "./McpResponseDisplay"
import { StorybookProvider, VSCodeWebview } from "@/components/common/StorybookDecorator"
import { ExtensionState } from "@shared/ExtensionMessage"
const meta: Meta<typeof McpResponseDisplay> = {
title: "Component/mcpDisplayMode",
component: McpResponseDisplay,
parameters: {
docs: {
description: {
component: `
The McpResponseDisplay component renders MCP (Model Context Protocol) server responses with rich content support.
**Features:**
- **Rich Display Mode**: Automatically detects and renders images and link previews
- **Plain Text Mode**: Fallback mode for simple text display
- **Collapsible**: Can be expanded/collapsed with header controls
- **Toggle Switch**: Users can switch between rich and plain display modes
- **URL Processing**: Extracts and processes URLs from response text
- **Error Handling**: Graceful error handling with fallbacks
- **Loading States**: Shows progress indicators during content processing
**Use Cases:**
- Displaying MCP server responses in chat interfaces
- Showing rich content like images and link previews
- Providing user control over content display modes
**Note**: In Storybook, rich content features may be limited due to mocking constraints.
The component will demonstrate the basic text display and UI structure.
`,
},
},
},
decorators: [VSCodeWebview],
argTypes: {
responseText: {
control: "text",
description: "The response text to display",
},
},
}
const defaultSettings = {
mcpDisplayMode: "plain",
mcpResponsesCollapsed: false,
} satisfies Partial<ExtensionState>
export default meta
type Story = StoryObj<typeof McpResponseDisplay>
// Basic text response
export const Default: Story = {
args: {
responseText: `Here's a simple text response from an MCP server.
This response contains multiple paragraphs and demonstrates the basic text rendering capabilities of the component.
The text is displayed in a monospace font with proper line breaks and formatting.`,
},
parameters: {
docs: {
description: {
story: "A basic text response without any URLs or rich content in plain mode.",
},
},
},
}
// Response with URLs (will show as plain text in Storybook)
export const LinksPreview: Story = {
args: {
responseText: `Here's a response that contains URLs:
Documentation: https://example.com/docs
API reference: https://api.example.com/reference
Sample image: https://via.placeholder.com/600x400/0066cc/ffffff?text=Sample+Image
In the actual application, these URLs would be processed for rich display when rich mode is enabled.`,
},
parameters: {
docs: {
description: {
story: "Response containing URLs. In the real application, these would be processed for rich display.",
},
},
},
decorators: [
(Story) => {
const mockState = {
...defaultSettings,
mcpDisplayMode: "rich",
} satisfies Partial<ExtensionState>
return (
<StorybookProvider mockState={mockState}>
<Story />
</StorybookProvider>
)
},
],
}
// Mixed content response
export const MixedContent: Story = {
args: {
responseText: `Here's a comprehensive response with mixed content:
## Documentation Links
- Main docs: https://example.com/docs
- API guide: https://api.example.com/guide
## Sample Images
Here's a screenshot: https://via.placeholder.com/500x300/28a745/ffffff?text=Screenshot
And a diagram: https://via.placeholder.com/400x400/6f42c1/ffffff?text=Diagram
## Additional Resources
- Tutorial: https://tutorial.example.com
- Examples: https://examples.example.com
This demonstrates how the component handles a mix of text, links, and images.
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
More content here to demonstrate scrolling and layout behavior with longer text content that might wrap across multiple lines and require proper text handling.
Sample images:
- https://via.placeholder.com/300x200/007bff/ffffff?text=Logo
- https://via.placeholder.com/400x300/28a745/ffffff?text=Feature+1
- https://via.placeholder.com/400x300/dc3545/ffffff?text=Feature+2
This tests the component's ability to handle longer content efficiently while maintaining good performance and user experience.`,
},
parameters: {
docs: {
description: {
story: "A comprehensive response mixing text, links, and images to showcase the component structure.",
},
},
},
decorators: [
(Story) => {
const mockState = {
...defaultSettings,
mcpDisplayMode: "rich",
} satisfies Partial<ExtensionState>
return (
<StorybookProvider mockState={mockState}>
<Story />
</StorybookProvider>
)
},
],
}
// Code-like response
export const CodeResponse: Story = {
args: {
responseText: `Here's some code with embedded URLs:
\`\`\`javascript
// API endpoint
const apiUrl = "https://api.example.com/v1/data";
// Image URL
const imageUrl = "https://via.placeholder.com/200x200/6c757d/ffffff?text=API+Response";
fetch(apiUrl)
.then(response => response.json())
.then(data => console.log(data));
\`\`\`
Documentation: https://docs.example.com/api`,
},
parameters: {
docs: {
description: {
story: "Response containing code blocks with embedded URLs.",
},
},
},
}
// Empty response
export const EmptyResponse: Story = {
args: {
responseText: "",
},
parameters: {
docs: {
description: {
story: "Handles empty response text gracefully.",
},
},
},
}
@@ -0,0 +1,165 @@
import type { Meta, StoryObj } from "@storybook/react-vite"
import React from "react"
import McpConfigurationView from "./McpConfigurationView"
import { StorybookProvider, VSCodeWebview } from "../../common/StorybookDecorator"
import { ExtensionState } from "@shared/ExtensionMessage"
import { McpMarketplaceCatalog, McpViewTab } from "@shared/mcp"
import { useExtensionState } from "@/context/ExtensionStateContext"
const mcpMarketplaceCatalog: McpMarketplaceCatalog = {
items: [
{
mcpId: "example-server",
githubUrl: "https://github.com/example/mcp-server",
name: "Example Server",
author: "Example Author",
description: "An example MCP server for demonstration",
codiconIcon: "database",
logoUrl: "https://example.com/logo.png",
category: "Data",
tags: ["example", "demo"],
requiresApiKey: false,
isRecommended: true,
githubStars: 100,
downloadCount: 500,
createdAt: "2024-01-01T00:00:00Z",
updatedAt: "2024-01-15T00:00:00Z",
lastGithubSync: "2024-01-15T00:00:00Z",
},
],
}
const mcpservers = [
{
name: "example-server",
config: "npx -y example-server",
status: "connected" as const,
tools: [{ name: "example_tool", description: "Example tool" }],
resources: [],
resourceTemplates: [],
},
{
name: "filesystem",
config: "npx -y @modelcontextprotocol/server-filesystem /path/to/files",
status: "connected" as const,
tools: [
{
name: "read_file",
description: "Read contents of a file",
inputSchema: {},
},
{
name: "write_file",
description: "Write contents to a file",
inputSchema: {},
},
],
resources: [],
resourceTemplates: [],
},
{
name: "database",
config: "npx -y @modelcontextprotocol/server-sqlite /path/to/database.db",
status: "disconnected" as const,
error: "Connection failed",
tools: [],
resources: [],
resourceTemplates: [],
},
]
const createStoryDecorator =
(
state: Partial<ExtensionState> = { mcpMarketplaceEnabled: true },
servers: McpMarketplaceCatalog = mcpMarketplaceCatalog,
mcpServers = mcpservers,
) =>
(Story: React.ComponentType) => {
const StoryWithCatalog = () => {
const { setMcpMarketplaceCatalog, setMcpServers } = useExtensionState()
React.useEffect(() => {
setMcpMarketplaceCatalog(servers)
setMcpServers(mcpServers)
// Don't call setMcpTab or navigateToMcp here as they interfere with the component's initialTab prop
}, [])
return <Story />
}
return (
<StorybookProvider
mockState={{
mcpMarketplaceEnabled: true,
...state,
}}>
<StoryWithCatalog />
</StorybookProvider>
)
}
const meta: Meta<typeof McpConfigurationView> = {
title: "Component/McpConfigurationView",
component: McpConfigurationView,
decorators: [VSCodeWebview],
argTypes: {
initialTab: {
control: "select",
options: ["marketplace", "addRemote", "installed"],
defaultValue: "marketplace",
},
},
parameters: {
layout: "fullscreen",
},
}
const mcpDecorator = (Story: React.ComponentType) => {
return (
<StorybookProvider>
<Story />
</StorybookProvider>
)
}
export default meta
type Story = StoryObj<typeof McpConfigurationView>
const defaultArgs = {
initialTab: "marketplace" as McpViewTab,
}
export const Default: Story = {
args: defaultArgs,
decorators: [mcpDecorator],
}
export const MarketplaceEnabled: Story = {
args: defaultArgs,
decorators: [createStoryDecorator({ mcpMarketplaceEnabled: true }, mcpMarketplaceCatalog)],
}
export const MarketplaceDisabled: Story = {
args: defaultArgs,
decorators: [createStoryDecorator({ mcpMarketplaceEnabled: false })],
}
export const DisconnectedServers: Story = {
args: {
...defaultArgs,
initialTab: "installed" as McpViewTab,
},
decorators: [
(Story) => {
const mockState = {
mcpMarketplaceEnabled: true,
} satisfies Partial<ExtensionState>
return (
<StorybookProvider mockState={mockState}>
<Story />
</StorybookProvider>
)
},
],
}
@@ -1,6 +1,5 @@
import { useExtensionState } from "@/context/ExtensionStateContext"
import { McpServiceClient } from "@/services/grpc-client"
import { vscode } from "@/utils/vscode"
import { McpViewTab } from "@shared/mcp"
import { EmptyRequest } from "@shared/proto/common"
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
@@ -19,18 +18,20 @@ type McpViewProps = {
const McpConfigurationView = ({ onDone, initialTab }: McpViewProps) => {
const { mcpMarketplaceEnabled, setMcpServers } = useExtensionState()
const [activeTab, setActiveTab] = useState<McpViewTab>(initialTab || (mcpMarketplaceEnabled ? "marketplace" : "installed"))
const [activeTab, setActiveTab] = useState<McpViewTab>(
mcpMarketplaceEnabled && initialTab === "marketplace" ? "marketplace" : "installed",
)
const handleTabChange = (tab: McpViewTab) => {
setActiveTab(tab)
}
useEffect(() => {
if (!mcpMarketplaceEnabled && activeTab === "marketplace") {
// If marketplace is disabled and we're on marketplace tab, switch to installed
setActiveTab("installed")
}
}, [mcpMarketplaceEnabled, activeTab])
// Only override the tab if no initialTab was explicitly provided and marketplace is disabled
const isMarketplaceSelected = activeTab === "marketplace" || initialTab === "marketplace"
// If marketplace is disabled and we're on marketplace tab, switch to installed
setActiveTab(mcpMarketplaceEnabled && isMarketplaceSelected ? "marketplace" : "installed")
}, [mcpMarketplaceEnabled, activeTab, initialTab])
// Get setter for MCP marketplace catalog from context
const { setMcpMarketplaceCatalog } = useExtensionState()
@@ -0,0 +1,246 @@
import type { Meta, StoryObj } from "@storybook/react-vite"
import React from "react"
import ApiOptions, { ApiOptionsProps } from "./ApiOptions"
import { ApiConfiguration } from "@shared/api"
import { StorybookProvider, VSCodeWebview } from "../common/StorybookDecorator"
import { ExtensionState } from "@shared/ExtensionMessage"
const mockApiConfiguration: ApiConfiguration = {
planModeApiProvider: undefined,
actModeApiProvider: "cline",
favoritedModelIds: [],
}
const createMockState = (config: Partial<ApiConfiguration>): Partial<ExtensionState> => ({
apiConfiguration: { ...mockApiConfiguration, ...config } as ApiConfiguration,
})
const createStoryDecorator = (config: Partial<ApiConfiguration>) => (Story: React.ComponentType) => (
<StorybookProvider mockState={createMockState(config)}>
<Story />
</StorybookProvider>
)
const meta: Meta<typeof ApiOptions> = {
title: "Component/ApiOptions",
component: ApiOptions,
decorators: [VSCodeWebview],
argTypes: {
showModelOptions: { control: "boolean", value: true },
apiErrorMessage: { control: "text" },
modelIdErrorMessage: { control: "text" },
isPopup: { control: "boolean", defaultValue: false },
currentMode: {
control: "select",
options: ["plan", "act"],
defaultValue: "act",
},
},
}
export default meta
type Story = StoryObj<typeof ApiOptions>
const defaultArgs = {
showModelOptions: true,
isPopup: false,
}
export const Default: Story = {}
export const ClineProvider: Story = {
args: defaultArgs,
decorators: [createStoryDecorator({ actModeApiModelId: "cline" })],
}
export const OpenRouterProvider: Story = {
args: defaultArgs,
decorators: [createStoryDecorator({ actModeApiProvider: "openrouter" })],
}
export const OpenAIProvider: Story = {
args: defaultArgs,
decorators: [
createStoryDecorator({
planModeApiProvider: "openai-native",
openAiApiKey: "sk-abc123...",
planModeOpenAiModelId: "gpt-4o",
}),
],
}
export const OllamaProvider: Story = {
args: defaultArgs,
decorators: [
createStoryDecorator({
actModeApiProvider: "ollama",
actModeOllamaModelId: "llama3.2:latest",
ollamaBaseUrl: "http://localhost:11434",
}),
],
}
export const BedrockProvider: Story = {
args: defaultArgs,
decorators: [
createStoryDecorator({
actModeApiProvider: "bedrock",
awsAccessKey: "AKIA...",
awsSecretKey: "secret123...",
awsRegion: "us-east-1",
actModeAwsBedrockCustomModelBaseId: "anthropic.claude-3-5-sonnet-20241022-v2:0",
}),
],
}
export const GeminiProvider: Story = {
args: defaultArgs,
decorators: [
createStoryDecorator({
actModeApiProvider: "gemini",
geminiApiKey: "AIza...",
actModeApiModelId: "gemini-1.5-pro-002",
}),
],
}
export const DeepSeekProvider: Story = {
args: defaultArgs,
decorators: [
createStoryDecorator({
actModeApiProvider: "deepseek",
deepSeekApiKey: "sk-abc123...",
actModeApiModelId: "deepseek-chat",
}),
],
}
export const VSCodeLMProvider: Story = {
args: defaultArgs,
decorators: [createStoryDecorator({ actModeApiProvider: "vscode-lm" })],
}
export const OpenAICompatibleProvider: Story = {
args: defaultArgs,
decorators: [
createStoryDecorator({
actModeApiProvider: "openai",
openAiApiKey: "custom-key-123",
openAiBaseUrl: "https://api.custom-provider.com/v1",
actModeApiModelId: "custom-model-v1",
}),
],
}
export const LMStudioProvider: Story = {
args: defaultArgs,
decorators: [
createStoryDecorator({
actModeApiProvider: "lmstudio",
lmStudioBaseUrl: "http://localhost:1234/v1",
actModeApiModelId: "local-model",
}),
],
}
const ErrorStatesComponent: React.FC<ApiOptionsProps> = (args) => {
const [errorType, setErrorType] = React.useState<"api" | "model" | "both" | "none">("api")
const errorProps = {
api: { apiErrorMessage: "Invalid API key. Please check your credentials and try again." },
model: { modelIdErrorMessage: "Selected model is not available. Please choose a different model." },
both: {
apiErrorMessage: "Connection failed: Unable to reach API endpoint.",
modelIdErrorMessage: "Model validation failed: Insufficient permissions for selected model.",
},
none: {},
}
return (
<StorybookProvider mockState={createMockState({})}>
<div className="mb-5">
<label className="block mb-2 font-medium">Error Type:</label>
<select
title="Error Type"
value={errorType}
onChange={(e) => setErrorType(e.target.value as typeof errorType)}
className="px-2 py-1 rounded border border-[var(--vscode-widget-border)] bg-[var(--vscode-input-background)] text-[var(--vscode-input-foreground)]">
<option value="api">API Error</option>
<option value="model">Model Error</option>
<option value="both">Both Errors</option>
<option value="none">No Errors</option>
</select>
</div>
<ApiOptions {...args} {...errorProps[errorType]} />
</StorybookProvider>
)
}
export const ErrorStates: Story = {
args: {
showModelOptions: true,
isPopup: false,
},
render: (args) => <ErrorStatesComponent {...args} />,
parameters: {
docs: {
description: {
story: "Interactive error states showing API, model, or both error messages.",
},
},
},
}
const ProviderComparisonComponent: React.FC<ApiOptionsProps> = (args) => {
const [selectedProvider, setSelectedProvider] = React.useState("anthropic")
const providerConfigs = {
anthropic: { actModeApiProvider: "anthropic" },
"openai-native": { actModeApiProvider: "openai-native", openAiApiKey: "sk-test..." },
openrouter: { actModeApiProvider: "openrouter", openRouterApiKey: "or-v1-test..." },
ollama: { actModeApiProvider: "ollama", ollamaBaseUrl: "http://localhost:11434" },
gemini: { actModeApiProvider: "gemini", geminiApiKey: "AIza..." },
} as const
const providers = Object.keys(providerConfigs).map((id) => ({
id,
name: id.charAt(0).toUpperCase() + id.slice(1).replace("-", " "),
}))
const mockState = React.useMemo(
() => createMockState(providerConfigs[selectedProvider as keyof typeof providerConfigs]),
[selectedProvider],
)
return (
<StorybookProvider mockState={mockState}>
<div className="mb-5">
<label className="block mb-2 font-medium">Switch Provider:</label>
<select
title="API Provider"
value={selectedProvider}
onChange={(e) => setSelectedProvider(e.target.value)}
className="px-2 py-1 rounded border border-[var(--vscode-widget-border)] bg-[var(--vscode-input-background)] text-[var(--vscode-input-foreground)]">
{providers.map((provider) => (
<option key={provider.id} value={provider.id}>
{provider.name}
</option>
))}
</select>
</div>
<ApiOptions {...args} />
</StorybookProvider>
)
}
export const ProviderComparison: Story = {
args: defaultArgs,
render: (args) => <ProviderComparisonComponent {...args} />,
parameters: {
docs: {
description: {
story: "Interactive comparison of different API providers with a provider switcher.",
},
},
},
}
@@ -1,7 +1,7 @@
import { useExtensionState } from "@/context/ExtensionStateContext"
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 { ModelsServiceClient } from "@/services/grpc-client"
import { StringRequest } from "@shared/proto/common"
import { VSCodeDropdown, VSCodeOption } from "@vscode/webview-ui-toolkit/react"
import { useCallback, useEffect, useState } from "react"
import { useInterval } from "react-use"
import styled from "styled-components"
@@ -38,12 +38,12 @@ import { LMStudioProvider } from "./providers/LMStudioProvider"
import { useApiConfigurationHandlers } from "./utils/useApiConfigurationHandlers"
import { GroqProvider } from "./providers/GroqProvider"
interface ApiOptionsProps {
showSubmitButton?: boolean
export interface ApiOptionsProps {
showModelOptions: boolean
apiErrorMessage?: string
modelIdErrorMessage?: string
isPopup?: boolean
currentMode: "plan" | "act"
}
// This is necessary to ensure dropdown opens downward, important for when this is used in popup
@@ -70,24 +70,17 @@ declare module "vscode" {
}
}
const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, isPopup, showSubmitButton }: ApiOptionsProps) => {
const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, isPopup, currentMode }: ApiOptionsProps) => {
// Use full context state for immediate save payload
const { apiConfiguration, uriScheme } = useExtensionState()
const { apiConfiguration } = useExtensionState()
const selectedProvider = apiConfiguration?.apiProvider
const selectedProvider =
(currentMode === "plan" ? apiConfiguration?.planModeApiProvider : apiConfiguration?.actModeApiProvider) || "cline"
const { handleFieldChange } = useApiConfigurationHandlers()
const { handleModeFieldChange } = 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") {
@@ -132,7 +125,11 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
id="api-provider"
value={selectedProvider}
onChange={(e: any) => {
handleFieldChange("apiProvider", e.target.value)
handleModeFieldChange(
{ plan: "planModeApiProvider", act: "actModeApiProvider" },
e.target.value,
currentMode,
)
}}
style={{
minWidth: 130,
@@ -143,13 +140,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-native">OpenAI</VSCodeOption>
<VSCodeOption value="openai">OpenAI Compatible</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>
@@ -171,96 +168,96 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
</DropdownContainer>
{apiConfiguration && selectedProvider === "cline" && (
<ClineProvider showModelOptions={showModelOptions} isPopup={isPopup} />
<ClineProvider showModelOptions={showModelOptions} isPopup={isPopup} currentMode={currentMode} />
)}
{apiConfiguration && selectedProvider === "asksage" && (
<AskSageProvider showModelOptions={showModelOptions} isPopup={isPopup} />
<AskSageProvider showModelOptions={showModelOptions} isPopup={isPopup} currentMode={currentMode} />
)}
{apiConfiguration && selectedProvider === "anthropic" && (
<AnthropicProvider showModelOptions={showModelOptions} isPopup={isPopup} />
<AnthropicProvider showModelOptions={showModelOptions} isPopup={isPopup} currentMode={currentMode} />
)}
{apiConfiguration && selectedProvider === "claude-code" && (
<ClaudeCodeProvider showModelOptions={showModelOptions} isPopup={isPopup} />
<ClaudeCodeProvider showModelOptions={showModelOptions} isPopup={isPopup} currentMode={currentMode} />
)}
{apiConfiguration && selectedProvider === "openai-native" && (
<OpenAINativeProvider showModelOptions={showModelOptions} isPopup={isPopup} />
<OpenAINativeProvider showModelOptions={showModelOptions} isPopup={isPopup} currentMode={currentMode} />
)}
{apiConfiguration && selectedProvider === "qwen" && (
<QwenProvider showModelOptions={showModelOptions} isPopup={isPopup} />
<QwenProvider showModelOptions={showModelOptions} isPopup={isPopup} currentMode={currentMode} />
)}
{apiConfiguration && selectedProvider === "doubao" && (
<DoubaoProvider showModelOptions={showModelOptions} isPopup={isPopup} />
<DoubaoProvider showModelOptions={showModelOptions} isPopup={isPopup} currentMode={currentMode} />
)}
{apiConfiguration && selectedProvider === "mistral" && (
<MistralProvider showModelOptions={showModelOptions} isPopup={isPopup} />
<MistralProvider showModelOptions={showModelOptions} isPopup={isPopup} currentMode={currentMode} />
)}
{apiConfiguration && selectedProvider === "openrouter" && (
<OpenRouterProvider showModelOptions={showModelOptions} isPopup={isPopup} uriScheme={uriScheme} />
<OpenRouterProvider showModelOptions={showModelOptions} isPopup={isPopup} currentMode={currentMode} />
)}
{apiConfiguration && selectedProvider === "deepseek" && (
<DeepSeekProvider showModelOptions={showModelOptions} isPopup={isPopup} />
<DeepSeekProvider showModelOptions={showModelOptions} isPopup={isPopup} currentMode={currentMode} />
)}
{apiConfiguration && selectedProvider === "together" && (
<TogetherProvider showModelOptions={showModelOptions} isPopup={isPopup} />
<TogetherProvider showModelOptions={showModelOptions} isPopup={isPopup} currentMode={currentMode} />
)}
{apiConfiguration && selectedProvider === "openai" && (
<OpenAICompatibleProvider showModelOptions={showModelOptions} isPopup={isPopup} />
<OpenAICompatibleProvider showModelOptions={showModelOptions} isPopup={isPopup} currentMode={currentMode} />
)}
{apiConfiguration && selectedProvider === "sambanova" && (
<SambanovaProvider showModelOptions={showModelOptions} isPopup={isPopup} />
<SambanovaProvider showModelOptions={showModelOptions} isPopup={isPopup} currentMode={currentMode} />
)}
{apiConfiguration && selectedProvider === "bedrock" && (
<BedrockProvider showModelOptions={showModelOptions} isPopup={isPopup} />
<BedrockProvider showModelOptions={showModelOptions} isPopup={isPopup} currentMode={currentMode} />
)}
{apiConfiguration && selectedProvider === "vertex" && (
<VertexProvider showModelOptions={showModelOptions} isPopup={isPopup} />
<VertexProvider showModelOptions={showModelOptions} isPopup={isPopup} currentMode={currentMode} />
)}
{apiConfiguration && selectedProvider === "gemini" && (
<GeminiProvider showModelOptions={showModelOptions} isPopup={isPopup} />
<GeminiProvider showModelOptions={showModelOptions} isPopup={isPopup} currentMode={currentMode} />
)}
{apiConfiguration && selectedProvider === "requesty" && (
<RequestyProvider showModelOptions={showModelOptions} isPopup={isPopup} />
<RequestyProvider showModelOptions={showModelOptions} isPopup={isPopup} currentMode={currentMode} />
)}
{apiConfiguration && selectedProvider === "fireworks" && (
<FireworksProvider showModelOptions={showModelOptions} isPopup={isPopup} />
<FireworksProvider showModelOptions={showModelOptions} isPopup={isPopup} currentMode={currentMode} />
)}
{apiConfiguration && selectedProvider === "vscode-lm" && <VSCodeLmProvider />}
{apiConfiguration && selectedProvider === "vscode-lm" && <VSCodeLmProvider currentMode={currentMode} />}
{apiConfiguration && selectedProvider === "groq" && (
<GroqProvider showModelOptions={showModelOptions} isPopup={isPopup} />
<GroqProvider showModelOptions={showModelOptions} isPopup={isPopup} currentMode={currentMode} />
)}
{apiConfiguration && selectedProvider === "litellm" && (
<LiteLlmProvider showModelOptions={showModelOptions} isPopup={isPopup} />
<LiteLlmProvider showModelOptions={showModelOptions} isPopup={isPopup} currentMode={currentMode} />
)}
{apiConfiguration && selectedProvider === "lmstudio" && (
<LMStudioProvider showModelOptions={showModelOptions} isPopup={isPopup} />
<LMStudioProvider showModelOptions={showModelOptions} isPopup={isPopup} currentMode={currentMode} />
)}
{apiConfiguration && selectedProvider === "ollama" && (
<OllamaProvider showModelOptions={showModelOptions} isPopup={isPopup} />
<OllamaProvider showModelOptions={showModelOptions} isPopup={isPopup} currentMode={currentMode} />
)}
{apiConfiguration && selectedProvider === "moonshot" && (
<MoonshotProvider showModelOptions={showModelOptions} isPopup={isPopup} />
<MoonshotProvider showModelOptions={showModelOptions} isPopup={isPopup} currentMode={currentMode} />
)}
{apiConfiguration && selectedProvider === "huggingface" && (
@@ -268,19 +265,19 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
)}
{apiConfiguration && selectedProvider === "nebius" && (
<NebiusProvider showModelOptions={showModelOptions} isPopup={isPopup} />
<NebiusProvider showModelOptions={showModelOptions} isPopup={isPopup} currentMode={currentMode} />
)}
{apiConfiguration && selectedProvider === "xai" && (
<XaiProvider showModelOptions={showModelOptions} isPopup={isPopup} />
<XaiProvider showModelOptions={showModelOptions} isPopup={isPopup} currentMode={currentMode} />
)}
{apiConfiguration && selectedProvider === "cerebras" && (
<CerebrasProvider showModelOptions={showModelOptions} isPopup={isPopup} />
<CerebrasProvider showModelOptions={showModelOptions} isPopup={isPopup} currentMode={currentMode} />
)}
{apiConfiguration && selectedProvider === "sapaicore" && (
<SapAiCoreProvider showModelOptions={showModelOptions} isPopup={isPopup} />
<SapAiCoreProvider showModelOptions={showModelOptions} isPopup={isPopup} currentMode={currentMode} />
)}
{apiErrorMessage && (
@@ -303,12 +300,6 @@ 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,15 +12,18 @@ 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"
export interface GroqModelPickerProps {
isPopup?: boolean
currentMode: "plan" | "act"
}
const GroqModelPicker: React.FC<GroqModelPickerProps> = ({ isPopup }) => {
const GroqModelPicker: React.FC<GroqModelPickerProps> = ({ isPopup, currentMode }) => {
const { apiConfiguration, groqModels: dynamicGroqModels, setGroqModels } = useExtensionState()
const { handleFieldsChange } = useApiConfigurationHandlers()
const [searchTerm, setSearchTerm] = useState(apiConfiguration?.groqModelId || groqDefaultModelId)
const { handleModeFieldsChange } = useApiConfigurationHandlers()
const modeFields = getModeSpecificFields(apiConfiguration, currentMode)
const [searchTerm, setSearchTerm] = useState(modeFields.groqModelId || groqDefaultModelId)
const [debouncedSearchTerm, setDebouncedSearchTerm] = useState(searchTerm)
const [isDropdownVisible, setIsDropdownVisible] = useState(false)
const [selectedIndex, setSelectedIndex] = useState(-1)
@@ -32,16 +35,23 @@ const GroqModelPicker: React.FC<GroqModelPickerProps> = ({ isPopup }) => {
// Use dynamic models if available, otherwise fall back to static models
const modelInfo = dynamicGroqModels?.[newModelId] || groqModels[newModelId as keyof typeof groqModels]
handleFieldsChange({
groqModelId: newModelId,
groqModelInfo: modelInfo,
})
handleModeFieldsChange(
{
groqModelId: { plan: "planModeGroqModelId", act: "actModeGroqModelId" },
groqModelInfo: { plan: "planModeGroqModelInfo", act: "actModeGroqModelInfo" },
},
{
groqModelId: newModelId,
groqModelInfo: modelInfo,
},
currentMode,
)
setSearchTerm(newModelId)
}
const { selectedModelId, selectedModelInfo } = useMemo(() => {
return normalizeApiConfiguration(apiConfiguration)
}, [apiConfiguration])
return normalizeApiConfiguration(apiConfiguration, currentMode)
}, [apiConfiguration, currentMode])
useMount(() => {
ModelsServiceClient.refreshGroqModels(EmptyRequest.create({}))
@@ -11,7 +11,7 @@ import { useMount } from "react-use"
import styled from "styled-components"
import { highlight } from "../history/HistoryView"
import { ModelInfoView } from "./common/ModelInfoView"
import { normalizeApiConfiguration } from "./utils/providerUtils"
import { getModeSpecificFields, normalizeApiConfiguration } from "./utils/providerUtils"
import FeaturedModelCard from "./FeaturedModelCard"
import ThinkingBudgetSlider from "./ThinkingBudgetSlider"
import { useApiConfigurationHandlers } from "./utils/useApiConfigurationHandlers"
@@ -39,6 +39,7 @@ const StarIcon = ({ isFavorite, onClick }: { isFavorite: boolean; onClick: (e: R
export interface OpenRouterModelPickerProps {
isPopup?: boolean
currentMode: "plan" | "act"
}
// Featured models for Cline provider
@@ -60,10 +61,11 @@ const featuredModels = [
},
]
const OpenRouterModelPicker: React.FC<OpenRouterModelPickerProps> = ({ isPopup }) => {
const { handleFieldsChange } = useApiConfigurationHandlers()
const OpenRouterModelPicker: React.FC<OpenRouterModelPickerProps> = ({ isPopup, currentMode }) => {
const { handleModeFieldsChange } = useApiConfigurationHandlers()
const { apiConfiguration, openRouterModels, refreshOpenRouterModels } = useExtensionState()
const [searchTerm, setSearchTerm] = useState(apiConfiguration?.openRouterModelId || openRouterDefaultModelId)
const modeFields = getModeSpecificFields(apiConfiguration, currentMode)
const [searchTerm, setSearchTerm] = useState(modeFields.openRouterModelId || openRouterDefaultModelId)
const [isSearchInputDirty, setIsSearchInputDirty] = useState(false)
const [isDropdownVisible, setIsDropdownVisible] = useState(false)
const [selectedIndex, setSelectedIndex] = useState(-1)
@@ -76,25 +78,32 @@ const OpenRouterModelPicker: React.FC<OpenRouterModelPickerProps> = ({ isPopup }
setSearchTerm(newModelId)
handleFieldsChange({
openRouterModelId: newModelId,
openRouterModelInfo: openRouterModels[newModelId],
})
handleModeFieldsChange(
{
openRouterModelId: { plan: "planModeOpenRouterModelId", act: "actModeOpenRouterModelId" },
openRouterModelInfo: { plan: "planModeOpenRouterModelInfo", act: "actModeOpenRouterModelInfo" },
},
{
openRouterModelId: newModelId,
openRouterModelInfo: openRouterModels[newModelId],
},
currentMode,
)
}
const { selectedModelId, selectedModelInfo } = useMemo(() => {
return normalizeApiConfiguration(apiConfiguration)
}, [apiConfiguration])
return normalizeApiConfiguration(apiConfiguration, currentMode)
}, [apiConfiguration, currentMode])
useMount(refreshOpenRouterModels)
// Sync external changes only when user isn't actively typing
useEffect(() => {
if (!isSearchInputDirty) {
const currentModelId = apiConfiguration?.openRouterModelId || openRouterDefaultModelId
const currentModelId = modeFields.openRouterModelId || openRouterDefaultModelId
setSearchTerm(currentModelId)
}
}, [apiConfiguration?.openRouterModelId, isSearchInputDirty])
}, [modeFields.openRouterModelId, isSearchInputDirty])
// Reset dirty flag after user stops typing (1 second timeout)
useEffect(() => {
@@ -121,10 +130,8 @@ const OpenRouterModelPicker: React.FC<OpenRouterModelPickerProps> = ({ isPopup }
const modelIds = useMemo(() => {
const unfilteredModelIds = Object.keys(openRouterModels).sort((a, b) => a.localeCompare(b))
return apiConfiguration?.apiProvider === "cline"
? unfilteredModelIds.filter((id) => !id.includes(":free"))
: unfilteredModelIds
}, [openRouterModels, apiConfiguration?.apiProvider])
return modeFields.apiProvider === "cline" ? unfilteredModelIds.filter((id) => !id.includes(":free")) : unfilteredModelIds
}, [openRouterModels, modeFields.apiProvider])
const searchableItems = useMemo(() => {
return modelIds.map((id) => ({
@@ -237,7 +244,7 @@ const OpenRouterModelPicker: React.FC<OpenRouterModelPickerProps> = ({ isPopup }
<span style={{ fontWeight: 500 }}>Model</span>
</label>
{apiConfiguration?.apiProvider === "cline" && (
{modeFields.apiProvider === "cline" && (
<div style={{ marginBottom: "6px", marginTop: 4 }}>
{featuredModels.map((model) => (
<FeaturedModelCard
@@ -326,7 +333,7 @@ const OpenRouterModelPicker: React.FC<OpenRouterModelPickerProps> = ({ isPopup }
{hasInfo ? (
<>
{showBudgetSlider && <ThinkingBudgetSlider />}
{showBudgetSlider && <ThinkingBudgetSlider currentMode={currentMode} />}
<ModelInfoView selectedModelId={selectedModelId} modelInfo={selectedModelInfo} isPopup={isPopup} />
</>
@@ -11,18 +11,20 @@ import { ModelsServiceClient } from "../../services/grpc-client"
import { CODE_BLOCK_BG_COLOR } from "../common/CodeBlock"
import { highlight } from "../history/HistoryView"
import { ModelInfoView } from "./common/ModelInfoView"
import { normalizeApiConfiguration } from "./utils/providerUtils"
import { getModeSpecificFields, normalizeApiConfiguration } from "./utils/providerUtils"
import ThinkingBudgetSlider from "./ThinkingBudgetSlider"
import { useApiConfigurationHandlers } from "./utils/useApiConfigurationHandlers"
export interface RequestyModelPickerProps {
isPopup?: boolean
currentMode: "plan" | "act"
}
const RequestyModelPicker: React.FC<RequestyModelPickerProps> = ({ isPopup }) => {
const RequestyModelPicker: React.FC<RequestyModelPickerProps> = ({ isPopup, currentMode }) => {
const { apiConfiguration, requestyModels, setRequestyModels } = useExtensionState()
const { handleFieldsChange } = useApiConfigurationHandlers()
const [searchTerm, setSearchTerm] = useState(apiConfiguration?.requestyModelId || requestyDefaultModelId)
const { handleModeFieldsChange } = useApiConfigurationHandlers()
const modeFields = getModeSpecificFields(apiConfiguration, currentMode)
const [searchTerm, setSearchTerm] = useState(modeFields.requestyModelId || requestyDefaultModelId)
const [isDropdownVisible, setIsDropdownVisible] = useState(false)
const [selectedIndex, setSelectedIndex] = useState(-1)
const dropdownRef = useRef<HTMLDivElement>(null)
@@ -32,16 +34,23 @@ const RequestyModelPicker: React.FC<RequestyModelPickerProps> = ({ isPopup }) =>
const handleModelChange = (newModelId: string) => {
// could be setting invalid model id/undefined info but validation will catch it
handleFieldsChange({
requestyModelId: newModelId,
requestyModelInfo: requestyModels[newModelId],
})
handleModeFieldsChange(
{
requestyModelId: { plan: "planModeRequestyModelId", act: "actModeRequestyModelId" },
requestyModelInfo: { plan: "planModeRequestyModelInfo", act: "actModeRequestyModelInfo" },
},
{
requestyModelId: newModelId,
requestyModelInfo: requestyModels[newModelId],
},
currentMode,
)
setSearchTerm(newModelId)
}
const { selectedModelId, selectedModelInfo } = useMemo(() => {
return normalizeApiConfiguration(apiConfiguration)
}, [apiConfiguration])
return normalizeApiConfiguration(apiConfiguration, currentMode)
}, [apiConfiguration, currentMode])
useMount(() => {
ModelsServiceClient.refreshRequestyModels(EmptyRequest.create({}))
@@ -226,7 +235,7 @@ const RequestyModelPicker: React.FC<RequestyModelPickerProps> = ({ isPopup }) =>
{hasInfo ? (
<>
{showBudgetSlider && <ThinkingBudgetSlider />}
{showBudgetSlider && <ThinkingBudgetSlider currentMode={currentMode} />}
<ModelInfoView selectedModelId={selectedModelId} modelInfo={selectedModelInfo} isPopup={isPopup} />
</>
) : (
@@ -0,0 +1,419 @@
import type { Meta, StoryObj } from "@storybook/react-vite"
import React from "react"
import SettingsView from "./SettingsView"
import { DEFAULT_AUTO_APPROVAL_SETTINGS } from "@shared/AutoApprovalSettings"
import { DEFAULT_BROWSER_SETTINGS } from "@shared/BrowserSettings"
import { DEFAULT_CHAT_SETTINGS } from "@shared/ChatSettings"
import { StorybookProvider, VSCodeWebview } from "../common/StorybookDecorator"
import { ExtensionState } from "@shared/ExtensionMessage"
const meta: Meta<typeof SettingsView> = {
title: "Views/SettingsView",
component: SettingsView,
parameters: {
layout: "fullscreen",
docs: {
description: {
component: `
The SettingsView component provides a comprehensive settings interface for Cline with tabbed navigation.
**Features:**
- **Tabbed Interface**: Organized settings into logical sections (API Configuration, General, Features, Browser, Terminal, Debug, About)
- **Responsive Design**: Adapts to different screen sizes with compact mode for narrow layouts
- **Plan/Act Mode Support**: Separate model configurations for planning and execution modes
- **Real-time Updates**: All settings save immediately without requiring a save button
- **Contextual Navigation**: Can navigate directly to specific settings sections
- **Debug Tools**: Development-only debug section for state management
- **Accessibility**: Proper ARIA labels and keyboard navigation support
**Tabs:**
- **API Configuration**: Model selection, API keys, and provider settings
- **General**: Language preferences, telemetry, and basic settings
- **Features**: Auto-approval, checkpoints, MCP marketplace, and feature toggles
- **Browser**: Browser automation settings and preferences
- **Terminal**: Terminal integration, profiles, and output settings
- **Debug**: Development tools for state reset and debugging (dev mode only)
- **About**: Version information and extension details
**Use Cases:**
- Configuring AI models and API providers
- Customizing extension behavior and features
- Managing terminal and browser integration
- Debugging and troubleshooting (development)
`,
},
},
},
decorators: [VSCodeWebview],
argTypes: {
onDone: {
action: "done clicked",
description: "Callback when the Done button is clicked",
},
targetSection: {
control: "select",
options: ["api-config", "general", "features", "browser", "terminal", "debug", "about"],
description: "The settings section to navigate to initially",
},
},
}
export default meta
type Story = StoryObj<typeof SettingsView>
// Default settings view
export const Default: Story = {
args: {
onDone: () => console.log("Settings done clicked"),
},
parameters: {
docs: {
description: {
story: "Default settings view starting with the API Configuration tab.",
},
},
},
}
// API Configuration tab focused
export const ApiConfiguration: Story = {
args: {
onDone: () => console.log("Settings done clicked"),
targetSection: "api-config",
},
parameters: {
docs: {
description: {
story: "Settings view focused on the API Configuration tab for model and provider setup.",
},
},
},
}
// General settings tab
export const GeneralSettings: Story = {
args: {
onDone: () => console.log("Settings done clicked"),
targetSection: "general",
},
parameters: {
docs: {
description: {
story: "General settings tab with language preferences and basic configuration options.",
},
},
},
}
// Features tab with enhanced state
export const FeaturesTab: Story = {
args: {
onDone: () => console.log("Settings done clicked"),
targetSection: "features",
},
decorators: [
(Story) => {
const defaultStates = {
autoApprovalSettings: {
...DEFAULT_AUTO_APPROVAL_SETTINGS,
},
apiConfiguration: {
actModeApiProvider: "cline",
planModeApiProvider: "cline",
},
enableCheckpointsSetting: true,
mcpMarketplaceEnabled: true,
mcpDisplayMode: "rich",
} satisfies Partial<ExtensionState>
return (
<StorybookProvider mockState={defaultStates}>
<Story />
</StorybookProvider>
)
},
],
parameters: {
docs: {
description: {
story: "Features tab showing auto-approval settings, checkpoints, and MCP marketplace options.",
},
},
},
}
// Browser settings tab
export const BrowserSettings: Story = {
args: {
onDone: () => console.log("Settings done clicked"),
targetSection: "browser",
},
decorators: [
(Story) => {
const mockState = {
browserSettings: {
...DEFAULT_BROWSER_SETTINGS,
viewport: { width: 1200, height: 800 },
userAgent: "Custom User Agent String",
},
}
return (
<StorybookProvider mockState={mockState}>
<Story />
</StorybookProvider>
)
},
],
parameters: {
docs: {
description: {
story: "Browser settings tab with viewport configuration and user agent settings.",
},
},
},
}
// Terminal settings tab
export const TerminalSettings: Story = {
args: {
onDone: () => console.log("Settings done clicked"),
targetSection: "terminal",
},
decorators: [
(Story) => {
const mockState = {
shellIntegrationTimeout: 6000,
terminalReuseEnabled: false,
terminalOutputLineLimit: 1000,
defaultTerminalProfile: "PowerShell",
availableTerminalProfiles: [
{ id: "powershell", name: "PowerShell", path: "powershell.exe" },
{ id: "cmd", name: "Command Prompt", path: "cmd.exe" },
{ id: "bash", name: "Git Bash", path: "bash.exe" },
],
}
return (
<StorybookProvider mockState={mockState}>
<Story />
</StorybookProvider>
)
},
],
parameters: {
docs: {
description: {
story: "Terminal settings tab with shell integration, profiles, and output configuration.",
},
},
},
}
// Debug tab (development mode)
export const DebugTab: Story = {
args: {
onDone: () => console.log("Settings done clicked"),
targetSection: "debug",
},
parameters: {
docs: {
description: {
story: "Debug tab with development tools for state management and troubleshooting. Only visible in development mode.",
},
},
},
}
// About tab
export const AboutTab: Story = {
args: {
onDone: () => console.log("Settings done clicked"),
targetSection: "about",
},
decorators: [
(Story) => {
return (
<StorybookProvider>
<Story />
</StorybookProvider>
)
},
],
parameters: {
docs: {
description: {
story: "About tab showing version information and extension details.",
},
},
},
}
// Plan mode configuration
export const PlanModeConfiguration: Story = {
args: {
onDone: () => console.log("Settings done clicked"),
targetSection: "api-config",
},
decorators: [
(Story) => {
const mockState = {
chatSettings: {
...DEFAULT_CHAT_SETTINGS,
mode: "plan" as const,
},
planActSeparateModelsSetting: true,
}
return (
<StorybookProvider mockState={mockState}>
<Story />
</StorybookProvider>
)
},
],
parameters: {
docs: {
description: {
story: "Settings view in Plan mode showing separate model configuration options.",
},
},
},
}
// Act mode configuration
export const ActModeConfiguration: Story = {
args: {
onDone: () => console.log("Settings done clicked"),
targetSection: "api-config",
},
decorators: [
(Story) => {
const mockState = {
chatSettings: {
...DEFAULT_CHAT_SETTINGS,
mode: "act" as const,
},
planActSeparateModelsSetting: true,
}
return (
<StorybookProvider mockState={mockState}>
<Story />
</StorybookProvider>
)
},
],
parameters: {
docs: {
description: {
story: "Settings view in Act mode showing execution-focused model configuration.",
},
},
},
}
// Compact mode (narrow layout)
export const CompactMode: Story = {
args: {
onDone: () => console.log("Settings done clicked"),
},
decorators: [
(Story) => {
return (
<StorybookProvider>
<Story />
</StorybookProvider>
)
},
],
parameters: {
docs: {
description: {
story: "Settings view in compact mode for narrow layouts, showing icon-only navigation with tooltips.",
},
},
},
}
// With comprehensive feature toggles
export const WithFeatureToggles: Story = {
args: {
onDone: () => console.log("Settings done clicked"),
targetSection: "features",
},
decorators: [
(Story) => {
const mockState = {
globalClineRulesToggles: {
"use-typescript": true,
"prefer-react": false,
"follow-conventions": true,
},
localClineRulesToggles: {
"project-specific": true,
"local-override": false,
},
globalWorkflowToggles: {
"auto-format": true,
"run-tests": false,
},
localWorkflowToggles: {
"build-on-save": true,
},
}
return (
<StorybookProvider mockState={mockState}>
<Story />
</StorybookProvider>
)
},
],
parameters: {
docs: {
description: {
story: "Features tab with various rule and workflow toggles enabled to demonstrate the full feature set.",
},
},
},
}
// Mode switching in progress
export const ModeSwitching: Story = {
args: {
onDone: () => console.log("Settings done clicked"),
targetSection: "api-config",
},
decorators: [
(Story) => {
// Simulate mode switching state
const [isSwitching, setIsSwitching] = React.useState(true)
React.useEffect(() => {
const timer = setTimeout(() => setIsSwitching(false), 2000)
return () => clearTimeout(timer)
}, [])
const mockState = {
chatSettings: {
...DEFAULT_CHAT_SETTINGS,
mode: "plan" as const,
},
}
return (
<StorybookProvider mockState={mockState}>
<Story />
</StorybookProvider>
)
},
],
parameters: {
docs: {
description: {
story: "Settings view showing the mode switching state with disabled controls during transition.",
},
},
},
}
@@ -103,7 +103,6 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
// Track active tab
const [activeTab, setActiveTab] = useState<string>(targetSection || SETTINGS_TABS[0].id)
// Track if we're currently switching modes
const [isSwitchingMode, setIsSwitchingMode] = useState(false)
const { version, chatSettings } = useExtensionState()
@@ -158,34 +157,6 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
}
}
const handlePlanActModeChange = async (tab: "plan" | "act") => {
// Prevent switching if already in that mode or if currently switching
if (tab === chatSettings.mode || isSwitchingMode) {
return
}
// All settings save immediately, so we can switch modes directly
setIsSwitchingMode(true)
try {
// Perform the mode switch
await StateServiceClient.togglePlanActMode(
TogglePlanActModeRequest.create({
chatSettings: {
mode: tab === "plan" ? PlanActMode.PLAN : PlanActMode.ACT,
preferredLanguage: chatSettings.preferredLanguage,
openAiReasoningEffort: chatSettings.openAIReasoningEffort,
},
}),
)
} catch (error) {
console.error("Failed to toggle Plan/Act mode:", error)
} finally {
// Always re-enable mode switching, even on error
setIsSwitchingMode(false)
}
}
// Update active tab when targetSection changes
useEffect(() => {
if (targetSection) {
@@ -313,13 +284,7 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
return (
<TabContent className="flex-1 overflow-auto">
{/* API Configuration Tab */}
{activeTab === "api-config" && (
<ApiConfigurationSection
isSwitchingMode={isSwitchingMode}
handlePlanActModeChange={handlePlanActModeChange}
renderSectionHeader={renderSectionHeader}
/>
)}
{activeTab === "api-config" && <ApiConfigurationSection renderSectionHeader={renderSectionHeader} />}
{/* General Settings Tab */}
{activeTab === "general" && <GeneralSettingsSection renderSectionHeader={renderSectionHeader} />}
@@ -4,6 +4,7 @@ import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"
import styled from "styled-components"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { useApiConfigurationHandlers } from "./utils/useApiConfigurationHandlers"
import { getModeSpecificFields } from "./utils/providerUtils"
// Constants
const DEFAULT_MIN_VALID_TOKENS = 1024
@@ -83,20 +84,23 @@ const RangeInput = styled.input<{ $value: number; $min: number; $max: number }>`
interface ThinkingBudgetSliderProps {
maxBudget?: number
currentMode: "plan" | "act"
}
const ThinkingBudgetSlider = ({ maxBudget }: ThinkingBudgetSliderProps) => {
const ThinkingBudgetSlider = ({ maxBudget, currentMode }: ThinkingBudgetSliderProps) => {
const { apiConfiguration } = useExtensionState()
const { handleFieldChange } = useApiConfigurationHandlers()
const { handleModeFieldChange } = useApiConfigurationHandlers()
const [isEnabled, setIsEnabled] = useState<boolean>((apiConfiguration?.thinkingBudgetTokens || 0) > 0)
const modeFields = getModeSpecificFields(apiConfiguration, currentMode)
const [isEnabled, setIsEnabled] = useState<boolean>((modeFields.thinkingBudgetTokens || 0) > 0)
const maxTokens = useMemo(
() =>
apiConfiguration?.apiProvider === "gemini"
modeFields.apiProvider === "gemini"
? geminiModels[geminiDefaultModelId].maxTokens
: anthropicModels["claude-3-7-sonnet-20250219"].maxTokens,
[apiConfiguration?.apiProvider],
[modeFields.apiProvider],
)
// use maxBudget prop if provided, otherwise apply the percentage cap to maxTokens
@@ -108,7 +112,7 @@ const ThinkingBudgetSlider = ({ maxBudget }: ThinkingBudgetSliderProps) => {
}, [maxBudget, maxTokens])
// Add local state for the slider value
const [localValue, setLocalValue] = useState(apiConfiguration?.thinkingBudgetTokens || 0)
const [localValue, setLocalValue] = useState(modeFields.thinkingBudgetTokens || 0)
const handleSliderChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
const value = parseInt(event.target.value, 10)
@@ -116,7 +120,11 @@ const ThinkingBudgetSlider = ({ maxBudget }: ThinkingBudgetSliderProps) => {
}, [])
const handleSliderComplete = () => {
handleFieldChange("thinkingBudgetTokens", localValue)
handleModeFieldChange(
{ plan: "planModeThinkingBudgetTokens", act: "actModeThinkingBudgetTokens" },
localValue,
currentMode,
)
}
const handleToggleChange = (event: any) => {
@@ -125,7 +133,7 @@ const ThinkingBudgetSlider = ({ maxBudget }: ThinkingBudgetSliderProps) => {
setIsEnabled(isChecked)
setLocalValue(newValue)
handleFieldChange("thinkingBudgetTokens", newValue)
handleModeFieldChange({ plan: "planModeThinkingBudgetTokens", act: "actModeThinkingBudgetTokens" }, newValue, currentMode)
}
return (
@@ -11,13 +11,16 @@ vi.mock("../../../context/ExtensionStateContext", async (importOriginal) => {
// your mocked methods
useExtensionState: vi.fn(() => ({
apiConfiguration: {
apiProvider: "requesty",
planModeApiProvider: "requesty",
actModeApiProvider: "requesty",
requestyApiKey: "",
requestyModelId: "",
planModeRequestyModelId: "",
actModeRequestyModelId: "",
},
setApiConfiguration: vi.fn(),
uriScheme: "vscode",
requestyModels: {},
planActSeparateModelsSetting: false,
})),
}
})
@@ -28,6 +31,7 @@ const mockExtensionState = (apiConfiguration: Partial<ApiConfiguration>) => {
setApiConfiguration: vi.fn(),
uriScheme: "vscode",
requestyModels: {},
planActSeparateModelsSetting: false,
} as any)
}
@@ -39,14 +43,15 @@ describe("ApiOptions Component", () => {
//@ts-expect-error - vscode is not defined in the global namespace in test environment
global.vscode = { postMessage: mockPostMessage }
mockExtensionState({
apiProvider: "requesty",
planModeApiProvider: "requesty",
actModeApiProvider: "requesty",
})
})
it("renders Requesty API Key input", () => {
render(
<ExtensionStateContextProvider>
<ApiOptions showModelOptions={true} />
<ApiOptions showModelOptions={true} currentMode="plan" />
</ExtensionStateContextProvider>,
)
const apiKeyInput = screen.getByPlaceholderText("Enter API Key...")
@@ -56,7 +61,7 @@ describe("ApiOptions Component", () => {
it("renders Requesty Model ID input", () => {
render(
<ExtensionStateContextProvider>
<ApiOptions showModelOptions={true} />
<ApiOptions showModelOptions={true} currentMode="plan" />
</ExtensionStateContextProvider>,
)
const modelIdInput = screen.getByPlaceholderText("Search and select a model...")
@@ -72,14 +77,15 @@ describe("ApiOptions Component", () => {
//@ts-expect-error - vscode is not defined in the global namespace in test environment
global.vscode = { postMessage: mockPostMessage }
mockExtensionState({
apiProvider: "together",
planModeApiProvider: "together",
actModeApiProvider: "together",
})
})
it("renders Together API Key input", () => {
render(
<ExtensionStateContextProvider>
<ApiOptions showModelOptions={true} />
<ApiOptions showModelOptions={true} currentMode="plan" />
</ExtensionStateContextProvider>,
)
const apiKeyInput = screen.getByPlaceholderText("Enter API Key...")
@@ -89,7 +95,7 @@ describe("ApiOptions Component", () => {
it("renders Together Model ID input", () => {
render(
<ExtensionStateContextProvider>
<ApiOptions showModelOptions={true} />
<ApiOptions showModelOptions={true} currentMode="plan" />
</ExtensionStateContextProvider>,
)
const modelIdInput = screen.getByPlaceholderText("Enter Model ID...")
@@ -106,9 +112,11 @@ describe("ApiOptions Component", () => {
global.vscode = { postMessage: mockPostMessage }
mockExtensionState({
apiProvider: "fireworks",
planModeApiProvider: "fireworks",
actModeApiProvider: "fireworks",
fireworksApiKey: "",
fireworksModelId: "",
planModeFireworksModelId: "",
actModeFireworksModelId: "",
fireworksModelMaxCompletionTokens: 2000,
fireworksModelMaxTokens: 4000,
})
@@ -117,7 +125,7 @@ describe("ApiOptions Component", () => {
it("renders Fireworks API Key input", () => {
render(
<ExtensionStateContextProvider>
<ApiOptions showModelOptions={true} />
<ApiOptions showModelOptions={true} currentMode="plan" />
</ExtensionStateContextProvider>,
)
const apiKeyInput = screen.getByPlaceholderText("Enter API Key...")
@@ -127,7 +135,7 @@ describe("ApiOptions Component", () => {
it("renders Fireworks Model ID input", () => {
render(
<ExtensionStateContextProvider>
<ApiOptions showModelOptions={true} />
<ApiOptions showModelOptions={true} currentMode="plan" />
</ExtensionStateContextProvider>,
)
const modelIdInput = screen.getByPlaceholderText("Enter Model ID...")
@@ -137,7 +145,7 @@ describe("ApiOptions Component", () => {
it("renders Fireworks Max Completion Tokens input", () => {
render(
<ExtensionStateContextProvider>
<ApiOptions showModelOptions={true} />
<ApiOptions showModelOptions={true} currentMode="plan" />
</ExtensionStateContextProvider>,
)
const maxCompletionTokensInput = screen.getByPlaceholderText("2000")
@@ -147,7 +155,7 @@ describe("ApiOptions Component", () => {
it("renders Fireworks Max Tokens input", () => {
render(
<ExtensionStateContextProvider>
<ApiOptions showModelOptions={true} />
<ApiOptions showModelOptions={true} currentMode="plan" />
</ExtensionStateContextProvider>,
)
const maxTokensInput = screen.getByPlaceholderText("4000")
@@ -163,14 +171,15 @@ describe("OpenApiInfoOptions", () => {
//@ts-expect-error - vscode is not defined in the global namespace in test environment
global.vscode = { postMessage: mockPostMessage }
mockExtensionState({
apiProvider: "openai",
planModeApiProvider: "openai",
actModeApiProvider: "openai",
})
})
it("renders OpenAI Supports Images input", () => {
render(
<ExtensionStateContextProvider>
<ApiOptions showModelOptions={true} />
<ApiOptions showModelOptions={true} currentMode="plan" />
</ExtensionStateContextProvider>,
)
fireEvent.click(screen.getByText("Model Configuration"))
@@ -181,7 +190,7 @@ describe("OpenApiInfoOptions", () => {
it("renders OpenAI Context Window Size input", () => {
render(
<ExtensionStateContextProvider>
<ApiOptions showModelOptions={true} />
<ApiOptions showModelOptions={true} currentMode="plan" />
</ExtensionStateContextProvider>,
)
fireEvent.click(screen.getByText("Model Configuration"))
@@ -192,7 +201,7 @@ describe("OpenApiInfoOptions", () => {
it("renders OpenAI Max Output Tokens input", () => {
render(
<ExtensionStateContextProvider>
<ApiOptions showModelOptions={true} />
<ApiOptions showModelOptions={true} currentMode="plan" />
</ExtensionStateContextProvider>,
)
fireEvent.click(screen.getByText("Model Configuration"))
@@ -210,7 +219,8 @@ describe("ApiOptions Component", () => {
global.vscode = { postMessage: mockPostMessage }
mockExtensionState({
apiProvider: "nebius",
planModeApiProvider: "nebius",
actModeApiProvider: "nebius",
nebiusApiKey: "",
})
})
@@ -218,7 +228,7 @@ describe("ApiOptions Component", () => {
it("renders Nebius API Key input", () => {
render(
<ExtensionStateContextProvider>
<ApiOptions showModelOptions={true} />
<ApiOptions showModelOptions={true} currentMode="plan" />
</ExtensionStateContextProvider>,
)
const apiKeyInput = screen.getByPlaceholderText("Enter API Key...")
@@ -228,7 +238,7 @@ describe("ApiOptions Component", () => {
it("renders Nebius Model ID select with a default model", () => {
render(
<ExtensionStateContextProvider>
<ApiOptions showModelOptions={true} />
<ApiOptions showModelOptions={true} currentMode="plan" />
</ExtensionStateContextProvider>,
)
const modelIdSelect = screen.getByLabelText("Model")
@@ -3,7 +3,7 @@ import { ApiKeyField } from "../common/ApiKeyField"
import { BaseUrlField } from "../common/BaseUrlField"
import { ModelSelector } from "../common/ModelSelector"
import { ModelInfoView } from "../common/ModelInfoView"
import { normalizeApiConfiguration } from "../utils/providerUtils"
import { getModeSpecificFields, normalizeApiConfiguration } from "../utils/providerUtils"
import ThinkingBudgetSlider from "../ThinkingBudgetSlider"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers"
@@ -21,17 +21,18 @@ export const SUPPORTED_ANTHROPIC_THINKING_MODELS = [
interface AnthropicProviderProps {
showModelOptions: boolean
isPopup?: boolean
currentMode: "plan" | "act"
}
/**
* The Anthropic provider configuration component
*/
export const AnthropicProvider = ({ showModelOptions, isPopup }: AnthropicProviderProps) => {
export const AnthropicProvider = ({ showModelOptions, isPopup, currentMode }: AnthropicProviderProps) => {
const { apiConfiguration } = useExtensionState()
const { handleFieldChange } = useApiConfigurationHandlers()
const { handleFieldChange, handleModeFieldChange } = useApiConfigurationHandlers()
// Get the normalized configuration
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration)
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration, currentMode)
return (
<div>
@@ -54,12 +55,18 @@ export const AnthropicProvider = ({ showModelOptions, isPopup }: AnthropicProvid
<ModelSelector
models={anthropicModels}
selectedModelId={selectedModelId}
onChange={(e) => handleFieldChange("apiModelId", e.target.value)}
onChange={(e) =>
handleModeFieldChange(
{ plan: "planModeApiModelId", act: "actModeApiModelId" },
e.target.value,
currentMode,
)
}
label="Model"
/>
{SUPPORTED_ANTHROPIC_THINKING_MODELS.includes(selectedModelId) && (
<ThinkingBudgetSlider maxBudget={selectedModelInfo.thinkingConfig?.maxBudget} />
<ThinkingBudgetSlider maxBudget={selectedModelInfo.thinkingConfig?.maxBudget} currentMode={currentMode} />
)}
<ModelInfoView selectedModelId={selectedModelId} modelInfo={selectedModelInfo} isPopup={isPopup} />
@@ -13,17 +13,18 @@ import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandler
interface AskSageProviderProps {
showModelOptions: boolean
isPopup?: boolean
currentMode: "plan" | "act"
}
/**
* The AskSage provider configuration component
*/
export const AskSageProvider = ({ showModelOptions, isPopup }: AskSageProviderProps) => {
export const AskSageProvider = ({ showModelOptions, isPopup, currentMode }: AskSageProviderProps) => {
const { apiConfiguration } = useExtensionState()
const { handleFieldChange } = useApiConfigurationHandlers()
const { handleFieldChange, handleModeFieldChange } = useApiConfigurationHandlers()
// Get the normalized configuration
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration)
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration, currentMode)
return (
<div>
@@ -48,7 +49,13 @@ export const AskSageProvider = ({ showModelOptions, isPopup }: AskSageProviderPr
<ModelSelector
models={askSageModels}
selectedModelId={selectedModelId}
onChange={(e) => handleFieldChange("apiModelId", e.target.value)}
onChange={(e) =>
handleModeFieldChange(
{ plan: "planModeApiModelId", act: "actModeApiModelId" },
e.target.value,
currentMode,
)
}
label="Model"
/>
@@ -5,7 +5,7 @@ import { DebouncedTextField } from "../common/DebouncedTextField"
import { ModelInfoView } from "../common/ModelInfoView"
import { DropdownContainer } from "../common/ModelSelector"
import ThinkingBudgetSlider from "../ThinkingBudgetSlider"
import { normalizeApiConfiguration } from "../utils/providerUtils"
import { normalizeApiConfiguration, getModeSpecificFields } from "../utils/providerUtils"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers"
@@ -15,13 +15,15 @@ const DROPDOWN_Z_INDEX = 1000
interface BedrockProviderProps {
showModelOptions: boolean
isPopup?: boolean
currentMode: "plan" | "act"
}
export const BedrockProvider = ({ showModelOptions, isPopup }: BedrockProviderProps) => {
export const BedrockProvider = ({ showModelOptions, isPopup, currentMode }: BedrockProviderProps) => {
const { apiConfiguration } = useExtensionState()
const { handleFieldChange, handleFieldsChange } = useApiConfigurationHandlers()
const { handleFieldChange, handleFieldsChange, handleModeFieldChange, handleModeFieldsChange } = useApiConfigurationHandlers()
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration)
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration, currentMode)
const modeFields = getModeSpecificFields(apiConfiguration, currentMode)
const [awsEndpointSelected, setAwsEndpointSelected] = useState(!!apiConfiguration?.awsBedrockEndpoint)
return (
@@ -207,15 +209,29 @@ export const BedrockProvider = ({ showModelOptions, isPopup }: BedrockProviderPr
<DropdownContainer zIndex={DROPDOWN_Z_INDEX - 2} className="dropdown-container">
<VSCodeDropdown
id="bedrock-model-dropdown"
value={apiConfiguration?.awsBedrockCustomSelected ? "custom" : selectedModelId}
value={modeFields.awsBedrockCustomSelected ? "custom" : selectedModelId}
onChange={(e: any) => {
const isCustom = e.target.value === "custom"
handleFieldsChange({
apiModelId: isCustom ? "" : e.target.value,
awsBedrockCustomSelected: isCustom,
awsBedrockCustomModelBaseId: bedrockDefaultModelId,
})
handleModeFieldsChange(
{
apiModelId: { plan: "planModeApiModelId", act: "actModeApiModelId" },
awsBedrockCustomSelected: {
plan: "planModeAwsBedrockCustomSelected",
act: "actModeAwsBedrockCustomSelected",
},
awsBedrockCustomModelBaseId: {
plan: "planModeAwsBedrockCustomModelBaseId",
act: "actModeAwsBedrockCustomModelBaseId",
},
},
{
apiModelId: isCustom ? "" : e.target.value,
awsBedrockCustomSelected: isCustom,
awsBedrockCustomModelBaseId: bedrockDefaultModelId,
},
currentMode,
)
}}
style={{ width: "100%" }}>
<VSCodeOption value="">Select a model...</VSCodeOption>
@@ -235,7 +251,7 @@ export const BedrockProvider = ({ showModelOptions, isPopup }: BedrockProviderPr
</VSCodeDropdown>
</DropdownContainer>
{apiConfiguration?.awsBedrockCustomSelected && (
{modeFields.awsBedrockCustomSelected && (
<div>
<p
style={{
@@ -248,8 +264,14 @@ export const BedrockProvider = ({ showModelOptions, isPopup }: BedrockProviderPr
</p>
<DebouncedTextField
id="bedrock-model-input"
initialValue={apiConfiguration?.apiModelId || ""}
onChange={(value) => handleFieldChange("apiModelId", value)}
initialValue={modeFields.apiModelId || ""}
onChange={(value) =>
handleModeFieldChange(
{ plan: "planModeApiModelId", act: "actModeApiModelId" },
value,
currentMode,
)
}
style={{ width: "100%", marginTop: 3 }}
placeholder="Enter custom model ID...">
<span style={{ fontWeight: 500 }}>Model ID</span>
@@ -260,8 +282,17 @@ export const BedrockProvider = ({ showModelOptions, isPopup }: BedrockProviderPr
<DropdownContainer zIndex={DROPDOWN_Z_INDEX - 3} className="dropdown-container">
<VSCodeDropdown
id="bedrock-base-model-dropdown"
value={apiConfiguration?.awsBedrockCustomModelBaseId || bedrockDefaultModelId}
onChange={(e: any) => handleFieldChange("awsBedrockCustomModelBaseId", e.target.value)}
value={modeFields.awsBedrockCustomModelBaseId || bedrockDefaultModelId}
onChange={(e: any) =>
handleModeFieldChange(
{
plan: "planModeAwsBedrockCustomModelBaseId",
act: "actModeAwsBedrockCustomModelBaseId",
},
e.target.value,
currentMode,
)
}
style={{ width: "100%" }}>
<VSCodeOption value="">Select a model...</VSCodeOption>
{Object.keys(bedrockModels).map((modelId) => (
@@ -284,13 +315,13 @@ export const BedrockProvider = ({ showModelOptions, isPopup }: BedrockProviderPr
{(selectedModelId === "anthropic.claude-3-7-sonnet-20250219-v1:0" ||
selectedModelId === "anthropic.claude-sonnet-4-20250514-v1:0" ||
selectedModelId === "anthropic.claude-opus-4-20250514-v1:0" ||
(apiConfiguration?.awsBedrockCustomSelected &&
apiConfiguration?.awsBedrockCustomModelBaseId === "anthropic.claude-3-7-sonnet-20250219-v1:0") ||
(apiConfiguration?.awsBedrockCustomSelected &&
apiConfiguration?.awsBedrockCustomModelBaseId === "anthropic.claude-sonnet-4-20250514-v1:0") ||
(apiConfiguration?.awsBedrockCustomSelected &&
apiConfiguration?.awsBedrockCustomModelBaseId === "anthropic.claude-opus-4-20250514-v1:0")) && (
<ThinkingBudgetSlider />
(modeFields.awsBedrockCustomSelected &&
modeFields.awsBedrockCustomModelBaseId === "anthropic.claude-3-7-sonnet-20250219-v1:0") ||
(modeFields.awsBedrockCustomSelected &&
modeFields.awsBedrockCustomModelBaseId === "anthropic.claude-sonnet-4-20250514-v1:0") ||
(modeFields.awsBedrockCustomSelected &&
modeFields.awsBedrockCustomModelBaseId === "anthropic.claude-opus-4-20250514-v1:0")) && (
<ThinkingBudgetSlider currentMode={currentMode} />
)}
<ModelInfoView selectedModelId={selectedModelId} modelInfo={selectedModelInfo} isPopup={isPopup} />
@@ -12,17 +12,18 @@ import { useExtensionState } from "@/context/ExtensionStateContext"
interface CerebrasProviderProps {
showModelOptions: boolean
isPopup?: boolean
currentMode: "plan" | "act"
}
/**
* The Cerebras provider configuration component
*/
export const CerebrasProvider = ({ showModelOptions, isPopup }: CerebrasProviderProps) => {
export const CerebrasProvider = ({ showModelOptions, isPopup, currentMode }: CerebrasProviderProps) => {
const { apiConfiguration } = useExtensionState()
const { handleFieldChange } = useApiConfigurationHandlers()
const { handleFieldChange, handleModeFieldChange } = useApiConfigurationHandlers()
// Get the normalized configuration
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration)
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration, currentMode)
return (
<div>
@@ -38,7 +39,13 @@ export const CerebrasProvider = ({ showModelOptions, isPopup }: CerebrasProvider
<ModelSelector
models={cerebrasModels}
selectedModelId={selectedModelId}
onChange={(e: any) => handleFieldChange("apiModelId", e.target.value)}
onChange={(e: any) =>
handleModeFieldChange(
{ plan: "planModeApiModelId", act: "actModeApiModelId" },
e.target.value,
currentMode,
)
}
label="Model"
/>
@@ -14,17 +14,18 @@ import ThinkingBudgetSlider from "../ThinkingBudgetSlider"
interface ClaudeCodeProviderProps {
showModelOptions: boolean
isPopup?: boolean
currentMode: "plan" | "act"
}
/**
* The Claude Code provider configuration component
*/
export const ClaudeCodeProvider = ({ showModelOptions, isPopup }: ClaudeCodeProviderProps) => {
export const ClaudeCodeProvider = ({ showModelOptions, isPopup, currentMode }: ClaudeCodeProviderProps) => {
const { apiConfiguration } = useExtensionState()
const { handleFieldChange } = useApiConfigurationHandlers()
const { handleFieldChange, handleModeFieldChange } = useApiConfigurationHandlers()
// Get the normalized configuration
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration)
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration, currentMode)
return (
<div>
@@ -51,12 +52,18 @@ export const ClaudeCodeProvider = ({ showModelOptions, isPopup }: ClaudeCodeProv
<ModelSelector
models={claudeCodeModels}
selectedModelId={selectedModelId}
onChange={(e: any) => handleFieldChange("apiModelId", e.target.value)}
onChange={(e: any) =>
handleModeFieldChange(
{ plan: "planModeApiModelId", act: "actModeApiModelId" },
e.target.value,
currentMode,
)
}
label="Model"
/>
{SUPPORTED_ANTHROPIC_THINKING_MODELS.includes(selectedModelId) && (
<ThinkingBudgetSlider maxBudget={selectedModelInfo.thinkingConfig?.maxBudget} />
<ThinkingBudgetSlider maxBudget={selectedModelInfo.thinkingConfig?.maxBudget} currentMode={currentMode} />
)}
<ModelInfoView selectedModelId={selectedModelId} modelInfo={selectedModelInfo} isPopup={isPopup} />
@@ -13,12 +13,13 @@ import { useExtensionState } from "@/context/ExtensionStateContext"
interface ClineProviderProps {
showModelOptions: boolean
isPopup?: boolean
currentMode: "plan" | "act"
}
/**
* The Cline provider configuration component
*/
export const ClineProvider = ({ showModelOptions, isPopup }: ClineProviderProps) => {
export const ClineProvider = ({ showModelOptions, isPopup, currentMode }: ClineProviderProps) => {
const { apiConfiguration } = useExtensionState()
const { handleFieldChange } = useApiConfigurationHandlers()
@@ -76,7 +77,7 @@ export const ClineProvider = ({ showModelOptions, isPopup }: ClineProviderProps)
)}
{/* OpenRouter Model Picker */}
<OpenRouterModelPicker isPopup={isPopup} />
<OpenRouterModelPicker isPopup={isPopup} currentMode={currentMode} />
</>
)}
</div>
@@ -12,17 +12,18 @@ import { useExtensionState } from "@/context/ExtensionStateContext"
interface DeepSeekProviderProps {
showModelOptions: boolean
isPopup?: boolean
currentMode: "plan" | "act"
}
/**
* The DeepSeek provider configuration component
*/
export const DeepSeekProvider = ({ showModelOptions, isPopup }: DeepSeekProviderProps) => {
export const DeepSeekProvider = ({ showModelOptions, isPopup, currentMode }: DeepSeekProviderProps) => {
const { apiConfiguration } = useExtensionState()
const { handleFieldChange } = useApiConfigurationHandlers()
const { handleFieldChange, handleModeFieldChange } = useApiConfigurationHandlers()
// Get the normalized configuration
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration)
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration, currentMode)
return (
<div>
@@ -38,7 +39,13 @@ export const DeepSeekProvider = ({ showModelOptions, isPopup }: DeepSeekProvider
<ModelSelector
models={deepSeekModels}
selectedModelId={selectedModelId}
onChange={(e: any) => handleFieldChange("apiModelId", e.target.value)}
onChange={(e: any) =>
handleModeFieldChange(
{ plan: "planModeApiModelId", act: "actModeApiModelId" },
e.target.value,
currentMode,
)
}
label="Model"
/>
@@ -12,17 +12,18 @@ import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandler
interface DoubaoProviderProps {
showModelOptions: boolean
isPopup?: boolean
currentMode: "plan" | "act"
}
/**
* The ByteDance Doubao provider configuration component
*/
export const DoubaoProvider = ({ showModelOptions, isPopup }: DoubaoProviderProps) => {
export const DoubaoProvider = ({ showModelOptions, isPopup, currentMode }: DoubaoProviderProps) => {
const { apiConfiguration } = useExtensionState()
const { handleFieldChange } = useApiConfigurationHandlers()
const { handleFieldChange, handleModeFieldChange } = useApiConfigurationHandlers()
// Get the normalized configuration
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration)
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration, currentMode)
return (
<div>
@@ -38,7 +39,13 @@ export const DoubaoProvider = ({ showModelOptions, isPopup }: DoubaoProviderProp
<ModelSelector
models={doubaoModels}
selectedModelId={selectedModelId}
onChange={(e: any) => handleFieldChange("apiModelId", e.target.value)}
onChange={(e: any) =>
handleModeFieldChange(
{ plan: "planModeApiModelId", act: "actModeApiModelId" },
e.target.value,
currentMode,
)
}
label="Model"
/>
@@ -2,6 +2,7 @@ import { ApiKeyField } from "../common/ApiKeyField"
import { DebouncedTextField } from "../common/DebouncedTextField"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers"
import { getModeSpecificFields } from "../utils/providerUtils"
import { ApiConfiguration } from "@shared/api"
/**
@@ -10,14 +11,17 @@ import { ApiConfiguration } from "@shared/api"
interface FireworksProviderProps {
showModelOptions: boolean
isPopup?: boolean
currentMode: "plan" | "act"
}
/**
* The Fireworks provider configuration component
*/
export const FireworksProvider = ({ showModelOptions, isPopup }: FireworksProviderProps) => {
export const FireworksProvider = ({ showModelOptions, isPopup, currentMode }: FireworksProviderProps) => {
const { apiConfiguration } = useExtensionState()
const { handleFieldChange } = useApiConfigurationHandlers()
const { handleModeFieldChange, handleFieldChange } = useApiConfigurationHandlers()
const { fireworksModelId } = getModeSpecificFields(apiConfiguration, currentMode)
// Handler for number input fields with validation
const handleNumberInputChange = (field: keyof ApiConfiguration, value: string) => {
@@ -43,8 +47,14 @@ export const FireworksProvider = ({ showModelOptions, isPopup }: FireworksProvid
{showModelOptions && (
<>
<DebouncedTextField
initialValue={apiConfiguration?.fireworksModelId || ""}
onChange={(value) => handleFieldChange("fireworksModelId", value)}
initialValue={fireworksModelId || ""}
onChange={(value) =>
handleModeFieldChange(
{ plan: "planModeFireworksModelId", act: "actModeFireworksModelId" },
value,
currentMode,
)
}
style={{ width: "100%" }}
placeholder={"Enter Model ID..."}>
<span style={{ fontWeight: 500 }}>Model ID</span>
@@ -17,17 +17,18 @@ const SUPPORTED_THINKING_MODELS = ["gemini-2.5-pro", "gemini-2.5-flash", "gemini
interface GeminiProviderProps {
showModelOptions: boolean
isPopup?: boolean
currentMode: "plan" | "act"
}
/**
* The Gemini provider configuration component
*/
export const GeminiProvider = ({ showModelOptions, isPopup }: GeminiProviderProps) => {
export const GeminiProvider = ({ showModelOptions, isPopup, currentMode }: GeminiProviderProps) => {
const { apiConfiguration } = useExtensionState()
const { handleFieldChange } = useApiConfigurationHandlers()
const { handleFieldChange, handleModeFieldChange } = useApiConfigurationHandlers()
// Get the normalized configuration
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration)
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration, currentMode)
return (
<div>
@@ -50,12 +51,18 @@ export const GeminiProvider = ({ showModelOptions, isPopup }: GeminiProviderProp
<ModelSelector
models={geminiModels}
selectedModelId={selectedModelId}
onChange={(e: any) => handleFieldChange("apiModelId", e.target.value)}
onChange={(e: any) =>
handleModeFieldChange(
{ plan: "planModeApiModelId", act: "actModeApiModelId" },
e.target.value,
currentMode,
)
}
label="Model"
/>
{SUPPORTED_THINKING_MODELS.includes(selectedModelId) && (
<ThinkingBudgetSlider maxBudget={selectedModelInfo.thinkingConfig?.maxBudget} />
<ThinkingBudgetSlider maxBudget={selectedModelInfo.thinkingConfig?.maxBudget} currentMode={currentMode} />
)}
<ModelInfoView selectedModelId={selectedModelId} modelInfo={selectedModelInfo} isPopup={isPopup} />
@@ -9,12 +9,13 @@ import { useExtensionState } from "@/context/ExtensionStateContext"
interface GroqProviderProps {
showModelOptions: boolean
isPopup?: boolean
currentMode: "plan" | "act"
}
/**
* The Groq provider configuration component
*/
export const GroqProvider = ({ showModelOptions, isPopup }: GroqProviderProps) => {
export const GroqProvider = ({ showModelOptions, isPopup, currentMode }: GroqProviderProps) => {
const { apiConfiguration } = useExtensionState()
const { handleFieldChange } = useApiConfigurationHandlers()
@@ -27,7 +28,7 @@ export const GroqProvider = ({ showModelOptions, isPopup }: GroqProviderProps) =
signupUrl="https://console.groq.com/keys"
/>
{showModelOptions && <GroqModelPicker isPopup={isPopup} />}
{showModelOptions && <GroqModelPicker isPopup={isPopup} currentMode={currentMode} />}
</div>
)
}
@@ -7,6 +7,7 @@ import { StringRequest } from "@shared/proto/common"
import { BaseUrlField } from "../common/BaseUrlField"
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { getModeSpecificFields } from "../utils/providerUtils"
/**
* Props for the LMStudioProvider component
@@ -14,14 +15,17 @@ import { useExtensionState } from "@/context/ExtensionStateContext"
interface LMStudioProviderProps {
showModelOptions: boolean
isPopup?: boolean
currentMode: "plan" | "act"
}
/**
* The LM Studio provider configuration component
*/
export const LMStudioProvider = ({ showModelOptions, isPopup }: LMStudioProviderProps) => {
export const LMStudioProvider = ({ showModelOptions, isPopup, currentMode }: LMStudioProviderProps) => {
const { apiConfiguration } = useExtensionState()
const { handleFieldChange } = useApiConfigurationHandlers()
const { handleFieldChange, handleModeFieldChange } = useApiConfigurationHandlers()
const { lmStudioModelId } = getModeSpecificFields(apiConfiguration, currentMode)
const [lmStudioModels, setLmStudioModels] = useState<string[]>([])
@@ -58,8 +62,10 @@ export const LMStudioProvider = ({ showModelOptions, isPopup }: LMStudioProvider
/>
<DebouncedTextField
initialValue={apiConfiguration?.lmStudioModelId || ""}
onChange={(value) => handleFieldChange("lmStudioModelId", value)}
initialValue={lmStudioModelId || ""}
onChange={(value) =>
handleModeFieldChange({ plan: "planModeLmStudioModelId", act: "actModeLmStudioModelId" }, value, currentMode)
}
style={{ width: "100%" }}
placeholder={"e.g. meta-llama-3.1-8b-instruct"}>
<span style={{ fontWeight: 500 }}>Model ID</span>
@@ -67,18 +73,20 @@ export const LMStudioProvider = ({ showModelOptions, isPopup }: LMStudioProvider
{lmStudioModels.length > 0 && (
<VSCodeRadioGroup
value={
lmStudioModels.includes(apiConfiguration?.lmStudioModelId || "") ? apiConfiguration?.lmStudioModelId : ""
}
value={lmStudioModels.includes(lmStudioModelId || "") ? lmStudioModelId : ""}
onChange={(e) => {
const value = (e.target as HTMLInputElement)?.value
// need to check value first since radio group returns empty string sometimes
if (value) {
handleFieldChange("lmStudioModelId", value)
handleModeFieldChange(
{ plan: "planModeLmStudioModelId", act: "actModeLmStudioModelId" },
value,
currentMode,
)
}
}}>
{lmStudioModels.map((model) => (
<VSCodeRadio key={model} value={model} checked={apiConfiguration?.lmStudioModelId === model}>
<VSCodeRadio key={model} value={model} checked={lmStudioModelId === model}>
{model}
</VSCodeRadio>
))}
@@ -3,7 +3,7 @@ import { liteLlmModelInfoSaneDefaults } from "@shared/api"
import { VSCodeCheckbox, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
import { DebouncedTextField } from "../common/DebouncedTextField"
import { getAsVar, VSC_DESCRIPTION_FOREGROUND } from "@/utils/vscStyles"
import { normalizeApiConfiguration } from "../utils/providerUtils"
import { normalizeApiConfiguration, getModeSpecificFields } from "../utils/providerUtils"
import { ModelInfoView } from "../common/ModelInfoView"
import ThinkingBudgetSlider from "../ThinkingBudgetSlider"
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers"
@@ -15,17 +15,21 @@ import { useExtensionState } from "@/context/ExtensionStateContext"
interface LiteLlmProviderProps {
showModelOptions: boolean
isPopup?: boolean
currentMode: "plan" | "act"
}
/**
* The LiteLLM provider configuration component
*/
export const LiteLlmProvider = ({ showModelOptions, isPopup }: LiteLlmProviderProps) => {
export const LiteLlmProvider = ({ showModelOptions, isPopup, currentMode }: LiteLlmProviderProps) => {
const { apiConfiguration } = useExtensionState()
const { handleFieldChange } = useApiConfigurationHandlers()
const { handleFieldChange, handleModeFieldChange } = useApiConfigurationHandlers()
// Get the normalized configuration
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration)
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration, currentMode)
// Get mode-specific fields
const { liteLlmModelId, liteLlmModelInfo } = getModeSpecificFields(apiConfiguration, currentMode)
// Local state for collapsible model configuration section
const [modelConfigurationSelected, setModelConfigurationSelected] = useState(false)
@@ -49,8 +53,10 @@ export const LiteLlmProvider = ({ showModelOptions, isPopup }: LiteLlmProviderPr
<span style={{ fontWeight: 500 }}>API Key</span>
</DebouncedTextField>
<DebouncedTextField
initialValue={apiConfiguration?.liteLlmModelId || ""}
onChange={(value) => handleFieldChange("liteLlmModelId", value)}
initialValue={liteLlmModelId || ""}
onChange={(value) =>
handleModeFieldChange({ plan: "planModeLiteLlmModelId", act: "actModeLiteLlmModelId" }, value, currentMode)
}
style={{ width: "100%" }}
placeholder={"e.g. anthropic/claude-sonnet-4-20250514"}>
<span style={{ fontWeight: 500 }}>Model ID</span>
@@ -77,7 +83,7 @@ export const LiteLlmProvider = ({ showModelOptions, isPopup }: LiteLlmProviderPr
</div>
<>
<ThinkingBudgetSlider />
<ThinkingBudgetSlider currentMode={currentMode} />
<p
style={{
fontSize: "12px",
@@ -118,50 +124,56 @@ export const LiteLlmProvider = ({ showModelOptions, isPopup }: LiteLlmProviderPr
{modelConfigurationSelected && (
<>
<VSCodeCheckbox
checked={!!apiConfiguration?.liteLlmModelInfo?.supportsImages}
checked={!!liteLlmModelInfo?.supportsImages}
onChange={(e: any) => {
const isChecked = e.target.checked === true
const modelInfo = apiConfiguration?.liteLlmModelInfo
? apiConfiguration.liteLlmModelInfo
: { ...liteLlmModelInfoSaneDefaults }
const modelInfo = liteLlmModelInfo ? liteLlmModelInfo : { ...liteLlmModelInfoSaneDefaults }
modelInfo.supportsImages = isChecked
handleFieldChange("liteLlmModelInfo", modelInfo)
handleModeFieldChange(
{ plan: "planModeLiteLlmModelInfo", act: "actModeLiteLlmModelInfo" },
modelInfo,
currentMode,
)
}}>
Supports Images
</VSCodeCheckbox>
<div style={{ display: "flex", gap: 10, marginTop: "5px" }}>
<DebouncedTextField
initialValue={
apiConfiguration?.liteLlmModelInfo?.contextWindow
? apiConfiguration.liteLlmModelInfo.contextWindow.toString()
liteLlmModelInfo?.contextWindow
? liteLlmModelInfo.contextWindow.toString()
: (liteLlmModelInfoSaneDefaults.contextWindow?.toString() ?? "")
}
style={{ flex: 1 }}
onChange={(value) => {
const modelInfo = apiConfiguration?.liteLlmModelInfo
? apiConfiguration.liteLlmModelInfo
: { ...liteLlmModelInfoSaneDefaults }
const modelInfo = liteLlmModelInfo ? liteLlmModelInfo : { ...liteLlmModelInfoSaneDefaults }
modelInfo.contextWindow = Number(value)
handleFieldChange("liteLlmModelInfo", modelInfo)
handleModeFieldChange(
{ plan: "planModeLiteLlmModelInfo", act: "actModeLiteLlmModelInfo" },
modelInfo,
currentMode,
)
}}>
<span style={{ fontWeight: 500 }}>Context Window Size</span>
</DebouncedTextField>
<DebouncedTextField
initialValue={
apiConfiguration?.liteLlmModelInfo?.maxTokens
? apiConfiguration.liteLlmModelInfo.maxTokens.toString()
liteLlmModelInfo?.maxTokens
? liteLlmModelInfo.maxTokens.toString()
: (liteLlmModelInfoSaneDefaults.maxTokens?.toString() ?? "")
}
style={{ flex: 1 }}
onChange={(value) => {
const modelInfo = apiConfiguration?.liteLlmModelInfo
? apiConfiguration.liteLlmModelInfo
: { ...liteLlmModelInfoSaneDefaults }
const modelInfo = liteLlmModelInfo ? liteLlmModelInfo : { ...liteLlmModelInfoSaneDefaults }
modelInfo.maxTokens = Number(value)
handleFieldChange("liteLlmModelInfo", modelInfo)
handleModeFieldChange(
{ plan: "planModeLiteLlmModelInfo", act: "actModeLiteLlmModelInfo" },
modelInfo,
currentMode,
)
}}>
<span style={{ fontWeight: 500 }}>Max Output Tokens</span>
</DebouncedTextField>
@@ -169,14 +181,12 @@ export const LiteLlmProvider = ({ showModelOptions, isPopup }: LiteLlmProviderPr
<div style={{ display: "flex", gap: 10, marginTop: "5px" }}>
<DebouncedTextField
initialValue={
apiConfiguration?.liteLlmModelInfo?.temperature !== undefined
? apiConfiguration.liteLlmModelInfo.temperature.toString()
liteLlmModelInfo?.temperature !== undefined
? liteLlmModelInfo.temperature.toString()
: (liteLlmModelInfoSaneDefaults.temperature?.toString() ?? "")
}
onChange={(value) => {
const modelInfo = apiConfiguration?.liteLlmModelInfo
? apiConfiguration.liteLlmModelInfo
: { ...liteLlmModelInfoSaneDefaults }
const modelInfo = liteLlmModelInfo ? liteLlmModelInfo : { ...liteLlmModelInfoSaneDefaults }
// Check if the input ends with a decimal point or has trailing zeros after decimal
const shouldPreserveFormat = value.endsWith(".") || (value.includes(".") && value.endsWith("0"))
@@ -184,7 +194,11 @@ export const LiteLlmProvider = ({ showModelOptions, isPopup }: LiteLlmProviderPr
modelInfo.temperature =
value === "" ? liteLlmModelInfoSaneDefaults.temperature : parseFloat(value)
handleFieldChange("liteLlmModelInfo", modelInfo)
handleModeFieldChange(
{ plan: "planModeLiteLlmModelInfo", act: "actModeLiteLlmModelInfo" },
modelInfo,
currentMode,
)
}}>
<span style={{ fontWeight: 500 }}>Temperature</span>
</DebouncedTextField>
@@ -12,17 +12,18 @@ import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandler
interface MistralProviderProps {
showModelOptions: boolean
isPopup?: boolean
currentMode: "plan" | "act"
}
/**
* The Mistral provider configuration component
*/
export const MistralProvider = ({ showModelOptions, isPopup }: MistralProviderProps) => {
export const MistralProvider = ({ showModelOptions, isPopup, currentMode }: MistralProviderProps) => {
const { apiConfiguration } = useExtensionState()
const { handleFieldChange } = useApiConfigurationHandlers()
const { handleFieldChange, handleModeFieldChange } = useApiConfigurationHandlers()
// Get the normalized configuration
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration)
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration, currentMode)
return (
<div>
@@ -38,7 +39,13 @@ export const MistralProvider = ({ showModelOptions, isPopup }: MistralProviderPr
<ModelSelector
models={mistralModels}
selectedModelId={selectedModelId}
onChange={(e: any) => handleFieldChange("apiModelId", e.target.value)}
onChange={(e: any) =>
handleModeFieldChange(
{ plan: "planModeApiModelId", act: "actModeApiModelId" },
e.target.value,
currentMode,
)
}
label="Model"
/>
@@ -14,20 +14,21 @@ import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"
interface MoonshotProviderProps {
showModelOptions: boolean
isPopup?: boolean
currentMode: "plan" | "act"
}
/**
* The Moonshot AI Studio provider configuration component
*/
export const MoonshotProvider = ({ showModelOptions, isPopup }: MoonshotProviderProps) => {
export const MoonshotProvider = ({ showModelOptions, isPopup, currentMode }: MoonshotProviderProps) => {
const { apiConfiguration } = useExtensionState()
const { handleFieldChange } = useApiConfigurationHandlers()
const { handleFieldChange, handleModeFieldChange } = useApiConfigurationHandlers()
// Local state for Chinese API endpoint checkbox
const [isChineseEndpoint, setIsChineseEndpoint] = useState(!!apiConfiguration?.moonshotApiLine)
// Get the normalized configuration
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration)
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration, currentMode)
const handleChineseEndpointToggle = (e: any) => {
const checked = e.target.checked === true
@@ -57,7 +58,13 @@ export const MoonshotProvider = ({ showModelOptions, isPopup }: MoonshotProvider
<ModelSelector
models={moonshotModels}
selectedModelId={selectedModelId}
onChange={(e: any) => handleFieldChange("apiModelId", e.target.value)}
onChange={(e: any) =>
handleModeFieldChange(
{ plan: "planModeApiModelId", act: "actModeApiModelId" },
e.target.value,
currentMode,
)
}
label="Model"
/>
@@ -12,17 +12,17 @@ import { useExtensionState } from "@/context/ExtensionStateContext"
interface NebiusProviderProps {
showModelOptions: boolean
isPopup?: boolean
currentMode: "plan" | "act"
}
/**
* The Nebius AI Studio provider configuration component
*/
export const NebiusProvider = ({ showModelOptions, isPopup }: NebiusProviderProps) => {
export const NebiusProvider = ({ showModelOptions, isPopup, currentMode }: NebiusProviderProps) => {
const { apiConfiguration } = useExtensionState()
const { handleFieldChange } = useApiConfigurationHandlers()
const { handleFieldChange, handleModeFieldChange } = useApiConfigurationHandlers()
// Get the normalized configuration
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration)
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration, currentMode)
return (
<div>
@@ -39,7 +39,13 @@ export const NebiusProvider = ({ showModelOptions, isPopup }: NebiusProviderProp
<ModelSelector
models={nebiusModels}
selectedModelId={selectedModelId}
onChange={(e: any) => handleFieldChange("apiModelId", e.target.value)}
onChange={(e: any) =>
handleModeFieldChange(
{ plan: "planModeApiModelId", act: "actModeApiModelId" },
e.target.value,
currentMode,
)
}
label="Model"
/>
@@ -8,6 +8,7 @@ import OllamaModelPicker from "../OllamaModelPicker"
import { BaseUrlField } from "../common/BaseUrlField"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers"
import { getModeSpecificFields } from "../utils/providerUtils"
/**
* Props for the OllamaProvider component
@@ -15,14 +16,17 @@ import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandler
interface OllamaProviderProps {
showModelOptions: boolean
isPopup?: boolean
currentMode: "plan" | "act"
}
/**
* The Ollama provider configuration component
*/
export const OllamaProvider = ({ showModelOptions, isPopup }: OllamaProviderProps) => {
export const OllamaProvider = ({ showModelOptions, isPopup, currentMode }: OllamaProviderProps) => {
const { apiConfiguration } = useExtensionState()
const { handleFieldChange } = useApiConfigurationHandlers()
const { handleFieldChange, handleModeFieldChange } = useApiConfigurationHandlers()
const { ollamaModelId } = getModeSpecificFields(apiConfiguration, currentMode)
const [ollamaModels, setOllamaModels] = useState<string[]>([])
@@ -64,9 +68,9 @@ export const OllamaProvider = ({ showModelOptions, isPopup }: OllamaProviderProp
</label>
<OllamaModelPicker
ollamaModels={ollamaModels}
selectedModelId={apiConfiguration?.ollamaModelId || ""}
selectedModelId={ollamaModelId || ""}
onModelChange={(modelId) => {
handleFieldChange("ollamaModelId", modelId)
handleModeFieldChange({ plan: "planModeOllamaModelId", act: "actModeOllamaModelId" }, modelId, currentMode)
}}
placeholder={ollamaModels.length > 0 ? "Search and select a model..." : "e.g. llama3.1"}
/>
@@ -8,7 +8,7 @@ import { DebouncedTextField } from "../common/DebouncedTextField"
import { ModelInfoView } from "../common/ModelInfoView"
import { ApiKeyField } from "../common/ApiKeyField"
import { BaseUrlField } from "../common/BaseUrlField"
import { normalizeApiConfiguration } from "../utils/providerUtils"
import { normalizeApiConfiguration, getModeSpecificFields } from "../utils/providerUtils"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers"
@@ -18,19 +18,23 @@ import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandler
interface OpenAICompatibleProviderProps {
showModelOptions: boolean
isPopup?: boolean
currentMode: "plan" | "act"
}
/**
* The OpenAI Compatible provider configuration component
*/
export const OpenAICompatibleProvider = ({ showModelOptions, isPopup }: OpenAICompatibleProviderProps) => {
export const OpenAICompatibleProvider = ({ showModelOptions, isPopup, currentMode }: OpenAICompatibleProviderProps) => {
const { apiConfiguration } = useExtensionState()
const { handleFieldChange } = useApiConfigurationHandlers()
const { handleFieldChange, handleModeFieldChange } = useApiConfigurationHandlers()
const [modelConfigurationSelected, setModelConfigurationSelected] = useState(false)
// Get the normalized configuration
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration)
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration, currentMode)
// Get mode-specific fields
const { openAiModelInfo } = getModeSpecificFields(apiConfiguration, currentMode)
// Debounced function to refresh OpenAI models (prevents excessive API calls while typing)
const debounceTimerRef = useRef<NodeJS.Timeout | null>(null)
@@ -86,8 +90,10 @@ export const OpenAICompatibleProvider = ({ showModelOptions, isPopup }: OpenAICo
/>
<DebouncedTextField
initialValue={apiConfiguration?.openAiModelId || ""}
onChange={(value) => handleFieldChange("openAiModelId", value)}
initialValue={selectedModelId || ""}
onChange={(value) =>
handleModeFieldChange({ plan: "planModeOpenAiModelId", act: "actModeOpenAiModelId" }, value, currentMode)
}
style={{ width: "100%", marginBottom: 10 }}
placeholder={"Enter Model ID..."}>
<span style={{ fontWeight: 500 }}>Model ID</span>
@@ -188,41 +194,47 @@ export const OpenAICompatibleProvider = ({ showModelOptions, isPopup }: OpenAICo
{modelConfigurationSelected && (
<>
<VSCodeCheckbox
checked={!!apiConfiguration?.openAiModelInfo?.supportsImages}
checked={!!openAiModelInfo?.supportsImages}
onChange={(e: any) => {
const isChecked = e.target.checked === true
const modelInfo = apiConfiguration?.openAiModelInfo
? apiConfiguration.openAiModelInfo
: { ...openAiModelInfoSaneDefaults }
const modelInfo = openAiModelInfo ? openAiModelInfo : { ...openAiModelInfoSaneDefaults }
modelInfo.supportsImages = isChecked
handleFieldChange("openAiModelInfo", modelInfo)
handleModeFieldChange(
{ plan: "planModeOpenAiModelInfo", act: "actModeOpenAiModelInfo" },
modelInfo,
currentMode,
)
}}>
Supports Images
</VSCodeCheckbox>
<VSCodeCheckbox
checked={!!apiConfiguration?.openAiModelInfo?.supportsImages}
checked={!!openAiModelInfo?.supportsImages}
onChange={(e: any) => {
const isChecked = e.target.checked === true
let modelInfo = apiConfiguration?.openAiModelInfo
? apiConfiguration.openAiModelInfo
: { ...openAiModelInfoSaneDefaults }
let modelInfo = openAiModelInfo ? openAiModelInfo : { ...openAiModelInfoSaneDefaults }
modelInfo.supportsImages = isChecked
handleFieldChange("openAiModelInfo", modelInfo)
handleModeFieldChange(
{ plan: "planModeOpenAiModelInfo", act: "actModeOpenAiModelInfo" },
modelInfo,
currentMode,
)
}}>
Supports browser use
</VSCodeCheckbox>
<VSCodeCheckbox
checked={!!apiConfiguration?.openAiModelInfo?.isR1FormatRequired}
checked={!!openAiModelInfo?.isR1FormatRequired}
onChange={(e: any) => {
const isChecked = e.target.checked === true
let modelInfo = apiConfiguration?.openAiModelInfo
? apiConfiguration.openAiModelInfo
: { ...openAiModelInfoSaneDefaults }
let modelInfo = openAiModelInfo ? openAiModelInfo : { ...openAiModelInfoSaneDefaults }
modelInfo = { ...modelInfo, isR1FormatRequired: isChecked }
handleFieldChange("openAiModelInfo", modelInfo)
handleModeFieldChange(
{ plan: "planModeOpenAiModelInfo", act: "actModeOpenAiModelInfo" },
modelInfo,
currentMode,
)
}}>
Enable R1 messages format
</VSCodeCheckbox>
@@ -230,34 +242,38 @@ export const OpenAICompatibleProvider = ({ showModelOptions, isPopup }: OpenAICo
<div style={{ display: "flex", gap: 10, marginTop: "5px" }}>
<DebouncedTextField
initialValue={
apiConfiguration?.openAiModelInfo?.contextWindow
? apiConfiguration.openAiModelInfo.contextWindow.toString()
openAiModelInfo?.contextWindow
? openAiModelInfo.contextWindow.toString()
: (openAiModelInfoSaneDefaults.contextWindow?.toString() ?? "")
}
style={{ flex: 1 }}
onChange={(value) => {
const modelInfo = apiConfiguration?.openAiModelInfo
? apiConfiguration.openAiModelInfo
: { ...openAiModelInfoSaneDefaults }
const modelInfo = openAiModelInfo ? openAiModelInfo : { ...openAiModelInfoSaneDefaults }
modelInfo.contextWindow = Number(value)
handleFieldChange("openAiModelInfo", modelInfo)
handleModeFieldChange(
{ plan: "planModeOpenAiModelInfo", act: "actModeOpenAiModelInfo" },
modelInfo,
currentMode,
)
}}>
<span style={{ fontWeight: 500 }}>Context Window Size</span>
</DebouncedTextField>
<DebouncedTextField
initialValue={
apiConfiguration?.openAiModelInfo?.maxTokens
? apiConfiguration.openAiModelInfo.maxTokens.toString()
openAiModelInfo?.maxTokens
? openAiModelInfo.maxTokens.toString()
: (openAiModelInfoSaneDefaults.maxTokens?.toString() ?? "")
}
style={{ flex: 1 }}
onChange={(value) => {
const modelInfo = apiConfiguration?.openAiModelInfo
? apiConfiguration.openAiModelInfo
: { ...openAiModelInfoSaneDefaults }
const modelInfo = openAiModelInfo ? openAiModelInfo : { ...openAiModelInfoSaneDefaults }
modelInfo.maxTokens = Number(value)
handleFieldChange("openAiModelInfo", modelInfo)
handleModeFieldChange(
{ plan: "planModeOpenAiModelInfo", act: "actModeOpenAiModelInfo" },
modelInfo,
currentMode,
)
}}>
<span style={{ fontWeight: 500 }}>Max Output Tokens</span>
</DebouncedTextField>
@@ -266,34 +282,38 @@ export const OpenAICompatibleProvider = ({ showModelOptions, isPopup }: OpenAICo
<div style={{ display: "flex", gap: 10, marginTop: "5px" }}>
<DebouncedTextField
initialValue={
apiConfiguration?.openAiModelInfo?.inputPrice
? apiConfiguration.openAiModelInfo.inputPrice.toString()
openAiModelInfo?.inputPrice
? openAiModelInfo.inputPrice.toString()
: (openAiModelInfoSaneDefaults.inputPrice?.toString() ?? "")
}
style={{ flex: 1 }}
onChange={(value) => {
const modelInfo = apiConfiguration?.openAiModelInfo
? apiConfiguration.openAiModelInfo
: { ...openAiModelInfoSaneDefaults }
const modelInfo = openAiModelInfo ? openAiModelInfo : { ...openAiModelInfoSaneDefaults }
modelInfo.inputPrice = Number(value)
handleFieldChange("openAiModelInfo", modelInfo)
handleModeFieldChange(
{ plan: "planModeOpenAiModelInfo", act: "actModeOpenAiModelInfo" },
modelInfo,
currentMode,
)
}}>
<span style={{ fontWeight: 500 }}>Input Price / 1M tokens</span>
</DebouncedTextField>
<DebouncedTextField
initialValue={
apiConfiguration?.openAiModelInfo?.outputPrice
? apiConfiguration.openAiModelInfo.outputPrice.toString()
openAiModelInfo?.outputPrice
? openAiModelInfo.outputPrice.toString()
: (openAiModelInfoSaneDefaults.outputPrice?.toString() ?? "")
}
style={{ flex: 1 }}
onChange={(value) => {
const modelInfo = apiConfiguration?.openAiModelInfo
? apiConfiguration.openAiModelInfo
: { ...openAiModelInfoSaneDefaults }
const modelInfo = openAiModelInfo ? openAiModelInfo : { ...openAiModelInfoSaneDefaults }
modelInfo.outputPrice = Number(value)
handleFieldChange("openAiModelInfo", modelInfo)
handleModeFieldChange(
{ plan: "planModeOpenAiModelInfo", act: "actModeOpenAiModelInfo" },
modelInfo,
currentMode,
)
}}>
<span style={{ fontWeight: 500 }}>Output Price / 1M tokens</span>
</DebouncedTextField>
@@ -302,14 +322,12 @@ export const OpenAICompatibleProvider = ({ showModelOptions, isPopup }: OpenAICo
<div style={{ display: "flex", gap: 10, marginTop: "5px" }}>
<DebouncedTextField
initialValue={
apiConfiguration?.openAiModelInfo?.temperature
? apiConfiguration.openAiModelInfo.temperature.toString()
openAiModelInfo?.temperature
? openAiModelInfo.temperature.toString()
: (openAiModelInfoSaneDefaults.temperature?.toString() ?? "")
}
onChange={(value) => {
const modelInfo = apiConfiguration?.openAiModelInfo
? apiConfiguration.openAiModelInfo
: { ...openAiModelInfoSaneDefaults }
const modelInfo = openAiModelInfo ? openAiModelInfo : { ...openAiModelInfoSaneDefaults }
const shouldPreserveFormat = value.endsWith(".") || (value.includes(".") && value.endsWith("0"))
@@ -320,7 +338,11 @@ export const OpenAICompatibleProvider = ({ showModelOptions, isPopup }: OpenAICo
? (value as any)
: parseFloat(value)
handleFieldChange("openAiModelInfo", modelInfo)
handleModeFieldChange(
{ plan: "planModeOpenAiModelInfo", act: "actModeOpenAiModelInfo" },
modelInfo,
currentMode,
)
}}>
<span style={{ fontWeight: 500 }}>Temperature</span>
</DebouncedTextField>
@@ -12,17 +12,18 @@ import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandler
interface OpenAINativeProviderProps {
showModelOptions: boolean
isPopup?: boolean
currentMode: "plan" | "act"
}
/**
* The OpenAI (native) provider configuration component
*/
export const OpenAINativeProvider = ({ showModelOptions, isPopup }: OpenAINativeProviderProps) => {
export const OpenAINativeProvider = ({ showModelOptions, isPopup, currentMode }: OpenAINativeProviderProps) => {
const { apiConfiguration } = useExtensionState()
const { handleFieldChange } = useApiConfigurationHandlers()
const { handleFieldChange, handleModeFieldChange } = useApiConfigurationHandlers()
// Get the normalized configuration
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration)
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration, currentMode)
return (
<div>
@@ -38,7 +39,13 @@ export const OpenAINativeProvider = ({ showModelOptions, isPopup }: OpenAINative
<ModelSelector
models={openAiNativeModels}
selectedModelId={selectedModelId}
onChange={(e: any) => handleFieldChange("apiModelId", e.target.value)}
onChange={(e: any) =>
handleModeFieldChange(
{ plan: "planModeApiModelId", act: "actModeApiModelId" },
e.target.value,
currentMode,
)
}
label="Model"
/>
@@ -53,14 +53,14 @@ const OpenRouterBalanceDisplay = ({ apiKey }: { apiKey: string }) => {
interface OpenRouterProviderProps {
showModelOptions: boolean
isPopup?: boolean
uriScheme?: string
currentMode: "plan" | "act"
}
/**
* The OpenRouter provider configuration component
*/
export const OpenRouterProvider = ({ showModelOptions, isPopup, uriScheme }: OpenRouterProviderProps) => {
const { apiConfiguration } = useExtensionState()
export const OpenRouterProvider = ({ showModelOptions, isPopup, currentMode }: OpenRouterProviderProps) => {
const { apiConfiguration, uriScheme } = useExtensionState()
const { handleFieldChange } = useApiConfigurationHandlers()
const [providerSortingSelected, setProviderSortingSelected] = useState(!!apiConfiguration?.openRouterProviderSorting)
@@ -142,7 +142,7 @@ export const OpenRouterProvider = ({ showModelOptions, isPopup, uriScheme }: Ope
</div>
)}
<OpenRouterModelPicker isPopup={isPopup} />
<OpenRouterModelPicker isPopup={isPopup} currentMode={currentMode} />
</>
)}
</div>
@@ -28,17 +28,18 @@ const SUPPORTED_THINKING_MODELS = [
interface QwenProviderProps {
showModelOptions: boolean
isPopup?: boolean
currentMode: "plan" | "act"
}
/**
* The Alibaba Qwen provider configuration component
*/
export const QwenProvider = ({ showModelOptions, isPopup }: QwenProviderProps) => {
export const QwenProvider = ({ showModelOptions, isPopup, currentMode }: QwenProviderProps) => {
const { apiConfiguration } = useExtensionState()
const { handleFieldChange } = useApiConfigurationHandlers()
const { handleFieldChange, handleModeFieldChange } = useApiConfigurationHandlers()
// Get the normalized configuration
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration)
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration, currentMode)
// Determine which models to use based on API line selection
const qwenModels = apiConfiguration?.qwenApiLine === "china" ? mainlandQwenModels : internationalQwenModels
@@ -83,13 +84,19 @@ export const QwenProvider = ({ showModelOptions, isPopup }: QwenProviderProps) =
<ModelSelector
models={qwenModels}
selectedModelId={selectedModelId}
onChange={(e: any) => handleFieldChange("apiModelId", e.target.value)}
onChange={(e: any) =>
handleModeFieldChange(
{ plan: "planModeApiModelId", act: "actModeApiModelId" },
e.target.value,
currentMode,
)
}
label="Model"
zIndex={DROPDOWN_Z_INDEX - 2}
/>
{SUPPORTED_THINKING_MODELS.includes(selectedModelId) && (
<ThinkingBudgetSlider maxBudget={selectedModelInfo.thinkingConfig?.maxBudget} />
<ThinkingBudgetSlider maxBudget={selectedModelInfo.thinkingConfig?.maxBudget} currentMode={currentMode} />
)}
<ModelInfoView selectedModelId={selectedModelId} modelInfo={selectedModelInfo} isPopup={isPopup} />
@@ -9,12 +9,13 @@ import { useExtensionState } from "@/context/ExtensionStateContext"
interface RequestyProviderProps {
showModelOptions: boolean
isPopup?: boolean
currentMode: "plan" | "act"
}
/**
* The Requesty provider configuration component
*/
export const RequestyProvider = ({ showModelOptions, isPopup }: RequestyProviderProps) => {
export const RequestyProvider = ({ showModelOptions, isPopup, currentMode }: RequestyProviderProps) => {
const { apiConfiguration } = useExtensionState()
const { handleFieldChange } = useApiConfigurationHandlers()
@@ -27,7 +28,7 @@ export const RequestyProvider = ({ showModelOptions, isPopup }: RequestyProvider
signupUrl="https://app.requesty.ai/manage-api"
/>
{showModelOptions && <RequestyModelPicker isPopup={isPopup} />}
{showModelOptions && <RequestyModelPicker isPopup={isPopup} currentMode={currentMode} />}
</div>
)
}
@@ -12,17 +12,18 @@ import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandler
interface SambanovaProviderProps {
showModelOptions: boolean
isPopup?: boolean
currentMode: "plan" | "act"
}
/**
* The SambaNova provider configuration component
*/
export const SambanovaProvider = ({ showModelOptions, isPopup }: SambanovaProviderProps) => {
export const SambanovaProvider = ({ showModelOptions, isPopup, currentMode }: SambanovaProviderProps) => {
const { apiConfiguration } = useExtensionState()
const { handleFieldChange } = useApiConfigurationHandlers()
const { handleFieldChange, handleModeFieldChange } = useApiConfigurationHandlers()
// Get the normalized configuration
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration)
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration, currentMode)
return (
<div>
@@ -38,7 +39,13 @@ export const SambanovaProvider = ({ showModelOptions, isPopup }: SambanovaProvid
<ModelSelector
models={sambanovaModels}
selectedModelId={selectedModelId}
onChange={(e: any) => handleFieldChange("apiModelId", e.target.value)}
onChange={(e: any) =>
handleModeFieldChange(
{ plan: "planModeApiModelId", act: "actModeApiModelId" },
e.target.value,
currentMode,
)
}
label="Model"
/>
@@ -13,16 +13,17 @@ import { useExtensionState } from "@/context/ExtensionStateContext"
interface SapAiCoreProviderProps {
showModelOptions: boolean
isPopup?: boolean
currentMode: "plan" | "act"
}
/**
* The SAP AI Core provider configuration component
*/
export const SapAiCoreProvider = ({ showModelOptions, isPopup }: SapAiCoreProviderProps) => {
export const SapAiCoreProvider = ({ showModelOptions, isPopup, currentMode }: SapAiCoreProviderProps) => {
const { apiConfiguration } = useExtensionState()
const { handleFieldChange } = useApiConfigurationHandlers()
const { handleFieldChange, handleModeFieldChange } = useApiConfigurationHandlers()
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration)
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration, currentMode)
return (
<div style={{ display: "flex", flexDirection: "column", gap: 5 }}>
@@ -97,7 +98,13 @@ export const SapAiCoreProvider = ({ showModelOptions, isPopup }: SapAiCoreProvid
<ModelSelector
models={sapAiCoreModels}
selectedModelId={selectedModelId}
onChange={(e: any) => handleFieldChange("apiModelId", e.target.value)}
onChange={(e: any) =>
handleModeFieldChange(
{ plan: "planModeApiModelId", act: "actModeApiModelId" },
e.target.value,
currentMode,
)
}
label="Model"
/>
@@ -3,6 +3,7 @@ import { DebouncedTextField } from "../common/DebouncedTextField"
import { ApiKeyField } from "../common/ApiKeyField"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers"
import { getModeSpecificFields } from "../utils/providerUtils"
/**
* Props for the TogetherProvider component
@@ -10,14 +11,17 @@ import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandler
interface TogetherProviderProps {
showModelOptions: boolean
isPopup?: boolean
currentMode: "plan" | "act"
}
/**
* The Together provider configuration component
*/
export const TogetherProvider = ({ showModelOptions, isPopup }: TogetherProviderProps) => {
export const TogetherProvider = ({ showModelOptions, isPopup, currentMode }: TogetherProviderProps) => {
const { apiConfiguration } = useExtensionState()
const { handleFieldChange } = useApiConfigurationHandlers()
const { handleFieldChange, handleModeFieldChange } = useApiConfigurationHandlers()
const { togetherModelId } = getModeSpecificFields(apiConfiguration, currentMode)
return (
<div>
@@ -27,8 +31,10 @@ export const TogetherProvider = ({ showModelOptions, isPopup }: TogetherProvider
providerName="Together"
/>
<DebouncedTextField
initialValue={apiConfiguration?.togetherModelId || ""}
onChange={(value) => handleFieldChange("togetherModelId", value)}
initialValue={togetherModelId || ""}
onChange={(value) =>
handleModeFieldChange({ plan: "planModeTogetherModelId", act: "actModeTogetherModelId" }, value, currentMode)
}
style={{ width: "100%" }}
placeholder={"Enter Model ID..."}>
<span style={{ fontWeight: 500 }}>Model ID</span>
@@ -7,11 +7,18 @@ import * as vscodemodels from "vscode"
import { DropdownContainer, DROPDOWN_Z_INDEX } from "../ApiOptions"
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { getModeSpecificFields } from "../utils/providerUtils"
export const VSCodeLmProvider = () => {
interface VSCodeLmProviderProps {
currentMode: "plan" | "act"
}
export const VSCodeLmProvider = ({ currentMode }: VSCodeLmProviderProps) => {
const [vsCodeLmModels, setVsCodeLmModels] = useState<vscodemodels.LanguageModelChatSelector[]>([])
const { apiConfiguration } = useExtensionState()
const { handleFieldChange } = useApiConfigurationHandlers()
const { handleFieldChange, handleModeFieldChange } = useApiConfigurationHandlers()
const { vsCodeLmModelSelector } = getModeSpecificFields(apiConfiguration, currentMode)
// Poll VS Code LM models
const requestVsCodeLmModels = useCallback(async () => {
@@ -42,8 +49,8 @@ export const VSCodeLmProvider = () => {
<VSCodeDropdown
id="vscode-lm-model"
value={
apiConfiguration?.vsCodeLmModelSelector
? `${apiConfiguration.vsCodeLmModelSelector.vendor ?? ""}/${apiConfiguration.vsCodeLmModelSelector.family ?? ""}`
vsCodeLmModelSelector
? `${vsCodeLmModelSelector.vendor ?? ""}/${vsCodeLmModelSelector.family ?? ""}`
: ""
}
onChange={(e) => {
@@ -53,7 +60,11 @@ export const VSCodeLmProvider = () => {
}
const [vendor, family] = value.split("/")
handleFieldChange("vsCodeLmModelSelector", { vendor, family })
handleModeFieldChange(
{ plan: "planModeVsCodeLmModelSelector", act: "actModeVsCodeLmModelSelector" },
{ vendor, family },
currentMode,
)
}}
style={{ width: "100%" }}>
<VSCodeOption value="">Select a model...</VSCodeOption>
@@ -15,6 +15,7 @@ import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandler
interface VertexProviderProps {
showModelOptions: boolean
isPopup?: boolean
currentMode: "plan" | "act"
}
// Vertex models that support thinking
@@ -30,12 +31,12 @@ const SUPPORTED_THINKING_MODELS = [
/**
* The GCP Vertex AI provider configuration component
*/
export const VertexProvider = ({ showModelOptions, isPopup }: VertexProviderProps) => {
export const VertexProvider = ({ showModelOptions, isPopup, currentMode }: VertexProviderProps) => {
const { apiConfiguration } = useExtensionState()
const { handleFieldChange } = useApiConfigurationHandlers()
const { handleFieldChange, handleModeFieldChange } = useApiConfigurationHandlers()
// Get the normalized configuration
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration)
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration, currentMode)
// Determine which models to use based on region
const modelsToUse = apiConfiguration?.vertexRegion === "global" ? vertexGlobalModels : vertexModels
@@ -98,13 +99,19 @@ export const VertexProvider = ({ showModelOptions, isPopup }: VertexProviderProp
<ModelSelector
models={modelsToUse}
selectedModelId={selectedModelId}
onChange={(e: any) => handleFieldChange("apiModelId", e.target.value)}
onChange={(e: any) =>
handleModeFieldChange(
{ plan: "planModeApiModelId", act: "actModeApiModelId" },
e.target.value,
currentMode,
)
}
label="Model"
zIndex={DROPDOWN_Z_INDEX - 2}
/>
{SUPPORTED_THINKING_MODELS.includes(selectedModelId) && (
<ThinkingBudgetSlider maxBudget={selectedModelInfo.thinkingConfig?.maxBudget} />
<ThinkingBudgetSlider maxBudget={selectedModelInfo.thinkingConfig?.maxBudget} currentMode={currentMode} />
)}
<ModelInfoView selectedModelId={selectedModelId} modelInfo={selectedModelInfo} isPopup={isPopup} />
@@ -4,7 +4,7 @@ import { useState, useEffect } from "react"
import { ApiKeyField } from "../common/ApiKeyField"
import { ModelSelector, DropdownContainer } from "../common/ModelSelector"
import { ModelInfoView } from "../common/ModelInfoView"
import { normalizeApiConfiguration } from "../utils/providerUtils"
import { getModeSpecificFields, normalizeApiConfiguration } from "../utils/providerUtils"
import { DROPDOWN_Z_INDEX } from "../ApiOptions"
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers"
import { useExtensionState } from "@/context/ExtensionStateContext"
@@ -15,17 +15,20 @@ import { useExtensionState } from "@/context/ExtensionStateContext"
interface XaiProviderProps {
showModelOptions: boolean
isPopup?: boolean
currentMode: "plan" | "act"
}
export const XaiProvider = ({ showModelOptions, isPopup }: XaiProviderProps) => {
export const XaiProvider = ({ showModelOptions, isPopup, currentMode }: XaiProviderProps) => {
const { apiConfiguration } = useExtensionState()
const { handleFieldChange } = useApiConfigurationHandlers()
const { handleFieldChange, handleModeFieldChange } = useApiConfigurationHandlers()
const modeFields = getModeSpecificFields(apiConfiguration, currentMode)
// Get the normalized configuration
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration)
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration, currentMode)
// Local state for reasoning effort toggle
const [reasoningEffortSelected, setReasoningEffortSelected] = useState(!!apiConfiguration?.reasoningEffort)
const [reasoningEffortSelected, setReasoningEffortSelected] = useState(!!modeFields.reasoningEffort)
return (
<div>
@@ -54,7 +57,13 @@ export const XaiProvider = ({ showModelOptions, isPopup }: XaiProviderProps) =>
<ModelSelector
models={xaiModels}
selectedModelId={selectedModelId}
onChange={(e: any) => handleFieldChange("apiModelId", e.target.value)}
onChange={(e: any) =>
handleModeFieldChange(
{ plan: "planModeApiModelId", act: "actModeApiModelId" },
e.target.value,
currentMode,
)
}
label="Model"
/>
@@ -67,7 +76,11 @@ export const XaiProvider = ({ showModelOptions, isPopup }: XaiProviderProps) =>
const isChecked = e.target.checked === true
setReasoningEffortSelected(isChecked)
if (!isChecked) {
handleFieldChange("reasoningEffort", "")
handleModeFieldChange(
{ plan: "planModeReasoningEffort", act: "actModeReasoningEffort" },
"",
currentMode,
)
}
}}>
Modify reasoning effort
@@ -82,9 +95,13 @@ export const XaiProvider = ({ showModelOptions, isPopup }: XaiProviderProps) =>
<VSCodeDropdown
id="reasoning-effort-dropdown"
style={{ width: "100%", marginTop: 3 }}
value={apiConfiguration?.reasoningEffort || "high"}
value={modeFields.reasoningEffort || "high"}
onChange={(e: any) => {
handleFieldChange("reasoningEffort", e.target.value)
handleModeFieldChange(
{ plan: "planModeReasoningEffort", act: "actModeReasoningEffort" },
e.target.value,
currentMode,
)
}}>
<VSCodeOption value="low">low</VSCodeOption>
<VSCodeOption value="high">high</VSCodeOption>
@@ -5,19 +5,18 @@ import Section from "../Section"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { StateServiceClient } from "@/services/grpc-client"
import { UpdateSettingsRequest } from "@shared/proto/state"
import { useState } from "react"
import { syncModeConfigurations } from "../utils/providerUtils"
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers"
interface ApiConfigurationSectionProps {
isSwitchingMode: boolean
handlePlanActModeChange: (mode: "plan" | "act") => Promise<void>
renderSectionHeader: (tabId: string) => JSX.Element | null
}
const ApiConfigurationSection = ({
isSwitchingMode,
handlePlanActModeChange,
renderSectionHeader,
}: ApiConfigurationSectionProps) => {
const { planActSeparateModelsSetting, chatSettings } = useExtensionState()
const ApiConfigurationSection = ({ renderSectionHeader }: ApiConfigurationSectionProps) => {
const { planActSeparateModelsSetting, chatSettings, apiConfiguration } = useExtensionState()
const [currentTab, setCurrentTab] = useState<"plan" | "act">(chatSettings.mode)
const { handleFieldsChange } = useApiConfigurationHandlers()
return (
<div>
{renderSectionHeader("api-config")}
@@ -27,34 +26,34 @@ const ApiConfigurationSection = ({
<div className="rounded-md mb-5 bg-[var(--vscode-panel-background)]">
<div className="flex gap-[1px] mb-[10px] -mt-2 border-0 border-b border-solid border-[var(--vscode-panel-border)]">
<TabButton
isActive={chatSettings.mode === "plan"}
onClick={() => handlePlanActModeChange("plan")}
disabled={isSwitchingMode}
isActive={currentTab === "plan"}
onClick={() => setCurrentTab("plan")}
disabled={currentTab === "plan"}
style={{
opacity: isSwitchingMode ? 0.6 : 1,
cursor: isSwitchingMode ? "not-allowed" : "pointer",
opacity: 1,
cursor: "pointer",
}}>
{isSwitchingMode && chatSettings.mode === "act" ? "Switching..." : "Plan Mode"}
Plan Mode
</TabButton>
<TabButton
isActive={chatSettings.mode === "act"}
onClick={() => handlePlanActModeChange("act")}
disabled={isSwitchingMode}
isActive={currentTab === "act"}
onClick={() => setCurrentTab("act")}
disabled={currentTab === "act"}
style={{
opacity: isSwitchingMode ? 0.6 : 1,
cursor: isSwitchingMode ? "not-allowed" : "pointer",
opacity: 1,
cursor: "pointer",
}}>
{isSwitchingMode && chatSettings.mode === "plan" ? "Switching..." : "Act Mode"}
Act Mode
</TabButton>
</div>
{/* Content container */}
<div className="-mb-3">
<ApiOptions showModelOptions={true} />
<ApiOptions showModelOptions={true} currentMode={currentTab} />
</div>
</div>
) : (
<ApiOptions showModelOptions={true} />
<ApiOptions showModelOptions={true} currentMode={chatSettings.mode} />
)}
<div className="mb-[5px]">
@@ -64,6 +63,12 @@ const ApiConfigurationSection = ({
onChange={async (e: any) => {
const checked = e.target.checked === true
try {
// If unchecking the toggle, wait a bit for state to update, then sync configurations
if (!checked) {
// Wait for React state to update after the protocol call
await new Promise((resolve) => setTimeout(resolve, 100))
await syncModeConfigurations(apiConfiguration, currentTab, handleFieldsChange)
}
await StateServiceClient.updateSettings(
UpdateSettingsRequest.create({
planActSeparateModelsSetting: checked,
@@ -62,9 +62,13 @@ export interface NormalizedApiConfig {
/**
* Normalizes API configuration to ensure consistent values
*/
export function normalizeApiConfiguration(apiConfiguration?: ApiConfiguration): NormalizedApiConfig {
const provider = apiConfiguration?.apiProvider || "anthropic"
const modelId = apiConfiguration?.apiModelId
export function normalizeApiConfiguration(
apiConfiguration: ApiConfiguration | undefined,
currentMode: "plan" | "act",
): NormalizedApiConfig {
const provider =
(currentMode === "plan" ? apiConfiguration?.planModeApiProvider : apiConfiguration?.actModeApiProvider) || "anthropic"
const modelId = currentMode === "plan" ? apiConfiguration?.planModeApiModelId : apiConfiguration?.actModeApiModelId
const getProviderData = (models: Record<string, ModelInfo>, defaultId: string) => {
let selectedModelId: string
@@ -89,8 +93,15 @@ export function normalizeApiConfiguration(apiConfiguration?: ApiConfiguration):
case "claude-code":
return getProviderData(claudeCodeModels, claudeCodeDefaultModelId)
case "bedrock":
if (apiConfiguration?.awsBedrockCustomSelected) {
const baseModelId = apiConfiguration.awsBedrockCustomModelBaseId
const awsBedrockCustomSelected =
currentMode === "plan"
? apiConfiguration?.planModeAwsBedrockCustomSelected
: apiConfiguration?.actModeAwsBedrockCustomSelected
if (awsBedrockCustomSelected) {
const baseModelId =
currentMode === "plan"
? apiConfiguration?.planModeAwsBedrockCustomModelBaseId
: apiConfiguration?.actModeAwsBedrockCustomModelBaseId
return {
selectedProvider: provider,
selectedModelId: modelId || bedrockDefaultModelId,
@@ -118,59 +129,89 @@ export function normalizeApiConfiguration(apiConfiguration?: ApiConfiguration):
case "asksage":
return getProviderData(askSageModels, askSageDefaultModelId)
case "openrouter":
const openRouterModelId =
currentMode === "plan" ? apiConfiguration?.planModeOpenRouterModelId : apiConfiguration?.actModeOpenRouterModelId
const openRouterModelInfo =
currentMode === "plan"
? apiConfiguration?.planModeOpenRouterModelInfo
: apiConfiguration?.actModeOpenRouterModelInfo
return {
selectedProvider: provider,
selectedModelId: apiConfiguration?.openRouterModelId || openRouterDefaultModelId,
selectedModelInfo: apiConfiguration?.openRouterModelInfo || openRouterDefaultModelInfo,
selectedModelId: openRouterModelId || openRouterDefaultModelId,
selectedModelInfo: openRouterModelInfo || openRouterDefaultModelInfo,
}
case "requesty":
const requestyModelId =
currentMode === "plan" ? apiConfiguration?.planModeRequestyModelId : apiConfiguration?.actModeRequestyModelId
const requestyModelInfo =
currentMode === "plan" ? apiConfiguration?.planModeRequestyModelInfo : apiConfiguration?.actModeRequestyModelInfo
return {
selectedProvider: provider,
selectedModelId: apiConfiguration?.requestyModelId || requestyDefaultModelId,
selectedModelInfo: apiConfiguration?.requestyModelInfo || requestyDefaultModelInfo,
selectedModelId: requestyModelId || requestyDefaultModelId,
selectedModelInfo: requestyModelInfo || requestyDefaultModelInfo,
}
case "cline":
const openRouterModelId = apiConfiguration?.openRouterModelId || openRouterDefaultModelId
const openRouterModelInfo = apiConfiguration?.openRouterModelInfo || openRouterDefaultModelInfo
const clineOpenRouterModelId =
(currentMode === "plan"
? apiConfiguration?.planModeOpenRouterModelId
: apiConfiguration?.actModeOpenRouterModelId) || openRouterDefaultModelId
const clineOpenRouterModelInfo =
(currentMode === "plan"
? apiConfiguration?.planModeOpenRouterModelInfo
: apiConfiguration?.actModeOpenRouterModelInfo) || openRouterDefaultModelInfo
return {
selectedProvider: provider,
selectedModelId: openRouterModelId,
selectedModelInfo: openRouterModelInfo,
selectedModelId: clineOpenRouterModelId,
selectedModelInfo: clineOpenRouterModelInfo,
}
case "openai":
const openAiModelId =
currentMode === "plan" ? apiConfiguration?.planModeOpenAiModelId : apiConfiguration?.actModeOpenAiModelId
const openAiModelInfo =
currentMode === "plan" ? apiConfiguration?.planModeOpenAiModelInfo : apiConfiguration?.actModeOpenAiModelInfo
return {
selectedProvider: provider,
selectedModelId: apiConfiguration?.openAiModelId || "",
selectedModelInfo: apiConfiguration?.openAiModelInfo || openAiModelInfoSaneDefaults,
selectedModelId: openAiModelId || "",
selectedModelInfo: openAiModelInfo || openAiModelInfoSaneDefaults,
}
case "ollama":
const ollamaModelId =
currentMode === "plan" ? apiConfiguration?.planModeOllamaModelId : apiConfiguration?.actModeOllamaModelId
return {
selectedProvider: provider,
selectedModelId: apiConfiguration?.ollamaModelId || "",
selectedModelId: ollamaModelId || "",
selectedModelInfo: openAiModelInfoSaneDefaults,
}
case "lmstudio":
const lmStudioModelId =
currentMode === "plan" ? apiConfiguration?.planModeLmStudioModelId : apiConfiguration?.actModeLmStudioModelId
return {
selectedProvider: provider,
selectedModelId: apiConfiguration?.lmStudioModelId || "",
selectedModelId: lmStudioModelId || "",
selectedModelInfo: openAiModelInfoSaneDefaults,
}
case "vscode-lm":
const vsCodeLmModelSelector =
currentMode === "plan"
? apiConfiguration?.planModeVsCodeLmModelSelector
: apiConfiguration?.actModeVsCodeLmModelSelector
return {
selectedProvider: provider,
selectedModelId: apiConfiguration?.vsCodeLmModelSelector
? `${apiConfiguration.vsCodeLmModelSelector.vendor}/${apiConfiguration.vsCodeLmModelSelector.family}`
: "",
selectedModelId: vsCodeLmModelSelector ? `${vsCodeLmModelSelector.vendor}/${vsCodeLmModelSelector.family}` : "",
selectedModelInfo: {
...openAiModelInfoSaneDefaults,
supportsImages: false, // VSCode LM API currently doesn't support images
},
}
case "litellm":
const liteLlmModelId =
currentMode === "plan" ? apiConfiguration?.planModeLiteLlmModelId : apiConfiguration?.actModeLiteLlmModelId
const liteLlmModelInfo =
currentMode === "plan" ? apiConfiguration?.planModeLiteLlmModelInfo : apiConfiguration?.actModeLiteLlmModelInfo
return {
selectedProvider: provider,
selectedModelId: apiConfiguration?.liteLlmModelId || "",
selectedModelInfo: apiConfiguration?.liteLlmModelInfo || liteLlmModelInfoSaneDefaults,
selectedModelId: liteLlmModelId || "",
selectedModelInfo: liteLlmModelInfo || liteLlmModelInfoSaneDefaults,
}
case "xai":
return getProviderData(xaiModels, xaiDefaultModelId)
@@ -189,13 +230,15 @@ export function normalizeApiConfiguration(apiConfiguration?: ApiConfiguration):
case "cerebras":
return getProviderData(cerebrasModels, cerebrasDefaultModelId)
case "groq":
const result = {
const groqModelId =
currentMode === "plan" ? apiConfiguration?.planModeGroqModelId : apiConfiguration?.actModeGroqModelId
const groqModelInfo =
currentMode === "plan" ? apiConfiguration?.planModeGroqModelInfo : apiConfiguration?.actModeGroqModelInfo
return {
selectedProvider: provider,
selectedModelId: apiConfiguration?.groqModelId || groqDefaultModelId,
selectedModelInfo: apiConfiguration?.groqModelInfo || groqModels[groqDefaultModelId],
selectedModelId: groqModelId || groqDefaultModelId,
selectedModelInfo: groqModelInfo || groqModels[groqDefaultModelId],
}
return result
case "sapaicore":
return getProviderData(sapAiCoreModels, sapAiCoreDefaultModelId)
default:
@@ -203,6 +246,218 @@ export function normalizeApiConfiguration(apiConfiguration?: ApiConfiguration):
}
}
/**
* Gets mode-specific field values from API configuration
* @param apiConfiguration The API configuration object
* @param mode The current mode ("plan" or "act")
* @returns Object containing mode-specific field values for clean destructuring
*/
export function getModeSpecificFields(apiConfiguration: ApiConfiguration | undefined, mode: "plan" | "act") {
if (!apiConfiguration) {
return {
// Core fields
apiProvider: undefined,
apiModelId: undefined,
// Provider-specific model IDs
togetherModelId: undefined,
fireworksModelId: undefined,
lmStudioModelId: undefined,
ollamaModelId: undefined,
liteLlmModelId: undefined,
requestyModelId: undefined,
openAiModelId: undefined,
openRouterModelId: undefined,
groqModelId: undefined,
// Model info objects
openAiModelInfo: undefined,
liteLlmModelInfo: undefined,
openRouterModelInfo: undefined,
requestyModelInfo: undefined,
groqModelInfo: undefined,
vsCodeLmModelSelector: undefined,
// AWS Bedrock fields
awsBedrockCustomSelected: undefined,
awsBedrockCustomModelBaseId: undefined,
// Other mode-specific fields
thinkingBudgetTokens: undefined,
reasoningEffort: undefined,
}
}
return {
// Core fields
apiProvider: mode === "plan" ? apiConfiguration.planModeApiProvider : apiConfiguration.actModeApiProvider,
apiModelId: mode === "plan" ? apiConfiguration.planModeApiModelId : apiConfiguration.actModeApiModelId,
// Provider-specific model IDs
togetherModelId: mode === "plan" ? apiConfiguration.planModeTogetherModelId : apiConfiguration.actModeTogetherModelId,
fireworksModelId: mode === "plan" ? apiConfiguration.planModeFireworksModelId : apiConfiguration.actModeFireworksModelId,
lmStudioModelId: mode === "plan" ? apiConfiguration.planModeLmStudioModelId : apiConfiguration.actModeLmStudioModelId,
ollamaModelId: mode === "plan" ? apiConfiguration.planModeOllamaModelId : apiConfiguration.actModeOllamaModelId,
liteLlmModelId: mode === "plan" ? apiConfiguration.planModeLiteLlmModelId : apiConfiguration.actModeLiteLlmModelId,
requestyModelId: mode === "plan" ? apiConfiguration.planModeRequestyModelId : apiConfiguration.actModeRequestyModelId,
openAiModelId: mode === "plan" ? apiConfiguration.planModeOpenAiModelId : apiConfiguration.actModeOpenAiModelId,
openRouterModelId:
mode === "plan" ? apiConfiguration.planModeOpenRouterModelId : apiConfiguration.actModeOpenRouterModelId,
groqModelId: mode === "plan" ? apiConfiguration.planModeGroqModelId : apiConfiguration.actModeGroqModelId,
// Model info objects
openAiModelInfo: mode === "plan" ? apiConfiguration.planModeOpenAiModelInfo : apiConfiguration.actModeOpenAiModelInfo,
liteLlmModelInfo: mode === "plan" ? apiConfiguration.planModeLiteLlmModelInfo : apiConfiguration.actModeLiteLlmModelInfo,
openRouterModelInfo:
mode === "plan" ? apiConfiguration.planModeOpenRouterModelInfo : apiConfiguration.actModeOpenRouterModelInfo,
requestyModelInfo:
mode === "plan" ? apiConfiguration.planModeRequestyModelInfo : apiConfiguration.actModeRequestyModelInfo,
groqModelInfo: mode === "plan" ? apiConfiguration.planModeGroqModelInfo : apiConfiguration.actModeGroqModelInfo,
vsCodeLmModelSelector:
mode === "plan" ? apiConfiguration.planModeVsCodeLmModelSelector : apiConfiguration.actModeVsCodeLmModelSelector,
// AWS Bedrock fields
awsBedrockCustomSelected:
mode === "plan"
? apiConfiguration.planModeAwsBedrockCustomSelected
: apiConfiguration.actModeAwsBedrockCustomSelected,
awsBedrockCustomModelBaseId:
mode === "plan"
? apiConfiguration.planModeAwsBedrockCustomModelBaseId
: apiConfiguration.actModeAwsBedrockCustomModelBaseId,
// Other mode-specific fields
thinkingBudgetTokens:
mode === "plan" ? apiConfiguration.planModeThinkingBudgetTokens : apiConfiguration.actModeThinkingBudgetTokens,
reasoningEffort: mode === "plan" ? apiConfiguration.planModeReasoningEffort : apiConfiguration.actModeReasoningEffort,
}
}
/**
* Synchronizes mode configurations by copying the source mode's settings to both modes
* This is used when the "Use different models for Plan and Act modes" toggle is unchecked
*/
export async function syncModeConfigurations(
apiConfiguration: ApiConfiguration | undefined,
sourceMode: "plan" | "act",
handleFieldsChange: (updates: Partial<ApiConfiguration>) => Promise<void>,
): Promise<void> {
if (!apiConfiguration) return
const sourceFields = getModeSpecificFields(apiConfiguration, sourceMode)
const { apiProvider } = sourceFields
if (!apiProvider) return
// Build the complete update object with both plan and act mode fields
const updates: Partial<ApiConfiguration> = {
// Always sync common fields
planModeApiProvider: sourceFields.apiProvider,
actModeApiProvider: sourceFields.apiProvider,
planModeThinkingBudgetTokens: sourceFields.thinkingBudgetTokens,
actModeThinkingBudgetTokens: sourceFields.thinkingBudgetTokens,
planModeReasoningEffort: sourceFields.reasoningEffort,
actModeReasoningEffort: sourceFields.reasoningEffort,
}
// Handle provider-specific fields
switch (apiProvider) {
case "openrouter":
case "cline":
updates.planModeOpenRouterModelId = sourceFields.openRouterModelId
updates.actModeOpenRouterModelId = sourceFields.openRouterModelId
updates.planModeOpenRouterModelInfo = sourceFields.openRouterModelInfo
updates.actModeOpenRouterModelInfo = sourceFields.openRouterModelInfo
break
case "requesty":
updates.planModeRequestyModelId = sourceFields.requestyModelId
updates.actModeRequestyModelId = sourceFields.requestyModelId
updates.planModeRequestyModelInfo = sourceFields.requestyModelInfo
updates.actModeRequestyModelInfo = sourceFields.requestyModelInfo
break
case "openai":
updates.planModeOpenAiModelId = sourceFields.openAiModelId
updates.actModeOpenAiModelId = sourceFields.openAiModelId
updates.planModeOpenAiModelInfo = sourceFields.openAiModelInfo
updates.actModeOpenAiModelInfo = sourceFields.openAiModelInfo
break
case "ollama":
updates.planModeOllamaModelId = sourceFields.ollamaModelId
updates.actModeOllamaModelId = sourceFields.ollamaModelId
break
case "lmstudio":
updates.planModeLmStudioModelId = sourceFields.lmStudioModelId
updates.actModeLmStudioModelId = sourceFields.lmStudioModelId
break
case "vscode-lm":
updates.planModeVsCodeLmModelSelector = sourceFields.vsCodeLmModelSelector
updates.actModeVsCodeLmModelSelector = sourceFields.vsCodeLmModelSelector
break
case "litellm":
updates.planModeLiteLlmModelId = sourceFields.liteLlmModelId
updates.actModeLiteLlmModelId = sourceFields.liteLlmModelId
updates.planModeLiteLlmModelInfo = sourceFields.liteLlmModelInfo
updates.actModeLiteLlmModelInfo = sourceFields.liteLlmModelInfo
break
case "groq":
updates.planModeGroqModelId = sourceFields.groqModelId
updates.actModeGroqModelId = sourceFields.groqModelId
updates.planModeGroqModelInfo = sourceFields.groqModelInfo
updates.actModeGroqModelInfo = sourceFields.groqModelInfo
break
case "together":
updates.planModeTogetherModelId = sourceFields.togetherModelId
updates.actModeTogetherModelId = sourceFields.togetherModelId
break
case "fireworks":
updates.planModeFireworksModelId = sourceFields.fireworksModelId
updates.actModeFireworksModelId = sourceFields.fireworksModelId
break
case "bedrock":
updates.planModeApiModelId = sourceFields.apiModelId
updates.actModeApiModelId = sourceFields.apiModelId
updates.planModeAwsBedrockCustomSelected = sourceFields.awsBedrockCustomSelected
updates.actModeAwsBedrockCustomSelected = sourceFields.awsBedrockCustomSelected
updates.planModeAwsBedrockCustomModelBaseId = sourceFields.awsBedrockCustomModelBaseId
updates.actModeAwsBedrockCustomModelBaseId = sourceFields.awsBedrockCustomModelBaseId
break
// Providers that use apiProvider + apiModelId fields
case "anthropic":
case "claude-code":
case "vertex":
case "gemini":
case "openai-native":
case "deepseek":
case "qwen":
case "doubao":
case "mistral":
case "asksage":
case "xai":
case "nebius":
case "sambanova":
case "cerebras":
case "sapaicore":
default:
updates.planModeApiModelId = sourceFields.apiModelId
updates.actModeApiModelId = sourceFields.apiModelId
break
}
// Make the atomic update
await handleFieldsChange(updates)
}
/**
* Gets the OpenRouter authentication URL
*/
@@ -6,7 +6,8 @@ import { UpdateApiConfigurationRequest } from "@shared/proto/models"
import { useCallback } from "react"
export const useApiConfigurationHandlers = () => {
const { apiConfiguration, uriScheme } = useExtensionState()
const { apiConfiguration, planActSeparateModelsSetting } = useExtensionState()
/**
* Updates a single field in the API configuration.
*
@@ -17,24 +18,19 @@ export const useApiConfigurationHandlers = () => {
* @param field - The field key to update
* @param value - The new value for the field
*/
const handleFieldChange = useCallback(
<K extends keyof ApiConfiguration>(field: K, value: ApiConfiguration[K]) => {
const updatedConfig = {
...apiConfiguration,
[field]: value,
}
const handleFieldChange = async <K extends keyof ApiConfiguration>(field: K, value: ApiConfiguration[K]) => {
const updatedConfig = {
...apiConfiguration,
[field]: value,
}
const protoConfig = convertApiConfigurationToProto(updatedConfig)
ModelsServiceClient.updateApiConfigurationProto(
UpdateApiConfigurationRequest.create({
apiConfiguration: protoConfig,
}),
).catch((error) => {
console.error(`Failed to update API configuration field ${field}:`, error)
})
},
[apiConfiguration],
)
const protoConfig = convertApiConfigurationToProto(updatedConfig)
await ModelsServiceClient.updateApiConfigurationProto(
UpdateApiConfigurationRequest.create({
apiConfiguration: protoConfig,
}),
)
}
/**
* Updates multiple fields in the API configuration at once.
@@ -45,24 +41,69 @@ export const useApiConfigurationHandlers = () => {
*
* @param updates - An object containing the fields to update and their new values
*/
const handleFieldsChange = useCallback(
(updates: Partial<ApiConfiguration>) => {
const updatedConfig = {
...apiConfiguration,
...updates,
}
const handleFieldsChange = async (updates: Partial<ApiConfiguration>) => {
const updatedConfig = {
...apiConfiguration,
...updates,
}
const protoConfig = convertApiConfigurationToProto(updatedConfig)
ModelsServiceClient.updateApiConfigurationProto(
UpdateApiConfigurationRequest.create({
apiConfiguration: protoConfig,
}),
).catch((error) => {
console.error("Failed to update API configuration fields:", error)
const protoConfig = convertApiConfigurationToProto(updatedConfig)
await ModelsServiceClient.updateApiConfigurationProto(
UpdateApiConfigurationRequest.create({
apiConfiguration: protoConfig,
}),
)
}
const handleModeFieldChange = async <PlanK extends keyof ApiConfiguration, ActK extends keyof ApiConfiguration>(
fieldPair: { plan: PlanK; act: ActK },
value: ApiConfiguration[PlanK] & ApiConfiguration[ActK], // Intersection ensures value is compatible with both field types
currentMode: "plan" | "act",
) => {
if (planActSeparateModelsSetting) {
const targetField = fieldPair[currentMode]
await handleFieldChange(targetField, value)
} else {
await handleFieldsChange({
[fieldPair.plan]: value,
[fieldPair.act]: value,
})
},
[apiConfiguration],
)
}
}
return { handleFieldChange, handleFieldsChange, uriScheme, apiConfiguration }
/**
* Updates multiple mode-specific fields in a single atomic operation.
*
* This prevents race conditions that can occur when making multiple separate
* handleModeFieldChange calls in rapid succession.
*
* @param fieldPairs - Object mapping keys to plan/act field pairs
* @param values - Object with values for each key
* @param currentMode - The current mode being targeted
*/
const handleModeFieldsChange = async <T extends Record<string, any>>(
fieldPairs: { [K in keyof T]: { plan: keyof ApiConfiguration; act: keyof ApiConfiguration } },
values: T,
currentMode: "plan" | "act",
) => {
if (planActSeparateModelsSetting) {
// Update only the current mode's fields
const updates: Partial<ApiConfiguration> = {}
Object.entries(fieldPairs).forEach(([key, { plan, act }]) => {
const targetField = currentMode === "plan" ? plan : act
updates[targetField] = values[key]
})
await handleFieldsChange(updates)
} else {
// Update both modes' fields
const updates: Partial<ApiConfiguration> = {}
Object.entries(fieldPairs).forEach(([key, { plan, act }]) => {
updates[plan] = values[key]
updates[act] = values[key]
})
await handleFieldsChange(updates)
}
}
return { handleFieldChange, handleFieldsChange, handleModeFieldChange, handleModeFieldsChange }
}
@@ -0,0 +1,13 @@
import type { Meta, StoryObj } from "@storybook/react-vite"
import WelcomeView from "./WelcomeView"
import { VSCodeWebview } from "../common/StorybookDecorator"
const meta: Meta<typeof WelcomeView> = {
title: "Views/WelcomeView",
component: WelcomeView,
decorators: [VSCodeWebview],
}
export default meta
export const Default: StoryObj<typeof meta> = {}
@@ -1,19 +1,37 @@
import { VSCodeButton, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
import { useState, memo } from "react"
import { useEffect, useState, memo } from "react"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { validateApiConfiguration } from "@/utils/validate"
import ApiOptions from "@/components/settings/ApiOptions"
import ClineLogoWhite from "@/assets/ClineLogoWhite"
import { AccountServiceClient } from "@/services/grpc-client"
import { EmptyRequest } from "@shared/proto/common"
import { AccountServiceClient, StateServiceClient } from "@/services/grpc-client"
import { EmptyRequest, BooleanRequest } from "@shared/proto/common"
const WelcomeView = memo(() => {
const { apiConfiguration, chatSettings } = useExtensionState()
const [apiErrorMessage, setApiErrorMessage] = useState<string | undefined>(undefined)
const [showApiOptions, setShowApiOptions] = useState(false)
const disableLetsGoButton = apiErrorMessage != null
const handleLogin = () => {
AccountServiceClient.accountLoginClicked(EmptyRequest.create()).catch((err) =>
console.error("Failed to get login URL:", err),
)
}
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)
}
}
useEffect(() => {
setApiErrorMessage(validateApiConfiguration(chatSettings.mode, apiConfiguration))
}, [apiConfiguration, chatSettings.mode])
return (
<div className="fixed inset-0 p-0 flex flex-col">
<div className="h-full px-5 overflow-auto">
@@ -49,7 +67,16 @@ const WelcomeView = memo(() => {
</VSCodeButton>
)}
<div className="mt-4.5">{showApiOptions && <ApiOptions showModelOptions={false} showSubmitButton={true} />}</div>
<div className="mt-4.5">
{showApiOptions && (
<div>
<ApiOptions showModelOptions={false} currentMode={chatSettings.mode} />
<VSCodeButton onClick={handleSubmit} disabled={disableLetsGoButton} className="mt-0.75">
Let's go!
</VSCodeButton>
</div>
)}
</div>
</div>
</div>
)
@@ -99,6 +99,9 @@ interface ExtensionStateContextType extends ExtensionState {
// Event callbacks
onRelinquishControl: (callback: () => void) => () => void
// Set initial state for testing purposes only
setExtensionStateForTest: (partialState: Partial<ExtensionState>) => void
}
const ExtensionStateContext = createContext<ExtensionStateContextType | undefined>(undefined)
@@ -237,6 +240,16 @@ export const ExtensionStateContextProvider: React.FC<{
const workspaceUpdatesUnsubscribeRef = useRef<(() => void) | null>(null)
const relinquishControlUnsubscribeRef = useRef<(() => void) | null>(null)
// Set extension state for testing purposes
const setExtensionStateForTest = useCallback((partialState: Partial<ExtensionState>) => {
if (process.env.IS_DEV === "true") {
setState((prevState) => ({
...prevState,
...partialState,
}))
}
}, [])
// Add ref for callbacks
const relinquishControlCallbacks = useRef<Set<() => void>>(new Set())
@@ -755,6 +768,7 @@ export const ExtensionStateContextProvider: React.FC<{
refreshOpenRouterModels,
onRelinquishControl,
setUserInfo: (userInfo?: UserInfo) => setState((prevState) => ({ ...prevState, userInfo })),
setExtensionStateForTest,
}
return <ExtensionStateContext.Provider value={contextValue}>{children}</ExtensionStateContext.Provider>
+25 -11
View File
@@ -1,8 +1,20 @@
import { ApiConfiguration, openRouterDefaultModelId, ModelInfo } from "@shared/api"
import { getModeSpecificFields } from "@/components/settings/utils/providerUtils"
export function validateApiConfiguration(apiConfiguration?: ApiConfiguration): string | undefined {
export function validateApiConfiguration(currentMode: "plan" | "act", apiConfiguration?: ApiConfiguration): string | undefined {
if (apiConfiguration) {
switch (apiConfiguration.apiProvider) {
const {
apiProvider,
openAiModelId,
requestyModelId,
fireworksModelId,
togetherModelId,
ollamaModelId,
lmStudioModelId,
vsCodeLmModelSelector,
} = getModeSpecificFields(apiConfiguration, currentMode)
switch (apiProvider) {
case "anthropic":
if (!apiConfiguration.apiKey) {
return "You must provide a valid API key or choose a different provider."
@@ -64,37 +76,37 @@ export function validateApiConfiguration(apiConfiguration?: ApiConfiguration): s
}
break
case "openai":
if (!apiConfiguration.openAiBaseUrl || !apiConfiguration.openAiApiKey || !apiConfiguration.openAiModelId) {
if (!apiConfiguration.openAiBaseUrl || !apiConfiguration.openAiApiKey || !openAiModelId) {
return "You must provide a valid base URL, API key, and model ID."
}
break
case "requesty":
if (!apiConfiguration.requestyApiKey || !apiConfiguration.requestyModelId) {
if (!apiConfiguration.requestyApiKey || !requestyModelId) {
return "You must provide a valid API key or choose a different provider."
}
break
case "fireworks":
if (!apiConfiguration.fireworksApiKey || !apiConfiguration.fireworksModelId) {
if (!apiConfiguration.fireworksApiKey || !fireworksModelId) {
return "You must provide a valid API key or choose a different provider."
}
break
case "together":
if (!apiConfiguration.togetherApiKey || !apiConfiguration.togetherModelId) {
if (!apiConfiguration.togetherApiKey || !togetherModelId) {
return "You must provide a valid API key or choose a different provider."
}
break
case "ollama":
if (!apiConfiguration.ollamaModelId) {
if (!ollamaModelId) {
return "You must provide a valid model ID."
}
break
case "lmstudio":
if (!apiConfiguration.lmStudioModelId) {
if (!lmStudioModelId) {
return "You must provide a valid model ID."
}
break
case "vscode-lm":
if (!apiConfiguration.vsCodeLmModelSelector) {
if (!vsCodeLmModelSelector) {
return "You must provide a valid model selector."
}
break
@@ -138,14 +150,16 @@ export function validateApiConfiguration(apiConfiguration?: ApiConfiguration): s
}
export function validateModelId(
currentMode: "plan" | "act",
apiConfiguration?: ApiConfiguration,
openRouterModels?: Record<string, ModelInfo>,
): string | undefined {
if (apiConfiguration) {
switch (apiConfiguration.apiProvider) {
const { apiProvider, openRouterModelId } = getModeSpecificFields(apiConfiguration, currentMode)
switch (apiProvider) {
case "openrouter":
case "cline":
const modelId = apiConfiguration.openRouterModelId || openRouterDefaultModelId // in case the user hasn't changed the model id, it will be undefined by default
const modelId = openRouterModelId || openRouterDefaultModelId // in case the user hasn't changed the model id, it will be undefined by default
if (!modelId) {
return "You must provide a model ID."
}
+2 -1
View File
@@ -10,7 +10,8 @@ module.exports = {
},
},
},
darkMode: "class",
// Toggle dark-mode based on .dark class or data-mode="dark"
darkMode: ["class", '[data-mode="dark"]', '[class="vs-dark"]'],
plugins: [
heroui({
defaultTheme: "vscode",