diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index e75113f9236..2f577e77ad2 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -1,7 +1,10 @@ import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react" -import { useEvent } from "react-use" +import { useEvent, useInterval } from "react-use" import { Checkbox, Dropdown, Pane, type DropdownOption } from "vscrui" import { VSCodeLink, VSCodeRadio, VSCodeRadioGroup, VSCodeTextField } from "@vscode/webview-ui-toolkit/react" +import SettingCheckbox from "./SettingCheckbox" +import SettingCombo from "./SettingCombo" +import SettingTextField from "./SettingTextField" import { TemperatureControl } from "./TemperatureControl" import * as vscodemodels from "vscode" @@ -42,7 +45,6 @@ import OpenAiModelPicker from "./OpenAiModelPicker" import { GlamaModelPicker } from "./GlamaModelPicker" import { UnboundModelPicker } from "./UnboundModelPicker" import { ModelInfoView } from "./ModelInfoView" -import { DROPDOWN_Z_INDEX } from "./styles" import { RequestyModelPicker } from "./RequestyModelPicker" interface ApiOptionsProps { @@ -103,7 +105,7 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage, fromWelcomeView }: A }, []) useEvent("message", handleMessage) - const createDropdown = (models: Record) => { + const createModelDropdown = (models: Record) => { const options: DropdownOption[] = [ { value: "", label: "Select a model..." }, ...Object.keys(models).map((modelId) => ({ @@ -112,112 +114,108 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage, fromWelcomeView }: A })), ] return ( - { + onChange={(value) => { handleInputChange("apiModelId")({ - target: { - value: (value as DropdownOption).value, - }, + target: { value }, }) }} - style={{ width: "100%" }} - options={options} - /> + options={options}> + + ) } return ( -
-
- - { - handleInputChange("apiProvider")({ - target: { - value: (value as DropdownOption).value, - }, - }) - }} - style={{ minWidth: 130, position: "relative", zIndex: DROPDOWN_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: "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" }, - ]} - /> -
- - {selectedProvider === "anthropic" && ( -
- - Anthropic API Key - - - { - setAnthropicBaseUrlSelected(checked) - if (!checked) { - handleInputChange("anthropicBaseUrl")({ - target: { - value: "", - }, - }) +
+ { + handleInputChange( + "apiProvider", + true, + )({ + target: { value }, + }) + }} + 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" }, + ]} + inline> + {selectedProvider === "anthropic" && ( +
+ + This key is stored locally and only used to make API requests from this extension. + {!apiConfiguration?.apiKey && ( + + {" "} + You can get an Anthropic API key by signing up here. + + )} + } - }}> - Use custom base URL - - - {anthropicBaseUrlSelected && ( - handleInputChange("apiKey")({ target: { value } })} /> - )} -

- This key is stored locally and only used to make API requests from this extension. - {!apiConfiguration?.apiKey && ( - - You can get an Anthropic API key by signing up here. - - )} -

-
- )} + { + setAnthropicBaseUrlSelected(checked) + if (!checked) { + handleInputChange("anthropicBaseUrl")({ + target: { + value: "", + }, + }) + } + }}> + handleInputChange("anthropicBaseUrl")({ target: { value } })} + /> + +
+ )} {selectedProvider === "glama" && (
@@ -225,7 +223,7 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage, fromWelcomeView }: A value={apiConfiguration?.glamaApiKey || ""} style={{ width: "100%" }} type="password" - onInput={handleInputChange("glamaApiKey")} + onBlur={handleInputChange("glamaApiKey")} placeholder="Enter API Key..."> Glama API Key @@ -254,7 +252,7 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage, fromWelcomeView }: A value={apiConfiguration?.requestyApiKey || ""} style={{ width: "100%" }} type="password" - onInput={handleInputChange("requestyApiKey")} + onBlur={handleInputChange("requestyApiKey")} placeholder="Enter API Key..."> Requesty API Key @@ -275,7 +273,7 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage, fromWelcomeView }: A value={apiConfiguration?.openAiNativeApiKey || ""} style={{ width: "100%" }} type="password" - onInput={handleInputChange("openAiNativeApiKey")} + onBlur={handleInputChange("openAiNativeApiKey")} placeholder="Enter API Key..."> OpenAI API Key @@ -303,7 +301,7 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage, fromWelcomeView }: A value={apiConfiguration?.mistralApiKey || ""} style={{ width: "100%" }} type="password" - onInput={handleInputChange("mistralApiKey")} + onBlur={handleInputChange("mistralApiKey")} placeholder="Enter API Key..."> Mistral API Key @@ -314,15 +312,17 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage, fromWelcomeView }: A color: "var(--vscode-descriptionForeground)", }}> This key is stored locally and only used to make API requests from this extension. - - You can get a La Plateforme (api.mistral.ai) or Codestral (codestral.mistral.ai) API key by - signing up here. - + {!apiConfiguration?.mistralApiKey && ( + + You can get a La Plateforme (api.mistral.ai) / Codestral (codestral.mistral.ai) API key + by signing up here. + + )}

{(apiConfiguration?.apiModelId?.startsWith("codestral-") || @@ -330,9 +330,8 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage, fromWelcomeView }: A
Codestral Base URL (Optional) @@ -355,7 +354,7 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage, fromWelcomeView }: A value={apiConfiguration?.openRouterApiKey || ""} style={{ width: "100%" }} type="password" - onInput={handleInputChange("openRouterApiKey")} + onBlur={handleInputChange("openRouterApiKey")} placeholder="Enter API Key..."> OpenRouter API Key @@ -399,7 +398,7 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage, fromWelcomeView }: A value={apiConfiguration?.openRouterBaseUrl || ""} style={{ width: "100%", marginTop: 3 }} type="url" - onInput={handleInputChange("openRouterBaseUrl")} + onBlur={handleInputChange("openRouterBaseUrl")} placeholder="Default: https://openrouter.ai/api/v1" /> )} @@ -437,7 +436,7 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage, fromWelcomeView }: A AWS Profile Name @@ -448,7 +447,7 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage, fromWelcomeView }: A value={apiConfiguration?.awsAccessKey || ""} style={{ width: "100%" }} type="password" - onInput={handleInputChange("awsAccessKey")} + onBlur={handleInputChange("awsAccessKey")} placeholder="Enter Access Key..."> AWS Access Key @@ -456,7 +455,7 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage, fromWelcomeView }: A value={apiConfiguration?.awsSecretKey || ""} style={{ width: "100%" }} type="password" - onInput={handleInputChange("awsSecretKey")} + onBlur={handleInputChange("awsSecretKey")} placeholder="Enter Secret Key..."> AWS Secret Key @@ -464,7 +463,7 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage, fromWelcomeView }: A value={apiConfiguration?.awsSessionToken || ""} style={{ width: "100%" }} type="password" - onInput={handleInputChange("awsSessionToken")} + onBlur={handleInputChange("awsSessionToken")} placeholder="Enter Session Token..."> AWS Session Token @@ -532,7 +531,7 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage, fromWelcomeView }: A Google Cloud Project ID @@ -590,7 +589,7 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage, fromWelcomeView }: A value={apiConfiguration?.geminiApiKey || ""} style={{ width: "100%" }} type="password" - onInput={handleInputChange("geminiApiKey")} + onBlur={handleInputChange("geminiApiKey")} placeholder="Enter API Key..."> Gemini API Key @@ -618,7 +617,7 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage, fromWelcomeView }: A value={apiConfiguration?.openAiBaseUrl || ""} style={{ width: "100%" }} type="url" - onInput={handleInputChange("openAiBaseUrl")} + onBlur={handleInputChange("openAiBaseUrl")} placeholder={"Enter base URL..."}> Base URL @@ -626,7 +625,7 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage, fromWelcomeView }: A value={apiConfiguration?.openAiApiKey || ""} style={{ width: "100%" }} type="password" - onInput={handleInputChange("openAiApiKey")} + onBlur={handleInputChange("openAiApiKey")} placeholder="Enter API Key..."> API Key @@ -669,214 +668,369 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage, fromWelcomeView }: A )} -
- - handleInputChange("openAiCustomModelInfo")({ - target: { value: openAiModelInfoSaneDefaults }, - }), - }, - ]}>
-

- Configure the capabilities and pricing for your custom OpenAI-compatible model.
- Be careful for the model capabilities, as they can affect how Roo Code can work. -

- - {/* Capabilities Section */} + marginTop: 15, + }} + /> + + handleInputChange("openAiCustomModelInfo")({ + target: { value: openAiModelInfoSaneDefaults }, + }), + }, + ]}>
- - Model Capabilities - -
-
- { - const value = apiConfiguration?.openAiCustomModelInfo?.maxTokens - if (!value) return "var(--vscode-input-border)" - return value > 0 - ? "var(--vscode-charts-green)" - : "var(--vscode-errorForeground)" - })(), - }} - title="Maximum number of tokens the model can generate in a single response" - onChange={(e: any) => { - const value = parseInt(e.target.value) - handleInputChange("openAiCustomModelInfo")({ - target: { - value: { - ...(apiConfiguration?.openAiCustomModelInfo || - openAiModelInfoSaneDefaults), - maxTokens: isNaN(value) ? undefined : value, + Configure the capabilities and pricing for your custom OpenAI-compatible model.{" "} +
+ Be careful for the model capabilities, as they can affect how Roo Code can work. +

+ + {/* Capabilities Section */} +
+ + Model Capabilities + +
+
+ { + const value = apiConfiguration?.openAiCustomModelInfo?.maxTokens + if (!value) return "var(--vscode-input-border)" + return value > 0 + ? "var(--vscode-charts-green)" + : "var(--vscode-errorForeground)" + })(), + }} + title="Maximum number of tokens the model can generate in a single response" + onChange={(e: any) => { + const value = parseInt(e.target.value) + handleInputChange("openAiCustomModelInfo")({ + target: { + value: { + ...(apiConfiguration?.openAiCustomModelInfo || + openAiModelInfoSaneDefaults), + maxTokens: isNaN(value) ? undefined : value, + }, + }, + }) + }} + placeholder="e.g. 4096"> + Max Output Tokens + +
+ + + Maximum number of tokens the model can generate in a response.{" "} +
+ (-1 is depend on server) +
+
+
+ +
+ { + const value = + apiConfiguration?.openAiCustomModelInfo?.contextWindow + if (!value) return "var(--vscode-input-border)" + return value > 0 + ? "var(--vscode-charts-green)" + : "var(--vscode-errorForeground)" + })(), + }} + title="Total number of tokens (input + output) the model can process in a single request" + onChange={(e: any) => { + const parsed = parseInt(e.target.value) + handleInputChange("openAiCustomModelInfo")({ + target: { + value: { + ...(apiConfiguration?.openAiCustomModelInfo || + openAiModelInfoSaneDefaults), + contextWindow: + e.target.value === "" + ? undefined + : isNaN(parsed) + ? openAiModelInfoSaneDefaults.contextWindow + : parsed, + }, }, - }, - }) - }} - placeholder="e.g. 4096"> - Max Output Tokens - + }) + }} + placeholder="e.g. 128000"> + Context Window Size + +
+ + + Total tokens (input + output) the model can process. This will help + Roo Code run correctly. + +
+
+
- - - Maximum number of tokens the model can generate in a response.
- (-1 is depend on server) + + Model Features + +
+
+
+ { + handleInputChange("openAiCustomModelInfo")({ + target: { + value: { + ...(apiConfiguration?.openAiCustomModelInfo || + openAiModelInfoSaneDefaults), + supportsImages: checked, + }, + }, + }) + }}> + Image Support + + +
+

+ Allows the model to analyze and understand images, essential for + visual code assistance +

+
+ +
+
+ { + handleInputChange("openAiCustomModelInfo")({ + target: { + value: { + ...(apiConfiguration?.openAiCustomModelInfo || + openAiModelInfoSaneDefaults), + supportsComputerUse: checked, + }, + }, + }) + }}> + Computer Use + + +
+

+ This model feature is for computer use like sonnet 3.5 support +

+
+
+
-
- +
+ { - const value = apiConfiguration?.openAiCustomModelInfo?.contextWindow - if (!value) return "var(--vscode-input-border)" - return value > 0 - ? "var(--vscode-charts-green)" - : "var(--vscode-errorForeground)" - })(), - }} - title="Total number of tokens (input + output) the model can process in a single request" - onChange={(e: any) => { - const parsed = parseInt(e.target.value) - handleInputChange("openAiCustomModelInfo")({ - target: { - value: { - ...(apiConfiguration?.openAiCustomModelInfo || - openAiModelInfoSaneDefaults), - contextWindow: - e.target.value === "" - ? undefined - : isNaN(parsed) - ? openAiModelInfoSaneDefaults.contextWindow - : parsed, - }, - }, - }) - }} - placeholder="e.g. 128000"> - Context Window Size - -
+ Model Pricing + + - - - Total tokens (input + output) the model can process. This will help Roo - Code run correctly. - -
+ Configure token-based pricing in USD per million tokens +
- - Model Features - - -
-
-
- { - handleInputChange("openAiCustomModelInfo")({ - target: { - value: { - ...(apiConfiguration?.openAiCustomModelInfo || - openAiModelInfoSaneDefaults), - supportsImages: checked, - }, - }, - }) - }}> - Image Support - +
+ { + const value = + apiConfiguration?.openAiCustomModelInfo?.inputPrice + if (!value && value !== 0) return "var(--vscode-input-border)" + return value >= 0 + ? "var(--vscode-charts-green)" + : "var(--vscode-errorForeground)" + })(), + }} + onChange={(e: any) => { + const parsed = parseFloat(e.target.value) + handleInputChange("openAiCustomModelInfo")({ + target: { + value: { + ...(apiConfiguration?.openAiCustomModelInfo ?? + openAiModelInfoSaneDefaults), + inputPrice: + e.target.value === "" + ? undefined + : isNaN(parsed) + ? openAiModelInfoSaneDefaults.inputPrice + : parsed, + }, + }, + }) + }} + placeholder="e.g. 0.0001"> +
+ Input Price
-

- Allows the model to analyze and understand images, essential for - visual code assistance -

-
+ +
-
+ -
- { - handleInputChange("openAiCustomModelInfo")({ - target: { - value: { - ...(apiConfiguration?.openAiCustomModelInfo || - openAiModelInfoSaneDefaults), - supportsComputerUse: checked, - }, - }, - }) - }}> - Computer Use - + width: "100%", + borderColor: (() => { + const value = + apiConfiguration?.openAiCustomModelInfo?.outputPrice + if (!value && value !== 0) return "var(--vscode-input-border)" + return value >= 0 + ? "var(--vscode-charts-green)" + : "var(--vscode-errorForeground)" + })(), + }} + onChange={(e: any) => { + const parsed = parseFloat(e.target.value) + handleInputChange("openAiCustomModelInfo")({ + target: { + value: { + ...(apiConfiguration?.openAiCustomModelInfo || + openAiModelInfoSaneDefaults), + outputPrice: + e.target.value === "" + ? undefined + : isNaN(parsed) + ? openAiModelInfoSaneDefaults.outputPrice + : parsed, + }, + }, + }) + }} + placeholder="e.g. 0.0002"> +
+ Output Price
-

- This model feature is for computer use like sonnet 3.5 support -

-
+
+ +
- {/* Pricing Section */} -
-
- - Model Pricing - - - Configure token-based pricing in USD per million tokens - -
- -
-
- { - const value = apiConfiguration?.openAiCustomModelInfo?.inputPrice - if (!value && value !== 0) return "var(--vscode-input-border)" - return value >= 0 - ? "var(--vscode-charts-green)" - : "var(--vscode-errorForeground)" - })(), - }} - onChange={(e: any) => { - const parsed = parseFloat(e.target.value) - handleInputChange("openAiCustomModelInfo")({ - target: { - value: { - ...(apiConfiguration?.openAiCustomModelInfo ?? - openAiModelInfoSaneDefaults), - inputPrice: - e.target.value === "" - ? undefined - : isNaN(parsed) - ? openAiModelInfoSaneDefaults.inputPrice - : parsed, - }, - }, - }) - }} - placeholder="e.g. 0.0001"> -
- Input Price - -
-
-
- -
- { - const value = apiConfiguration?.openAiCustomModelInfo?.outputPrice - if (!value && value !== 0) return "var(--vscode-input-border)" - return value >= 0 - ? "var(--vscode-charts-green)" - : "var(--vscode-errorForeground)" - })(), - }} - onChange={(e: any) => { - const parsed = parseFloat(e.target.value) - handleInputChange("openAiCustomModelInfo")({ - target: { - value: { - ...(apiConfiguration?.openAiCustomModelInfo || - openAiModelInfoSaneDefaults), - outputPrice: - e.target.value === "" - ? undefined - : isNaN(parsed) - ? openAiModelInfoSaneDefaults.outputPrice - : parsed, - }, - }, - }) - }} - placeholder="e.g. 0.0002"> -
- Output Price - -
-
-
-
-
-
- -
- - {/* end Model Info Configuration */} + {/* end Model Info Configuration */} -

- - (Note: Roo Code uses complex prompts and works best - with Claude models. Less capable models may not work as expected.) - -

-
- )} +

+ + (Note: Roo Code uses complex prompts and works + best with Claude models. Less capable models may not work as expected.) + +

+
+ )} - {selectedProvider === "lmstudio" && ( -
- - Base URL (optional) - - - Model ID - - {lmStudioModels.length > 0 && ( - { - const value = (e.target as HTMLInputElement)?.value - // need to check value first since radio group returns empty string sometimes - if (value) { - handleInputChange("lmStudioModelId")({ - target: { value }, - }) + {selectedProvider === "lmstudio" && ( +
+ handleInputChange("lmStudioBaseUrl")({ target: { value } })} + /> + handleInputChange("lmStudioModelId")({ target: { value } })} + /> + {lmStudioModels.length > 0 && ( + { + const value = (e.target as HTMLInputElement)?.value + // need to check value first since radio group returns empty string sometimes + if (value) { + handleInputChange("lmStudioModelId")({ + target: { value }, + }) + } + }}> + {lmStudioModels.map((model) => ( + + {model} + + ))} + + )} +

- {lmStudioModels.map((model) => ( - - {model} - - ))} - - )} -

- LM Studio allows you to run models locally on your computer. For instructions on how to get - started, see their - - quickstart guide. - - You will also need to start LM Studio's{" "} - - local server - {" "} - feature to use it with this extension.{" "} - - (Note: Roo Code uses complex prompts and works best - with Claude models. Less capable models may not work as expected.) - -

-
- )} + LM Studio allows you to run models locally on your computer. For instructions on how to get + started, see their + + quickstart guide. + + You will also need to start LM Studio's{" "} + + local server + {" "} + feature to use it with this extension.{" "} + + (Note: Roo Code uses complex prompts and works + best with Claude models. Less capable models may not work as expected.) + +

+
+ )} {selectedProvider === "deepseek" && (
@@ -1188,7 +1195,7 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage, fromWelcomeView }: A value={apiConfiguration?.deepSeekApiKey || ""} style={{ width: "100%" }} type="password" - onInput={handleInputChange("deepSeekApiKey")} + onBlur={handleInputChange("deepSeekApiKey")} placeholder="Enter API Key..."> DeepSeek API Key @@ -1210,67 +1217,65 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage, fromWelcomeView }: A
)} - {selectedProvider === "vscode-lm" && ( -
-
- - {vsCodeLmModels.length > 0 ? ( - { - const valueStr = (value as DropdownOption)?.value - if (!valueStr) { - return + {selectedProvider === "vscode-lm" && ( +
+
+ {vsCodeLmModels.length > 0 ? ( + ({ - value: `${model.vendor}/${model.family}`, - label: `${model.vendor} - ${model.family}`, - })), - ]} - /> - ) : ( + onChange={(valueStr) => { + if (!valueStr) { + return + } + const [vendor, family] = valueStr.split("/") + handleInputChange("vsCodeLmModelSelector")({ + target: { + value: { vendor, family }, + }, + }) + }} + options={[ + { value: "", label: "Select a model..." }, + ...vsCodeLmModels.map((model) => ({ + value: `${model.vendor}/${model.family}`, + label: `${model.vendor} - ${model.family}`, + })), + ]} + /> + ) : ( +

+ The VS Code Language Model API allows you to run models provided by other VS Code + extensions (including but not limited to GitHub Copilot). The easiest way to get + started is to install the Copilot and Copilot Chat extensions from the VS Code + Marketplace. +

+ )} +

- The VS Code Language Model API allows you to run models provided by other VS Code - extensions (including but not limited to GitHub Copilot). The easiest way to get started - is to install the Copilot and Copilot Chat extensions from the VS Code Marketplace. + Note: This is a very experimental integration and may not work as expected. Please + report any issues to the Roo-Code GitHub repository.

- )} - -

- Note: This is a very experimental integration and may not work as expected. Please report - any issues to the Roo-Code GitHub repository. -

+
-
- )} + )} {selectedProvider === "ollama" && (
@@ -1278,14 +1283,14 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage, fromWelcomeView }: A value={apiConfiguration?.ollamaBaseUrl || ""} style={{ width: "100%" }} type="url" - onInput={handleInputChange("ollamaBaseUrl")} + onBlur={handleInputChange("ollamaBaseUrl")} placeholder={"Default: http://localhost:11434"}> Base URL (optional) Model ID @@ -1336,106 +1341,89 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage, fromWelcomeView }: A
)} - {selectedProvider === "unbound" && ( -
- - Unbound API Key - - {!apiConfiguration?.unboundApiKey && ( - - Get Unbound API Key - - )} + {selectedProvider === "unbound" && ( +
+ handleInputChange("unboundApiKey")({ target: { value } })}> + {!apiConfiguration?.unboundApiKey && ( + + Get Unbound API Key + + )} + + +
+ )} + + {apiErrorMessage && (

- This key is stored locally and only used to make API requests from this extension. + {apiErrorMessage}

- -
- )} - - {apiErrorMessage && ( -

- {apiErrorMessage} -

- )} + )} - {selectedProvider === "glama" && } + {selectedProvider === "glama" && } - {selectedProvider === "openrouter" && } - {selectedProvider === "requesty" && } + {selectedProvider === "openrouter" && } + {selectedProvider === "requesty" && } - {selectedProvider !== "glama" && - selectedProvider !== "openrouter" && - selectedProvider !== "requesty" && - selectedProvider !== "openai" && - selectedProvider !== "ollama" && - selectedProvider !== "lmstudio" && - selectedProvider !== "unbound" && ( - <> -
- - {selectedProvider === "anthropic" && createDropdown(anthropicModels)} - {selectedProvider === "bedrock" && createDropdown(bedrockModels)} - {selectedProvider === "vertex" && createDropdown(vertexModels)} - {selectedProvider === "gemini" && createDropdown(geminiModels)} - {selectedProvider === "openai-native" && createDropdown(openAiNativeModels)} - {selectedProvider === "deepseek" && createDropdown(deepSeekModels)} - {selectedProvider === "mistral" && createDropdown(mistralModels)} -
+ {selectedProvider !== "glama" && + selectedProvider !== "openrouter" && + selectedProvider !== "requesty" && + selectedProvider !== "openai" && + selectedProvider !== "ollama" && + selectedProvider !== "lmstudio" && + selectedProvider !== "unbound" && ( + <> +
+ {selectedProvider === "anthropic" && createModelDropdown(anthropicModels)} + {selectedProvider === "bedrock" && createModelDropdown(bedrockModels)} + {selectedProvider === "vertex" && createModelDropdown(vertexModels)} + {selectedProvider === "gemini" && createModelDropdown(geminiModels)} + {selectedProvider === "openai-native" && createModelDropdown(openAiNativeModels)} + {selectedProvider === "deepseek" && createModelDropdown(deepSeekModels)} + {selectedProvider === "mistral" && createModelDropdown(mistralModels)} +
+ + )} - + { + handleInputChange("modelTemperature")({ + target: { value }, + }) + }} + maxValue={2} /> - +
)} - {!fromWelcomeView && ( -
- { - handleInputChange("modelTemperature")({ - target: { value }, - }) - }} - maxValue={2} - /> -
- )} - - {modelIdErrorMessage && ( -

- {modelIdErrorMessage} -

- )} + {modelIdErrorMessage && ( +

+ {modelIdErrorMessage} +

+ )} +
) } diff --git a/webview-ui/src/components/settings/ExperimentalFeature.tsx b/webview-ui/src/components/settings/ExperimentalFeature.tsx deleted file mode 100644 index ee813dd17b1..00000000000 --- a/webview-ui/src/components/settings/ExperimentalFeature.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react" - -interface ExperimentalFeatureProps { - name: string - description: string - enabled: boolean - onChange: (value: boolean) => void -} - -const ExperimentalFeature = ({ name, description, enabled, onChange }: ExperimentalFeatureProps) => { - return ( -
-
- ⚠️ - onChange(e.target.checked)}> - {name} - -
-

- {description} -

-
- ) -} - -export default ExperimentalFeature diff --git a/webview-ui/src/components/settings/ModelPicker.tsx b/webview-ui/src/components/settings/ModelPicker.tsx index b21b37ef0f4..3e83eda080c 100644 --- a/webview-ui/src/components/settings/ModelPicker.tsx +++ b/webview-ui/src/components/settings/ModelPicker.tsx @@ -141,7 +141,14 @@ export const ModelPicker = ({ useEffect(() => setValue(selectedModelId), [selectedModelId]) return ( - <> +
Model
@@ -219,6 +226,6 @@ export const ModelPicker = ({
)} - +
) } diff --git a/webview-ui/src/components/settings/SettingCheckbox.tsx b/webview-ui/src/components/settings/SettingCheckbox.tsx new file mode 100644 index 00000000000..26ae2b03ee1 --- /dev/null +++ b/webview-ui/src/components/settings/SettingCheckbox.tsx @@ -0,0 +1,43 @@ +import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react" + +interface SettingCheckboxProps { + name: string + description: React.ReactNode + checked?: boolean + onChange: (value: boolean) => void + experimental?: boolean + children?: React.ReactNode +} + +const SettingCheckbox = ({ + name, + description, + checked: enabled, + onChange, + experimental, + children, +}: SettingCheckboxProps) => { + return ( +
+
+ {experimental && ⚠️} + onChange(e.target.checked)}> + {name} + +
+

+ {description} +

+ {enabled && children} +
+ ) +} + +export default SettingCheckbox diff --git a/webview-ui/src/components/settings/SettingCombo.tsx b/webview-ui/src/components/settings/SettingCombo.tsx new file mode 100644 index 00000000000..174e6fdd4fb --- /dev/null +++ b/webview-ui/src/components/settings/SettingCombo.tsx @@ -0,0 +1,55 @@ +import { Dropdown } from "vscrui" +import type { DropdownOption } from "vscrui" + +interface SettingComboProps { + id?: string + name: string + description?: string + value?: string + onChange: (value: string) => void + options: DropdownOption[] + children?: React.ReactNode + inline?: boolean +} + +const SettingCombo = ({ id, name, description, value, onChange, options, children, inline }: SettingComboProps) => { + const defaultValue = options.length > 0 ? options[0].value : undefined + return ( +
+
+ +
+ { + onChange((value as DropdownOption).value) + }} + style={{ width: "100%" }} + options={options} + /> +
+
+ {description && ( +

+ {description} +

+ )} + {children} +
+ ) +} + +export default SettingCombo diff --git a/webview-ui/src/components/settings/SettingSlider.tsx b/webview-ui/src/components/settings/SettingSlider.tsx new file mode 100644 index 00000000000..4e2a82ec1a9 --- /dev/null +++ b/webview-ui/src/components/settings/SettingSlider.tsx @@ -0,0 +1,83 @@ +import { memo } from "react" + +interface SettingSliderProps { + name: string + description?: string + value: number + onChange: (value: number) => void + min: number + max: number + step: number + unit?: string + style?: React.CSSProperties +} + +const SettingSlider = ({ + name, + description, + value, + onChange, + min, + max, + step, + unit = "", + style, +}: SettingSliderProps) => { + const sliderStyle = { + flexGrow: 1, + accentColor: "var(--vscode-button-background)", + height: "2px", + } + + const labelStyle = { + minWidth: "45px", + lineHeight: "20px", + paddingBottom: "2px", + paddingLeft: "5px", + } + + return ( +
+ {name} +
+ onChange(parseFloat(e.target.value))} + style={sliderStyle} + aria-label={name.toLowerCase()} + /> + + {value} + {unit} + +
+ {description && ( +

+ {description} +

+ )} +
+ ) +} + +export default memo(SettingSlider) diff --git a/webview-ui/src/components/settings/SettingTextField.tsx b/webview-ui/src/components/settings/SettingTextField.tsx new file mode 100644 index 00000000000..838c14a2cf4 --- /dev/null +++ b/webview-ui/src/components/settings/SettingTextField.tsx @@ -0,0 +1,47 @@ +import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react" + +interface SettingTextFieldProps { + name: string + description: React.ReactNode + value?: string + placeholder?: string + type?: "text" | "password" | "url" // Limited to types actually used in ApiOptions.tsx + onBlur: (value: string) => void + children?: React.ReactNode +} + +const SettingTextField = ({ + name, + description, + value, + placeholder, + type = "text", + onBlur, + children, +}: SettingTextFieldProps) => { + return ( +
+ onBlur(e.target.value)} + placeholder={placeholder}> + {name} + +

+ {description} +

+ {children} +
+ ) +} + +export default SettingTextField diff --git a/webview-ui/src/components/settings/SettingsView.tsx b/webview-ui/src/components/settings/SettingsView.tsx index bef57d1f5ae..9c2ff7d480a 100644 --- a/webview-ui/src/components/settings/SettingsView.tsx +++ b/webview-ui/src/components/settings/SettingsView.tsx @@ -1,14 +1,14 @@ -import { VSCodeButton, VSCodeCheckbox, VSCodeLink, VSCodeTextField } from "@vscode/webview-ui-toolkit/react" +import { VSCodeButton, VSCodeLink, VSCodeTextField } from "@vscode/webview-ui-toolkit/react" import { memo, useEffect, useState } from "react" import { useExtensionState } from "../../context/ExtensionStateContext" import { validateApiConfiguration, validateModelId } from "../../utils/validate" import { vscode } from "../../utils/vscode" import ApiOptions from "./ApiOptions" -import ExperimentalFeature from "./ExperimentalFeature" +import SettingCheckbox from "./SettingCheckbox" +import SettingCombo from "./SettingCombo" +import SettingSlider from "./SettingSlider" import { EXPERIMENT_IDS, experimentConfigsMap } from "../../../../src/shared/experiments" import ApiConfigManager from "./ApiConfigManager" -import { Dropdown } from "vscrui" -import type { DropdownOption } from "vscrui" type SettingsViewProps = { onDone: () => void @@ -144,20 +144,6 @@ const SettingsView = ({ onDone }: SettingsViewProps) => { } } - const sliderLabelStyle = { - minWidth: "45px", - textAlign: "right" as const, - lineHeight: "20px", - paddingBottom: "2px", - } - - const sliderStyle = { - flexGrow: 1, - maxWidth: "80%", - accentColor: "var(--vscode-button-background)", - height: "2px", - } - return (
{
-
+

Provider Settings

{
-
+

Auto-Approve Settings

The following settings allow Roo to automatically perform operations without requiring approval. @@ -234,533 +227,309 @@ const SettingsView = ({ onDone }: SettingsViewProps) => { risks.

-
- setAlwaysAllowReadOnly(e.target.checked)}> - Always approve read-only operations - + + + + + + + + + + + + + + + + + + Allowed Auto-Execute Commands

- When enabled, Roo will automatically view directory contents and read files without - requiring you to click the Approve button. -

-
- -
- setAlwaysAllowWrite(e.target.checked)}> - Always approve write operations - -

- Automatically create and edit files without requiring approval -

- {alwaysAllowWrite && ( -
-
- setWriteDelayMs(parseInt(e.target.value))} - style={{ - flex: 1, - accentColor: "var(--vscode-button-background)", - height: "2px", - }} - /> - {writeDelayMs}ms -
-

- Delay after writes to allow diagnostics to detect potential problems -

-
- )} -
- -
- setAlwaysAllowBrowser(e.target.checked)}> - Always approve browser actions - -

- Automatically perform browser actions without requiring approval -
- Note: Only applies when the model supports computer use -

-
- -
- setAlwaysApproveResubmit(e.target.checked)}> - Always retry failed API requests - -

- Automatically retry failed API requests when server returns an error response -

- {alwaysApproveResubmit && ( -
-
- setRequestDelaySeconds(parseInt(e.target.value))} - style={{ - flex: 1, - accentColor: "var(--vscode-button-background)", - height: "2px", - }} - /> - {requestDelaySeconds}s -
-

- Delay before retrying the request -

-
- )} -
- -
- setAlwaysAllowMcp(e.target.checked)}> - Always approve MCP tools - -

- Enable auto-approval of individual MCP tools in the MCP Servers view (requires both this - setting and the tool's individual "Always allow" checkbox) -

-
- -
- setAlwaysAllowModeSwitch(e.target.checked)}> - Always approve mode switching & task creation - -

- Automatically switch between different AI modes and create new tasks without requiring - approval -

-
- -
- setAlwaysAllowExecute(e.target.checked)}> - Always approve allowed execute operations - -

- Automatically execute allowed terminal commands without requiring approval + Command prefixes that can be auto-executed when "Always approve execute operations" is + enabled.

- {alwaysAllowExecute && ( -
- Allowed Auto-Execute Commands -

- Command prefixes that can be auto-executed when "Always approve execute operations" - is enabled. -

- -
- setCommandInput(e.target.value)} - onKeyDown={(e: any) => { - if (e.key === "Enter") { - e.preventDefault() - handleAddCommand() - } - }} - placeholder="Enter command prefix (e.g., 'git ')" - style={{ flexGrow: 1 }} - /> - Add -
+
+ setCommandInput(e.target.value)} + onKeyDown={(e: any) => { + if (e.key === "Enter") { + e.preventDefault() + handleAddCommand() + } + }} + placeholder="Enter command prefix (e.g., 'git ')" + style={{ flexGrow: 1 }} + /> + Add +
+
+ {(allowedCommands ?? []).map((cmd, index) => (
- {(allowedCommands ?? []).map((cmd, index) => ( -
- {cmd} - { - const newCommands = (allowedCommands ?? []).filter( - (_, i) => i !== index, - ) - setAllowedCommands(newCommands) - vscode.postMessage({ - type: "allowedCommands", - commands: newCommands, - }) - }}> - - -
- ))} + {cmd} + { + const newCommands = (allowedCommands ?? []).filter((_, i) => i !== index) + setAllowedCommands(newCommands) + vscode.postMessage({ + type: "allowedCommands", + commands: newCommands, + }) + }}> + +
-
- )} -
+ ))} +
+
-
+

Browser Settings

-
- -
- { - setBrowserViewportSize((value as DropdownOption).value) - }} - style={{ width: "100%" }} - options={[ - { value: "1280x800", label: "Large Desktop (1280x800)" }, - { value: "900x600", label: "Small Desktop (900x600)" }, - { value: "768x1024", label: "Tablet (768x1024)" }, - { value: "360x640", label: "Mobile (360x640)" }, - ]} - /> -
-

- Select the viewport size for browser interactions. This affects how websites are displayed - and interacted with. -

-
+
-
- Screenshot quality -
- setScreenshotQuality(parseInt(e.target.value))} - style={{ - ...sliderStyle, - }} - /> - {screenshotQuality ?? 75}% -
-
-

- Adjust the WebP quality of browser screenshots. Higher values provide clearer screenshots - but increase token usage. -

+
-
+

Notification Settings

-
- setSoundEnabled(e.target.checked)}> - Enable sound effects - -

- When enabled, Roo will play sound effects for notifications and events. -

-
- {soundEnabled && ( -
-
- Volume - setSoundVolume(parseFloat(e.target.value))} - style={{ - flexGrow: 1, - accentColor: "var(--vscode-button-background)", - height: "2px", - }} - aria-label="Volume" - /> - - {((soundVolume ?? 0.5) * 100).toFixed(0)}% - -
-
- )} + + setSoundVolume(value / 100)} + min={0} + max={100} + step={1} + unit="%" + /> +
-
+

Advanced Settings

-
-
- Rate limit -
- setRateLimitSeconds(parseInt(e.target.value))} - style={{ ...sliderStyle }} - /> - {rateLimitSeconds}s -
-
-

- Minimum time between API requests. -

-
-
-
- Terminal output limit -
- setTerminalOutputLineLimit(parseInt(e.target.value))} - style={{ ...sliderStyle }} - /> - {terminalOutputLineLimit ?? 500} -
-
-

- Maximum number of lines to include in terminal output when executing commands. When exceeded - lines will be removed from the middle, saving tokens. -

-
- -
-
- Open tabs context limit -
- setMaxOpenTabsContext(parseInt(e.target.value))} - style={{ ...sliderStyle }} - /> - {maxOpenTabsContext ?? 20} -
-
-

- Maximum number of VSCode open tabs to include in context. Higher values provide more context - but increase token usage. -

-
- -
- { - setDiffEnabled(e.target.checked) - if (!e.target.checked) { - // Reset experimental strategy when diffs are disabled - setExperimentEnabled(EXPERIMENT_IDS.DIFF_STRATEGY, false) + + + + { + setDiffEnabled(checked) + if (!checked) { + // Reset experimental strategy when diffs are disabled + setExperimentEnabled(EXPERIMENT_IDS.DIFF_STRATEGY, false) + } + }}> + { + setFuzzyMatchThreshold(value / 100) + }} + min={80} + max={100} + step={0.5} + unit="%" + /> + setExperimentEnabled(EXPERIMENT_IDS.DIFF_STRATEGY, enabled)} + experimental={true} + /> + + + { + setCheckpointsEnabled(enabled) + }} + experimental={true} + /> + + {Object.entries(experimentConfigsMap) + .filter((config) => config[0] !== "DIFF_STRATEGY") + .map((config) => ( + + setExperimentEnabled( + EXPERIMENT_IDS[config[0] as keyof typeof EXPERIMENT_IDS], + enabled, + ) } - }}> - Enable editing through diffs - -

- When enabled, Roo will be able to edit files more quickly and will automatically reject - truncated full-file writes. Works best with the latest Claude 3.5 Sonnet model. -

- - {diffEnabled && ( -
-
- Match precision -
- { - setFuzzyMatchThreshold(parseFloat(e.target.value)) - }} - style={{ - ...sliderStyle, - }} - /> - - {Math.round((fuzzyMatchThreshold || 1) * 100)}% - -
-

- This slider controls how precisely code sections must match when applying diffs. - Lower values allow more flexible matching but increase the risk of incorrect - replacements. Use values below 100% with extreme caution. -

- - setExperimentEnabled(EXPERIMENT_IDS.DIFF_STRATEGY, enabled) - } - /> -
-
- )} - -
-
- ⚠️ - { - setCheckpointsEnabled(e.target.checked) - }}> - Enable experimental checkpoints - -
-

- When enabled, Roo will save a checkpoint whenever a file in the workspace is modified, - added or deleted, letting you easily revert to a previous state. -

-
- - {Object.entries(experimentConfigsMap) - .filter((config) => config[0] !== "DIFF_STRATEGY") - .map((config) => ( - - setExperimentEnabled( - EXPERIMENT_IDS[config[0] as keyof typeof EXPERIMENT_IDS], - enabled, - ) - } - /> - ))} -
+ experimental={true} + /> + ))}
{ color: "var(--vscode-descriptionForeground)", fontSize: "12px", lineHeight: "1.2", - marginTop: "auto", + marginTop: "40px", padding: "10px 8px 15px 0px", }}>

diff --git a/webview-ui/src/components/settings/TemperatureControl.tsx b/webview-ui/src/components/settings/TemperatureControl.tsx index 422356bf69f..544b5a92e9e 100644 --- a/webview-ui/src/components/settings/TemperatureControl.tsx +++ b/webview-ui/src/components/settings/TemperatureControl.tsx @@ -1,5 +1,5 @@ -import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react" import { useEffect, useState } from "react" +import SettingCheckbox from "./SettingCheckbox" interface TemperatureControlProps { value: number | undefined @@ -19,62 +19,53 @@ export const TemperatureControl = ({ value, onChange, maxValue = 1 }: Temperatur }, [value]) return ( -

- { - const isChecked = e.target.checked - setIsCustomTemperature(isChecked) - if (!isChecked) { - onChange(undefined) // Unset the temperature - } else if (value !== undefined) { - onChange(value) // Use the value from apiConfiguration, if set - } + { + setIsCustomTemperature(isChecked) + if (!isChecked) { + onChange(undefined) // Unset the temperature + } else if (value !== undefined) { + onChange(value) // Use the value from apiConfiguration, if set + } + }}> +
- Use custom temperature - - -

- Controls randomness in the model's responses. -

- - {isCustomTemperature && ( -
-
- setInputValue(e.target.value)} - onBlur={(e) => { - const newValue = parseFloat(e.target.value) - if (!isNaN(newValue) && newValue >= 0 && newValue <= maxValue) { - onChange(newValue) - setInputValue(newValue.toString()) - } else { - setInputValue(value?.toString() ?? "0") // Reset to last valid value - } - }} - style={{ - width: "60px", - padding: "4px 8px", - border: "1px solid var(--vscode-input-border)", - background: "var(--vscode-input-background)", - color: "var(--vscode-input-foreground)", - }} - /> -
-

- Higher values make output more random, lower values make it more deterministic. -

+
+ setInputValue(e.target.value)} + onBlur={(e) => { + const newValue = parseFloat(e.target.value) + if (!isNaN(newValue) && newValue >= 0 && newValue <= maxValue) { + onChange(newValue) + setInputValue(newValue.toString()) + } else { + setInputValue(value?.toString() ?? "0") // Reset to last valid value + } + }} + style={{ + width: "60px", + padding: "4px 8px", + border: "1px solid var(--vscode-input-border)", + background: "var(--vscode-input-background)", + color: "var(--vscode-input-foreground)", + }} + />
- )} -
+

+ Higher values make output more random, lower values make it more deterministic. +

+
+
) } diff --git a/webview-ui/src/components/settings/__tests__/ApiOptions.test.tsx b/webview-ui/src/components/settings/__tests__/ApiOptions.test.tsx index 36906929123..263960511f4 100644 --- a/webview-ui/src/components/settings/__tests__/ApiOptions.test.tsx +++ b/webview-ui/src/components/settings/__tests__/ApiOptions.test.tsx @@ -13,6 +13,27 @@ jest.mock("@vscode/webview-ui-toolkit/react", () => ({ VSCodeLink: ({ children, href }: any) => {children}, VSCodeRadio: ({ children, value, checked }: any) => , VSCodeRadioGroup: ({ children }: any) =>
{children}
, + VSCodeCheckbox: ({ children, checked, onChange }: any) => ( + + ), +})) + +// Mock SettingCheckbox component +jest.mock("../SettingCheckbox", () => ({ + __esModule: true, + default: ({ name, description, enabled, onChange, children }: any) => ( +
+ +

{description}

+ {enabled && children} +
+ ), })) // Mock other components diff --git a/webview-ui/src/components/settings/__tests__/SettingsView.test.tsx b/webview-ui/src/components/settings/__tests__/SettingsView.test.tsx index 37654326107..e15898feaf0 100644 --- a/webview-ui/src/components/settings/__tests__/SettingsView.test.tsx +++ b/webview-ui/src/components/settings/__tests__/SettingsView.test.tsx @@ -162,7 +162,7 @@ describe("SettingsView - Sound Settings", () => { // Volume slider should be visible const volumeSlider = screen.getByRole("slider", { name: /volume/i }) expect(volumeSlider).toBeInTheDocument() - expect(volumeSlider).toHaveValue("0.5") + expect(volumeSlider).toHaveValue("50") }) it("updates volume and sends message to VSCode when slider changes", () => { @@ -176,7 +176,7 @@ describe("SettingsView - Sound Settings", () => { // Change volume const volumeSlider = screen.getByRole("slider", { name: /volume/i }) - fireEvent.change(volumeSlider, { target: { value: "0.75" } }) + fireEvent.change(volumeSlider, { target: { value: "75" } }) // Click Done to save settings const doneButton = screen.getByText("Done")