Use the Slider component everywhere (#1786)

* Use the Slider component everywhere

* onValueCommit -> onValueChange

* More cleanup

* Fix tests

* Add missing translations

* Update webview-ui/src/components/settings/ContextManagementSettings.tsx

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

* SelectDropdown fixes

* Fix tests

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
This commit is contained in:
Chris Estreich 2025-03-18 15:27:28 -07:00 committed by GitHub
parent 2267cad93d
commit f4abdf4b95
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
40 changed files with 706 additions and 768 deletions

View file

@ -141,10 +141,17 @@ function checkAreaTranslations(area) {
}
// Load file contents
const englishFileContents = englishFiles.map((file) => ({
name: file,
content: JSON.parse(fs.readFileSync(path.join(englishDir, file), "utf8")),
}))
let englishFileContents
try {
englishFileContents = englishFiles.map((file) => ({
name: file,
content: JSON.parse(fs.readFileSync(path.join(englishDir, file), "utf8")),
}))
} catch (e) {
console.error(`Error: File '${englishDir}' is not a valid JSON file`)
process.exit(1)
}
console.log(
`Checking ${englishFileContents.length} translation file(s): ${englishFileContents.map((f) => f.name).join(", ")}`,
@ -167,7 +174,14 @@ function checkAreaTranslations(area) {
}
// Load the locale file
const localeContent = JSON.parse(fs.readFileSync(localeFilePath, "utf8"))
let localeContent
try {
localeContent = JSON.parse(fs.readFileSync(localeFilePath, "utf8"))
} catch (e) {
console.error(`Error: File '${localeFilePath}' is not a valid JSON file`)
process.exit(1)
}
// Find all keys in the English file
const englishKeys = findKeys(englishContent)

View file

@ -103,15 +103,3 @@ export const VSCodeRadio: React.FC<VSCodeProps> = ({ children, value, checked, o
export const VSCodeRadioGroup: React.FC<VSCodeProps> = ({ children, onChange, ...props }) =>
React.createElement("div", { role: "radiogroup", onChange, ...props }, children)
export const VSCodeSlider: React.FC<VSCodeProps> = ({ value, onChange, ...props }) =>
React.createElement("input", {
type: "range",
value,
onChange: (e: any) => onChange?.({ target: { value: Number(e.target.value) } }),
min: 0,
max: 1,
step: 0.01,
style: { flexGrow: 1, height: "2px" },
...props,
})

View file

@ -1,9 +1,9 @@
import React from "react"
export const Checkbox = ({ children, checked, onChange }: any) =>
export const Checkbox = ({ children, onChange }: any) =>
React.createElement("div", { "data-testid": "mock-checkbox", onClick: onChange }, children)
export const Dropdown = ({ children, value, onChange }: any) =>
export const Dropdown = ({ children, onChange }: any) =>
React.createElement("div", { "data-testid": "mock-dropdown", onClick: onChange }, children)
export const Pane = ({ children }: any) => React.createElement("div", { "data-testid": "mock-pane" }, children)

View file

@ -6,26 +6,22 @@ import { Cog } from "lucide-react"
import { EXPERIMENT_IDS, ExperimentId } from "../../../../src/shared/experiments"
import { cn } from "@/lib/utils"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Slider } from "@/components/ui"
import { SetCachedStateField, SetExperimentEnabled } from "./types"
import { sliderLabelStyle } from "./styles"
import { SectionHeader } from "./SectionHeader"
import { Section } from "./Section"
type AdvancedSettingsProps = HTMLAttributes<HTMLDivElement> & {
rateLimitSeconds: number
terminalShellIntegrationTimeout: number | undefined
diffEnabled?: boolean
fuzzyMatchThreshold?: number
setCachedStateField: SetCachedStateField<
"rateLimitSeconds" | "diffEnabled" | "fuzzyMatchThreshold" | "terminalShellIntegrationTimeout"
>
setCachedStateField: SetCachedStateField<"rateLimitSeconds" | "diffEnabled" | "fuzzyMatchThreshold">
experiments: Record<ExperimentId, boolean>
setExperimentEnabled: SetExperimentEnabled
}
export const AdvancedSettings = ({
rateLimitSeconds,
terminalShellIntegrationTimeout,
diffEnabled,
fuzzyMatchThreshold,
setCachedStateField,
@ -35,6 +31,7 @@ export const AdvancedSettings = ({
...props
}: AdvancedSettingsProps) => {
const { t } = useAppTranslation()
return (
<div className={cn("flex flex-col gap-2", className)} {...props}>
<SectionHeader>
@ -49,50 +46,18 @@ export const AdvancedSettings = ({
<div className="flex flex-col gap-2">
<span className="font-medium">{t("settings:advanced.rateLimit.label")}</span>
<div className="flex items-center gap-2">
<input
type="range"
min="0"
max="60"
step="1"
value={rateLimitSeconds}
onChange={(e) => setCachedStateField("rateLimitSeconds", parseInt(e.target.value))}
className="h-2 focus:outline-0 w-4/5 accent-vscode-button-background"
<Slider
min={0}
max={60}
step={1}
value={[rateLimitSeconds]}
onValueChange={([value]) => setCachedStateField("rateLimitSeconds", value)}
/>
<span style={{ ...sliderLabelStyle }}>{rateLimitSeconds}s</span>
<span className="w-10">{rateLimitSeconds}s</span>
</div>
</div>
<p className="text-vscode-descriptionForeground text-sm mt-0">
<div className="text-vscode-descriptionForeground text-sm mt-1">
{t("settings:advanced.rateLimit.description")}
</p>
</div>
<div>
<div className="flex flex-col gap-2">
<span className="font-medium">Terminal shell integration timeout</span>
<div className="flex items-center gap-2">
<input
type="range"
min="1000"
max="60000"
step="1000"
value={terminalShellIntegrationTimeout}
onChange={(e) =>
setCachedStateField(
"terminalShellIntegrationTimeout",
Math.min(60000, Math.max(1000, parseInt(e.target.value))),
)
}
className="h-2 focus:outline-0 w-4/5 accent-vscode-button-background"
/>
<span style={{ ...sliderLabelStyle }}>
{(terminalShellIntegrationTimeout ?? 4000) / 1000}s
</span>
</div>
<p className="text-vscode-descriptionForeground text-sm mt-0">
Maximum time to wait for shell integration to initialize before executing commands. For
users with long shell startup times, this value may need to be increased if you see "Shell
Integration Unavailable" errors in the terminal.
</p>
</div>
</div>
@ -109,48 +74,53 @@ export const AdvancedSettings = ({
}}>
<span className="font-medium">{t("settings:advanced.diff.label")}</span>
</VSCodeCheckbox>
<p className="text-vscode-descriptionForeground text-sm mt-0">
<div className="text-vscode-descriptionForeground text-sm">
{t("settings:advanced.diff.description")}
</p>
{diffEnabled && (
<div className="flex flex-col gap-2 mt-3 mb-2 pl-3 border-l-2 border-vscode-button-background">
<div className="flex flex-col gap-2">
<span className="font-medium">{t("settings:advanced.diff.strategy.label")}</span>
<select
value={
experiments[EXPERIMENT_IDS.DIFF_STRATEGY]
? "unified"
: experiments[EXPERIMENT_IDS.MULTI_SEARCH_AND_REPLACE]
? "multiBlock"
: "standard"
}
onChange={(e) => {
const value = e.target.value
if (value === "standard") {
setExperimentEnabled(EXPERIMENT_IDS.DIFF_STRATEGY, false)
setExperimentEnabled(EXPERIMENT_IDS.MULTI_SEARCH_AND_REPLACE, false)
} else if (value === "unified") {
setExperimentEnabled(EXPERIMENT_IDS.DIFF_STRATEGY, true)
setExperimentEnabled(EXPERIMENT_IDS.MULTI_SEARCH_AND_REPLACE, false)
} else if (value === "multiBlock") {
setExperimentEnabled(EXPERIMENT_IDS.DIFF_STRATEGY, false)
setExperimentEnabled(EXPERIMENT_IDS.MULTI_SEARCH_AND_REPLACE, true)
}
}}
className="p-2 rounded w-full bg-vscode-input-background text-vscode-input-foreground border border-vscode-input-border outline-none focus:border-vscode-focusBorder">
<option value="standard">
{t("settings:advanced.diff.strategy.options.standard")}
</option>
<option value="multiBlock">
{t("settings:advanced.diff.strategy.options.multiBlock")}
</option>
<option value="unified">
{t("settings:advanced.diff.strategy.options.unified")}
</option>
</select>
</div>
</div>
</div>
<p className="text-vscode-descriptionForeground text-sm mt-1">
{diffEnabled && (
<div className="flex flex-col gap-3 pl-3 border-l-2 border-vscode-button-background">
<div>
<label className="block font-medium mb-1">
{t("settings:advanced.diff.strategy.label")}
</label>
<Select
value={
experiments[EXPERIMENT_IDS.DIFF_STRATEGY]
? "unified"
: experiments[EXPERIMENT_IDS.MULTI_SEARCH_AND_REPLACE]
? "multiBlock"
: "standard"
}
onValueChange={(value) => {
if (value === "standard") {
setExperimentEnabled(EXPERIMENT_IDS.DIFF_STRATEGY, false)
setExperimentEnabled(EXPERIMENT_IDS.MULTI_SEARCH_AND_REPLACE, false)
} else if (value === "unified") {
setExperimentEnabled(EXPERIMENT_IDS.DIFF_STRATEGY, true)
setExperimentEnabled(EXPERIMENT_IDS.MULTI_SEARCH_AND_REPLACE, false)
} else if (value === "multiBlock") {
setExperimentEnabled(EXPERIMENT_IDS.DIFF_STRATEGY, false)
setExperimentEnabled(EXPERIMENT_IDS.MULTI_SEARCH_AND_REPLACE, true)
}
}}>
<SelectTrigger className="w-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="standard">
{t("settings:advanced.diff.strategy.options.standard")}
</SelectItem>
<SelectItem value="multiBlock">
{t("settings:advanced.diff.strategy.options.multiBlock")}
</SelectItem>
<SelectItem value="unified">
{t("settings:advanced.diff.strategy.options.unified")}
</SelectItem>
</SelectContent>
</Select>
<div className="text-vscode-descriptionForeground text-sm mt-1">
{!experiments[EXPERIMENT_IDS.DIFF_STRATEGY] &&
!experiments[EXPERIMENT_IDS.MULTI_SEARCH_AND_REPLACE] &&
t("settings:advanced.diff.strategy.descriptions.standard")}
@ -158,31 +128,29 @@ export const AdvancedSettings = ({
t("settings:advanced.diff.strategy.descriptions.unified")}
{experiments[EXPERIMENT_IDS.MULTI_SEARCH_AND_REPLACE] &&
t("settings:advanced.diff.strategy.descriptions.multiBlock")}
</p>
<span className="font-medium mt-3">{t("settings:advanced.diff.matchPrecision.label")}</span>
<div className="flex items-center gap-2">
<input
type="range"
min="0.8"
max="1"
step="0.005"
value={fuzzyMatchThreshold ?? 1.0}
onChange={(e) => {
setCachedStateField("fuzzyMatchThreshold", parseFloat(e.target.value))
}}
className="h-2 focus:outline-0 w-4/5 accent-vscode-button-background"
/>
<span style={{ ...sliderLabelStyle }}>
{Math.round((fuzzyMatchThreshold || 1) * 100)}%
</span>
</div>
<p className="text-vscode-descriptionForeground text-sm mt-0">
{t("settings:advanced.diff.matchPrecision.description")}
</p>
</div>
)}
</div>
<div>
<label className="block font-medium mb-1">
{t("settings:advanced.diff.matchPrecision.label")}
</label>
<div className="flex items-center gap-2">
<Slider
min={0.8}
max={1}
step={0.005}
value={[fuzzyMatchThreshold ?? 1.0]}
onValueChange={([value]) => setCachedStateField("fuzzyMatchThreshold", value)}
/>
<span className="w-10">{Math.round((fuzzyMatchThreshold || 1) * 100)}%</span>
</div>
<div className="text-vscode-descriptionForeground text-sm mt-1">
{t("settings:advanced.diff.matchPrecision.description")}
</div>
</div>
</div>
)}
</Section>
</div>
)

View file

@ -1,11 +1,21 @@
import { VSCodeButton, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import { memo, useEffect, useRef, useState } from "react"
import { useAppTranslation } from "@/i18n/TranslationContext"
import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import { ApiConfigMeta } from "../../../../src/shared/ExtensionMessage"
import { Dropdown } from "vscrui"
import type { DropdownOption } from "vscrui"
import { Dialog, DialogContent, DialogTitle } from "../ui/dialog"
import { Button, Input } from "../ui"
import { useAppTranslation } from "@/i18n/TranslationContext"
import {
Button,
Input,
Dialog,
DialogContent,
DialogTitle,
Select,
SelectTrigger,
SelectValue,
SelectContent,
SelectItem,
} from "@/components/ui"
interface ApiConfigManagerProps {
currentApiConfigName?: string
@ -25,6 +35,7 @@ const ApiConfigManager = ({
onUpsertConfig,
}: ApiConfigManagerProps) => {
const { t } = useAppTranslation()
const [isRenaming, setIsRenaming] = useState(false)
const [isCreating, setIsCreating] = useState(false)
const [inputValue, setInputValue] = useState("")
@ -39,12 +50,12 @@ const ApiConfigManager = ({
const nameExists = listApiConfigMeta?.some((config) => config.name.toLowerCase() === trimmed.toLowerCase())
// For new profiles, any existing name is invalid
// For new profiles, any existing name is invalid.
if (isNewProfile && nameExists) {
return t("settings:providers.nameExists")
}
// For rename, only block if trying to rename to a different existing profile
// For rename, only block if trying to rename to a different existing profile.
if (!isNewProfile && nameExists && trimmed.toLowerCase() !== currentApiConfigName?.toLowerCase()) {
return t("settings:providers.nameExists")
}
@ -64,7 +75,7 @@ const ApiConfigManager = ({
setError(null)
}
// Focus input when entering rename mode
// Focus input when entering rename mode.
useEffect(() => {
if (isRenaming) {
const timeoutId = setTimeout(() => inputRef.current?.focus(), 0)
@ -72,7 +83,7 @@ const ApiConfigManager = ({
}
}, [isRenaming])
// Focus input when opening new dialog
// Focus input when opening new dialog.
useEffect(() => {
if (isCreating) {
const timeoutId = setTimeout(() => newProfileInputRef.current?.focus(), 0)
@ -80,7 +91,7 @@ const ApiConfigManager = ({
}
}, [isCreating])
// Reset state when current profile changes
// Reset state when current profile changes.
useEffect(() => {
resetCreateState()
resetRenameState()
@ -137,7 +148,7 @@ const ApiConfigManager = ({
const handleDelete = () => {
if (!currentApiConfigName || !listApiConfigMeta || listApiConfigMeta.length <= 1) return
// Let the extension handle both deletion and selection
// Let the extension handle both deletion and selection.
onDeleteConfig(currentApiConfigName)
}
@ -145,15 +156,11 @@ const ApiConfigManager = ({
return (
<div className="flex flex-col gap-1">
<label htmlFor="config-profile">
<span className="font-medium">{t("settings:providers.configProfile")}</span>
</label>
<label className="block font-medium mb-1">{t("settings:providers.configProfile")}</label>
{isRenaming ? (
<div
data-testid="rename-form"
style={{ display: "flex", gap: "4px", alignItems: "center", flexDirection: "column" }}>
<div style={{ display: "flex", gap: "4px", alignItems: "center", width: "100%" }}>
<div data-testid="rename-form">
<div className="flex items-center gap-1">
<VSCodeTextField
ref={inputRef}
value={inputValue}
@ -163,100 +170,75 @@ const ApiConfigManager = ({
setError(null)
}}
placeholder={t("settings:providers.enterNewName")}
style={{ flexGrow: 1 }}
onKeyDown={(e: unknown) => {
const event = e as { key: string }
if (event.key === "Enter" && inputValue.trim()) {
onKeyDown={({ key }) => {
if (key === "Enter" && inputValue.trim()) {
handleSave()
} else if (event.key === "Escape") {
} else if (key === "Escape") {
handleCancel()
}
}}
className="grow"
/>
<VSCodeButton
appearance="icon"
<Button
variant="ghost"
size="icon"
disabled={!inputValue.trim()}
onClick={handleSave}
title={t("settings:common.save")}
data-testid="save-rename-button"
style={{
padding: 0,
margin: 0,
height: "28px",
width: "28px",
minWidth: "28px",
}}>
data-testid="save-rename-button">
<span className="codicon codicon-check" />
</VSCodeButton>
<VSCodeButton
appearance="icon"
</Button>
<Button
variant="ghost"
size="icon"
onClick={handleCancel}
title={t("settings:common.cancel")}
data-testid="cancel-rename-button"
style={{
padding: 0,
margin: 0,
height: "28px",
width: "28px",
minWidth: "28px",
}}>
data-testid="cancel-rename-button">
<span className="codicon codicon-close" />
</VSCodeButton>
</Button>
</div>
{error && (
<p className="text-red-500 text-sm mt-2" data-testid="error-message">
<div className="text-vscode-descriptionForeground text-sm mt-1" data-testid="error-message">
{error}
</p>
</div>
)}
</div>
) : (
<>
<div style={{ display: "flex", gap: "4px", alignItems: "center" }}>
<Dropdown
id="config-profile"
value={currentApiConfigName}
onChange={(value: unknown) => {
onSelectConfig((value as DropdownOption).value)
}}
role="combobox"
options={listApiConfigMeta.map((config) => ({
value: config.name,
label: config.name,
}))}
className="w-full"
/>
<VSCodeButton
appearance="icon"
<div className="flex items-center gap-1">
<Select value={currentApiConfigName} onValueChange={onSelectConfig}>
<SelectTrigger className="grow">
<SelectValue placeholder={t("settings:common.select")} />
</SelectTrigger>
<SelectContent>
{listApiConfigMeta.map((config) => (
<SelectItem key={config.name} value={config.name}>
{config.name}
</SelectItem>
))}
</SelectContent>
</Select>
<Button
variant="ghost"
size="icon"
onClick={handleAdd}
title={t("settings:providers.addProfile")}
data-testid="add-profile-button"
style={{
padding: 0,
margin: 0,
height: "28px",
width: "28px",
minWidth: "28px",
}}>
data-testid="add-profile-button">
<span className="codicon codicon-add" />
</VSCodeButton>
</Button>
{currentApiConfigName && (
<>
<VSCodeButton
appearance="icon"
<Button
variant="ghost"
size="icon"
onClick={handleStartRename}
title={t("settings:providers.renameProfile")}
data-testid="rename-profile-button"
style={{
padding: 0,
margin: 0,
height: "28px",
width: "28px",
minWidth: "28px",
}}>
data-testid="rename-profile-button">
<span className="codicon codicon-edit" />
</VSCodeButton>
<VSCodeButton
appearance="icon"
</Button>
<Button
variant="ghost"
size="icon"
onClick={handleDelete}
title={
isOnlyProfile
@ -264,27 +246,15 @@ const ApiConfigManager = ({
: t("settings:providers.deleteProfile")
}
data-testid="delete-profile-button"
disabled={isOnlyProfile}
style={{
padding: 0,
margin: 0,
height: "28px",
width: "28px",
minWidth: "28px",
}}>
disabled={isOnlyProfile}>
<span className="codicon codicon-trash" />
</VSCodeButton>
</Button>
</>
)}
</div>
<p
style={{
fontSize: "12px",
margin: "5px 0 12px",
color: "var(--vscode-descriptionForeground)",
}}>
<div className="text-vscode-descriptionForeground text-sm mt-1">
{t("settings:providers.description")}
</p>
</div>
</>
)}

View file

@ -249,7 +249,7 @@ const ApiOptions = ({
value={selectedProvider}
onValueChange={(value) => setApiConfigurationField("apiProvider", value as ApiProvider)}>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select" />
<SelectValue placeholder={t("settings:common.select")} />
</SelectTrigger>
<SelectContent>
<SelectItem value="openrouter">OpenRouter</SelectItem>
@ -521,7 +521,7 @@ const ApiOptions = ({
value={apiConfiguration?.awsRegion || ""}
onValueChange={(value) => setApiConfigurationField("awsRegion", value)}>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select" />
<SelectValue placeholder={t("settings:common.select")} />
</SelectTrigger>
<SelectContent>
{AWS_REGIONS.map(({ value, label }) => (
@ -595,7 +595,7 @@ const ApiOptions = ({
value={apiConfiguration?.vertexRegion || ""}
onValueChange={(value) => setApiConfigurationField("vertexRegion", value)}>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select" />
<SelectValue placeholder={t("settings:common.select")} />
</SelectTrigger>
<SelectContent>
{VERTEX_REGIONS.map(({ value, label }) => (
@ -1222,7 +1222,7 @@ const ApiOptions = ({
return { vendor, family }
})}>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select" />
<SelectValue placeholder={t("settings:common.select")} />
</SelectTrigger>
<SelectContent>
{vsCodeLmModels.map((model) => (
@ -1361,7 +1361,7 @@ const ApiOptions = ({
setApiConfigurationField("openRouterSpecificProvider", value)
}}>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select" />
<SelectValue placeholder={t("settings:common.select")} />
</SelectTrigger>
<SelectContent>
<SelectItem value={OPENROUTER_DEFAULT_PROVIDER_NAME}>
@ -1374,7 +1374,7 @@ const ApiOptions = ({
))}
</SelectContent>
</Select>
<div className="text-sm text-vscode-descriptionForeground">
<div className="text-sm text-vscode-descriptionForeground mt-1">
{t("settings:providers.openRouter.providerRouting.description")}{" "}
<a href="https://openrouter.ai/docs/features/provider-routing">
{t("settings:providers.openRouter.providerRouting.learnMore")}.
@ -1441,7 +1441,7 @@ const ApiOptions = ({
}
}}>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select" />
<SelectValue placeholder={t("settings:common.select")} />
</SelectTrigger>
<SelectContent>
{selectedProviderModelOptions.map((option) => (

View file

@ -4,7 +4,7 @@ import { VSCodeButton, VSCodeCheckbox, VSCodeTextField } from "@vscode/webview-u
import { CheckCheck } from "lucide-react"
import { vscode } from "@/utils/vscode"
import { ExtensionStateContextType } from "@/context/ExtensionStateContext"
import { Slider } from "@/components/ui"
import { SetCachedStateField } from "./types"
import { SectionHeader } from "./SectionHeader"
@ -22,7 +22,19 @@ type AutoApproveSettingsProps = HTMLAttributes<HTMLDivElement> & {
alwaysAllowSubtasks?: boolean
alwaysAllowExecute?: boolean
allowedCommands?: string[]
setCachedStateField: SetCachedStateField<keyof ExtensionStateContextType>
setCachedStateField: SetCachedStateField<
| "alwaysAllowReadOnly"
| "alwaysAllowWrite"
| "writeDelayMs"
| "alwaysAllowBrowser"
| "alwaysApproveResubmit"
| "requestDelaySeconds"
| "alwaysAllowMcp"
| "alwaysAllowModeSwitch"
| "alwaysAllowSubtasks"
| "alwaysAllowExecute"
| "allowedCommands"
>
}
export const AutoApproveSettings = ({
@ -71,9 +83,9 @@ export const AutoApproveSettings = ({
data-testid="always-allow-readonly-checkbox">
<span className="font-medium">{t("settings:autoApprove.readOnly.label")}</span>
</VSCodeCheckbox>
<p className="text-vscode-descriptionForeground text-sm mt-0">
<div className="text-vscode-descriptionForeground text-sm mt-1">
{t("settings:autoApprove.readOnly.description")}
</p>
</div>
</div>
<div>
@ -83,36 +95,32 @@ export const AutoApproveSettings = ({
data-testid="always-allow-write-checkbox">
<span className="font-medium">{t("settings:autoApprove.write.label")}</span>
</VSCodeCheckbox>
<p className="text-vscode-descriptionForeground text-sm mt-0">
<div className="text-vscode-descriptionForeground text-sm mt-1">
{t("settings:autoApprove.write.description")}
</p>
{alwaysAllowWrite && (
<div
style={{
marginTop: 10,
paddingLeft: 10,
borderLeft: "2px solid var(--vscode-button-background)",
}}>
<div style={{ display: "flex", alignItems: "center", gap: "10px" }}>
<input
type="range"
min="0"
max="5000"
step="100"
value={writeDelayMs}
onChange={(e) => setCachedStateField("writeDelayMs", parseInt(e.target.value))}
data-testid="write-delay-slider"
className="h-2 focus:outline-0 w-4/5 accent-vscode-button-background"
/>
<span style={{ minWidth: "45px", textAlign: "left" }}>{writeDelayMs}ms</span>
</div>
<p className="text-vscode-descriptionForeground text-sm mt-1">
{t("settings:autoApprove.write.delayLabel")}
</p>
</div>
)}
</div>
</div>
{alwaysAllowWrite && (
<div className="flex flex-col gap-3 pl-3 border-l-2 border-vscode-button-background">
<div>
<div className="flex items-center gap-2">
<Slider
min={0}
max={5000}
step={100}
value={[writeDelayMs]}
onValueChange={([value]) => setCachedStateField("writeDelayMs", value)}
data-testid="write-delay-slider"
/>
<span className="w-20">{writeDelayMs}ms</span>
</div>
<div className="text-vscode-descriptionForeground text-sm mt-1">
{t("settings:autoApprove.write.delayLabel")}
</div>
</div>
</div>
)}
<div>
<VSCodeCheckbox
checked={alwaysAllowBrowser}
@ -120,11 +128,10 @@ export const AutoApproveSettings = ({
data-testid="always-allow-browser-checkbox">
<span className="font-medium">{t("settings:autoApprove.browser.label")}</span>
</VSCodeCheckbox>
<p className="text-vscode-descriptionForeground text-sm mt-0">
{t("settings:autoApprove.browser.description")}
<br />
{t("settings:autoApprove.browser.note")}
</p>
<div className="text-vscode-descriptionForeground text-sm mt-1">
<div>{t("settings:autoApprove.browser.description")}</div>
<div>{t("settings:autoApprove.browser.note")}</div>
</div>
</div>
<div>
@ -134,38 +141,32 @@ export const AutoApproveSettings = ({
data-testid="always-approve-resubmit-checkbox">
<span className="font-medium">{t("settings:autoApprove.retry.label")}</span>
</VSCodeCheckbox>
<p className="text-vscode-descriptionForeground text-sm mt-0">
<div className="text-vscode-descriptionForeground text-sm mt-1">
{t("settings:autoApprove.retry.description")}
</p>
{alwaysApproveResubmit && (
<div
style={{
marginTop: 10,
paddingLeft: 10,
borderLeft: "2px solid var(--vscode-button-background)",
}}>
<div style={{ display: "flex", alignItems: "center", gap: "10px" }}>
<input
type="range"
min="5"
max="100"
step="1"
value={requestDelaySeconds}
onChange={(e) =>
setCachedStateField("requestDelaySeconds", parseInt(e.target.value))
}
data-testid="request-delay-slider"
className="h-2 focus:outline-0 w-4/5 accent-vscode-button-background"
/>
<span style={{ minWidth: "45px", textAlign: "left" }}>{requestDelaySeconds}s</span>
</div>
<p className="text-vscode-descriptionForeground text-sm mt-0">
{t("settings:autoApprove.retry.delayLabel")}
</p>
</div>
)}
</div>
</div>
{alwaysApproveResubmit && (
<div className="flex flex-col gap-3 pl-3 border-l-2 border-vscode-button-background">
<div>
<div className="flex items-center gap-2">
<Slider
min={5}
max={100}
step={1}
value={[requestDelaySeconds]}
onValueChange={([value]) => setCachedStateField("requestDelaySeconds", value)}
data-testid="request-delay-slider"
/>
<span className="w-20">{requestDelaySeconds}s</span>
</div>
<div className="text-vscode-descriptionForeground text-sm mt-1">
{t("settings:autoApprove.retry.delayLabel")}
</div>
</div>
</div>
)}
<div>
<VSCodeCheckbox
checked={alwaysAllowMcp}
@ -173,9 +174,9 @@ export const AutoApproveSettings = ({
data-testid="always-allow-mcp-checkbox">
<span className="font-medium">{t("settings:autoApprove.mcp.label")}</span>
</VSCodeCheckbox>
<p className="text-vscode-descriptionForeground text-sm mt-0">
<div className="text-vscode-descriptionForeground text-sm mt-1">
{t("settings:autoApprove.mcp.description")}
</p>
</div>
</div>
<div>
@ -185,9 +186,9 @@ export const AutoApproveSettings = ({
data-testid="always-allow-mode-switch-checkbox">
<span className="font-medium">{t("settings:autoApprove.modeSwitch.label")}</span>
</VSCodeCheckbox>
<p className="text-vscode-descriptionForeground text-sm mt-0">
<div className="text-vscode-descriptionForeground text-sm mt-1">
{t("settings:autoApprove.modeSwitch.description")}
</p>
</div>
</div>
<div>
@ -197,9 +198,9 @@ export const AutoApproveSettings = ({
data-testid="always-allow-subtasks-checkbox">
<span className="font-medium">{t("settings:autoApprove.subtasks.label")}</span>
</VSCodeCheckbox>
<p className="text-vscode-descriptionForeground text-sm mt-0">
<div className="text-vscode-descriptionForeground text-sm mt-1">
{t("settings:autoApprove.subtasks.description")}
</p>
</div>
</div>
<div>
@ -209,71 +210,63 @@ export const AutoApproveSettings = ({
data-testid="always-allow-execute-checkbox">
<span className="font-medium">{t("settings:autoApprove.execute.label")}</span>
</VSCodeCheckbox>
<p className="text-vscode-descriptionForeground text-sm mt-0">
<div className="text-vscode-descriptionForeground text-sm mt-1">
{t("settings:autoApprove.execute.description")}
</p>
{alwaysAllowExecute && (
<div
style={{
marginTop: 10,
paddingLeft: 10,
borderLeft: "2px solid var(--vscode-button-background)",
}}>
<span className="font-medium" data-testid="allowed-commands-heading">
</div>
</div>
{alwaysAllowExecute && (
<div className="flex flex-col gap-3 pl-3 border-l-2 border-vscode-button-background">
<div>
<label className="block font-medium mb-1" data-testid="allowed-commands-heading">
{t("settings:autoApprove.execute.allowedCommands")}
</span>
<p className="text-vscode-descriptionForeground text-sm mt-0">
</label>
<div className="text-vscode-descriptionForeground text-sm mt-1">
{t("settings:autoApprove.execute.allowedCommandsDescription")}
</p>
<div style={{ display: "flex", gap: "5px", marginTop: "10px" }}>
<VSCodeTextField
value={commandInput}
onInput={(e: any) => setCommandInput(e.target.value)}
onKeyDown={(e: any) => {
if (e.key === "Enter") {
e.preventDefault()
handleAddCommand()
}
}}
placeholder={t("settings:autoApprove.execute.commandPlaceholder")}
data-testid="command-input"
style={{ flexGrow: 1 }}
/>
<VSCodeButton onClick={handleAddCommand} data-testid="add-command-button">
{t("settings:autoApprove.execute.addButton")}
</VSCodeButton>
</div>
<div
style={{
marginTop: "10px",
display: "flex",
flexWrap: "wrap",
gap: "5px",
}}>
{(allowedCommands ?? []).map((cmd, index) => (
<div
key={index}
className="border border-vscode-input-border bg-primary text-primary-foreground flex items-center gap-1 rounded-xs px-1.5 p-0.5">
<span>{cmd}</span>
<VSCodeButton
appearance="icon"
className="text-primary-foreground"
data-testid={`remove-command-${index}`}
onClick={() => {
const newCommands = (allowedCommands ?? []).filter(
(_, i) => i !== index,
)
setCachedStateField("allowedCommands", newCommands)
vscode.postMessage({ type: "allowedCommands", commands: newCommands })
}}>
<span className="codicon codicon-close" />
</VSCodeButton>
</div>
))}
</div>
</div>
)}
</div>
<div className="flex gap-2">
<VSCodeTextField
value={commandInput}
onInput={(e: any) => setCommandInput(e.target.value)}
onKeyDown={(e: any) => {
if (e.key === "Enter") {
e.preventDefault()
handleAddCommand()
}
}}
placeholder={t("settings:autoApprove.execute.commandPlaceholder")}
className="grow"
data-testid="command-input"
/>
<VSCodeButton onClick={handleAddCommand} data-testid="add-command-button">
{t("settings:autoApprove.execute.addButton")}
</VSCodeButton>
</div>
<div className="flex flex-wrap gap-2">
{(allowedCommands ?? []).map((cmd, index) => (
<div
key={index}
className="border border-vscode-input-border bg-primary text-primary-foreground flex items-center gap-1 rounded-xs px-1.5 p-0.5">
<span>{cmd}</span>
<VSCodeButton
appearance="icon"
className="text-primary-foreground"
data-testid={`remove-command-${index}`}
onClick={() => {
const newCommands = (allowedCommands ?? []).filter((_, i) => i !== index)
setCachedStateField("allowedCommands", newCommands)
vscode.postMessage({ type: "allowedCommands", commands: newCommands })
}}>
<span className="codicon codicon-close" />
</VSCodeButton>
</div>
))}
</div>
</div>
)}
</Section>
</div>
)

View file

@ -4,10 +4,9 @@ import { SquareMousePointer } from "lucide-react"
import { vscode } from "@/utils/vscode"
import { useAppTranslation } from "@/i18n/TranslationContext"
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from "@/components/ui"
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue, Slider } from "@/components/ui"
import { SetCachedStateField } from "./types"
import { sliderLabelStyle } from "./styles"
import { SectionHeader } from "./SectionHeader"
import { Section } from "./Section"
@ -128,119 +127,110 @@ export const BrowserSettings = ({
onChange={(e: any) => setCachedStateField("browserToolEnabled", e.target.checked)}>
<span className="font-medium">{t("settings:browser.enable.label")}</span>
</VSCodeCheckbox>
<p className="text-vscode-descriptionForeground text-sm mt-0">
<div className="text-vscode-descriptionForeground text-sm mt-1">
{t("settings:browser.enable.description")}
</p>
{browserToolEnabled && (
<div
style={{
marginLeft: 0,
paddingLeft: 10,
borderLeft: "2px solid var(--vscode-button-background)",
}}>
<div>
<label className="block font-medium mb-1">{t("settings:browser.viewport.label")}</label>
<Select
value={browserViewportSize}
onValueChange={(value) => setCachedStateField("browserViewportSize", value)}>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{options.map(({ value, label }) => (
<SelectItem key={value} value={value}>
{label}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
<p className="text-vscode-descriptionForeground text-sm mt-1">
{t("settings:browser.viewport.description")}
</p>
</div>
<div>
<div style={{ display: "flex", flexDirection: "column", gap: "5px" }}>
<span className="font-medium">{t("settings:browser.screenshotQuality.label")}</span>
<div style={{ display: "flex", alignItems: "center", gap: "5px" }}>
<input
type="range"
min="1"
max="100"
step="1"
value={screenshotQuality ?? 75}
className="h-2 focus:outline-0 w-4/5 accent-vscode-button-background"
onChange={(e) =>
setCachedStateField("screenshotQuality", parseInt(e.target.value))
}
/>
<span style={{ ...sliderLabelStyle }}>{screenshotQuality ?? 75}%</span>
</div>
</div>
<p className="text-vscode-descriptionForeground text-sm mt-0">
{t("settings:browser.screenshotQuality.description")}
</p>
</div>
<div className="mt-4">
<div className="mb-2">
<VSCodeCheckbox
checked={remoteBrowserEnabled}
onChange={(e: any) => {
// Update the global state - remoteBrowserEnabled now means "enable remote browser connection"
setCachedStateField("remoteBrowserEnabled", e.target.checked)
if (!e.target.checked) {
// If disabling remote browser, clear the custom URL
setCachedStateField("remoteBrowserHost", undefined)
}
}}>
<span className="font-medium">{t("settings:browser.remote.label")}</span>
</VSCodeCheckbox>
<p className="text-vscode-descriptionForeground text-sm mt-0 ml-6">
{t("settings:browser.remote.description")}
</p>
</div>
{remoteBrowserEnabled && (
<>
<div style={{ display: "flex", gap: "5px", marginTop: "10px" }}>
<VSCodeTextField
value={remoteBrowserHost ?? ""}
onChange={(e: any) =>
setCachedStateField(
"remoteBrowserHost",
e.target.value || undefined,
)
}
placeholder={t("settings:browser.remote.urlPlaceholder")}
style={{ flexGrow: 1 }}
/>
<VSCodeButton
disabled={testingConnection}
onClick={remoteBrowserHost ? testConnection : discoverBrowser}>
{testingConnection || discovering
? t("settings:browser.remote.testingButton")
: t("settings:browser.remote.testButton")}
</VSCodeButton>
</div>
{testResult && (
<div
className={`p-2 mt-2 mb-2 rounded text-sm ${
testResult.success
? "bg-green-800/20 text-green-400"
: "bg-red-800/20 text-red-400"
}`}>
{testResult.message}
</div>
)}
<p className="text-vscode-descriptionForeground text-sm mt-2">
{t("settings:browser.remote.instructions")}
</p>
</>
)}
</div>
</div>
{browserToolEnabled && (
<div className="flex flex-col gap-3 pl-3 border-l-2 border-vscode-button-background">
<div>
<label className="block font-medium mb-1">{t("settings:browser.viewport.label")}</label>
<Select
value={browserViewportSize}
onValueChange={(value) => setCachedStateField("browserViewportSize", value)}>
<SelectTrigger className="w-full">
<SelectValue placeholder={t("settings:common.select")} />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{options.map(({ value, label }) => (
<SelectItem key={value} value={value}>
{label}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
<div className="text-vscode-descriptionForeground text-sm mt-1">
{t("settings:browser.viewport.description")}
</div>
</div>
)}
</div>
<div>
<label className="block font-medium mb-1">
{t("settings:browser.screenshotQuality.label")}
</label>
<div className="flex items-center gap-2">
<Slider
min={1}
max={100}
step={1}
value={[screenshotQuality ?? 75]}
onValueChange={([value]) => setCachedStateField("screenshotQuality", value)}
/>
<span className="w-10">{screenshotQuality ?? 75}%</span>
</div>
<div className="text-vscode-descriptionForeground text-sm mt-1">
{t("settings:browser.screenshotQuality.description")}
</div>
</div>
<div>
<VSCodeCheckbox
checked={remoteBrowserEnabled}
onChange={(e: any) => {
// Update the global state - remoteBrowserEnabled now means "enable remote browser connection".
setCachedStateField("remoteBrowserEnabled", e.target.checked)
if (!e.target.checked) {
// If disabling remote browser, clear the custom URL.
setCachedStateField("remoteBrowserHost", undefined)
}
}}>
<label className="block font-medium mb-1">{t("settings:browser.remote.label")}</label>
</VSCodeCheckbox>
<div className="text-vscode-descriptionForeground text-sm mt-1">
{t("settings:browser.remote.description")}
</div>
</div>
{remoteBrowserEnabled && (
<>
<div className="flex items-center gap-2">
<VSCodeTextField
value={remoteBrowserHost ?? ""}
onChange={(e: any) =>
setCachedStateField("remoteBrowserHost", e.target.value || undefined)
}
placeholder={t("settings:browser.remote.urlPlaceholder")}
style={{ flexGrow: 1 }}
/>
<VSCodeButton
disabled={testingConnection}
onClick={remoteBrowserHost ? testConnection : discoverBrowser}>
{testingConnection || discovering
? t("settings:browser.remote.testingButton")
: t("settings:browser.remote.testButton")}
</VSCodeButton>
</div>
{testResult && (
<div
className={`p-2 rounded-xs text-sm ${
testResult.success
? "bg-green-800/20 text-green-400"
: "bg-red-800/20 text-red-400"
}`}>
{testResult.message}
</div>
)}
<div className="text-vscode-descriptionForeground text-sm mt-1">
{t("settings:browser.remote.instructions")}
</div>
</>
)}
</div>
)}
</Section>
</div>
)

View file

@ -4,9 +4,9 @@ import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"
import { Database } from "lucide-react"
import { cn } from "@/lib/utils"
import { Slider } from "@/components/ui"
import { SetCachedStateField } from "./types"
import { sliderLabelStyle } from "./styles"
import { SectionHeader } from "./SectionHeader"
import { Section } from "./Section"
@ -37,61 +37,55 @@ export const ContextManagementSettings = ({
<Section>
<div>
<div className="flex flex-col gap-2">
<span className="font-medium">{t("settings:contextManagement.openTabs.label")}</span>
<div className="flex items-center gap-2">
<input
type="range"
min="0"
max="500"
step="1"
value={maxOpenTabsContext ?? 20}
onChange={(e) => setCachedStateField("maxOpenTabsContext", parseInt(e.target.value))}
className="h-2 focus:outline-0 w-4/5 accent-vscode-button-background"
data-testid="open-tabs-limit-slider"
/>
<span style={{ ...sliderLabelStyle }}>{maxOpenTabsContext ?? 20}</span>
</div>
<span className="block font-medium mb-1">{t("settings:contextManagement.openTabs.label")}</span>
<div className="flex items-center gap-2">
<Slider
min={0}
max={500}
step={1}
value={[maxOpenTabsContext ?? 20]}
onValueChange={([value]) => setCachedStateField("maxOpenTabsContext", value)}
data-testid="open-tabs-limit-slider"
/>
<span className="w-10">{maxOpenTabsContext ?? 20}</span>
</div>
<p className="text-vscode-descriptionForeground text-sm mt-0">
<div className="text-vscode-descriptionForeground text-sm mt-1">
{t("settings:contextManagement.openTabs.description")}
</p>
</div>
</div>
<div>
<div className="flex flex-col gap-2">
<span className="font-medium">{t("settings:contextManagement.workspaceFiles.label")}</span>
<div className="flex items-center gap-2">
<input
type="range"
min="0"
max="500"
step="1"
value={maxWorkspaceFiles ?? 200}
onChange={(e) => setCachedStateField("maxWorkspaceFiles", parseInt(e.target.value))}
className="h-2 focus:outline-0 w-4/5 accent-vscode-button-background"
data-testid="workspace-files-limit-slider"
/>
<span style={{ ...sliderLabelStyle }}>{maxWorkspaceFiles ?? 200}</span>
</div>
<span className="block font-medium mb-1">
{t("settings:contextManagement.workspaceFiles.label")}
</span>
<div className="flex items-center gap-2">
<Slider
min={0}
max={500}
step={1}
value={[maxWorkspaceFiles ?? 200]}
onValueChange={([value]) => setCachedStateField("maxWorkspaceFiles", value)}
data-testid="workspace-files-limit-slider"
/>
<span className="w-10">{maxWorkspaceFiles ?? 200}</span>
</div>
<p className="text-vscode-descriptionForeground text-sm mt-0">
<div className="text-vscode-descriptionForeground text-sm mt-1">
{t("settings:contextManagement.workspaceFiles.description")}
</p>
</div>
</div>
<div>
<VSCodeCheckbox
checked={showRooIgnoredFiles}
onChange={(e: any) => {
setCachedStateField("showRooIgnoredFiles", e.target.checked)
}}
onChange={(e: any) => setCachedStateField("showRooIgnoredFiles", e.target.checked)}
data-testid="show-rooignored-files-checkbox">
<span className="font-medium">{t("settings:contextManagement.rooignore.label")}</span>
<label className="block font-medium mb-1">
{t("settings:contextManagement.rooignore.label")}
</label>
</VSCodeCheckbox>
<p className="text-vscode-descriptionForeground text-sm mt-0">
<div className="text-vscode-descriptionForeground text-sm mt-1">
{t("settings:contextManagement.rooignore.description")}
</p>
</div>
</div>
</Section>
</div>

View file

@ -47,7 +47,7 @@ export const LanguageSettings = ({ language, setCachedStateField, className, ...
<Section>
<Select value={language} onValueChange={(value) => setCachedStateField("language", value)}>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select" />
<SelectValue placeholder={t("settings:common.select")} />
</SelectTrigger>
<SelectContent>
<SelectGroup>

View file

@ -3,6 +3,7 @@ import { VSCodeLink } from "@vscode/webview-ui-toolkit/react"
import { Trans } from "react-i18next"
import { ChevronsUpDown, Check, X } from "lucide-react"
import { useAppTranslation } from "@/i18n/TranslationContext"
import { cn } from "@/lib/utils"
import {
Command,
@ -54,6 +55,8 @@ export const ModelPicker = ({
setApiConfigurationField,
defaultModelInfo,
}: ModelPickerProps) => {
const { t } = useAppTranslation()
const [open, setOpen] = useState(false)
const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false)
const isInitialized = useRef(false)
@ -117,7 +120,7 @@ export const ModelPicker = ({
role="combobox"
aria-expanded={open}
className="w-full justify-between">
<div>{selectedModelId ?? "Select"}</div>
<div>{selectedModelId ?? t("settings:common.select")}</div>
<ChevronsUpDown className="opacity-50" />
</Button>
</PopoverTrigger>

View file

@ -6,6 +6,7 @@ import { Bell } from "lucide-react"
import { SetCachedStateField } from "./types"
import { SectionHeader } from "./SectionHeader"
import { Section } from "./Section"
import { Slider } from "../ui"
type NotificationSettingsProps = HTMLAttributes<HTMLDivElement> & {
ttsEnabled?: boolean
@ -41,31 +42,32 @@ export const NotificationSettings = ({
data-testid="tts-enabled-checkbox">
<span className="font-medium">{t("settings:notifications.tts.label")}</span>
</VSCodeCheckbox>
<p className="text-vscode-descriptionForeground text-sm mt-0">
<div className="text-vscode-descriptionForeground text-sm mt-1">
{t("settings:notifications.tts.description")}
</p>
{ttsEnabled && (
<div className="pl-[10px] ml-0 border-l-2 border-l-vscode-button-background">
<div className="flex items-center gap-[5px]">
<input
type="range"
min="0.1"
max="2.0"
step="0.01"
value={ttsSpeed ?? 1.0}
onChange={(e) => setCachedStateField("ttsSpeed", parseFloat(e.target.value))}
className="h-2 focus:outline-0 w-4/5 accent-vscode-button-background"
aria-label="Speed"
</div>
</div>
{ttsEnabled && (
<div className="flex flex-col gap-3 pl-3 border-l-2 border-vscode-button-background">
<div>
<label className="block font-medium mb-1">
{t("settings:notifications.tts.speedLabel")}
</label>
<div className="flex items-center gap-2">
<Slider
min={0.1}
max={2.0}
step={0.01}
value={[ttsSpeed ?? 1.0]}
onValueChange={([value]) => setCachedStateField("ttsSpeed", value)}
data-testid="tts-speed-slider"
/>
<span className="min-w-[35px] text-left">{((ttsSpeed ?? 1.0) * 100).toFixed(0)}%</span>
<span className="w-10">{((ttsSpeed ?? 1.0) * 100).toFixed(0)}%</span>
</div>
<p className="text-vscode-descriptionForeground text-sm mt-1">
{t("settings:notifications.tts.speedLabel")}
</p>
</div>
)}
</div>
</div>
)}
<div>
<VSCodeCheckbox
checked={soundEnabled}
@ -73,33 +75,31 @@ export const NotificationSettings = ({
data-testid="sound-enabled-checkbox">
<span className="font-medium">{t("settings:notifications.sound.label")}</span>
</VSCodeCheckbox>
<p className="text-vscode-descriptionForeground text-sm mt-0">
<div className="text-vscode-descriptionForeground text-sm mt-1">
{t("settings:notifications.sound.description")}
</p>
{soundEnabled && (
<div className="pl-[10px] ml-0 border-l-2 border-l-vscode-button-background">
<div className="flex items-center gap-[5px]">
<input
type="range"
min="0"
max="1"
step="0.01"
value={soundVolume ?? 0.5}
onChange={(e) => setCachedStateField("soundVolume", parseFloat(e.target.value))}
className="h-2 focus:outline-0 w-4/5 accent-vscode-button-background"
aria-label="Volume"
</div>
</div>
{soundEnabled && (
<div className="flex flex-col gap-3 pl-3 border-l-2 border-vscode-button-background">
<div>
<label className="block font-medium mb-1">
{t("settings:notifications.sound.volumeLabel")}
</label>
<div className="flex items-center gap-2">
<Slider
min={0}
max={1}
step={0.01}
value={[soundVolume ?? 0.5]}
onValueChange={([value]) => setCachedStateField("soundVolume", value)}
data-testid="sound-volume-slider"
/>
<span className="min-w-[35px] text-left">
{((soundVolume ?? 0.5) * 100).toFixed(0)}%
</span>
<span className="w-10">{((soundVolume ?? 0.5) * 100).toFixed(0)}%</span>
</div>
<p className="text-vscode-descriptionForeground text-sm mt-1">
{t("settings:notifications.sound.volumeLabel")}
</p>
</div>
)}
</div>
</div>
)}
</Section>
</div>
)

View file

@ -5,5 +5,5 @@ import { cn } from "@/lib/utils"
type SectionProps = HTMLAttributes<HTMLDivElement>
export const Section = ({ className, ...props }: SectionProps) => (
<div className={cn("flex flex-col gap-2 p-5", className)} {...props} />
<div className={cn("flex flex-col gap-3 p-5", className)} {...props} />
)

View file

@ -466,7 +466,6 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone },
<div ref={advancedRef}>
<AdvancedSettings
rateLimitSeconds={rateLimitSeconds}
terminalShellIntegrationTimeout={terminalShellIntegrationTimeout}
diffEnabled={diffEnabled}
fuzzyMatchThreshold={fuzzyMatchThreshold}
setCachedStateField={setCachedStateField}

View file

@ -3,18 +3,22 @@ import { useEffect, useState } from "react"
import { useAppTranslation } from "@/i18n/TranslationContext"
import { useDebounce } from "react-use"
import { Slider } from "@/components/ui"
interface TemperatureControlProps {
value: number | undefined | null
onChange: (value: number | undefined | null) => void
maxValue?: number // Some providers like OpenAI use 0-2 range
maxValue?: number // Some providers like OpenAI use 0-2 range.
}
export const TemperatureControl = ({ value, onChange, maxValue = 1 }: TemperatureControlProps) => {
const { t } = useAppTranslation()
const [isCustomTemperature, setIsCustomTemperature] = useState(value !== undefined)
const [inputValue, setInputValue] = useState(value)
useDebounce(() => onChange(inputValue), 50, [onChange, inputValue])
// Sync internal state with prop changes when switching profiles
// Sync internal state with prop changes when switching profiles.
useEffect(() => {
const hasCustomTemperature = value !== undefined && value !== null
setIsCustomTemperature(hasCustomTemperature)
@ -29,39 +33,37 @@ export const TemperatureControl = ({ value, onChange, maxValue = 1 }: Temperatur
onChange={(e: any) => {
const isChecked = e.target.checked
setIsCustomTemperature(isChecked)
if (!isChecked) {
setInputValue(null) // Unset the temperature, note that undefined is unserializable
setInputValue(null) // Unset the temperature, note that undefined is unserializable.
} else {
setInputValue(value ?? 0) // Use the value from apiConfiguration, if set
setInputValue(value ?? 0) // Use the value from apiConfiguration, if set.
}
}}>
<span className="font-medium">{t("settings:temperature.useCustom")}</span>
<label className="block font-medium mb-1">{t("settings:temperature.useCustom")}</label>
</VSCodeCheckbox>
<div className="text-sm text-vscode-descriptionForeground">{t("settings:temperature.description")}</div>
<div className="text-sm text-vscode-descriptionForeground mt-1">
{t("settings:temperature.description")}
</div>
</div>
{isCustomTemperature && (
<div
style={{
marginLeft: 0,
paddingLeft: 10,
borderLeft: "2px solid var(--vscode-button-background)",
}}>
<div style={{ display: "flex", alignItems: "center", gap: "5px" }}>
<input
type="range"
min="0"
max={maxValue}
step="0.01"
value={inputValue ?? 0}
className="h-2 focus:outline-0 w-4/5 accent-vscode-button-background"
onChange={(e) => setInputValue(parseFloat(e.target.value))}
/>
<span>{inputValue}</span>
<div className="flex flex-col gap-3 pl-3 border-l-2 border-vscode-button-background">
<div>
<div className="flex items-center gap-2">
<Slider
min={0}
max={maxValue}
step={0.01}
value={[inputValue ?? 0]}
onValueChange={([value]) => setInputValue(value)}
/>
<span className="w-10">{inputValue}</span>
</div>
<div className="text-vscode-descriptionForeground text-sm mt-1">
{t("settings:temperature.rangeDescription")}
</div>
</div>
<p className="text-vscode-descriptionForeground text-sm mt-1">
{t("settings:temperature.rangeDescription")}
</p>
</div>
)}
</>

View file

@ -3,9 +3,9 @@ import { useAppTranslation } from "@/i18n/TranslationContext"
import { SquareTerminal } from "lucide-react"
import { cn } from "@/lib/utils"
import { Slider } from "@/components/ui"
import { SetCachedStateField } from "./types"
import { sliderLabelStyle } from "./styles"
import { SectionHeader } from "./SectionHeader"
import { Section } from "./Section"
@ -35,54 +35,44 @@ export const TerminalSettings = ({
<Section>
<div>
<div className="flex flex-col gap-2">
<span className="font-medium">{t("settings:terminal.outputLineLimit.label")}</span>
<div className="flex items-center gap-2">
<input
type="range"
min="100"
max="5000"
step="100"
value={terminalOutputLineLimit ?? 500}
onChange={(e) =>
setCachedStateField("terminalOutputLineLimit", parseInt(e.target.value))
}
className="h-2 focus:outline-0 w-4/5 accent-vscode-button-background"
data-testid="terminal-output-limit-slider"
/>
<span style={{ ...sliderLabelStyle }}>{terminalOutputLineLimit ?? 500}</span>
</div>
<label className="block font-medium mb-1">{t("settings:terminal.outputLineLimit.label")}</label>
<div className="flex items-center gap-2">
<Slider
min={100}
max={5000}
step={100}
value={[terminalOutputLineLimit ?? 500]}
onValueChange={([value]) => setCachedStateField("terminalOutputLineLimit", value)}
data-testid="terminal-output-limit-slider"
/>
<span className="w-10">{terminalOutputLineLimit ?? 500}</span>
</div>
<p className="text-vscode-descriptionForeground text-sm mt-0">
<div className="text-vscode-descriptionForeground text-sm mt-1">
{t("settings:terminal.outputLineLimit.description")}
</p>
</div>
</div>
<div>
<div className="flex flex-col gap-2">
<span className="font-medium">{t("settings:terminal.shellIntegrationTimeout.label")}</span>
<div className="flex items-center gap-2">
<input
type="range"
min="1000"
max="60000"
step="1000"
value={terminalShellIntegrationTimeout}
onChange={(e) =>
setCachedStateField(
"terminalShellIntegrationTimeout",
Math.min(60000, Math.max(1000, parseInt(e.target.value))),
)
}
className="h-2 focus:outline-0 w-4/5 accent-vscode-button-background"
/>
<span style={{ ...sliderLabelStyle }}>
{(terminalShellIntegrationTimeout ?? 5000) / 1000}s
</span>
</div>
<p className="text-vscode-descriptionForeground text-sm mt-0">
{t("settings:terminal.shellIntegrationTimeout.description")}
</p>
<label className="block font-medium mb-1">
{t("settings:terminal.shellIntegrationTimeout.label")}
</label>
<div className="flex items-center gap-2">
<Slider
min={1000}
max={60000}
step={1000}
value={[terminalShellIntegrationTimeout ?? 5000]}
onValueChange={([value]) =>
setCachedStateField(
"terminalShellIntegrationTimeout",
Math.min(60000, Math.max(1000, value)),
)
}
/>
<span className="w-10">{(terminalShellIntegrationTimeout ?? 5000) / 1000}s</span>
</div>
<div className="text-vscode-descriptionForeground text-sm mt-1">
{t("settings:terminal.shellIntegrationTimeout.description")}
</div>
</div>
</Section>

View file

@ -1,13 +1,12 @@
// npx jest src/components/settings/__tests__/ApiConfigManager.test.tsx
import React from "react"
import { render, screen, fireEvent, within } from "@testing-library/react"
import ApiConfigManager from "../ApiConfigManager"
// Mock VSCode components
jest.mock("@vscode/webview-ui-toolkit/react", () => ({
VSCodeButton: ({ children, onClick, title, disabled, "data-testid": dataTestId }: any) => (
<button onClick={onClick} title={title} disabled={disabled} data-testid={dataTestId}>
{children}
</button>
),
VSCodeTextField: ({ value, onInput, placeholder, onKeyDown, "data-testid": dataTestId }: any) => (
<input
value={value}
@ -20,22 +19,8 @@ jest.mock("@vscode/webview-ui-toolkit/react", () => ({
),
}))
jest.mock("vscrui", () => ({
Dropdown: ({ id, value, onChange, options, role }: any) => (
<div data-testid={`mock-dropdown-${id}`}>
<select value={value} onChange={(e) => onChange({ value: e.target.value })} data-testid={id} role={role}>
{options.map((opt: any) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
</div>
),
}))
// Mock Dialog component
jest.mock("@/components/ui/dialog", () => ({
jest.mock("@/components/ui", () => ({
...jest.requireActual("@/components/ui"),
Dialog: ({ children, open, onOpenChange }: any) => (
<div role="dialog" aria-modal="true" style={{ display: open ? "block" : "none" }} data-testid="dialog">
{children}
@ -43,10 +28,6 @@ jest.mock("@/components/ui/dialog", () => ({
),
DialogContent: ({ children }: any) => <div data-testid="dialog-content">{children}</div>,
DialogTitle: ({ children }: any) => <div data-testid="dialog-title">{children}</div>,
}))
// Mock UI components
jest.mock("@/components/ui", () => ({
Button: ({ children, onClick, disabled, variant, "data-testid": dataTestId }: any) => (
<button onClick={onClick} disabled={disabled} data-testid={dataTestId}>
{children}
@ -61,6 +42,25 @@ jest.mock("@/components/ui", () => ({
data-testid={dataTestId}
/>
),
Select: ({ children, value, onValueChange }: any) => (
<select
value={value}
onChange={(e) => {
if (onValueChange) onValueChange(e.target.value)
}}
data-testid="select-component">
<option value="Default Config">Default Config</option>
<option value="Another Config">Another Config</option>
</select>
),
SelectTrigger: ({ children }: any) => <div className="select-trigger-mock">{children}</div>,
SelectValue: ({ children }: any) => <div className="select-value-mock">{children}</div>,
SelectContent: ({ children }: any) => <div className="select-content-mock">{children}</div>,
SelectItem: ({ children, value }: any) => (
<option value={value} className="select-item-mock">
{children}
</option>
),
}))
describe("ApiConfigManager", () => {
@ -215,7 +215,7 @@ describe("ApiConfigManager", () => {
it("allows selecting a different config", () => {
render(<ApiConfigManager {...defaultProps} />)
const select = screen.getByRole("combobox")
const select = screen.getByTestId("select-component")
fireEvent.change(select, { target: { value: "Another Config" } })
expect(mockOnSelectConfig).toHaveBeenCalledWith("Another Config")

View file

@ -1,9 +1,31 @@
// npx jest src/components/settings/__tests__/ContextManagementSettings.test.ts
import { render, screen, fireEvent } from "@testing-library/react"
import { ContextManagementSettings } from "../ContextManagementSettings"
class MockResizeObserver {
observe() {}
unobserve() {}
disconnect() {}
}
global.ResizeObserver = MockResizeObserver
jest.mock("@/components/ui", () => ({
...jest.requireActual("@/components/ui"),
Slider: ({ value, onValueChange, "data-testid": dataTestId }: any) => (
<input
type="range"
value={value[0]}
onChange={(e) => onValueChange([parseFloat(e.target.value)])}
data-testid={dataTestId}
/>
),
}))
describe("ContextManagementSettings", () => {
const defaultProps = {
terminalOutputLineLimit: 500,
maxOpenTabsContext: 20,
maxWorkspaceFiles: 200,
showRooIgnoredFiles: false,
@ -20,12 +42,10 @@ describe("ContextManagementSettings", () => {
// Open tabs context limit
const openTabsSlider = screen.getByTestId("open-tabs-limit-slider")
expect(openTabsSlider).toBeInTheDocument()
expect(openTabsSlider).toHaveValue("20")
// Workspace files limit
const workspaceFilesSlider = screen.getByTestId("workspace-files-limit-slider")
expect(workspaceFilesSlider).toBeInTheDocument()
expect(workspaceFilesSlider).toHaveValue("200")
// Show .rooignore'd files
const showRooIgnoredFilesCheckbox = screen.getByTestId("show-rooignored-files-checkbox")

View file

@ -78,28 +78,22 @@ jest.mock("@vscode/webview-ui-toolkit/react", () => ({
data-testid={dataTestId}
/>
),
VSCodeTextArea: () => <textarea />,
VSCodeLink: ({ children, href }: any) => <a href={href || "#"}>{children}</a>,
VSCodeDropdown: ({ children, value, onChange }: any) => (
<select value={value} onChange={onChange}>
{children}
</select>
),
VSCodeOption: ({ children, value }: any) => <option value={value}>{children}</option>,
VSCodeRadio: ({ children, value, checked, onChange }: any) => (
<input type="radio" value={value} checked={checked} onChange={onChange} />
),
VSCodeRadioGroup: ({ children, value, onChange }: any) => <div onChange={onChange}>{children}</div>,
VSCodeSlider: ({ value, onChange, "data-testid": dataTestId }: any) => (
}))
// Mock Slider component
jest.mock("@/components/ui", () => ({
...jest.requireActual("@/components/ui"),
Slider: ({ value, onValueChange, "data-testid": dataTestId }: any) => (
<input
type="range"
value={value}
onChange={(e) => onChange({ target: { value: Number(e.target.value) } })}
min={0}
max={1}
value={value[0]}
onChange={(e) => onValueChange([parseFloat(e.target.value)])}
data-testid={dataTestId}
step={0.01}
style={{ flexGrow: 1, height: "2px" }}
/>
),
}))
@ -127,6 +121,14 @@ const mockPostMessage = (state: any) => {
)
}
class MockResizeObserver {
observe() {}
unobserve() {}
disconnect() {}
}
global.ResizeObserver = MockResizeObserver
const renderSettingsView = () => {
const onDone = jest.fn()
const queryClient = new QueryClient()

View file

@ -1,6 +1,29 @@
// npx jest src/components/settings/__tests__/TemperatureControl.test.ts
import { render, screen, fireEvent } from "@testing-library/react"
import { TemperatureControl } from "../TemperatureControl"
class MockResizeObserver {
observe() {}
unobserve() {}
disconnect() {}
}
global.ResizeObserver = MockResizeObserver
jest.mock("@/components/ui", () => ({
...jest.requireActual("@/components/ui"),
Slider: ({ value, onValueChange, "data-testid": dataTestId }: any) => (
<input
type="range"
value={value[0]}
onChange={(e) => onValueChange([parseFloat(e.target.value)])}
data-testid={dataTestId}
/>
),
}))
describe("TemperatureControl", () => {
it("renders with default temperature disabled", () => {
const onChange = jest.fn()
@ -29,67 +52,34 @@ describe("TemperatureControl", () => {
const checkbox = screen.getByRole("checkbox")
// Uncheck - should clear temperature
// Uncheck - should clear temperature.
fireEvent.click(checkbox)
// Waiting for debounce
// Waiting for debounce.
await new Promise((x) => setTimeout(x, 100))
expect(onChange).toHaveBeenCalledWith(null)
// Check - should restore previous temperature
// Check - should restore previous temperature.
fireEvent.click(checkbox)
// Waiting for debounce
// Waiting for debounce.
await new Promise((x) => setTimeout(x, 100))
expect(onChange).toHaveBeenCalledWith(0.7)
})
it("updates temperature when input loses focus", async () => {
const onChange = jest.fn()
render(<TemperatureControl value={0.7} onChange={onChange} />)
const input = screen.getByRole("slider")
fireEvent.change(input, { target: { value: "0.8" } })
fireEvent.blur(input)
// Waiting for debounce
await new Promise((x) => setTimeout(x, 100))
expect(onChange).toHaveBeenCalledWith(0.8)
})
it("respects maxValue prop", async () => {
const onChange = jest.fn()
render(<TemperatureControl value={1.5} onChange={onChange} maxValue={2} />)
const input = screen.getByRole("slider")
// Valid value within max
fireEvent.change(input, { target: { value: "1.8" } })
fireEvent.blur(input)
// Waiting for debounce
await new Promise((x) => setTimeout(x, 100))
expect(onChange).toHaveBeenCalledWith(1.8)
// Invalid value above max
fireEvent.change(input, { target: { value: "2.5" } })
fireEvent.blur(input)
expect(input).toHaveValue("2") // Clamped between 0 and 2
// Waiting for debounce
await new Promise((x) => setTimeout(x, 100))
expect(onChange).toHaveBeenCalledWith(2)
})
it("syncs checkbox state when value prop changes", () => {
const onChange = jest.fn()
const { rerender } = render(<TemperatureControl value={0.7} onChange={onChange} />)
// Initially checked
// Initially checked.
const checkbox = screen.getByRole("checkbox")
expect(checkbox).toBeChecked()
// Update to undefined
// Update to undefined.
rerender(<TemperatureControl value={undefined} onChange={onChange} />)
expect(checkbox).not.toBeChecked()
// Update back to a value
// Update back to a value.
rerender(<TemperatureControl value={0.5} onChange={onChange} />)
expect(checkbox).toBeChecked()
})

View file

@ -2,7 +2,6 @@ import { render, screen, fireEvent } from "@testing-library/react"
import { ThinkingBudget } from "../ThinkingBudget"
import { ModelInfo } from "../../../../../src/shared/api"
// Mock Slider component
jest.mock("@/components/ui", () => ({
Slider: ({ value, onValueChange, min, max }: any) => (
<input

View file

@ -76,10 +76,3 @@ export const StyledMarkdown = styled.div`
}
}
`
export const sliderLabelStyle = {
minWidth: "45px",
textAlign: "right" as const,
lineHeight: "20px",
paddingBottom: "2px",
}

View file

@ -1,4 +1,6 @@
import React, { ReactNode } from "react"
// npx jest webview-ui/src/components/ui/__tests__/select-dropdown.test.tsx
import { ReactNode } from "react"
import { render, screen, fireEvent } from "@testing-library/react"
import { SelectDropdown, DropdownOptionType } from "../select-dropdown"
@ -47,6 +49,10 @@ jest.mock("../dropdown-menu", () => {
),
DropdownMenuSeparator: () => <div data-testid="dropdown-separator" />,
DropdownMenuShortcut: ({ children }: { children: ReactNode }) => (
<span data-testid="dropdown-shortcut">{children}</span>
),
}
})

View file

@ -9,7 +9,9 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
DropdownMenuSeparator,
DropdownMenuShortcut,
} from "./dropdown-menu"
import { Check } from "lucide-react"
export enum DropdownOptionType {
ITEM = "item",
@ -30,7 +32,6 @@ export interface SelectDropdownProps {
onChange: (value: string) => void
disabled?: boolean
title?: string
className?: string
triggerClassName?: string
contentClassName?: string
sideOffset?: number
@ -48,7 +49,6 @@ export const SelectDropdown = React.forwardRef<React.ElementRef<typeof DropdownM
onChange,
disabled = false,
title = "",
className = "",
triggerClassName = "",
contentClassName = "",
sideOffset = 4,
@ -141,12 +141,14 @@ export const SelectDropdown = React.forwardRef<React.ElementRef<typeof DropdownM
<DropdownMenuItem
key={`item-${option.value}`}
disabled={option.disabled}
className={cn(
"cursor-pointer text-xs focus:bg-vscode-list-hoverBackground focus:text-vscode-list-hoverForeground",
option.value === value && "bg-vscode-list-focusBackground",
)}
className="text-xs focus:bg-vscode-list-activeSelectionBackground cursor-pointer"
onClick={() => handleSelect(option)}>
{option.label}
{option.value === value && (
<DropdownMenuShortcut>
<Check className="w-3 h-3" />
</DropdownMenuShortcut>
)}
</DropdownMenuItem>
)
})}

View file

@ -11,7 +11,7 @@ const Slider = React.forwardRef<
ref={ref}
className={cn("relative flex w-full touch-none select-none items-center", className)}
{...props}>
<SliderPrimitive.Track className="relative w-full h-[8px] grow overflow-hidden bg-vscode-button-secondaryBackground border border-[#767676] dark:border-[#858585] rounded-sm">
<SliderPrimitive.Track className="relative w-full h-[8px] grow overflow-hidden bg-accent border border-[#767676] dark:border-[#858585] rounded-sm">
<SliderPrimitive.Range className="absolute h-full bg-vscode-button-background" />
</SliderPrimitive.Track>
<SliderPrimitive.Thumb className="block h-3 w-3 rounded-full border border-primary/50 bg-primary shadow transition-colors cursor-pointer focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50" />

View file

@ -3,7 +3,8 @@
"save": "Desar",
"done": "Fet",
"cancel": "Cancel·lar",
"reset": "Restablir"
"reset": "Restablir",
"select": "Seleccionar"
},
"header": {
"title": "Configuració",
@ -74,7 +75,7 @@
},
"providers": {
"configProfile": "Perfil de configuració",
"description": "Descripció",
"description": "Deseu diferents configuracions d'API per canviar ràpidament entre proveïdors i configuracions.",
"apiProvider": "Proveïdor d'API",
"openRouterApiKey": "Clau API d'OpenRouter",
"nameEmpty": "El nom no pot estar buit",

View file

@ -3,7 +3,8 @@
"save": "Speichern",
"done": "Fertig",
"cancel": "Abbrechen",
"reset": "Zurücksetzen"
"reset": "Zurücksetzen",
"select": "Auswählen"
},
"header": {
"title": "Einstellungen",
@ -74,7 +75,7 @@
},
"providers": {
"configProfile": "Konfigurationsprofil",
"description": "Beschreibung",
"description": "Speichern Sie verschiedene API-Konfigurationen, um schnell zwischen Anbietern und Einstellungen zu wechseln.",
"apiProvider": "API-Anbieter",
"model": "Modell",
"nameEmpty": "Name darf nicht leer sein",

View file

@ -3,7 +3,8 @@
"save": "Save",
"done": "Done",
"cancel": "Cancel",
"reset": "Reset"
"reset": "Reset",
"select": "Select"
},
"header": {
"title": "Settings",
@ -74,7 +75,7 @@
},
"providers": {
"configProfile": "Configuration Profile",
"description": "Description",
"description": "Save different API configurations to quickly switch between providers and settings.",
"apiProvider": "API Provider",
"model": "Model",
"nameEmpty": "Name cannot be empty",

View file

@ -3,7 +3,8 @@
"save": "Guardar",
"done": "Hecho",
"cancel": "Cancelar",
"reset": "Restablecer"
"reset": "Restablecer",
"select": "Seleccionar"
},
"header": {
"title": "Configuración",
@ -74,7 +75,7 @@
},
"providers": {
"configProfile": "Perfil de configuración",
"description": "Descripción",
"description": "Guarde diferentes configuraciones de API para cambiar rápidamente entre proveedores y ajustes.",
"apiProvider": "Proveedor de API",
"model": "Modelo",
"nameEmpty": "El nombre no puede estar vacío",

View file

@ -3,7 +3,8 @@
"save": "Enregistrer",
"done": "Terminé",
"cancel": "Annuler",
"reset": "Réinitialiser"
"reset": "Réinitialiser",
"select": "Sélectionner"
},
"header": {
"title": "Paramètres",
@ -74,7 +75,7 @@
},
"providers": {
"configProfile": "Profil de configuration",
"description": "Description",
"description": "Enregistrez différentes configurations d'API pour basculer rapidement entre les fournisseurs et les paramètres.",
"apiProvider": "Fournisseur d'API",
"model": "Modèle",
"nameEmpty": "Le nom ne peut pas être vide",

View file

@ -3,7 +3,8 @@
"save": "सहेजें",
"done": "पूर्ण",
"cancel": "रद्द करें",
"reset": "रीसेट करें"
"reset": "रीसेट करें",
"select": "चुनें"
},
"header": {
"title": "सेटिंग्स",
@ -74,7 +75,7 @@
},
"providers": {
"configProfile": "कॉन्फिगरेशन प्रोफाइल",
"description": "विवरण",
"description": "विभिन्न API कॉन्फ़िगरेशन सहेजें ताकि प्रदाताओं और सेटिंग्स के बीच त्वरित रूप से स्विच कर सकें।",
"apiProvider": "API प्रदाता",
"model": "मॉडल",
"nameEmpty": "नाम खाली नहीं हो सकता",

View file

@ -3,7 +3,8 @@
"save": "Salva",
"done": "Fatto",
"cancel": "Annulla",
"reset": "Ripristina"
"reset": "Ripristina",
"select": "Seleziona"
},
"header": {
"title": "Impostazioni",
@ -74,7 +75,7 @@
},
"providers": {
"configProfile": "Profilo di configurazione",
"description": "Descrizione",
"description": "Salva diverse configurazioni API per passare rapidamente tra fornitori e impostazioni.",
"apiProvider": "Fornitore API",
"model": "Modello",
"nameEmpty": "Il nome non può essere vuoto",

View file

@ -3,7 +3,8 @@
"save": "保存",
"done": "完了",
"cancel": "キャンセル",
"reset": "リセット"
"reset": "リセット",
"select": "選択"
},
"header": {
"title": "設定",
@ -74,7 +75,7 @@
},
"providers": {
"configProfile": "設定プロファイル",
"description": "説明",
"description": "異なるAPI設定を保存して、プロバイダーと設定をすばやく切り替えることができます。",
"apiProvider": "APIプロバイダー",
"model": "モデル",
"nameEmpty": "名前を空にすることはできません",

View file

@ -3,7 +3,8 @@
"save": "저장",
"done": "완료",
"cancel": "취소",
"reset": "초기화"
"reset": "초기화",
"select": "선택"
},
"header": {
"title": "설정",
@ -74,7 +75,7 @@
},
"providers": {
"configProfile": "구성 프로필",
"description": "설명",
"description": "다양한 API 구성을 저장하여 제공자와 설정 간에 빠르게 전환할 수 있습니다.",
"apiProvider": "API 제공자",
"model": "모델",
"nameEmpty": "이름은 비워둘 수 없습니다",

View file

@ -3,7 +3,8 @@
"save": "Zapisz",
"done": "Gotowe",
"cancel": "Anuluj",
"reset": "Resetuj"
"reset": "Resetuj",
"select": "Wybierz"
},
"header": {
"title": "Ustawienia",
@ -74,7 +75,7 @@
},
"providers": {
"configProfile": "Profil konfiguracji",
"description": "Opis",
"description": "Zapisz różne konfiguracje API, aby szybko przełączać się między dostawcami i ustawieniami.",
"apiProvider": "Dostawca API",
"model": "Model",
"nameEmpty": "Nazwa nie może być pusta",

View file

@ -3,7 +3,8 @@
"save": "Salvar",
"done": "Concluído",
"cancel": "Cancelar",
"reset": "Redefinir"
"reset": "Redefinir",
"select": "Selecionar"
},
"header": {
"title": "Configurações",
@ -74,7 +75,7 @@
},
"providers": {
"configProfile": "Perfil de configuração",
"description": "Descrição",
"description": "Salve diferentes configurações de API para alternar rapidamente entre provedores e configurações.",
"apiProvider": "Provedor de API",
"model": "Modelo",
"nameEmpty": "O nome não pode estar vazio",

View file

@ -3,7 +3,8 @@
"save": "Kaydet",
"done": "Tamamlandı",
"cancel": "İptal",
"reset": "Sıfırla"
"reset": "Sıfırla",
"select": "Seç"
},
"header": {
"title": "Ayarlar",
@ -74,7 +75,7 @@
},
"providers": {
"configProfile": "Yapılandırma Profili",
"description": "ıklama",
"description": "Sağlayıcılar ve ayarlar arasında hızlıca geçiş yapmak için farklı API yapılandırmalarını kaydedin.",
"apiProvider": "API Sağlayıcı",
"model": "Model",
"nameEmpty": "İsim boş olamaz",

View file

@ -3,7 +3,8 @@
"save": "Lưu",
"done": "Hoàn thành",
"cancel": "Hủy",
"reset": "Đặt lại"
"reset": "Đặt lại",
"select": "Chọn"
},
"header": {
"title": "Cài đặt",
@ -74,7 +75,7 @@
},
"providers": {
"configProfile": "Hồ sơ cấu hình",
"description": "Mô tả",
"description": "Lưu các cấu hình API khác nhau để nhanh chóng chuyển đổi giữa các nhà cung cấp và cài đặt.",
"apiProvider": "Nhà cung cấp API",
"model": "Mẫu",
"nameEmpty": "Tên không được để trống",

View file

@ -3,7 +3,8 @@
"save": "保存",
"done": "完成",
"cancel": "取消",
"reset": "重置"
"reset": "重置",
"select": "选择"
},
"header": {
"title": "设置",
@ -74,7 +75,7 @@
},
"providers": {
"configProfile": "配置文件",
"description": "描述",
"description": "保存不同的 API 配置以快速切换提供商和设置。",
"apiProvider": "API 提供商",
"model": "模型",
"nameEmpty": "名称不能为空",

View file

@ -3,7 +3,8 @@
"save": "儲存",
"done": "完成",
"cancel": "取消",
"reset": "重設"
"reset": "重設",
"select": "選擇"
},
"header": {
"title": "設定",
@ -74,7 +75,7 @@
},
"providers": {
"configProfile": "設定檔案",
"description": "描述",
"description": "儲存不同的 API 設定以快速切換提供者和設定。",
"apiProvider": "API 提供者",
"model": "模型",
"nameEmpty": "名稱不能為空",