diff --git a/webview-ui/package-lock.json b/webview-ui/package-lock.json index b52f8ab311d..97960851c97 100644 --- a/webview-ui/package-lock.json +++ b/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", diff --git a/webview-ui/package.json b/webview-ui/package.json index a178837ee01..589ccbf6743 100644 --- a/webview-ui/package.json +++ b/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", diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index 7736046048c..c5a02dc1173 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/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> = { anthropic: anthropicModels, @@ -54,6 +55,25 @@ const modelsByProvider: Record> = { 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 = ({ - + onValueChange={handleInputChange("apiProvider", dropdownEventTransform)}> + + + + + + {providers.map(({ value, label }) => ( + + {label} + + ))} + + + {errorMessage && } @@ -424,10 +436,10 @@ const ApiOptions = ({ <> + placeholder="Enter API Key..." + className="w-full"> Mistral API Key
@@ -575,16 +587,16 @@ const ApiOptions = ({
+ placeholder="Enter Credentials JSON..." + className="w-full"> Google Cloud Credentials + placeholder="Enter Key File Path..." + className="w-full"> Google Cloud Key File Path + placeholder="Enter API Key..." + className="w-full"> Gemini API Key
@@ -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"> Max Output Tokens
@@ -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"> Context Window Size
@@ -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">
Input Price { 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">
Output Price + placeholder={"e.g. lmstudio-community/llama-3.2-1b-instruct"} + className="w-full"> Draft Model ID
diff --git a/webview-ui/src/components/settings/__tests__/ApiOptions.test.tsx b/webview-ui/src/components/settings/__tests__/ApiOptions.test.tsx index 364e57a7158..008c5819db8 100644 --- a/webview-ui/src/components/settings/__tests__/ApiOptions.test.tsx +++ b/webview-ui/src/components/settings/__tests__/ApiOptions.test.tsx @@ -35,6 +35,31 @@ jest.mock("vscrui", () => ({ Pane: ({ children }: any) =>
{children}
, })) +// Mock @shadcn/ui components +jest.mock("@/components/ui", () => ({ + Select: ({ children, value, onValueChange }: any) => ( +
+ +
+ ), + SelectTrigger: ({ children }: any) =>
{children}
, + SelectValue: ({ children }: any) =>
{children}
, + SelectContent: ({ children }: any) =>
{children}
, + SelectGroup: ({ children }: any) =>
{children}
, + SelectItem: ({ children, value }: any) => ( + + ), + Button: ({ children, onClick }: any) => ( + + ), +})) + jest.mock("../TemperatureControl", () => ({ TemperatureControl: ({ value, onChange }: any) => (
diff --git a/webview-ui/src/components/ui/index.ts b/webview-ui/src/components/ui/index.ts index b444b377886..1a2456a72fb 100644 --- a/webview-ui/src/components/ui/index.ts +++ b/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" diff --git a/webview-ui/src/components/ui/select.tsx b/webview-ui/src/components/ui/select.tsx new file mode 100644 index 00000000000..50f89b3760f --- /dev/null +++ b/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) { + return +} + +function SelectGroup({ ...props }: React.ComponentProps) { + return +} + +function SelectValue({ ...props }: React.ComponentProps) { + return +} + +function SelectTrigger({ className, children, ...props }: React.ComponentProps) { + return ( + + {children} + + + + + ) +} + +function SelectContent({ + className, + children, + position = "popper", + ...props +}: React.ComponentProps) { + return ( + + + + + {children} + + + + + ) +} + +function SelectLabel({ className, ...props }: React.ComponentProps) { + return ( + + ) +} + +function SelectItem({ className, children, ...props }: React.ComponentProps) { + return ( + + + + + + + {children} + + ) +} + +function SelectSeparator({ className, ...props }: React.ComponentProps) { + return ( + + ) +} + +function SelectScrollUpButton({ className, ...props }: React.ComponentProps) { + return ( + + + + ) +} + +function SelectScrollDownButton({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +export { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectScrollDownButton, + SelectScrollUpButton, + SelectSeparator, + SelectTrigger, + SelectValue, +}