Skip to content

Commit f954f97

Browse files
author
Chris Hasson
committed
feat(ui): Differentiate auto-approval warnings for requests and cost
This commit enhances the auto-approval warning system to distinguish between reaching the request limit and the cost limit. - **`src/core/task/Task.ts`**: Modified to pass a `type` parameter (`"requests"` or `"cost"`) to the `ask` method when the auto-approval limit is reached. - **`webview-ui/src/components/chat/AutoApprovedRequestLimitWarning.tsx`**: Updated to use the new `type` parameter to dynamically select appropriate i18n keys for title, description, and button text, providing clearer messages to the user. - **`webview-ui/src/components/settings/MaxCostInput.tsx`**: Refactored to use a new `FormattedTextField` component, simplifying input handling and validation for the max cost setting. This change removes redundant state management and event handlers. - **`webview-ui/src/components/settings/__tests__/MaxCostInput.spec.tsx`**: Updated tests to reflect the changes in `MaxCostInput`, specifically removing tests related to blur/enter events and focusing on direct value changes. - **`webview-ui/src/i18n/locales/*/chat.json`**: Added new translation keys (`autoApprovedCostLimitReached.title`, `description`, `button`) across all supported languages to support the cost limit warning. This change improves user clarity and experience by providing specific feedback when either the request count or the monetary cost limit for auto-approved actions is reached.
1 parent cc69429 commit f954f97

File tree

26 files changed

+164
-99
lines changed

26 files changed

+164
-99
lines changed

src/core/task/Task.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1952,7 +1952,10 @@ export class Task extends EventEmitter<ClineEvents> {
19521952
this.consecutiveAutoApprovedRequestsCount++
19531953

19541954
if (this.consecutiveAutoApprovedRequestsCount > maxRequests) {
1955-
const { response } = await this.ask("auto_approval_max_req_reached", JSON.stringify({ count: maxRequests }))
1955+
const { response } = await this.ask(
1956+
"auto_approval_max_req_reached",
1957+
JSON.stringify({ count: maxRequests, type: "requests" }),
1958+
)
19561959
// If we get past the promise, it means the user approved and did not start a new task
19571960
if (response === "yesButtonClicked") {
19581961
this.consecutiveAutoApprovedRequestsCount = 0
@@ -1967,7 +1970,7 @@ export class Task extends EventEmitter<ClineEvents> {
19671970
if (this.consecutiveAutoApprovedCost > maxCost) {
19681971
const { response } = await this.ask(
19691972
"auto_approval_max_req_reached",
1970-
JSON.stringify({ count: maxCost.toFixed(2) }),
1973+
JSON.stringify({ count: maxCost.toFixed(2), type: "cost" }),
19711974
)
19721975
// If we get past the promise, it means the user approved and did not start a new task
19731976
if (response === "yesButtonClicked") {

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

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,29 @@ type AutoApprovedRequestLimitWarningProps = {
1212

1313
export const AutoApprovedRequestLimitWarning = memo(({ message }: AutoApprovedRequestLimitWarningProps) => {
1414
const [buttonClicked, setButtonClicked] = useState(false)
15-
const { count } = JSON.parse(message.text ?? "{}")
15+
const { count, type = "requests" } = JSON.parse(message.text ?? "{}")
1616

1717
if (buttonClicked) {
1818
return null
1919
}
2020

21+
const isCostLimit = type === "cost"
22+
const titleKey = isCostLimit
23+
? "ask.autoApprovedCostLimitReached.title"
24+
: "ask.autoApprovedRequestLimitReached.title"
25+
const descriptionKey = isCostLimit
26+
? "ask.autoApprovedCostLimitReached.description"
27+
: "ask.autoApprovedRequestLimitReached.description"
28+
const buttonKey = isCostLimit
29+
? "ask.autoApprovedCostLimitReached.button"
30+
: "ask.autoApprovedRequestLimitReached.button"
31+
2132
return (
2233
<>
2334
<div style={{ display: "flex", alignItems: "center", gap: "8px", color: "var(--vscode-foreground)" }}>
2435
<span className="codicon codicon-warning" />
2536
<span style={{ fontWeight: "bold" }}>
26-
<Trans i18nKey="ask.autoApprovedRequestLimitReached.title" ns="chat" />
37+
<Trans i18nKey={titleKey} ns="chat" />
2738
</span>
2839
</div>
2940

@@ -37,7 +48,7 @@ export const AutoApprovedRequestLimitWarning = memo(({ message }: AutoApprovedRe
3748
justifyContent: "center",
3849
}}>
3950
<div className="flex justify-between items-center">
40-
<Trans i18nKey="ask.autoApprovedRequestLimitReached.description" ns="chat" values={{ count }} />
51+
<Trans i18nKey={descriptionKey} ns="chat" values={{ count }} />
4152
</div>
4253
<VSCodeButton
4354
style={{ width: "100%", padding: "6px", borderRadius: "4px" }}
@@ -46,7 +57,7 @@ export const AutoApprovedRequestLimitWarning = memo(({ message }: AutoApprovedRe
4657
setButtonClicked(true)
4758
vscode.postMessage({ type: "askResponse", askResponse: "yesButtonClicked" })
4859
}}>
49-
<Trans i18nKey="ask.autoApprovedRequestLimitReached.button" ns="chat" />
60+
<Trans i18nKey={buttonKey} ns="chat" />
5061
</VSCodeButton>
5162
</div>
5263
</>

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

Lines changed: 30 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,26 @@
11
import { useTranslation } from "react-i18next"
22
import { vscode } from "@/utils/vscode"
3-
import { useCallback, useState, useEffect } from "react"
4-
import { DecoratedVSCodeTextField } from "@/components/common/DecoratedVSCodeTextField"
3+
import { useCallback } from "react"
4+
import { FormattedTextField, InputFormatter } from "../common/FormattedTextField"
5+
6+
const unlimitedDecimalFormatter: InputFormatter<number> = {
7+
parse: (input: string) => {
8+
if (input.trim() === "") return undefined
9+
const value = parseFloat(input)
10+
return !isNaN(value) && value >= 0 ? value : undefined
11+
},
12+
format: (value: number | undefined) => {
13+
return value === undefined || value === Infinity ? "" : value.toString()
14+
},
15+
filter: (input: string) => {
16+
let cleanValue = input.replace(/[^0-9.]/g, "")
17+
const parts = cleanValue.split(".")
18+
if (parts.length > 2) {
19+
cleanValue = parts[0] + "." + parts.slice(1).join("")
20+
}
21+
return cleanValue
22+
},
23+
}
524

625
interface MaxCostInputProps {
726
allowedMaxCost?: number
@@ -11,63 +30,13 @@ interface MaxCostInputProps {
1130

1231
export function MaxCostInput({ allowedMaxCost, onValueChange, className }: MaxCostInputProps) {
1332
const { t } = useTranslation()
14-
const [inputValue, setInputValue] = useState("")
15-
16-
// Update input value when allowedMaxCost prop changes
17-
useEffect(() => {
18-
const displayValue = (allowedMaxCost ?? Infinity) === Infinity ? "" : (allowedMaxCost?.toString() ?? "")
19-
setInputValue(displayValue)
20-
}, [allowedMaxCost])
21-
22-
const parseAndValidateInput = useCallback((value: string) => {
23-
if (value.trim() === "") {
24-
return undefined
25-
}
26-
const numericValue = parseFloat(value)
27-
return !isNaN(numericValue) && numericValue >= 0 ? numericValue : undefined
28-
}, [])
29-
30-
const handleInput = useCallback((e: any) => {
31-
const input = e.target as HTMLInputElement
32-
// Only allow numbers and decimal points
33-
let cleanValue = input.value.replace(/[^0-9.]/g, "")
34-
35-
// Prevent multiple decimal points
36-
const parts = cleanValue.split(".")
37-
if (parts.length > 2) {
38-
cleanValue = parts[0] + "." + parts.slice(1).join("")
39-
}
40-
41-
// Update the input value immediately for user feedback
42-
input.value = cleanValue
43-
setInputValue(cleanValue)
44-
}, [])
45-
46-
const handleBlurOrEnter = useCallback(
47-
(value: string) => {
48-
const parsedValue = parseAndValidateInput(value)
49-
onValueChange(parsedValue)
50-
vscode.postMessage({ type: "allowedMaxCost", value: parsedValue })
51-
},
52-
[parseAndValidateInput, onValueChange],
53-
)
54-
55-
const handleBlur = useCallback(
56-
(e: any) => {
57-
const value = e.target.value
58-
handleBlurOrEnter(value)
59-
},
60-
[handleBlurOrEnter],
61-
)
6233

63-
const handleKeyDown = useCallback(
64-
(e: any) => {
65-
if (e.key === "Enter") {
66-
const value = e.target.value
67-
handleBlurOrEnter(value)
68-
}
34+
const handleValueChange = useCallback(
35+
(value: number | undefined) => {
36+
onValueChange(value)
37+
vscode.postMessage({ type: "allowedMaxCost", value })
6938
},
70-
[handleBlurOrEnter],
39+
[onValueChange],
7140
)
7241

7342
return (
@@ -77,12 +46,11 @@ export function MaxCostInput({ allowedMaxCost, onValueChange, className }: MaxCo
7746
<div>{t("settings:autoApprove.apiCostLimit.title")}</div>
7847
</div>
7948
<div className="flex items-center">
80-
<DecoratedVSCodeTextField
49+
<FormattedTextField
50+
value={allowedMaxCost}
51+
onValueChange={handleValueChange}
52+
formatter={unlimitedDecimalFormatter}
8153
placeholder={t("settings:autoApprove.apiCostLimit.unlimited")}
82-
value={inputValue}
83-
onInput={handleInput}
84-
onBlur={handleBlur}
85-
onKeyDown={handleKeyDown}
8654
style={{ flex: 1, maxWidth: "200px" }}
8755
data-testid="max-cost-input"
8856
leftNodes={[<span key="dollar">$</span>]}

webview-ui/src/components/settings/__tests__/MaxCostInput.spec.tsx

Lines changed: 4 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ vi.mock("react-i18next", () => ({
1111
const translations: Record<string, string> = {
1212
"settings:autoApprove.apiCostLimit.title": "Max API Cost",
1313
"settings:autoApprove.apiCostLimit.unlimited": "Unlimited",
14-
"settings:autoApprove.apiCostLimit.description": "Limit the total API cost",
1514
}
1615
return { t: (key: string) => translations[key] || key }
1716
},
@@ -38,63 +37,38 @@ describe("MaxCostInput", () => {
3837
expect(input).toHaveValue("5.5")
3938
})
4039

41-
it("calls onValueChange when input loses focus", () => {
40+
it("calls onValueChange when input changes", () => {
4241
render(<MaxCostInput allowedMaxCost={undefined} onValueChange={mockOnValueChange} />)
4342

4443
const input = screen.getByPlaceholderText("Unlimited")
4544
fireEvent.input(input, { target: { value: "10.25" } })
46-
fireEvent.blur(input)
4745

4846
expect(mockOnValueChange).toHaveBeenCalledWith(10.25)
4947
})
5048

51-
it("calls onValueChange when Enter key is pressed", () => {
52-
render(<MaxCostInput allowedMaxCost={undefined} onValueChange={mockOnValueChange} />)
53-
54-
const input = screen.getByPlaceholderText("Unlimited")
55-
fireEvent.input(input, { target: { value: "5.50" } })
56-
fireEvent.keyDown(input, { key: "Enter" })
57-
58-
expect(mockOnValueChange).toHaveBeenCalledWith(5.5)
59-
})
60-
61-
it("calls onValueChange with undefined when input is cleared and blurred", () => {
49+
it("calls onValueChange with undefined when input is cleared", () => {
6250
render(<MaxCostInput allowedMaxCost={5.0} onValueChange={mockOnValueChange} />)
6351

6452
const input = screen.getByPlaceholderText("Unlimited")
6553
fireEvent.input(input, { target: { value: "" } })
66-
fireEvent.blur(input)
6754

6855
expect(mockOnValueChange).toHaveBeenCalledWith(undefined)
6956
})
7057

71-
it("handles decimal input correctly on blur", () => {
58+
it("handles decimal input correctly", () => {
7259
render(<MaxCostInput allowedMaxCost={undefined} onValueChange={mockOnValueChange} />)
7360

7461
const input = screen.getByPlaceholderText("Unlimited")
7562
fireEvent.input(input, { target: { value: "2.99" } })
76-
fireEvent.blur(input)
7763

7864
expect(mockOnValueChange).toHaveBeenCalledWith(2.99)
7965
})
8066

81-
it("allows typing zero without immediate parsing", () => {
82-
render(<MaxCostInput allowedMaxCost={undefined} onValueChange={mockOnValueChange} />)
83-
84-
const input = screen.getByPlaceholderText("Unlimited")
85-
fireEvent.input(input, { target: { value: "0" } })
86-
87-
// Should not call onValueChange during typing
88-
expect(mockOnValueChange).not.toHaveBeenCalled()
89-
expect(input).toHaveValue("0")
90-
})
91-
92-
it("accepts zero as a valid value on blur", () => {
67+
it("accepts zero as a valid value", () => {
9368
render(<MaxCostInput allowedMaxCost={undefined} onValueChange={mockOnValueChange} />)
9469

9570
const input = screen.getByPlaceholderText("Unlimited")
9671
fireEvent.input(input, { target: { value: "0" } })
97-
fireEvent.blur(input)
9872

9973
expect(mockOnValueChange).toHaveBeenCalledWith(0)
10074
})
@@ -104,7 +78,6 @@ describe("MaxCostInput", () => {
10478

10579
const input = screen.getByPlaceholderText("Unlimited")
10680
fireEvent.input(input, { target: { value: "0.15" } })
107-
fireEvent.blur(input)
10881

10982
expect(mockOnValueChange).toHaveBeenCalledWith(0.15)
11083
})

webview-ui/src/i18n/locales/ar/chat.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,11 @@
329329
"title": "تم بلوغ حد الطلبات الموافق عليها تلقائياً",
330330
"description": "Kilo Code وصل لحد {{count}} طلب API موافَق عليه تلقائيًا. تبي تعيد العداد وتكمل المهمة؟",
331331
"button": "إعادة الضبط والمتابعة"
332+
},
333+
"autoApprovedCostLimitReached": {
334+
"button": "إعادة ضبط والاستمرار",
335+
"title": "تم الوصول إلى حد التكلفة المعتمدة تلقائيًا",
336+
"description": "وصل كيلو كود إلى حد التكلفة المعتمد تلقائيًا البالغ ${{count}}. هل ترغب في إعادة ضبط التكلفة والمتابعة بالمهمة؟"
332337
}
333338
},
334339
"indexingStatus": {

webview-ui/src/i18n/locales/ca/chat.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,11 @@
303303
"title": "S'ha arribat al límit de sol·licituds aprovades automàticament",
304304
"description": "Kilo Code ha arribat al límit aprovat automàticament de {{count}} sol·licitud(s) d'API. Vols reiniciar el comptador i continuar amb la tasca?",
305305
"button": "Reiniciar i continuar"
306+
},
307+
"autoApprovedCostLimitReached": {
308+
"title": "S'ha assolit el límit de cost d'aprovació automàtica",
309+
"description": "Kilo Code ha arribat al límit de cost aprovat automàticament de ${{count}}. Voldríeu restablir el cost i continuar amb la tasca?",
310+
"button": "Restablir i continuar"
306311
}
307312
},
308313
"codebaseSearch": {

webview-ui/src/i18n/locales/cs/chat.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,11 @@
325325
"title": "Dosažen limit automaticky schválených požadavků",
326326
"description": "Kilo Code dosáhl automaticky schváleného limitu {{count}} API požadavků. Chceš resetovat počítadlo a pokračovat v úkolu?",
327327
"button": "Resetovat a pokračovat"
328+
},
329+
"autoApprovedCostLimitReached": {
330+
"title": "Dosažen limit automaticky schválených nákladů",
331+
"description": "Kilo Code dosáhl automaticky schváleného limitu nákladů ${{count}}. Chcete resetovat náklady a pokračovat v úkolu?",
332+
"button": "Resetovat a pokračovat"
328333
}
329334
},
330335
"indexingStatus": {

webview-ui/src/i18n/locales/de/chat.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,11 @@
303303
"title": "Limit für automatisch genehmigte Anfragen erreicht",
304304
"description": "Kilo Code hat das automatisch genehmigte Limit von {{count}} API-Anfrage(n) erreicht. Möchtest du den Zähler zurücksetzen und mit der Aufgabe fortfahren?",
305305
"button": "Zurücksetzen und fortfahren"
306+
},
307+
"autoApprovedCostLimitReached": {
308+
"title": "Automatisch genehmigte Kostengrenze erreicht",
309+
"description": "Kilo Code hat das automatisch genehmigte Kostenlimit von ${{count}} erreicht. Möchten Sie die Kosten zurücksetzen und mit der Aufgabe fortfahren?",
310+
"button": "Zurücksetzen und Fortfahren"
306311
}
307312
},
308313
"codebaseSearch": {

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,11 @@
320320
"title": "Auto-Approved Request Limit Reached",
321321
"description": "Kilo Code has reached the auto-approved limit of {{count}} API request(s). Would you like to reset the count and proceed with the task?",
322322
"button": "Reset and Continue"
323+
},
324+
"autoApprovedCostLimitReached": {
325+
"title": "Auto-Approved Cost Limit Reached",
326+
"description": "Kilo Code has reached the auto-approved cost limit of ${{count}}. Would you like to reset the cost and proceed with the task?",
327+
"button": "Reset and Continue"
323328
}
324329
},
325330
"indexingStatus": {

webview-ui/src/i18n/locales/es/chat.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,11 @@
303303
"title": "Límite de Solicitudes Auto-aprobadas Alcanzado",
304304
"description": "Kilo Code ha alcanzado el límite auto-aprobado de {{count}} solicitud(es) API. ¿Deseas reiniciar el contador y continuar con la tarea?",
305305
"button": "Reiniciar y Continuar"
306+
},
307+
"autoApprovedCostLimitReached": {
308+
"title": "Límite de Costos con Aprobación Automática Alcanzado",
309+
"button": "Restablecer y continuar",
310+
"description": "Kilo Code ha alcanzado el límite de costo aprobado automáticamente de ${{count}}. ¿Le gustaría restablecer el costo y continuar con la tarea?"
306311
}
307312
},
308313
"codebaseSearch": {

0 commit comments

Comments
 (0)