Skip to content

Commit 1627ebf

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 8d9ef82 commit 1627ebf

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
@@ -2101,7 +2101,10 @@ export class Task extends EventEmitter<ClineEvents> {
21012101
this.consecutiveAutoApprovedRequestsCount++
21022102

21032103
if (this.consecutiveAutoApprovedRequestsCount > maxRequests) {
2104-
const { response } = await this.ask("auto_approval_max_req_reached", JSON.stringify({ count: maxRequests }))
2104+
const { response } = await this.ask(
2105+
"auto_approval_max_req_reached",
2106+
JSON.stringify({ count: maxRequests, type: "requests" }),
2107+
)
21052108
// If we get past the promise, it means the user approved and did not start a new task
21062109
if (response === "yesButtonClicked") {
21072110
this.consecutiveAutoApprovedRequestsCount = 0
@@ -2116,7 +2119,7 @@ export class Task extends EventEmitter<ClineEvents> {
21162119
if (this.consecutiveAutoApprovedCost > maxCost) {
21172120
const { response } = await this.ask(
21182121
"auto_approval_max_req_reached",
2119-
JSON.stringify({ count: maxCost.toFixed(2) }),
2122+
JSON.stringify({ count: maxCost.toFixed(2), type: "cost" }),
21202123
)
21212124
// If we get past the promise, it means the user approved and did not start a new task
21222125
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
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

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

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

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

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

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

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,11 @@
337337
"title": "Auto-Approved Request Limit Reached",
338338
"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?",
339339
"button": "Reset and Continue"
340+
},
341+
"autoApprovedCostLimitReached": {
342+
"title": "Auto-Approved Cost Limit Reached",
343+
"description": "Kilo Code has reached the auto-approved cost limit of ${{count}}. Would you like to reset the cost and proceed with the task?",
344+
"button": "Reset and Continue"
340345
}
341346
},
342347
"indexingStatus": {

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

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)