Merge pull request #369 from samhvw8/fix/roo-cline-select-api-config

fix api config profile
This commit is contained in:
Matt Rubens
2025-01-15 20:32:48 -05:00
committed by GitHub
9 changed files with 222 additions and 145 deletions

View File

@@ -961,10 +961,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
await this.configManager.SaveConfig(message.text, message.apiConfiguration); await this.configManager.SaveConfig(message.text, message.apiConfiguration);
let listApiConfig = await this.configManager.ListConfig(); let listApiConfig = await this.configManager.ListConfig();
// Update listApiConfigMeta first to ensure UI has latest data
await this.updateGlobalState("listApiConfigMeta", listApiConfig);
await Promise.all([ await Promise.all([
this.updateGlobalState("listApiConfigMeta", listApiConfig),
this.updateApiConfiguration(message.apiConfiguration), this.updateApiConfiguration(message.apiConfiguration),
this.updateGlobalState("currentApiConfigName", message.text), this.updateGlobalState("currentApiConfigName", message.text),
]) ])
@@ -1006,12 +1004,9 @@ export class ClineProvider implements vscode.WebviewViewProvider {
try { try {
const apiConfig = await this.configManager.LoadConfig(message.text); const apiConfig = await this.configManager.LoadConfig(message.text);
const listApiConfig = await this.configManager.ListConfig(); 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([ await Promise.all([
this.updateGlobalState("listApiConfigMeta", listApiConfig),
this.updateGlobalState("currentApiConfigName", message.text), this.updateGlobalState("currentApiConfigName", message.text),
this.updateApiConfiguration(apiConfig), this.updateApiConfiguration(apiConfig),
]) ])

View File

@@ -15,7 +15,7 @@ module.exports.jest = function(config) {
// Configure transform ignore patterns for ES modules // Configure transform ignore patterns for ES modules
config.transformIgnorePatterns = [ 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; return config;

View File

@@ -31,6 +31,7 @@
"shell-quote": "^1.8.2", "shell-quote": "^1.8.2",
"styled-components": "^6.1.13", "styled-components": "^6.1.13",
"typescript": "^4.9.5", "typescript": "^4.9.5",
"vscrui": "^0.2.0",
"web-vitals": "^2.1.4" "web-vitals": "^2.1.4"
}, },
"devDependencies": { "devDependencies": {
@@ -15155,6 +15156,20 @@
"url": "https://opencollective.com/unified" "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": { "node_modules/w3c-hr-time": {
"version": "1.0.2", "version": "1.0.2",
"license": "MIT", "license": "MIT",

View File

@@ -26,6 +26,7 @@
"shell-quote": "^1.8.2", "shell-quote": "^1.8.2",
"styled-components": "^6.1.13", "styled-components": "^6.1.13",
"typescript": "^4.9.5", "typescript": "^4.9.5",
"vscrui": "^0.2.0",
"web-vitals": "^2.1.4" "web-vitals": "^2.1.4"
}, },
"scripts": { "scripts": {

View File

@@ -1,11 +1,10 @@
import { Checkbox, Dropdown } from "vscrui"
import type { DropdownOption } from "vscrui"
import { import {
VSCodeCheckbox,
VSCodeDropdown,
VSCodeLink, VSCodeLink,
VSCodeOption,
VSCodeRadio, VSCodeRadio,
VSCodeRadioGroup, VSCodeRadioGroup,
VSCodeTextField, VSCodeTextField
} from "@vscode/webview-ui-toolkit/react" } from "@vscode/webview-ui-toolkit/react"
import { Fragment, memo, useCallback, useEffect, useMemo, useState } from "react" import { Fragment, memo, useCallback, useEffect, useMemo, useState } from "react"
import { useEvent, useInterval } from "react-use" import { useEvent, useInterval } from "react-use"
@@ -95,35 +94,26 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) =
}, []) }, [])
useEvent("message", handleMessage) 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<string, ModelInfo>) => { const createDropdown = (models: Record<string, ModelInfo>) => {
const options: DropdownOption[] = [
{ value: "", label: "Select a model..." },
...Object.keys(models).map((modelId) => ({
value: modelId,
label: modelId,
}))
]
return ( return (
<VSCodeDropdown <Dropdown
id="model-id" id="model-id"
value={selectedModelId} value={selectedModelId}
onChange={handleInputChange("apiModelId")} onChange={(value: unknown) => {handleInputChange("apiModelId")({
style={{ width: "100%" }}> target: {
<VSCodeOption value="">Select a model...</VSCodeOption> value: (value as DropdownOption).value
{Object.keys(models).map((modelId) => ( }
<VSCodeOption })}}
key={modelId} style={{ width: "100%" }}
value={modelId} options={options}
style={{ />
whiteSpace: "normal",
wordWrap: "break-word",
maxWidth: "100%",
}}>
{modelId}
</VSCodeOption>
))}
</VSCodeDropdown>
) )
} }
@@ -133,24 +123,32 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) =
<label htmlFor="api-provider"> <label htmlFor="api-provider">
<span style={{ fontWeight: 500 }}>API Provider</span> <span style={{ fontWeight: 500 }}>API Provider</span>
</label> </label>
<VSCodeDropdown <Dropdown
id="api-provider" id="api-provider"
value={selectedProvider} value={selectedProvider}
onChange={handleInputChange("apiProvider")} onChange={(value: unknown) => {
style={{ minWidth: 130, position: "relative", zIndex: OPENROUTER_MODEL_PICKER_Z_INDEX + 1 }}> handleInputChange("apiProvider")({
<VSCodeOption value="openrouter">OpenRouter</VSCodeOption> target: {
<VSCodeOption value="anthropic">Anthropic</VSCodeOption> value: (value as DropdownOption).value
<VSCodeOption value="gemini">Google Gemini</VSCodeOption> }
<VSCodeOption value="deepseek">DeepSeek</VSCodeOption> })
<VSCodeOption value="openai-native">OpenAI</VSCodeOption> }}
<VSCodeOption value="openai">OpenAI Compatible</VSCodeOption> style={{ minWidth: 130, position: "relative", zIndex: OPENROUTER_MODEL_PICKER_Z_INDEX + 1 }}
<VSCodeOption value="vertex">GCP Vertex AI</VSCodeOption> options={[
<VSCodeOption value="bedrock">AWS Bedrock</VSCodeOption> { value: "openrouter", label: "OpenRouter" },
<VSCodeOption value="glama">Glama</VSCodeOption> { value: "anthropic", label: "Anthropic" },
<VSCodeOption value="vscode-lm">VS Code LM API</VSCodeOption> { value: "gemini", label: "Google Gemini" },
<VSCodeOption value="lmstudio">LM Studio</VSCodeOption> { value: "deepseek", label: "DeepSeek" },
<VSCodeOption value="ollama">Ollama</VSCodeOption> { value: "openai-native", label: "OpenAI" },
</VSCodeDropdown> { value: "openai", label: "OpenAI Compatible" },
{ value: "vertex", label: "GCP Vertex AI" },
{ value: "bedrock", label: "AWS Bedrock" },
{ value: "glama", label: "Glama" },
{ value: "vscode-lm", label: "VS Code LM API" },
{ value: "lmstudio", label: "LM Studio" },
{ value: "ollama", label: "Ollama" }
]}
/>
</div> </div>
{selectedProvider === "anthropic" && ( {selectedProvider === "anthropic" && (
@@ -164,17 +162,16 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) =
<span style={{ fontWeight: 500 }}>Anthropic API Key</span> <span style={{ fontWeight: 500 }}>Anthropic API Key</span>
</VSCodeTextField> </VSCodeTextField>
<VSCodeCheckbox <Checkbox
checked={anthropicBaseUrlSelected} checked={anthropicBaseUrlSelected}
onChange={(e: any) => { onChange={(checked: boolean) => {
const isChecked = e.target.checked === true setAnthropicBaseUrlSelected(checked)
setAnthropicBaseUrlSelected(isChecked) if (!checked) {
if (!isChecked) {
setApiConfiguration({ ...apiConfiguration, anthropicBaseUrl: "" }) setApiConfiguration({ ...apiConfiguration, anthropicBaseUrl: "" })
} }
}}> }}>
Use custom base URL Use custom base URL
</VSCodeCheckbox> </Checkbox>
{anthropicBaseUrlSelected && ( {anthropicBaseUrlSelected && (
<VSCodeTextField <VSCodeTextField
@@ -293,14 +290,15 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) =
</span> </span>
)} */} )} */}
</p> </p>
<VSCodeCheckbox <Checkbox
checked={apiConfiguration?.openRouterUseMiddleOutTransform || false} checked={apiConfiguration?.openRouterUseMiddleOutTransform || false}
onChange={(e: any) => { onChange={(checked: boolean) => {
const isChecked = e.target.checked === true handleInputChange("openRouterUseMiddleOutTransform")({
setApiConfiguration({ ...apiConfiguration, openRouterUseMiddleOutTransform: isChecked }) target: { value: checked },
})
}}> }}>
Compress prompts and message chains to the context size (<a href="https://openrouter.ai/docs/transforms">OpenRouter Transforms</a>) Compress prompts and message chains to the context size (<a href="https://openrouter.ai/docs/transforms">OpenRouter Transforms</a>)
</VSCodeCheckbox> </Checkbox>
<br /> <br />
</div> </div>
)} )}
@@ -335,45 +333,44 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) =
<label htmlFor="aws-region-dropdown"> <label htmlFor="aws-region-dropdown">
<span style={{ fontWeight: 500 }}>AWS Region</span> <span style={{ fontWeight: 500 }}>AWS Region</span>
</label> </label>
<VSCodeDropdown <Dropdown
id="aws-region-dropdown" id="aws-region-dropdown"
value={apiConfiguration?.awsRegion || ""} value={apiConfiguration?.awsRegion || ""}
style={{ width: "100%" }} style={{ width: "100%" }}
onChange={handleInputChange("awsRegion")}> onChange={(value: unknown) => {handleInputChange("awsRegion")({
<VSCodeOption value="">Select a region...</VSCodeOption> target: {
{/* 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. */} value: (value as DropdownOption).value
<VSCodeOption value="us-east-1">us-east-1</VSCodeOption> }
<VSCodeOption value="us-east-2">us-east-2</VSCodeOption> })}}
{/* <VSCodeOption value="us-west-1">us-west-1</VSCodeOption> */} options={[
<VSCodeOption value="us-west-2">us-west-2</VSCodeOption> { value: "", label: "Select a region..." },
{/* <VSCodeOption value="af-south-1">af-south-1</VSCodeOption> */} { value: "us-east-1", label: "us-east-1" },
{/* <VSCodeOption value="ap-east-1">ap-east-1</VSCodeOption> */} { value: "us-east-2", label: "us-east-2" },
<VSCodeOption value="ap-south-1">ap-south-1</VSCodeOption> { value: "us-west-2", label: "us-west-2" },
<VSCodeOption value="ap-northeast-1">ap-northeast-1</VSCodeOption> { value: "ap-south-1", label: "ap-south-1" },
<VSCodeOption value="ap-northeast-2">ap-northeast-2</VSCodeOption> { value: "ap-northeast-1", label: "ap-northeast-1" },
{/* <VSCodeOption value="ap-northeast-3">ap-northeast-3</VSCodeOption> */} { value: "ap-northeast-2", label: "ap-northeast-2" },
<VSCodeOption value="ap-southeast-1">ap-southeast-1</VSCodeOption> { value: "ap-southeast-1", label: "ap-southeast-1" },
<VSCodeOption value="ap-southeast-2">ap-southeast-2</VSCodeOption> { value: "ap-southeast-2", label: "ap-southeast-2" },
<VSCodeOption value="ca-central-1">ca-central-1</VSCodeOption> { value: "ca-central-1", label: "ca-central-1" },
<VSCodeOption value="eu-central-1">eu-central-1</VSCodeOption> { value: "eu-central-1", label: "eu-central-1" },
<VSCodeOption value="eu-west-1">eu-west-1</VSCodeOption> { value: "eu-west-1", label: "eu-west-1" },
<VSCodeOption value="eu-west-2">eu-west-2</VSCodeOption> { value: "eu-west-2", label: "eu-west-2" },
<VSCodeOption value="eu-west-3">eu-west-3</VSCodeOption> { value: "eu-west-3", label: "eu-west-3" },
{/* <VSCodeOption value="eu-north-1">eu-north-1</VSCodeOption> */} { value: "sa-east-1", label: "sa-east-1" },
{/* <VSCodeOption value="me-south-1">me-south-1</VSCodeOption> */} { value: "us-gov-west-1", label: "us-gov-west-1" }
<VSCodeOption value="sa-east-1">sa-east-1</VSCodeOption> ]}
<VSCodeOption value="us-gov-west-1">us-gov-west-1</VSCodeOption> />
{/* <VSCodeOption value="us-gov-east-1">us-gov-east-1</VSCodeOption> */}
</VSCodeDropdown>
</div> </div>
<VSCodeCheckbox <Checkbox
checked={apiConfiguration?.awsUseCrossRegionInference || false} checked={apiConfiguration?.awsUseCrossRegionInference || false}
onChange={(e: any) => { onChange={(checked: boolean) => {
const isChecked = e.target.checked === true handleInputChange("awsUseCrossRegionInference")({
setApiConfiguration({ ...apiConfiguration, awsUseCrossRegionInference: isChecked }) target: { value: checked },
})
}}> }}>
Use cross-region inference Use cross-region inference
</VSCodeCheckbox> </Checkbox>
<p <p
style={{ style={{
fontSize: "12px", fontSize: "12px",
@@ -400,18 +397,24 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) =
<label htmlFor="vertex-region-dropdown"> <label htmlFor="vertex-region-dropdown">
<span style={{ fontWeight: 500 }}>Google Cloud Region</span> <span style={{ fontWeight: 500 }}>Google Cloud Region</span>
</label> </label>
<VSCodeDropdown <Dropdown
id="vertex-region-dropdown" id="vertex-region-dropdown"
value={apiConfiguration?.vertexRegion || ""} value={apiConfiguration?.vertexRegion || ""}
style={{ width: "100%" }} style={{ width: "100%" }}
onChange={handleInputChange("vertexRegion")}> onChange={(value: unknown) => {handleInputChange("vertexRegion")({
<VSCodeOption value="">Select a region...</VSCodeOption> target: {
<VSCodeOption value="us-east5">us-east5</VSCodeOption> value: (value as DropdownOption).value
<VSCodeOption value="us-central1">us-central1</VSCodeOption> }
<VSCodeOption value="europe-west1">europe-west1</VSCodeOption> })}}
<VSCodeOption value="europe-west4">europe-west4</VSCodeOption> options={[
<VSCodeOption value="asia-southeast1">asia-southeast1</VSCodeOption> { value: "", label: "Select a region..." },
</VSCodeDropdown> { 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" }
]}
/>
</div> </div>
<p <p
style={{ style={{
@@ -484,29 +487,26 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) =
</VSCodeTextField> </VSCodeTextField>
<OpenAiModelPicker /> <OpenAiModelPicker />
<div style={{ display: 'flex', alignItems: 'center' }}> <div style={{ display: 'flex', alignItems: 'center' }}>
<VSCodeCheckbox <Checkbox
checked={apiConfiguration?.openAiStreamingEnabled ?? true} checked={apiConfiguration?.openAiStreamingEnabled ?? true}
onChange={(e: any) => { onChange={(checked: boolean) => {
const isChecked = e.target.checked handleInputChange("openAiStreamingEnabled")({
setApiConfiguration({ target: { value: checked },
...apiConfiguration,
openAiStreamingEnabled: isChecked
}) })
}}> }}>
Enable streaming Enable streaming
</VSCodeCheckbox> </Checkbox>
</div> </div>
<VSCodeCheckbox <Checkbox
checked={azureApiVersionSelected} checked={azureApiVersionSelected}
onChange={(e: any) => { onChange={(checked: boolean) => {
const isChecked = e.target.checked === true setAzureApiVersionSelected(checked)
setAzureApiVersionSelected(isChecked) if (!checked) {
if (!isChecked) {
setApiConfiguration({ ...apiConfiguration, azureApiVersion: "" }) setApiConfiguration({ ...apiConfiguration, azureApiVersion: "" })
} }
}}> }}>
Set Azure API version Set Azure API version
</VSCodeCheckbox> </Checkbox>
{azureApiVersionSelected && ( {azureApiVersionSelected && (
<VSCodeTextField <VSCodeTextField
value={apiConfiguration?.azureApiVersion || ""} value={apiConfiguration?.azureApiVersion || ""}
@@ -633,29 +633,28 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) =
<span style={{ fontWeight: 500 }}>Language Model</span> <span style={{ fontWeight: 500 }}>Language Model</span>
</label> </label>
{vsCodeLmModels.length > 0 ? ( {vsCodeLmModels.length > 0 ? (
<VSCodeDropdown <Dropdown
id="vscode-lm-model" id="vscode-lm-model"
value={apiConfiguration?.vsCodeLmModelSelector ? value={apiConfiguration?.vsCodeLmModelSelector ?
`${apiConfiguration.vsCodeLmModelSelector.vendor ?? ""}/${apiConfiguration.vsCodeLmModelSelector.family ?? ""}` : `${apiConfiguration.vsCodeLmModelSelector.vendor ?? ""}/${apiConfiguration.vsCodeLmModelSelector.family ?? ""}` :
""} ""}
onChange={(e) => { onChange={(value: unknown) => {
const value = (e.target as HTMLInputElement).value; const valueStr = (value as DropdownOption).value;
const [vendor, family] = value.split('/'); const [vendor, family] = valueStr.split('/');
setApiConfiguration({ setApiConfiguration({
...apiConfiguration, ...apiConfiguration,
vsCodeLmModelSelector: value ? { vendor, family } : undefined vsCodeLmModelSelector: valueStr ? { vendor, family } : undefined
}); });
}} }}
style={{ width: "100%" }}> style={{ width: "100%" }}
<VSCodeOption value="">Select a model...</VSCodeOption> options={[
{vsCodeLmModels.map((model) => ( { value: "", label: "Select a model..." },
<VSCodeOption ...vsCodeLmModels.map((model) => ({
key={`${model.vendor}/${model.family}`} value: `${model.vendor}/${model.family}`,
value={`${model.vendor}/${model.family}`}> label: `${model.vendor} - ${model.family}`
{model.vendor} - {model.family} }))
</VSCodeOption> ]}
))} />
</VSCodeDropdown>
) : ( ) : (
<p style={{ <p style={{
fontSize: "12px", fontSize: "12px",

View File

@@ -1,4 +1,5 @@
import { VSCodeLink, VSCodeTextField } from "@vscode/webview-ui-toolkit/react" import { VSCodeLink, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import debounce from "debounce"
import { Fzf } from "fzf" import { Fzf } from "fzf"
import React, { KeyboardEvent, memo, useEffect, useMemo, useRef, useState } from "react" import React, { KeyboardEvent, memo, useEffect, useMemo, useRef, useState } from "react"
import { useRemark } from "react-remark" import { useRemark } from "react-remark"
@@ -44,8 +45,24 @@ const GlamaModelPicker: React.FC = () => {
} }
}, [apiConfiguration, searchTerm]) }, [apiConfiguration, searchTerm])
useMount(() => { const debouncedRefreshModels = useMemo(
() =>
debounce(
() => {
vscode.postMessage({ type: "refreshGlamaModels" }) vscode.postMessage({ type: "refreshGlamaModels" })
},
50
),
[]
)
useMount(() => {
debouncedRefreshModels()
// Cleanup debounced function
return () => {
debouncedRefreshModels.clear()
}
}) })
useEffect(() => { useEffect(() => {

View File

@@ -1,6 +1,7 @@
import { VSCodeLink, VSCodeTextField } from "@vscode/webview-ui-toolkit/react" import { VSCodeLink, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import { Fzf } from "fzf" import { Fzf } from "fzf"
import React, { KeyboardEvent, memo, useEffect, useMemo, useRef, useState } from "react" import React, { KeyboardEvent, memo, useEffect, useMemo, useRef, useState } from "react"
import debounce from "debounce"
import { useRemark } from "react-remark" import { useRemark } from "react-remark"
import styled from "styled-components" import styled from "styled-components"
import { useExtensionState } from "../../context/ExtensionStateContext" import { useExtensionState } from "../../context/ExtensionStateContext"
@@ -34,18 +35,38 @@ const OpenAiModelPicker: React.FC = () => {
} }
}, [apiConfiguration, searchTerm]) }, [apiConfiguration, searchTerm])
const debouncedRefreshModels = useMemo(
() =>
debounce(
(baseUrl: string, apiKey: string) => {
vscode.postMessage({
type: "refreshOpenAiModels",
values: {
baseUrl,
apiKey
}
})
},
50
),
[]
)
useEffect(() => { useEffect(() => {
if (!apiConfiguration?.openAiBaseUrl || !apiConfiguration?.openAiApiKey) { if (!apiConfiguration?.openAiBaseUrl || !apiConfiguration?.openAiApiKey) {
return return
} }
vscode.postMessage({ debouncedRefreshModels(
type: "refreshOpenAiModels", values: { apiConfiguration.openAiBaseUrl,
baseUrl: apiConfiguration?.openAiBaseUrl, apiConfiguration.openAiApiKey
apiKey: apiConfiguration?.openAiApiKey )
// Cleanup debounced function
return () => {
debouncedRefreshModels.clear()
} }
}) }, [apiConfiguration?.openAiBaseUrl, apiConfiguration?.openAiApiKey, debouncedRefreshModels])
}, [apiConfiguration?.openAiBaseUrl, apiConfiguration?.openAiApiKey])
useEffect(() => { useEffect(() => {
const handleClickOutside = (event: MouseEvent) => { const handleClickOutside = (event: MouseEvent) => {

View File

@@ -1,4 +1,5 @@
import { VSCodeLink, VSCodeTextField } from "@vscode/webview-ui-toolkit/react" import { VSCodeLink, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import debounce from "debounce"
import { Fzf } from "fzf" import { Fzf } from "fzf"
import React, { KeyboardEvent, memo, useEffect, useMemo, useRef, useState } from "react" import React, { KeyboardEvent, memo, useEffect, useMemo, useRef, useState } from "react"
import { useRemark } from "react-remark" import { useRemark } from "react-remark"
@@ -43,8 +44,24 @@ const OpenRouterModelPicker: React.FC = () => {
} }
}, [apiConfiguration, searchTerm]) }, [apiConfiguration, searchTerm])
useMount(() => { const debouncedRefreshModels = useMemo(
() =>
debounce(
() => {
vscode.postMessage({ type: "refreshOpenRouterModels" }) vscode.postMessage({ type: "refreshOpenRouterModels" })
},
50
),
[]
)
useMount(() => {
debouncedRefreshModels()
// Cleanup debounced function
return () => {
debouncedRefreshModels.clear()
}
}) })
useEffect(() => { useEffect(() => {

View File

@@ -1,14 +1,26 @@
import '@testing-library/jest-dom'; 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', { Object.defineProperty(window, 'matchMedia', {
writable: true, writable: true,
value: jest.fn().mockImplementation(query => ({ value: jest.fn().mockImplementation(query => ({
matches: false, matches: false,
media: query, media: query,
onchange: null, onchange: null,
addListener: jest.fn(), // Deprecated addListener: jest.fn(), // deprecated
removeListener: jest.fn(), // Deprecated removeListener: jest.fn(), // deprecated
addEventListener: jest.fn(), addEventListener: jest.fn(),
removeEventListener: jest.fn(), removeEventListener: jest.fn(),
dispatchEvent: jest.fn(), dispatchEvent: jest.fn(),