diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts
index 17810c1..a648f8e 100644
--- a/src/core/webview/ClineProvider.ts
+++ b/src/core/webview/ClineProvider.ts
@@ -961,10 +961,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),
])
@@ -1006,12 +1004,9 @@ export class ClineProvider implements vscode.WebviewViewProvider {
try {
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/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/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 68d6c4a..dfee51a 100644
--- a/webview-ui/src/components/settings/ApiOptions.tsx
+++ b/webview-ui/src/components/settings/ApiOptions.tsx
@@ -1,11 +1,10 @@
+import { Checkbox, Dropdown } from "vscrui"
+import type { DropdownOption } from "vscrui"
import {
- VSCodeCheckbox,
- VSCodeDropdown,
VSCodeLink,
- VSCodeOption,
VSCodeRadio,
VSCodeRadioGroup,
- VSCodeTextField,
+ VSCodeTextField
} from "@vscode/webview-ui-toolkit/react"
import { Fragment, memo, useCallback, useEffect, useMemo, useState } from "react"
import { useEvent, useInterval } from "react-use"
@@ -95,35 +94,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
Google Cloud Region
-
{ } }, [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(() => { 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(),