Răsfoiți Sursa

Fix settings dropdown issues

cte 9 luni în urmă
părinte
comite
ebacc14b48

+ 224 - 0
webview-ui/package-lock.json

@@ -15,6 +15,7 @@
 				"@radix-ui/react-icons": "^1.3.2",
 				"@radix-ui/react-popover": "^1.1.6",
 				"@radix-ui/react-progress": "^1.1.2",
+				"@radix-ui/react-select": "^2.1.6",
 				"@radix-ui/react-separator": "^1.1.2",
 				"@radix-ui/react-slider": "^1.2.3",
 				"@radix-ui/react-slot": "^1.1.2",
@@ -4688,6 +4689,229 @@
 				}
 			}
 		},
+		"node_modules/@radix-ui/react-select": {
+			"version": "2.1.6",
+			"resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.1.6.tgz",
+			"integrity": "sha512-T6ajELxRvTuAMWH0YmRJ1qez+x4/7Nq7QIx7zJ0VK3qaEWdnWpNbEDnmWldG1zBDwqrLy5aLMUWcoGirVj5kMg==",
+			"license": "MIT",
+			"dependencies": {
+				"@radix-ui/number": "1.1.0",
+				"@radix-ui/primitive": "1.1.1",
+				"@radix-ui/react-collection": "1.1.2",
+				"@radix-ui/react-compose-refs": "1.1.1",
+				"@radix-ui/react-context": "1.1.1",
+				"@radix-ui/react-direction": "1.1.0",
+				"@radix-ui/react-dismissable-layer": "1.1.5",
+				"@radix-ui/react-focus-guards": "1.1.1",
+				"@radix-ui/react-focus-scope": "1.1.2",
+				"@radix-ui/react-id": "1.1.0",
+				"@radix-ui/react-popper": "1.2.2",
+				"@radix-ui/react-portal": "1.1.4",
+				"@radix-ui/react-primitive": "2.0.2",
+				"@radix-ui/react-slot": "1.1.2",
+				"@radix-ui/react-use-callback-ref": "1.1.0",
+				"@radix-ui/react-use-controllable-state": "1.1.0",
+				"@radix-ui/react-use-layout-effect": "1.1.0",
+				"@radix-ui/react-use-previous": "1.1.0",
+				"@radix-ui/react-visually-hidden": "1.1.2",
+				"aria-hidden": "^1.2.4",
+				"react-remove-scroll": "^2.6.3"
+			},
+			"peerDependencies": {
+				"@types/react": "*",
+				"@types/react-dom": "*",
+				"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+				"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+			},
+			"peerDependenciesMeta": {
+				"@types/react": {
+					"optional": true
+				},
+				"@types/react-dom": {
+					"optional": true
+				}
+			}
+		},
+		"node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-arrow": {
+			"version": "1.1.2",
+			"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.2.tgz",
+			"integrity": "sha512-G+KcpzXHq24iH0uGG/pF8LyzpFJYGD4RfLjCIBfGdSLXvjLHST31RUiRVrupIBMvIppMgSzQ6l66iAxl03tdlg==",
+			"license": "MIT",
+			"dependencies": {
+				"@radix-ui/react-primitive": "2.0.2"
+			},
+			"peerDependencies": {
+				"@types/react": "*",
+				"@types/react-dom": "*",
+				"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+				"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+			},
+			"peerDependenciesMeta": {
+				"@types/react": {
+					"optional": true
+				},
+				"@types/react-dom": {
+					"optional": true
+				}
+			}
+		},
+		"node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-collection": {
+			"version": "1.1.2",
+			"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.2.tgz",
+			"integrity": "sha512-9z54IEKRxIa9VityapoEYMuByaG42iSy1ZXlY2KcuLSEtq8x4987/N6m15ppoMffgZX72gER2uHe1D9Y6Unlcw==",
+			"license": "MIT",
+			"dependencies": {
+				"@radix-ui/react-compose-refs": "1.1.1",
+				"@radix-ui/react-context": "1.1.1",
+				"@radix-ui/react-primitive": "2.0.2",
+				"@radix-ui/react-slot": "1.1.2"
+			},
+			"peerDependencies": {
+				"@types/react": "*",
+				"@types/react-dom": "*",
+				"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+				"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+			},
+			"peerDependenciesMeta": {
+				"@types/react": {
+					"optional": true
+				},
+				"@types/react-dom": {
+					"optional": true
+				}
+			}
+		},
+		"node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-dismissable-layer": {
+			"version": "1.1.5",
+			"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.5.tgz",
+			"integrity": "sha512-E4TywXY6UsXNRhFrECa5HAvE5/4BFcGyfTyK36gP+pAW1ed7UTK4vKwdr53gAJYwqbfCWC6ATvJa3J3R/9+Qrg==",
+			"license": "MIT",
+			"dependencies": {
+				"@radix-ui/primitive": "1.1.1",
+				"@radix-ui/react-compose-refs": "1.1.1",
+				"@radix-ui/react-primitive": "2.0.2",
+				"@radix-ui/react-use-callback-ref": "1.1.0",
+				"@radix-ui/react-use-escape-keydown": "1.1.0"
+			},
+			"peerDependencies": {
+				"@types/react": "*",
+				"@types/react-dom": "*",
+				"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+				"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+			},
+			"peerDependenciesMeta": {
+				"@types/react": {
+					"optional": true
+				},
+				"@types/react-dom": {
+					"optional": true
+				}
+			}
+		},
+		"node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-focus-scope": {
+			"version": "1.1.2",
+			"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.2.tgz",
+			"integrity": "sha512-zxwE80FCU7lcXUGWkdt6XpTTCKPitG1XKOwViTxHVKIJhZl9MvIl2dVHeZENCWD9+EdWv05wlaEkRXUykU27RA==",
+			"license": "MIT",
+			"dependencies": {
+				"@radix-ui/react-compose-refs": "1.1.1",
+				"@radix-ui/react-primitive": "2.0.2",
+				"@radix-ui/react-use-callback-ref": "1.1.0"
+			},
+			"peerDependencies": {
+				"@types/react": "*",
+				"@types/react-dom": "*",
+				"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+				"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+			},
+			"peerDependenciesMeta": {
+				"@types/react": {
+					"optional": true
+				},
+				"@types/react-dom": {
+					"optional": true
+				}
+			}
+		},
+		"node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-popper": {
+			"version": "1.2.2",
+			"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.2.tgz",
+			"integrity": "sha512-Rvqc3nOpwseCyj/rgjlJDYAgyfw7OC1tTkKn2ivhaMGcYt8FSBlahHOZak2i3QwkRXUXgGgzeEe2RuqeEHuHgA==",
+			"license": "MIT",
+			"dependencies": {
+				"@floating-ui/react-dom": "^2.0.0",
+				"@radix-ui/react-arrow": "1.1.2",
+				"@radix-ui/react-compose-refs": "1.1.1",
+				"@radix-ui/react-context": "1.1.1",
+				"@radix-ui/react-primitive": "2.0.2",
+				"@radix-ui/react-use-callback-ref": "1.1.0",
+				"@radix-ui/react-use-layout-effect": "1.1.0",
+				"@radix-ui/react-use-rect": "1.1.0",
+				"@radix-ui/react-use-size": "1.1.0",
+				"@radix-ui/rect": "1.1.0"
+			},
+			"peerDependencies": {
+				"@types/react": "*",
+				"@types/react-dom": "*",
+				"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+				"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+			},
+			"peerDependenciesMeta": {
+				"@types/react": {
+					"optional": true
+				},
+				"@types/react-dom": {
+					"optional": true
+				}
+			}
+		},
+		"node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-portal": {
+			"version": "1.1.4",
+			"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.4.tgz",
+			"integrity": "sha512-sn2O9k1rPFYVyKd5LAJfo96JlSGVFpa1fS6UuBJfrZadudiw5tAmru+n1x7aMRQ84qDM71Zh1+SzK5QwU0tJfA==",
+			"license": "MIT",
+			"dependencies": {
+				"@radix-ui/react-primitive": "2.0.2",
+				"@radix-ui/react-use-layout-effect": "1.1.0"
+			},
+			"peerDependencies": {
+				"@types/react": "*",
+				"@types/react-dom": "*",
+				"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+				"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+			},
+			"peerDependenciesMeta": {
+				"@types/react": {
+					"optional": true
+				},
+				"@types/react-dom": {
+					"optional": true
+				}
+			}
+		},
+		"node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-primitive": {
+			"version": "2.0.2",
+			"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.2.tgz",
+			"integrity": "sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w==",
+			"license": "MIT",
+			"dependencies": {
+				"@radix-ui/react-slot": "1.1.2"
+			},
+			"peerDependencies": {
+				"@types/react": "*",
+				"@types/react-dom": "*",
+				"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+				"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+			},
+			"peerDependenciesMeta": {
+				"@types/react": {
+					"optional": true
+				},
+				"@types/react-dom": {
+					"optional": true
+				}
+			}
+		},
 		"node_modules/@radix-ui/react-separator": {
 			"version": "1.1.2",
 			"resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.2.tgz",

+ 1 - 0
webview-ui/package.json

@@ -22,6 +22,7 @@
 		"@radix-ui/react-icons": "^1.3.2",
 		"@radix-ui/react-popover": "^1.1.6",
 		"@radix-ui/react-progress": "^1.1.2",
+		"@radix-ui/react-select": "^2.1.6",
 		"@radix-ui/react-separator": "^1.1.2",
 		"@radix-ui/react-slider": "^1.2.3",
 		"@radix-ui/react-slot": "^1.1.2",

+ 78 - 46
webview-ui/src/components/settings/ApiOptions.tsx

@@ -4,6 +4,8 @@ import { Checkbox, Dropdown, type DropdownOption } from "vscrui"
 import { VSCodeLink, VSCodeRadio, VSCodeRadioGroup, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
 import * as vscodemodels from "vscode"
 
+import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue, Button } from "@/components/ui"
+
 import {
 	ApiConfiguration,
 	ModelInfo,
@@ -42,7 +44,6 @@ import { TemperatureControl } from "./TemperatureControl"
 import { validateApiConfiguration, validateModelId } from "@/utils/validate"
 import { ApiErrorMessage } from "./ApiErrorMessage"
 import { ThinkingBudget } from "./ThinkingBudget"
-import { Button } from "../ui"
 
 const modelsByProvider: Record<string, Record<string, ModelInfo>> = {
 	anthropic: anthropicModels,
@@ -54,6 +55,25 @@ const modelsByProvider: Record<string, Record<string, ModelInfo>> = {
 	mistral: mistralModels,
 }
 
+const providers = [
+	{ 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: "vscode-lm", label: "VS Code LM API" },
+	{ value: "mistral", label: "Mistral" },
+	{ value: "lmstudio", label: "LM Studio" },
+	{ value: "ollama", label: "Ollama" },
+	{ value: "unbound", label: "Unbound" },
+	{ value: "requesty", label: "Requesty" },
+	{ value: "human-relay", label: "Human Relay" },
+]
+
 interface ApiOptionsProps {
 	uriScheme: string | undefined
 	apiConfiguration: ApiConfiguration
@@ -238,30 +258,22 @@ const ApiOptions = ({
 				<label htmlFor="api-provider" className="font-medium">
 					API Provider
 				</label>
-				<Dropdown
-					id="api-provider"
+				<Select
 					value={selectedProvider}
-					onChange={handleInputChange("apiProvider", dropdownEventTransform)}
-					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: "vscode-lm", label: "VS Code LM API" },
-						{ value: "mistral", label: "Mistral" },
-						{ value: "lmstudio", label: "LM Studio" },
-						{ value: "ollama", label: "Ollama" },
-						{ value: "unbound", label: "Unbound" },
-						{ value: "requesty", label: "Requesty" },
-						{ value: "human-relay", label: "Human Relay" },
-					]}
-					className="w-full"
-				/>
+					onValueChange={handleInputChange("apiProvider", dropdownEventTransform)}>
+					<SelectTrigger className="w-full">
+						<SelectValue placeholder="Select" />
+					</SelectTrigger>
+					<SelectContent>
+						<SelectGroup>
+							{providers.map(({ value, label }) => (
+								<SelectItem key={value} value={value}>
+									{label}
+								</SelectItem>
+							))}
+						</SelectGroup>
+					</SelectContent>
+				</Select>
 			</div>
 
 			{errorMessage && <ApiErrorMessage errorMessage={errorMessage} />}
@@ -424,10 +436,10 @@ const ApiOptions = ({
 				<>
 					<VSCodeTextField
 						value={apiConfiguration?.mistralApiKey || ""}
-						style={{ width: "100%" }}
 						type="password"
 						onInput={handleInputChange("mistralApiKey")}
-						placeholder="Enter API Key...">
+						placeholder="Enter API Key..."
+						className="w-full">
 						<span className="font-medium">Mistral API Key</span>
 					</VSCodeTextField>
 					<div className="text-sm text-vscode-descriptionForeground -mt-2">
@@ -575,16 +587,16 @@ const ApiOptions = ({
 					</div>
 					<VSCodeTextField
 						value={apiConfiguration?.vertexJsonCredentials || ""}
-						style={{ width: "100%" }}
 						onInput={handleInputChange("vertexJsonCredentials")}
-						placeholder="Enter Credentials JSON...">
+						placeholder="Enter Credentials JSON..."
+						className="w-full">
 						<span className="font-medium">Google Cloud Credentials</span>
 					</VSCodeTextField>
 					<VSCodeTextField
 						value={apiConfiguration?.vertexKeyFile || ""}
-						style={{ width: "100%" }}
 						onInput={handleInputChange("vertexKeyFile")}
-						placeholder="Enter Key File Path...">
+						placeholder="Enter Key File Path..."
+						className="w-full">
 						<span className="font-medium">Google Cloud Key File Path</span>
 					</VSCodeTextField>
 					<VSCodeTextField
@@ -620,10 +632,10 @@ const ApiOptions = ({
 				<>
 					<VSCodeTextField
 						value={apiConfiguration?.geminiApiKey || ""}
-						style={{ width: "100%" }}
 						type="password"
 						onInput={handleInputChange("geminiApiKey")}
-						placeholder="Enter API Key...">
+						placeholder="Enter API Key..."
+						className="w-full">
 						<span className="font-medium">Gemini API Key</span>
 					</VSCodeTextField>
 					<div className="text-sm text-vscode-descriptionForeground -mt-2">
@@ -713,10 +725,13 @@ const ApiOptions = ({
 								}
 								type="text"
 								style={{
-									width: "100%",
 									borderColor: (() => {
 										const value = apiConfiguration?.openAiCustomModelInfo?.maxTokens
-										if (!value) return "var(--vscode-input-border)"
+
+										if (!value) {
+											return "var(--vscode-input-border)"
+										}
+
 										return value > 0
 											? "var(--vscode-charts-green)"
 											: "var(--vscode-errorForeground)"
@@ -725,12 +740,14 @@ const ApiOptions = ({
 								title="Maximum number of tokens the model can generate in a single response"
 								onInput={handleInputChange("openAiCustomModelInfo", (e) => {
 									const value = parseInt((e.target as HTMLInputElement).value)
+
 									return {
 										...(apiConfiguration?.openAiCustomModelInfo || openAiModelInfoSaneDefaults),
 										maxTokens: isNaN(value) ? undefined : value,
 									}
 								})}
-								placeholder="e.g. 4096">
+								placeholder="e.g. 4096"
+								className="w-full">
 								<span className="font-medium">Max Output Tokens</span>
 							</VSCodeTextField>
 							<div className="text-sm text-vscode-descriptionForeground">
@@ -748,10 +765,13 @@ const ApiOptions = ({
 								}
 								type="text"
 								style={{
-									width: "100%",
 									borderColor: (() => {
 										const value = apiConfiguration?.openAiCustomModelInfo?.contextWindow
-										if (!value) return "var(--vscode-input-border)"
+
+										if (!value) {
+											return "var(--vscode-input-border)"
+										}
+
 										return value > 0
 											? "var(--vscode-charts-green)"
 											: "var(--vscode-errorForeground)"
@@ -761,6 +781,7 @@ const ApiOptions = ({
 								onInput={handleInputChange("openAiCustomModelInfo", (e) => {
 									const value = (e.target as HTMLInputElement).value
 									const parsed = parseInt(value)
+
 									return {
 										...(apiConfiguration?.openAiCustomModelInfo || openAiModelInfoSaneDefaults),
 										contextWindow: isNaN(parsed)
@@ -768,7 +789,8 @@ const ApiOptions = ({
 											: parsed,
 									}
 								})}
-								placeholder="e.g. 128000">
+								placeholder="e.g. 128000"
+								className="w-full">
 								<span className="font-medium">Context Window Size</span>
 							</VSCodeTextField>
 							<div className="text-sm text-vscode-descriptionForeground">
@@ -834,10 +856,13 @@ const ApiOptions = ({
 								}
 								type="text"
 								style={{
-									width: "100%",
 									borderColor: (() => {
 										const value = apiConfiguration?.openAiCustomModelInfo?.inputPrice
-										if (!value && value !== 0) return "var(--vscode-input-border)"
+
+										if (!value && value !== 0) {
+											return "var(--vscode-input-border)"
+										}
+
 										return value >= 0
 											? "var(--vscode-charts-green)"
 											: "var(--vscode-errorForeground)"
@@ -846,12 +871,14 @@ const ApiOptions = ({
 								onChange={handleInputChange("openAiCustomModelInfo", (e) => {
 									const value = (e.target as HTMLInputElement).value
 									const parsed = parseFloat(value)
+
 									return {
 										...(apiConfiguration?.openAiCustomModelInfo ?? openAiModelInfoSaneDefaults),
 										inputPrice: isNaN(parsed) ? openAiModelInfoSaneDefaults.inputPrice : parsed,
 									}
 								})}
-								placeholder="e.g. 0.0001">
+								placeholder="e.g. 0.0001"
+								className="w-full">
 								<div className="flex items-center gap-1">
 									<span className="font-medium">Input Price</span>
 									<i
@@ -872,10 +899,13 @@ const ApiOptions = ({
 								}
 								type="text"
 								style={{
-									width: "100%",
 									borderColor: (() => {
 										const value = apiConfiguration?.openAiCustomModelInfo?.outputPrice
-										if (!value && value !== 0) return "var(--vscode-input-border)"
+
+										if (!value && value !== 0) {
+											return "var(--vscode-input-border)"
+										}
+
 										return value >= 0
 											? "var(--vscode-charts-green)"
 											: "var(--vscode-errorForeground)"
@@ -884,12 +914,14 @@ const ApiOptions = ({
 								onChange={handleInputChange("openAiCustomModelInfo", (e) => {
 									const value = (e.target as HTMLInputElement).value
 									const parsed = parseFloat(value)
+
 									return {
 										...(apiConfiguration?.openAiCustomModelInfo || openAiModelInfoSaneDefaults),
 										outputPrice: isNaN(parsed) ? openAiModelInfoSaneDefaults.outputPrice : parsed,
 									}
 								})}
-								placeholder="e.g. 0.0002">
+								placeholder="e.g. 0.0002"
+								className="w-full">
 								<div className="flex items-center gap-1">
 									<span className="font-medium">Output Price</span>
 									<i
@@ -960,9 +992,9 @@ const ApiOptions = ({
 							<div>
 								<VSCodeTextField
 									value={apiConfiguration?.lmStudioDraftModelId || ""}
-									style={{ width: "100%" }}
 									onInput={handleInputChange("lmStudioDraftModelId")}
-									placeholder={"e.g. lmstudio-community/llama-3.2-1b-instruct"}>
+									placeholder={"e.g. lmstudio-community/llama-3.2-1b-instruct"}
+									className="w-full">
 									<span className="font-medium">Draft Model ID</span>
 								</VSCodeTextField>
 								<div className="text-sm text-vscode-descriptionForeground">

+ 2 - 1
webview-ui/src/components/ui/index.ts

@@ -11,6 +11,7 @@ export * from "./popover"
 export * from "./progress"
 export * from "./separator"
 export * from "./slider"
+export * from "./select-dropdown"
+export * from "./select"
 export * from "./textarea"
 export * from "./tooltip"
-export * from "./select-dropdown"

+ 144 - 0
webview-ui/src/components/ui/select.tsx

@@ -0,0 +1,144 @@
+import * as React from "react"
+import * as SelectPrimitive from "@radix-ui/react-select"
+import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+function Select({ ...props }: React.ComponentProps<typeof SelectPrimitive.Root>) {
+	return <SelectPrimitive.Root data-slot="select" {...props} />
+}
+
+function SelectGroup({ ...props }: React.ComponentProps<typeof SelectPrimitive.Group>) {
+	return <SelectPrimitive.Group data-slot="select-group" {...props} />
+}
+
+function SelectValue({ ...props }: React.ComponentProps<typeof SelectPrimitive.Value>) {
+	return <SelectPrimitive.Value data-slot="select-value" {...props} />
+}
+
+function SelectTrigger({ className, children, ...props }: React.ComponentProps<typeof SelectPrimitive.Trigger>) {
+	return (
+		<SelectPrimitive.Trigger
+			data-slot="select-trigger"
+			className={cn(
+				"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-vscode-focusBorder aria-invalid:border-destructive flex h-7 w-fit items-center justify-between gap-2 rounded-xs border bg-vscode-input-background hover:bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none disabled:cursor-not-allowed disabled:opacity-50 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 cursor-pointer",
+				className,
+			)}
+			{...props}>
+			{children}
+			<SelectPrimitive.Icon asChild>
+				<ChevronDownIcon className="size-4 opacity-50" />
+			</SelectPrimitive.Icon>
+		</SelectPrimitive.Trigger>
+	)
+}
+
+function SelectContent({
+	className,
+	children,
+	position = "popper",
+	...props
+}: React.ComponentProps<typeof SelectPrimitive.Content>) {
+	return (
+		<SelectPrimitive.Portal>
+			<SelectPrimitive.Content
+				data-slot="select-content"
+				className={cn(
+					"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-72 min-w-[8rem] overflow-hidden rounded-xs border border-vscode-focusBorder shadow-md",
+					position === "popper" &&
+						"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
+					className,
+				)}
+				position={position}
+				{...props}>
+				<SelectScrollUpButton />
+				<SelectPrimitive.Viewport
+					className={cn(
+						"p-1",
+						position === "popper" &&
+							"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1",
+					)}>
+					{children}
+				</SelectPrimitive.Viewport>
+				<SelectScrollDownButton />
+			</SelectPrimitive.Content>
+		</SelectPrimitive.Portal>
+	)
+}
+
+function SelectLabel({ className, ...props }: React.ComponentProps<typeof SelectPrimitive.Label>) {
+	return (
+		<SelectPrimitive.Label
+			data-slot="select-label"
+			className={cn("px-2 py-1.5 text-sm font-medium", className)}
+			{...props}
+		/>
+	)
+}
+
+function SelectItem({ className, children, ...props }: React.ComponentProps<typeof SelectPrimitive.Item>) {
+	return (
+		<SelectPrimitive.Item
+			data-slot="select-item"
+			className={cn(
+				"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-xs py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2 cursor-pointer",
+				className,
+			)}
+			{...props}>
+			<span className="absolute right-2 flex size-3.5 items-center justify-center">
+				<SelectPrimitive.ItemIndicator>
+					<CheckIcon className="size-4" />
+				</SelectPrimitive.ItemIndicator>
+			</span>
+			<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
+		</SelectPrimitive.Item>
+	)
+}
+
+function SelectSeparator({ className, ...props }: React.ComponentProps<typeof SelectPrimitive.Separator>) {
+	return (
+		<SelectPrimitive.Separator
+			data-slot="select-separator"
+			className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
+			{...props}
+		/>
+	)
+}
+
+function SelectScrollUpButton({ className, ...props }: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
+	return (
+		<SelectPrimitive.ScrollUpButton
+			data-slot="select-scroll-up-button"
+			className={cn("flex cursor-default items-center justify-center py-1", className)}
+			{...props}>
+			<ChevronUpIcon className="size-4" />
+		</SelectPrimitive.ScrollUpButton>
+	)
+}
+
+function SelectScrollDownButton({
+	className,
+	...props
+}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
+	return (
+		<SelectPrimitive.ScrollDownButton
+			data-slot="select-scroll-down-button"
+			className={cn("flex cursor-default items-center justify-center py-1", className)}
+			{...props}>
+			<ChevronDownIcon className="size-4" />
+		</SelectPrimitive.ScrollDownButton>
+	)
+}
+
+export {
+	Select,
+	SelectContent,
+	SelectGroup,
+	SelectItem,
+	SelectLabel,
+	SelectScrollDownButton,
+	SelectScrollUpButton,
+	SelectSeparator,
+	SelectTrigger,
+	SelectValue,
+}