sam hoang 11 месяцев назад
Родитель
Сommit
40fd397407

+ 3 - 7
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),
 								])

+ 15 - 0
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",

+ 1 - 0
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": {

+ 110 - 109
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<string, ModelInfo>) => {
+		const options: DropdownOption[] = [
+			{ value: "", label: "Select a model..." },
+			...Object.keys(models).map((modelId) => ({
+				value: modelId,
+				label: modelId,
+			}))
+		]
 		return (
-			<VSCodeDropdown
+			<Dropdown
 				id="model-id"
 				value={selectedModelId}
-				onChange={handleInputChange("apiModelId")}
-				style={{ width: "100%" }}>
-				<VSCodeOption value="">Select a model...</VSCodeOption>
-				{Object.keys(models).map((modelId) => (
-					<VSCodeOption
-						key={modelId}
-						value={modelId}
-						style={{
-							whiteSpace: "normal",
-							wordWrap: "break-word",
-							maxWidth: "100%",
-						}}>
-						{modelId}
-					</VSCodeOption>
-				))}
-			</VSCodeDropdown>
+				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) =
 				<label htmlFor="api-provider">
 					<span style={{ fontWeight: 500 }}>API Provider</span>
 				</label>
-				<VSCodeDropdown
+				<Dropdown
 					id="api-provider"
 					value={selectedProvider}
-					onChange={handleInputChange("apiProvider")}
-					style={{ minWidth: 130, position: "relative", zIndex: OPENROUTER_MODEL_PICKER_Z_INDEX + 1 }}>
-					<VSCodeOption value="openrouter">OpenRouter</VSCodeOption>
-					<VSCodeOption value="anthropic">Anthropic</VSCodeOption>
-					<VSCodeOption value="gemini">Google Gemini</VSCodeOption>
-					<VSCodeOption value="deepseek">DeepSeek</VSCodeOption>
-					<VSCodeOption value="openai-native">OpenAI</VSCodeOption>
-					<VSCodeOption value="openai">OpenAI Compatible</VSCodeOption>
-					<VSCodeOption value="vertex">GCP Vertex AI</VSCodeOption>
-					<VSCodeOption value="bedrock">AWS Bedrock</VSCodeOption>
-					<VSCodeOption value="glama">Glama</VSCodeOption>
-					<VSCodeOption value="lmstudio">LM Studio</VSCodeOption>
-					<VSCodeOption value="ollama">Ollama</VSCodeOption>
-				</VSCodeDropdown>
+					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" }
+					]}
+				/>
 			</div>
 
 			{selectedProvider === "anthropic" && (
@@ -158,17 +156,16 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) =
 						<span style={{ fontWeight: 500 }}>Anthropic API Key</span>
 					</VSCodeTextField>
 
-					<VSCodeCheckbox
+					<Checkbox
 						checked={anthropicBaseUrlSelected}
-						onChange={(e: any) => {
-							const isChecked = e.target.checked === true
-							setAnthropicBaseUrlSelected(isChecked)
-							if (!isChecked) {
+						onChange={(checked: boolean) => {
+							setAnthropicBaseUrlSelected(checked)
+							if (!checked) {
 								setApiConfiguration({ ...apiConfiguration, anthropicBaseUrl: "" })
 							}
 						}}>
 						Use custom base URL
-					</VSCodeCheckbox>
+					</Checkbox>
 
 					{anthropicBaseUrlSelected && (
 						<VSCodeTextField
@@ -286,15 +283,16 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) =
 							</span>
 						)} */}
 					</p>
-					<VSCodeCheckbox
+					<Checkbox
 						checked={apiConfiguration?.openRouterUseMiddleOutTransform || false}
-						onChange={(e: any) => {
-							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 (<a href="https://openrouter.ai/docs/transforms">OpenRouter Transforms</a>)
-					</VSCodeCheckbox>
-					<br/>
+					</Checkbox>
+					<br />
 				</div>
 			)}
 
@@ -328,45 +326,44 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) =
 						<label htmlFor="aws-region-dropdown">
 							<span style={{ fontWeight: 500 }}>AWS Region</span>
 						</label>
-						<VSCodeDropdown
+						<Dropdown
 							id="aws-region-dropdown"
 							value={apiConfiguration?.awsRegion || ""}
 							style={{ width: "100%" }}
-							onChange={handleInputChange("awsRegion")}>
-							<VSCodeOption value="">Select a region...</VSCodeOption>
-							{/* 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. */}
-							<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> */}
-							<VSCodeOption value="us-west-2">us-west-2</VSCodeOption>
-							{/* <VSCodeOption value="af-south-1">af-south-1</VSCodeOption> */}
-							{/* <VSCodeOption value="ap-east-1">ap-east-1</VSCodeOption> */}
-							<VSCodeOption value="ap-south-1">ap-south-1</VSCodeOption>
-							<VSCodeOption value="ap-northeast-1">ap-northeast-1</VSCodeOption>
-							<VSCodeOption value="ap-northeast-2">ap-northeast-2</VSCodeOption>
-							{/* <VSCodeOption value="ap-northeast-3">ap-northeast-3</VSCodeOption> */}
-							<VSCodeOption value="ap-southeast-1">ap-southeast-1</VSCodeOption>
-							<VSCodeOption value="ap-southeast-2">ap-southeast-2</VSCodeOption>
-							<VSCodeOption value="ca-central-1">ca-central-1</VSCodeOption>
-							<VSCodeOption value="eu-central-1">eu-central-1</VSCodeOption>
-							<VSCodeOption value="eu-west-1">eu-west-1</VSCodeOption>
-							<VSCodeOption value="eu-west-2">eu-west-2</VSCodeOption>
-							<VSCodeOption value="eu-west-3">eu-west-3</VSCodeOption>
-							{/* <VSCodeOption value="eu-north-1">eu-north-1</VSCodeOption> */}
-							{/* <VSCodeOption value="me-south-1">me-south-1</VSCodeOption> */}
-							<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>
+							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" }
+							]}
+						/>
 					</div>
-					<VSCodeCheckbox
+					<Checkbox
 						checked={apiConfiguration?.awsUseCrossRegionInference || false}
-						onChange={(e: any) => {
-							const isChecked = e.target.checked === true
-							setApiConfiguration({ ...apiConfiguration, awsUseCrossRegionInference: isChecked })
+						onChange={(checked: boolean) => {
+							handleInputChange("awsUseCrossRegionInference")({
+								target: { value: checked },
+							})
 						}}>
 						Use cross-region inference
-					</VSCodeCheckbox>
+					</Checkbox>
 					<p
 						style={{
 							fontSize: "12px",
@@ -393,18 +390,24 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) =
 						<label htmlFor="vertex-region-dropdown">
 							<span style={{ fontWeight: 500 }}>Google Cloud Region</span>
 						</label>
-						<VSCodeDropdown
+						<Dropdown
 							id="vertex-region-dropdown"
 							value={apiConfiguration?.vertexRegion || ""}
 							style={{ width: "100%" }}
-							onChange={handleInputChange("vertexRegion")}>
-							<VSCodeOption value="">Select a region...</VSCodeOption>
-							<VSCodeOption value="us-east5">us-east5</VSCodeOption>
-							<VSCodeOption value="us-central1">us-central1</VSCodeOption>
-							<VSCodeOption value="europe-west1">europe-west1</VSCodeOption>
-							<VSCodeOption value="europe-west4">europe-west4</VSCodeOption>
-							<VSCodeOption value="asia-southeast1">asia-southeast1</VSCodeOption>
-						</VSCodeDropdown>
+							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" }
+							]}
+						/>
 					</div>
 					<p
 						style={{
@@ -477,29 +480,27 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) =
 					</VSCodeTextField>
 					<OpenAiModelPicker />
 					<div style={{ display: 'flex', alignItems: 'center' }}>
-						<VSCodeCheckbox
+						<Checkbox
 							checked={apiConfiguration?.openAiStreamingEnabled ?? true}
-							onChange={(e: any) => {
-								const isChecked = e.target.checked
-								setApiConfiguration({
-									...apiConfiguration,
-									openAiStreamingEnabled: isChecked
+							onChange={(checked: boolean) => {
+								console.log("isChecked", checked)
+								handleInputChange("openAiStreamingEnabled")({
+									target: { value: checked },
 								})
 							}}>
 							Enable streaming
-						</VSCodeCheckbox>
+						</Checkbox>
 					</div>
-					<VSCodeCheckbox
+					<Checkbox
 						checked={azureApiVersionSelected}
-						onChange={(e: any) => {
-							const isChecked = e.target.checked === true
-							setAzureApiVersionSelected(isChecked)
-							if (!isChecked) {
+						onChange={(checked: boolean) => {
+							setAzureApiVersionSelected(checked)
+							if (!checked) {
 								setApiConfiguration({ ...apiConfiguration, azureApiVersion: "" })
 							}
 						}}>
 						Set Azure API version
-					</VSCodeCheckbox>
+					</Checkbox>
 					{azureApiVersionSelected && (
 						<VSCodeTextField
 							value={apiConfiguration?.azureApiVersion || ""}

+ 18 - 1
webview-ui/src/components/settings/GlamaModelPicker.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"
@@ -44,8 +45,24 @@ const GlamaModelPicker: React.FC = () => {
 		}
 	}, [apiConfiguration, searchTerm])
 
+	const debouncedRefreshModels = useMemo(
+		() =>
+			debounce(
+				() => {
+					vscode.postMessage({ type: "refreshGlamaModels" })
+				},
+				50
+			),
+		[]
+	)
+
 	useMount(() => {
-		vscode.postMessage({ type: "refreshGlamaModels" })
+		debouncedRefreshModels()
+		
+		// Cleanup debounced function
+		return () => {
+			debouncedRefreshModels.clear()
+		}
 	})
 
 	useEffect(() => {

+ 28 - 7
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) => {

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