Skip to content

Commit ee02aab

Browse files
committed
feat(settings): implement rate limit control component and update settings
- Removed the global rate limit setting from the `AdvancedSettings` component. - Integrated the new `RateLimitControl` into the `ApiOptions` component. - Updated the localization files to include labels and descriptions for the rate limit settings. - Added tests for the `RateLimitControl` to ensure proper functionality and user interaction.
1 parent 53683c6 commit ee02aab

File tree

8 files changed

+122
-43
lines changed

8 files changed

+122
-43
lines changed

benchmark/src/cli.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ async function runLanguage({ runId, model, language }: { runId: number; model: s
2929

3030
const exercises = filesystem
3131
.subdirectories(languagePath)
32-
.map((exercise) => path.basename(exercise))
33-
.filter((exercise) => !exercise.startsWith("."))
32+
.map((exercise: string) => path.basename(exercise))
33+
.filter((exercise: string) => !exercise.startsWith("."))
3434

3535
for (const exercise of exercises) {
3636
await runExercise({ runId, model, language, exercise })
@@ -84,7 +84,9 @@ async function askLanguage(prompt: GluegunPrompt) {
8484
type: "select",
8585
name: "language",
8686
message: "Which language?",
87-
choices: languages.map((language) => path.basename(language)).filter((language) => !language.startsWith(".")),
87+
choices: languages
88+
.map((language: string) => path.basename(language))
89+
.filter((language: string) => !language.startsWith(".")),
8890
})
8991

9092
return language
@@ -101,7 +103,7 @@ async function askExercise(prompt: GluegunPrompt, language: string) {
101103
type: "select",
102104
name: "exercise",
103105
message: "Which exercise?",
104-
choices: exercises.map((exercise) => path.basename(exercise)),
106+
choices: exercises.map((exercise: string) => path.basename(exercise)),
105107
})
106108

107109
return exercise
@@ -131,7 +133,7 @@ async function main() {
131133
.version()
132134
.command({
133135
name: "run",
134-
run: ({ config, parameters }) => {
136+
run: ({ config, parameters }: { config: any; parameters: any }) => {
135137
config.language = parameters.first
136138
config.exercise = parameters.second
137139

src/core/webview/ClineProvider.ts

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1366,14 +1366,9 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
13661366
await this.postStateToWebview()
13671367
break
13681368
case "rateLimitSeconds":
1369-
// Update the current API configuration with the rate limit value
1370-
const currentApiConfigName = (await this.getGlobalState("currentApiConfigName")) as string
1371-
if (currentApiConfigName) {
1372-
const apiConfig = await this.configManager.loadConfig(currentApiConfigName)
1373-
apiConfig.rateLimitSeconds = message.value ?? 0
1374-
await this.configManager.saveConfig(currentApiConfigName, apiConfig)
1375-
await this.updateApiConfiguration(apiConfig)
1376-
}
1369+
// Rate limit seconds is now part of the API configuration
1370+
// and will be saved when the API configuration is saved
1371+
await this.updateGlobalState("rateLimitSeconds", message.value ?? 0)
13771372
await this.postStateToWebview()
13781373
break
13791374
case "writeDelayMs":

webview-ui/src/components/settings/AdvancedSettings.tsx

Lines changed: 1 addition & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,13 @@ import { SectionHeader } from "./SectionHeader"
1313
import { Section } from "./Section"
1414

1515
type AdvancedSettingsProps = HTMLAttributes<HTMLDivElement> & {
16-
rateLimitSeconds?: number
1716
diffEnabled?: boolean
1817
fuzzyMatchThreshold?: number
19-
setCachedStateField: SetCachedStateField<"rateLimitSeconds" | "diffEnabled" | "fuzzyMatchThreshold">
18+
setCachedStateField: SetCachedStateField<"diffEnabled" | "fuzzyMatchThreshold">
2019
experiments: Record<ExperimentId, boolean>
2120
setExperimentEnabled: SetExperimentEnabled
2221
}
2322
export const AdvancedSettings = ({
24-
rateLimitSeconds,
2523
diffEnabled,
2624
fuzzyMatchThreshold,
2725
setCachedStateField,
@@ -41,27 +39,6 @@ export const AdvancedSettings = ({
4139
</SectionHeader>
4240

4341
<Section>
44-
<div>
45-
<div className="flex flex-col gap-2">
46-
<span className="font-medium">{t("settings:advanced.rateLimit.label")}</span>
47-
<div className="flex items-center gap-2">
48-
<input
49-
type="range"
50-
min="0"
51-
max="60"
52-
step="1"
53-
value={rateLimitSeconds || 0}
54-
onChange={(e) => setCachedStateField("rateLimitSeconds", parseInt(e.target.value))}
55-
className="h-2 focus:outline-0 w-4/5 accent-vscode-button-background"
56-
/>
57-
<span style={{ ...sliderLabelStyle }}>{rateLimitSeconds || 0}s</span>
58-
</div>
59-
</div>
60-
<p className="text-vscode-descriptionForeground text-sm mt-0">
61-
{t("settings:advanced.rateLimit.description")}
62-
</p>
63-
</div>
64-
6542
<div>
6643
<VSCodeCheckbox
6744
checked={diffEnabled}

webview-ui/src/components/settings/ApiOptions.tsx

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import { vscode } from "../../utils/vscode"
4242
import { VSCodeButtonLink } from "../common/VSCodeButtonLink"
4343
import { ModelInfoView } from "./ModelInfoView"
4444
import { ModelPicker } from "./ModelPicker"
45+
import { RateLimitControl } from "./RateLimitControl"
4546
import { TemperatureControl } from "./TemperatureControl"
4647
import { validateApiConfiguration, validateModelId, validateBedrockArn } from "@/utils/validate"
4748
import { ApiErrorMessage } from "./ApiErrorMessage"
@@ -1505,11 +1506,20 @@ const ApiOptions = ({
15051506
)}
15061507

15071508
{!fromWelcomeView && (
1508-
<TemperatureControl
1509-
value={apiConfiguration?.modelTemperature}
1510-
onChange={handleInputChange("modelTemperature", noTransform)}
1511-
maxValue={2}
1512-
/>
1509+
<>
1510+
<TemperatureControl
1511+
value={apiConfiguration?.modelTemperature}
1512+
onChange={handleInputChange("modelTemperature", noTransform)}
1513+
maxValue={2}
1514+
/>
1515+
<RateLimitControl
1516+
value={apiConfiguration?.rateLimitSeconds}
1517+
onChange={handleInputChange("rateLimitSeconds", (value) => {
1518+
// Ensure we return a number or undefined, not null
1519+
return value === null ? undefined : value
1520+
})}
1521+
/>
1522+
</>
15131523
)}
15141524
</div>
15151525
)
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { useAppTranslation } from "@/i18n/TranslationContext"
2+
import { useEffect, useState } from "react"
3+
import { useDebounce } from "react-use"
4+
5+
interface RateLimitControlProps {
6+
value: number | undefined | null
7+
onChange: (value: number | undefined | null) => void
8+
}
9+
10+
export const RateLimitControl = ({ value, onChange }: RateLimitControlProps) => {
11+
const { t } = useAppTranslation()
12+
const [inputValue, setInputValue] = useState(value)
13+
useDebounce(() => onChange(inputValue), 50, [onChange, inputValue])
14+
15+
// Sync internal state with prop changes when switching profiles
16+
useEffect(() => {
17+
setInputValue(value)
18+
}, [value])
19+
20+
return (
21+
<div>
22+
<div className="flex flex-col gap-2">
23+
<span className="font-medium">{t("settings:modelInfo.rateLimit.label")}</span>
24+
<div className="flex items-center gap-2">
25+
<input
26+
type="range"
27+
min="0"
28+
max="60"
29+
step="1"
30+
value={inputValue ?? 0}
31+
onChange={(e) => setInputValue(parseInt(e.target.value))}
32+
className="h-2 focus:outline-0 w-4/5 accent-vscode-button-background"
33+
/>
34+
<span>{inputValue ?? 0}s</span>
35+
</div>
36+
</div>
37+
<p className="text-vscode-descriptionForeground text-sm mt-0">
38+
{t("settings:modelInfo.rateLimit.description")}
39+
</p>
40+
</div>
41+
)
42+
}

webview-ui/src/components/settings/SettingsView.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -440,7 +440,6 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone },
440440

441441
<div ref={advancedRef}>
442442
<AdvancedSettings
443-
rateLimitSeconds={rateLimitSeconds || 0}
444443
diffEnabled={diffEnabled}
445444
fuzzyMatchThreshold={fuzzyMatchThreshold}
446445
setCachedStateField={setCachedStateField}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { render, fireEvent } from "@testing-library/react"
2+
import { RateLimitControl } from "../RateLimitControl"
3+
4+
// Mock the translation hook
5+
jest.mock("@/i18n/TranslationContext", () => ({
6+
useAppTranslation: () => ({
7+
t: (key: string) => key,
8+
}),
9+
}))
10+
11+
describe("RateLimitControl", () => {
12+
it("renders with the provided value", () => {
13+
const onChange = jest.fn()
14+
const { getByRole } = render(<RateLimitControl value={10} onChange={onChange} />)
15+
16+
const slider = getByRole("slider") as HTMLInputElement
17+
expect(slider.value).toBe("10")
18+
})
19+
20+
it("calls onChange when the slider value changes", () => {
21+
const onChange = jest.fn()
22+
const { getByRole } = render(<RateLimitControl value={10} onChange={onChange} />)
23+
24+
const slider = getByRole("slider") as HTMLInputElement
25+
fireEvent.change(slider, { target: { value: "20" } })
26+
27+
// The onChange is debounced, so we need to wait for it to be called
28+
setTimeout(() => {
29+
expect(onChange).toHaveBeenCalledWith(20)
30+
}, 100)
31+
})
32+
33+
it("updates the displayed value when the slider changes", () => {
34+
const onChange = jest.fn()
35+
const { getByRole, getByText } = render(<RateLimitControl value={10} onChange={onChange} />)
36+
37+
const slider = getByRole("slider") as HTMLInputElement
38+
fireEvent.change(slider, { target: { value: "20" } })
39+
40+
expect(getByText("20s")).toBeInTheDocument()
41+
})
42+
43+
it("handles null or undefined values", () => {
44+
const onChange = jest.fn()
45+
const { getByRole } = render(<RateLimitControl value={null} onChange={onChange} />)
46+
47+
const slider = getByRole("slider") as HTMLInputElement
48+
expect(slider.value).toBe("0")
49+
})
50+
})

webview-ui/src/i18n/locales/en/settings.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,10 @@
325325
"enableStreaming": "Enable streaming",
326326
"useAzure": "Use Azure",
327327
"azureApiVersion": "Set Azure API version",
328+
"rateLimit": {
329+
"label": "Rate limit",
330+
"description": "Minimum time between API requests."
331+
},
328332
"gemini": {
329333
"freeRequests": "* Free up to {{count}} requests per minute. After that, billing depends on prompt size.",
330334
"pricingDetails": "For more info, see pricing details."

0 commit comments

Comments
 (0)