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(() => {