Skip to content

Commit 0b0bccd

Browse files
author
Chris Hasson
committed
refactor(ui): extract MaxRequestsInput component
The `MaxRequestsInput` component was extracted from `AutoApproveMenu.tsx` into its own file (`MaxRequestsInput.tsx`) to improve reusability and maintainability. This component is now used in both `AutoApproveMenu.tsx` and `AutoApproveSettings.tsx` to manage the auto-approve API request limit. Previously this option was only available in the Chat Approval popup menu
1 parent 6ef6248 commit 0b0bccd

File tree

6 files changed

+205
-37
lines changed

6 files changed

+205
-37
lines changed

.changeset/tangy-pugs-yawn.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"kilo-code": minor
3+
---
4+
5+
Add 'max requests' section to the Auto-Approve Settings page

webview-ui/src/components/chat/AutoApproveMenu.tsx

Lines changed: 9 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import { useCallback, useMemo, useState } from "react"
22
import { Trans } from "react-i18next"
3-
import { VSCodeCheckbox, VSCodeLink, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
3+
import { VSCodeCheckbox, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
44

55
import { vscode } from "@src/utils/vscode"
66
import { useExtensionState } from "@src/context/ExtensionStateContext"
77
import { useAppTranslation } from "@src/i18n/TranslationContext"
88
import { AutoApproveToggle, AutoApproveSetting, autoApproveSettingsConfig } from "../settings/AutoApproveToggle"
9+
import { MaxRequestsInput } from "../settings/MaxRequestsInput"
910

1011
interface AutoApproveMenuProps {
1112
style?: React.CSSProperties
@@ -209,42 +210,13 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => {
209210

210211
<AutoApproveToggle {...toggles} onToggle={onAutoApproveToggle} />
211212

212-
{/* Auto-approve API request count limit input row inspired by Cline */}
213-
<div
214-
style={{
215-
display: "flex",
216-
alignItems: "center",
217-
gap: "8px",
218-
marginTop: "10px",
219-
marginBottom: "8px",
220-
color: "var(--vscode-descriptionForeground)",
221-
}}>
222-
<span style={{ flexShrink: 1, minWidth: 0 }}>
223-
<Trans i18nKey="settings:autoApprove.apiRequestLimit.title" />:
224-
</span>
225-
<VSCodeTextField
226-
placeholder={t("settings:autoApprove.apiRequestLimit.unlimited")}
227-
value={(allowedMaxRequests ?? Infinity) === Infinity ? "" : allowedMaxRequests?.toString()}
228-
onInput={(e) => {
229-
const input = e.target as HTMLInputElement
230-
// Remove any non-numeric characters
231-
input.value = input.value.replace(/[^0-9]/g, "")
232-
const value = parseInt(input.value)
233-
const parsedValue = !isNaN(value) && value > 0 ? value : undefined
234-
setAllowedMaxRequests(parsedValue)
235-
vscode.postMessage({ type: "allowedMaxRequests", value: parsedValue })
236-
}}
237-
style={{ flex: 1 }}
238-
/>
239-
</div>
240-
<div
241-
style={{
242-
color: "var(--vscode-descriptionForeground)",
243-
fontSize: "12px",
244-
marginBottom: "10px",
245-
}}>
246-
<Trans i18nKey="settings:autoApprove.apiRequestLimit.description" />
247-
</div>
213+
{/* kilocode_change start */}
214+
<MaxRequestsInput
215+
allowedMaxRequests={allowedMaxRequests ?? undefined}
216+
onValueChange={(value) => setAllowedMaxRequests(value)}
217+
variant="menu"
218+
/>
219+
{/* kilocode_change end */}
248220
</div>
249221
)}
250222
</div>

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { SetCachedStateField } from "./types"
1010
import { SectionHeader } from "./SectionHeader"
1111
import { Section } from "./Section"
1212
import { AutoApproveToggle } from "./AutoApproveToggle"
13+
import { MaxRequestsInput } from "./MaxRequestsInput" // kilocode_change
1314

1415
type AutoApproveSettingsProps = HTMLAttributes<HTMLDivElement> & {
1516
alwaysAllowReadOnly?: boolean
@@ -28,6 +29,7 @@ type AutoApproveSettingsProps = HTMLAttributes<HTMLDivElement> & {
2829
alwaysAllowFollowupQuestions?: boolean
2930
followupAutoApproveTimeoutMs?: number
3031
allowedCommands?: string[]
32+
allowedMaxRequests?: number | undefined // kilocode_change
3133
showAutoApproveMenu?: boolean // kilocode_change
3234
setCachedStateField: SetCachedStateField<
3335
| "alwaysAllowReadOnly"
@@ -46,6 +48,7 @@ type AutoApproveSettingsProps = HTMLAttributes<HTMLDivElement> & {
4648
| "alwaysAllowFollowupQuestions"
4749
| "followupAutoApproveTimeoutMs"
4850
| "allowedCommands"
51+
| "allowedMaxRequests" // kilocode_change
4952
| "showAutoApproveMenu" // kilocode_change
5053
>
5154
}
@@ -67,6 +70,7 @@ export const AutoApproveSettings = ({
6770
alwaysAllowFollowupQuestions,
6871
followupAutoApproveTimeoutMs = 60000,
6972
allowedCommands,
73+
allowedMaxRequests, // kilocode_change
7074
showAutoApproveMenu, // kilocode_change
7175
setCachedStateField,
7276
...props
@@ -123,6 +127,14 @@ export const AutoApproveSettings = ({
123127
alwaysAllowFollowupQuestions={alwaysAllowFollowupQuestions}
124128
onToggle={(key, value) => setCachedStateField(key, value)}
125129
/>
130+
{/* kilocode_change start */}
131+
<MaxRequestsInput
132+
allowedMaxRequests={allowedMaxRequests}
133+
onValueChange={(value) => setCachedStateField("allowedMaxRequests", value)}
134+
variant="settings"
135+
testId="max-requests-input"
136+
/>
137+
{/* kilocode_change end */}
126138

127139
{/* ADDITIONAL SETTINGS */}
128140

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
// kilocode_change - new file
2+
import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
3+
import { Trans, useTranslation } from "react-i18next"
4+
import { vscode } from "@/utils/vscode"
5+
6+
interface MaxRequestsInputProps {
7+
allowedMaxRequests?: number | undefined
8+
onValueChange: (value: number | undefined) => void
9+
variant?: "menu" | "settings"
10+
className?: string
11+
style?: React.CSSProperties
12+
testId?: string
13+
}
14+
15+
export function MaxRequestsInput({
16+
allowedMaxRequests,
17+
onValueChange,
18+
variant = "settings",
19+
className,
20+
style,
21+
testId,
22+
}: MaxRequestsInputProps) {
23+
const { t } = useTranslation()
24+
25+
const handleInput = (e: any) => {
26+
const input = e.target as HTMLInputElement
27+
input.value = input.value.replace(/[^0-9]/g, "")
28+
const value = parseInt(input.value)
29+
const parsedValue = !isNaN(value) && value > 0 ? value : undefined
30+
onValueChange(parsedValue)
31+
vscode.postMessage({ type: "allowedMaxRequests", value: parsedValue })
32+
}
33+
34+
const inputValue = (allowedMaxRequests ?? Infinity) === Infinity ? "" : allowedMaxRequests?.toString()
35+
36+
if (variant === "menu") {
37+
return (
38+
<>
39+
<div
40+
style={{
41+
display: "flex",
42+
alignItems: "center",
43+
gap: "8px",
44+
marginTop: "10px",
45+
marginBottom: "8px",
46+
color: "var(--vscode-descriptionForeground)",
47+
...style,
48+
}}
49+
className={className}>
50+
<span style={{ flexShrink: 1, minWidth: 0 }}>
51+
<Trans i18nKey="settings:autoApprove.apiRequestLimit.title" />:
52+
</span>
53+
<VSCodeTextField
54+
placeholder={t("settings:autoApprove.apiRequestLimit.unlimited")}
55+
value={inputValue}
56+
onInput={handleInput}
57+
style={{ flex: 1 }}
58+
data-testid={testId}
59+
/>
60+
</div>
61+
<div
62+
style={{
63+
color: "var(--vscode-descriptionForeground)",
64+
fontSize: "12px",
65+
marginBottom: "10px",
66+
}}>
67+
<Trans i18nKey="settings:autoApprove.apiRequestLimit.description" />
68+
</div>
69+
</>
70+
)
71+
}
72+
73+
return (
74+
<div
75+
className={`flex flex-col gap-3 pl-3 border-l-2 border-vscode-button-background ${className || ""}`}
76+
style={style}>
77+
<div className="flex items-center gap-4 font-bold">
78+
<span className="codicon codicon-pulse" />
79+
<div>{t("settings:autoApprove.apiRequestLimit.title")}</div>
80+
</div>
81+
<div className="flex items-center gap-2">
82+
<VSCodeTextField
83+
placeholder={t("settings:autoApprove.apiRequestLimit.unlimited")}
84+
value={inputValue}
85+
onInput={handleInput}
86+
style={{ flex: 1, maxWidth: "200px" }}
87+
data-testid={testId || "max-requests-input"}
88+
/>
89+
</div>
90+
<div className="text-vscode-descriptionForeground text-sm">
91+
{t("settings:autoApprove.apiRequestLimit.description")}
92+
</div>
93+
</div>
94+
)
95+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -700,6 +700,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
700700
alwaysAllowFollowupQuestions={alwaysAllowFollowupQuestions}
701701
followupAutoApproveTimeoutMs={followupAutoApproveTimeoutMs}
702702
allowedCommands={allowedCommands}
703+
allowedMaxRequests={allowedMaxRequests ?? undefined}
703704
setCachedStateField={setCachedStateField}
704705
/>
705706
)}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
// kilocode_change - new file
2+
import { render, screen, fireEvent } from "@testing-library/react"
3+
import { vi } from "vitest"
4+
import { MaxRequestsInput } from "../MaxRequestsInput"
5+
6+
vi.mock("@/utils/vscode", () => ({
7+
vscode: { postMessage: vi.fn() },
8+
}))
9+
10+
const translations: Record<string, string> = {
11+
"settings:autoApprove.apiRequestLimit.title": "Max API Requests",
12+
"settings:autoApprove.apiRequestLimit.unlimited": "Unlimited",
13+
"settings:autoApprove.apiRequestLimit.description": "Limit the number of API requests",
14+
}
15+
vi.mock("react-i18next", () => ({
16+
useTranslation: () => ({
17+
t: (key: string) => translations[key] || key,
18+
}),
19+
Trans: ({ i18nKey }: { i18nKey: string; children?: React.ReactNode }) => (
20+
<span>{translations[i18nKey] || i18nKey}</span>
21+
),
22+
}))
23+
24+
describe("MaxRequestsInput", () => {
25+
const mockOnValueChange = vi.fn()
26+
27+
it("renders with settings variant by default", () => {
28+
render(<MaxRequestsInput allowedMaxRequests={10} onValueChange={mockOnValueChange} />)
29+
30+
expect(screen.getByText("Max API Requests")).toBeInTheDocument()
31+
expect(screen.getByText("Limit the number of API requests")).toBeInTheDocument()
32+
expect(screen.getByDisplayValue("10")).toBeInTheDocument()
33+
})
34+
35+
it("renders with menu variant styling", () => {
36+
render(<MaxRequestsInput allowedMaxRequests={5} onValueChange={mockOnValueChange} variant="menu" />)
37+
38+
expect(screen.getByText("Max API Requests")).toBeInTheDocument()
39+
expect(screen.getByText("Limit the number of API requests")).toBeInTheDocument()
40+
expect(screen.getByDisplayValue("5")).toBeInTheDocument()
41+
})
42+
43+
it("shows empty input when allowedMaxRequests is undefined", () => {
44+
render(<MaxRequestsInput allowedMaxRequests={undefined} onValueChange={mockOnValueChange} />)
45+
46+
const input = screen.getByPlaceholderText("Unlimited")
47+
expect(input).toHaveValue("")
48+
})
49+
50+
it("shows empty input when allowedMaxRequests is Infinity", () => {
51+
render(<MaxRequestsInput allowedMaxRequests={Infinity} onValueChange={mockOnValueChange} />)
52+
53+
const input = screen.getByPlaceholderText("Unlimited")
54+
expect(input).toHaveValue("")
55+
})
56+
57+
it("filters non-numeric input and calls onValueChange", () => {
58+
render(<MaxRequestsInput allowedMaxRequests={undefined} onValueChange={mockOnValueChange} />)
59+
60+
const input = screen.getByPlaceholderText("Unlimited")
61+
fireEvent.input(input, { target: { value: "abc123def" } })
62+
63+
expect(mockOnValueChange).toHaveBeenCalledWith(123)
64+
})
65+
66+
it("calls onValueChange with undefined for invalid input", () => {
67+
render(<MaxRequestsInput allowedMaxRequests={undefined} onValueChange={mockOnValueChange} />)
68+
69+
const input = screen.getByPlaceholderText("Unlimited")
70+
fireEvent.input(input, { target: { value: "abc" } })
71+
72+
expect(mockOnValueChange).toHaveBeenCalledWith(undefined)
73+
})
74+
75+
it("calls onValueChange with undefined for zero input", () => {
76+
render(<MaxRequestsInput allowedMaxRequests={undefined} onValueChange={mockOnValueChange} />)
77+
78+
const input = screen.getByPlaceholderText("Unlimited")
79+
fireEvent.input(input, { target: { value: "0" } })
80+
81+
expect(mockOnValueChange).toHaveBeenCalledWith(undefined)
82+
})
83+
})

0 commit comments

Comments
 (0)