From 40fd397407ae12077a9de13e60c30d10a9c59c03 Mon Sep 17 00:00:00 2001 From: sam hoang Date: Wed, 15 Jan 2025 21:04:02 +0700 Subject: [PATCH 1/3] fix api config profile --- src/core/webview/ClineProvider.ts | 10 +- webview-ui/package-lock.json | 15 ++ webview-ui/package.json | 1 + .../src/components/settings/ApiOptions.tsx | 219 +++++++++--------- .../components/settings/GlamaModelPicker.tsx | 19 +- .../components/settings/OpenAiModelPicker.tsx | 35 ++- .../settings/OpenRouterModelPicker.tsx | 19 +- 7 files changed, 193 insertions(+), 125 deletions(-) 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(() => { From 6476c43695d020ace82c9024e5461863e3d57257 Mon Sep 17 00:00:00 2001 From: sam hoang Date: Wed, 15 Jan 2025 21:10:14 +0700 Subject: [PATCH 2/3] test: enhance jest test setup and add documentation structure Add vscrui to jest transform ignore patterns Add crypto.getRandomValues mock for tests Initialize cline documentation structure with required files --- webview-ui/config-overrides.js | 2 +- webview-ui/src/setupTests.ts | 18 +++++++++++++++--- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/webview-ui/config-overrides.js b/webview-ui/config-overrides.js index 2078f36..857b363 100644 --- a/webview-ui/config-overrides.js +++ b/webview-ui/config-overrides.js @@ -15,7 +15,7 @@ module.exports.jest = function(config) { // Configure transform ignore patterns for ES modules config.transformIgnorePatterns = [ - '/node_modules/(?!(rehype-highlight|react-remark|unist-util-visit|unist-util-find-after|vfile|unified|bail|is-plain-obj|trough|vfile-message|unist-util-stringify-position|mdast-util-from-markdown|mdast-util-to-string|micromark|decode-named-character-reference|character-entities|markdown-table|zwitch|longest-streak|escape-string-regexp|unist-util-is|hast-util-to-text|@vscode/webview-ui-toolkit|@microsoft/fast-react-wrapper|@microsoft/fast-element|@microsoft/fast-foundation|@microsoft/fast-web-utilities|exenv-es6)/)' + '/node_modules/(?!(rehype-highlight|react-remark|unist-util-visit|unist-util-find-after|vfile|unified|bail|is-plain-obj|trough|vfile-message|unist-util-stringify-position|mdast-util-from-markdown|mdast-util-to-string|micromark|decode-named-character-reference|character-entities|markdown-table|zwitch|longest-streak|escape-string-regexp|unist-util-is|hast-util-to-text|@vscode/webview-ui-toolkit|@microsoft/fast-react-wrapper|@microsoft/fast-element|@microsoft/fast-foundation|@microsoft/fast-web-utilities|exenv-es6|vscrui)/)' ]; return config; diff --git a/webview-ui/src/setupTests.ts b/webview-ui/src/setupTests.ts index dbba64a..b59e605 100644 --- a/webview-ui/src/setupTests.ts +++ b/webview-ui/src/setupTests.ts @@ -1,14 +1,26 @@ import '@testing-library/jest-dom'; -// Mock window.matchMedia +// Mock crypto.getRandomValues +Object.defineProperty(window, 'crypto', { + value: { + getRandomValues: function(buffer: Uint8Array) { + for (let i = 0; i < buffer.length; i++) { + buffer[i] = Math.floor(Math.random() * 256); + } + return buffer; + } + } +}); + +// Mock matchMedia Object.defineProperty(window, 'matchMedia', { writable: true, value: jest.fn().mockImplementation(query => ({ matches: false, media: query, onchange: null, - addListener: jest.fn(), // Deprecated - removeListener: jest.fn(), // Deprecated + addListener: jest.fn(), // deprecated + removeListener: jest.fn(), // deprecated addEventListener: jest.fn(), removeEventListener: jest.fn(), dispatchEvent: jest.fn(), From 26119644b3e087536250227446cbe0e9eb311d24 Mon Sep 17 00:00:00 2001 From: sam hoang Date: Wed, 15 Jan 2025 21:13:40 +0700 Subject: [PATCH 3/3] remove console log --- src/core/webview/ClineProvider.ts | 1 - webview-ui/src/components/settings/ApiOptions.tsx | 1 - 2 files changed, 2 deletions(-) diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index a2d3814..3c8ce13 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -997,7 +997,6 @@ 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(); diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index 53b5e6e..51ba9bf 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -483,7 +483,6 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) = { - console.log("isChecked", checked) handleInputChange("openAiStreamingEnabled")({ target: { value: checked }, })