diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts
index 4d8706b..a2d3814 100644
--- a/src/core/webview/ClineProvider.ts
+++ b/src/core/webview/ClineProvider.ts
@@ -956,10 +956,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
await this.configManager.SaveConfig(message.text, message.apiConfiguration);
let listApiConfig = await this.configManager.ListConfig();
- // Update listApiConfigMeta first to ensure UI has latest data
- await this.updateGlobalState("listApiConfigMeta", listApiConfig);
-
await Promise.all([
+ this.updateGlobalState("listApiConfigMeta", listApiConfig),
this.updateApiConfiguration(message.apiConfiguration),
this.updateGlobalState("currentApiConfigName", message.text),
])
@@ -999,14 +997,12 @@ export class ClineProvider implements vscode.WebviewViewProvider {
case "loadApiConfiguration":
if (message.text) {
try {
+ console.log("loadApiConfiguration", message.text)
const apiConfig = await this.configManager.LoadConfig(message.text);
const listApiConfig = await this.configManager.ListConfig();
- const config = listApiConfig?.find(c => c.name === message.text);
- // Update listApiConfigMeta first to ensure UI has latest data
- await this.updateGlobalState("listApiConfigMeta", listApiConfig);
-
await Promise.all([
+ this.updateGlobalState("listApiConfigMeta", listApiConfig),
this.updateGlobalState("currentApiConfigName", message.text),
this.updateApiConfiguration(apiConfig),
])
diff --git a/webview-ui/package-lock.json b/webview-ui/package-lock.json
index 781a331..acddd07 100644
--- a/webview-ui/package-lock.json
+++ b/webview-ui/package-lock.json
@@ -31,6 +31,7 @@
"shell-quote": "^1.8.2",
"styled-components": "^6.1.13",
"typescript": "^4.9.5",
+ "vscrui": "^0.2.0",
"web-vitals": "^2.1.4"
},
"devDependencies": {
@@ -15155,6 +15156,20 @@
"url": "https://opencollective.com/unified"
}
},
+ "node_modules/vscrui": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/vscrui/-/vscrui-0.2.0.tgz",
+ "integrity": "sha512-fvxZM/uIYOMN3fUbE2In+R1VrNj8PKcfAdh+Us2bJaPGuG9ySkR6xkV2aJVqXxWDX77U3v/UQGc5e7URrB52Gw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/estruyf"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^17 || ^18"
+ }
+ },
"node_modules/w3c-hr-time": {
"version": "1.0.2",
"license": "MIT",
diff --git a/webview-ui/package.json b/webview-ui/package.json
index 6b4d192..a7c616d 100644
--- a/webview-ui/package.json
+++ b/webview-ui/package.json
@@ -26,6 +26,7 @@
"shell-quote": "^1.8.2",
"styled-components": "^6.1.13",
"typescript": "^4.9.5",
+ "vscrui": "^0.2.0",
"web-vitals": "^2.1.4"
},
"scripts": {
diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx
index cc30ae9..53b5e6e 100644
--- a/webview-ui/src/components/settings/ApiOptions.tsx
+++ b/webview-ui/src/components/settings/ApiOptions.tsx
@@ -1,8 +1,7 @@
+import { Checkbox, Dropdown } from "vscrui"
+import type { DropdownOption } from "vscrui"
import {
- VSCodeCheckbox,
- VSCodeDropdown,
VSCodeLink,
- VSCodeOption,
VSCodeRadio,
VSCodeRadioGroup,
VSCodeTextField,
@@ -90,35 +89,26 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) =
}, [])
useEvent("message", handleMessage)
- /*
- VSCodeDropdown has an open bug where dynamically rendered options don't auto select the provided value prop. You can see this for yourself by comparing it with normal select/option elements, which work as expected.
- https://github.com/microsoft/vscode-webview-ui-toolkit/issues/433
-
- In our case, when the user switches between providers, we recalculate the selectedModelId depending on the provider, the default model for that provider, and a modelId that the user may have selected. Unfortunately, the VSCodeDropdown component wouldn't select this calculated value, and would default to the first "Select a model..." option instead, which makes it seem like the model was cleared out when it wasn't.
-
- As a workaround, we create separate instances of the dropdown for each provider, and then conditionally render the one that matches the current provider.
- */
const createDropdown = (models: Record) => {
+ const options: DropdownOption[] = [
+ { value: "", label: "Select a model..." },
+ ...Object.keys(models).map((modelId) => ({
+ value: modelId,
+ label: modelId,
+ }))
+ ]
return (
-
- Select a model...
- {Object.keys(models).map((modelId) => (
-
- {modelId}
-
- ))}
-
+ onChange={(value: unknown) => {handleInputChange("apiModelId")({
+ target: {
+ value: (value as DropdownOption).value
+ }
+ })}}
+ style={{ width: "100%" }}
+ options={options}
+ />
)
}
@@ -128,23 +118,31 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) =
-
- OpenRouter
- Anthropic
- Google Gemini
- DeepSeek
- OpenAI
- OpenAI Compatible
- GCP Vertex AI
- AWS Bedrock
- Glama
- LM Studio
- Ollama
-
+ onChange={(value: unknown) => {
+ handleInputChange("apiProvider")({
+ target: {
+ value: (value as DropdownOption).value
+ }
+ })
+ }}
+ style={{ minWidth: 130, position: "relative", zIndex: OPENROUTER_MODEL_PICKER_Z_INDEX + 1 }}
+ options={[
+ { value: "openrouter", label: "OpenRouter" },
+ { value: "anthropic", label: "Anthropic" },
+ { value: "gemini", label: "Google Gemini" },
+ { value: "deepseek", label: "DeepSeek" },
+ { value: "openai-native", label: "OpenAI" },
+ { value: "openai", label: "OpenAI Compatible" },
+ { value: "vertex", label: "GCP Vertex AI" },
+ { value: "bedrock", label: "AWS Bedrock" },
+ { value: "glama", label: "Glama" },
+ { value: "lmstudio", label: "LM Studio" },
+ { value: "ollama", label: "Ollama" }
+ ]}
+ />
{selectedProvider === "anthropic" && (
@@ -158,17 +156,16 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) =
Anthropic API Key
- {
- const isChecked = e.target.checked === true
- setAnthropicBaseUrlSelected(isChecked)
- if (!isChecked) {
+ onChange={(checked: boolean) => {
+ setAnthropicBaseUrlSelected(checked)
+ if (!checked) {
setApiConfiguration({ ...apiConfiguration, anthropicBaseUrl: "" })
}
}}>
Use custom base URL
-
+
{anthropicBaseUrlSelected && (
)} */}
- {
- const isChecked = e.target.checked === true
- setApiConfiguration({ ...apiConfiguration, openRouterUseMiddleOutTransform: isChecked })
+ onChange={(checked: boolean) => {
+ handleInputChange("openRouterUseMiddleOutTransform")({
+ target: { value: checked },
+ })
}}>
Compress prompts and message chains to the context size (OpenRouter Transforms)
-
-
+
+
)}
@@ -328,45 +326,44 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) =
-
- Select a region...
- {/* The user will have to choose a region that supports the model they use, but this shouldn't be a problem since they'd have to request access for it in that region in the first place. */}
- us-east-1
- us-east-2
- {/* us-west-1 */}
- us-west-2
- {/* af-south-1 */}
- {/* ap-east-1 */}
- ap-south-1
- ap-northeast-1
- ap-northeast-2
- {/* ap-northeast-3 */}
- ap-southeast-1
- ap-southeast-2
- ca-central-1
- eu-central-1
- eu-west-1
- eu-west-2
- eu-west-3
- {/* eu-north-1 */}
- {/* me-south-1 */}
- sa-east-1
- us-gov-west-1
- {/* us-gov-east-1 */}
-
+ onChange={(value: unknown) => {handleInputChange("awsRegion")({
+ target: {
+ value: (value as DropdownOption).value
+ }
+ })}}
+ options={[
+ { value: "", label: "Select a region..." },
+ { value: "us-east-1", label: "us-east-1" },
+ { value: "us-east-2", label: "us-east-2" },
+ { value: "us-west-2", label: "us-west-2" },
+ { value: "ap-south-1", label: "ap-south-1" },
+ { value: "ap-northeast-1", label: "ap-northeast-1" },
+ { value: "ap-northeast-2", label: "ap-northeast-2" },
+ { value: "ap-southeast-1", label: "ap-southeast-1" },
+ { value: "ap-southeast-2", label: "ap-southeast-2" },
+ { value: "ca-central-1", label: "ca-central-1" },
+ { value: "eu-central-1", label: "eu-central-1" },
+ { value: "eu-west-1", label: "eu-west-1" },
+ { value: "eu-west-2", label: "eu-west-2" },
+ { value: "eu-west-3", label: "eu-west-3" },
+ { value: "sa-east-1", label: "sa-east-1" },
+ { value: "us-gov-west-1", label: "us-gov-west-1" }
+ ]}
+ />
- {
- const isChecked = e.target.checked === true
- setApiConfiguration({ ...apiConfiguration, awsUseCrossRegionInference: isChecked })
+ onChange={(checked: boolean) => {
+ handleInputChange("awsUseCrossRegionInference")({
+ target: { value: checked },
+ })
}}>
Use cross-region inference
-
+
Google Cloud Region
-
- Select a region...
- us-east5
- us-central1
- europe-west1
- europe-west4
- asia-southeast1
-
+ onChange={(value: unknown) => {handleInputChange("vertexRegion")({
+ target: {
+ value: (value as DropdownOption).value
+ }
+ })}}
+ options={[
+ { value: "", label: "Select a region..." },
+ { value: "us-east5", label: "us-east5" },
+ { value: "us-central1", label: "us-central1" },
+ { value: "europe-west1", label: "europe-west1" },
+ { value: "europe-west4", label: "europe-west4" },
+ { value: "asia-southeast1", label: "asia-southeast1" }
+ ]}
+ />
- {
- const isChecked = e.target.checked
- setApiConfiguration({
- ...apiConfiguration,
- openAiStreamingEnabled: isChecked
+ onChange={(checked: boolean) => {
+ console.log("isChecked", checked)
+ handleInputChange("openAiStreamingEnabled")({
+ target: { value: checked },
})
}}>
Enable streaming
-
+
- {
- const isChecked = e.target.checked === true
- setAzureApiVersionSelected(isChecked)
- if (!isChecked) {
+ onChange={(checked: boolean) => {
+ setAzureApiVersionSelected(checked)
+ if (!checked) {
setApiConfiguration({ ...apiConfiguration, azureApiVersion: "" })
}
}}>
Set Azure API version
-
+
{azureApiVersionSelected && (
{
}
}, [apiConfiguration, searchTerm])
+ const debouncedRefreshModels = useMemo(
+ () =>
+ debounce(
+ () => {
+ vscode.postMessage({ type: "refreshGlamaModels" })
+ },
+ 50
+ ),
+ []
+ )
+
useMount(() => {
- vscode.postMessage({ type: "refreshGlamaModels" })
+ debouncedRefreshModels()
+
+ // Cleanup debounced function
+ return () => {
+ debouncedRefreshModels.clear()
+ }
})
useEffect(() => {
diff --git a/webview-ui/src/components/settings/OpenAiModelPicker.tsx b/webview-ui/src/components/settings/OpenAiModelPicker.tsx
index 7e8a81f..33166ba 100644
--- a/webview-ui/src/components/settings/OpenAiModelPicker.tsx
+++ b/webview-ui/src/components/settings/OpenAiModelPicker.tsx
@@ -1,6 +1,7 @@
import { VSCodeLink, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import { Fzf } from "fzf"
import React, { KeyboardEvent, memo, useEffect, useMemo, useRef, useState } from "react"
+import debounce from "debounce"
import { useRemark } from "react-remark"
import styled from "styled-components"
import { useExtensionState } from "../../context/ExtensionStateContext"
@@ -34,18 +35,38 @@ const OpenAiModelPicker: React.FC = () => {
}
}, [apiConfiguration, searchTerm])
+ const debouncedRefreshModels = useMemo(
+ () =>
+ debounce(
+ (baseUrl: string, apiKey: string) => {
+ vscode.postMessage({
+ type: "refreshOpenAiModels",
+ values: {
+ baseUrl,
+ apiKey
+ }
+ })
+ },
+ 50
+ ),
+ []
+ )
+
useEffect(() => {
if (!apiConfiguration?.openAiBaseUrl || !apiConfiguration?.openAiApiKey) {
return
}
- vscode.postMessage({
- type: "refreshOpenAiModels", values: {
- baseUrl: apiConfiguration?.openAiBaseUrl,
- apiKey: apiConfiguration?.openAiApiKey
- }
- })
- }, [apiConfiguration?.openAiBaseUrl, apiConfiguration?.openAiApiKey])
+ debouncedRefreshModels(
+ apiConfiguration.openAiBaseUrl,
+ apiConfiguration.openAiApiKey
+ )
+
+ // Cleanup debounced function
+ return () => {
+ debouncedRefreshModels.clear()
+ }
+ }, [apiConfiguration?.openAiBaseUrl, apiConfiguration?.openAiApiKey, debouncedRefreshModels])
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
diff --git a/webview-ui/src/components/settings/OpenRouterModelPicker.tsx b/webview-ui/src/components/settings/OpenRouterModelPicker.tsx
index f164fb3..568d99d 100644
--- a/webview-ui/src/components/settings/OpenRouterModelPicker.tsx
+++ b/webview-ui/src/components/settings/OpenRouterModelPicker.tsx
@@ -1,4 +1,5 @@
import { VSCodeLink, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
+import debounce from "debounce"
import { Fzf } from "fzf"
import React, { KeyboardEvent, memo, useEffect, useMemo, useRef, useState } from "react"
import { useRemark } from "react-remark"
@@ -43,8 +44,24 @@ const OpenRouterModelPicker: React.FC = () => {
}
}, [apiConfiguration, searchTerm])
+ const debouncedRefreshModels = useMemo(
+ () =>
+ debounce(
+ () => {
+ vscode.postMessage({ type: "refreshOpenRouterModels" })
+ },
+ 50
+ ),
+ []
+ )
+
useMount(() => {
- vscode.postMessage({ type: "refreshOpenRouterModels" })
+ debouncedRefreshModels()
+
+ // Cleanup debounced function
+ return () => {
+ debouncedRefreshModels.clear()
+ }
})
useEffect(() => {