Skip to content

Commit eec55b3

Browse files
author
Chris Hasson
committed
feat(settings): Add API cost limit to auto-approval settings
This commit introduces a new auto-approval setting that allows users to define a maximum API cost. The system will automatically approve requests up to this cost limit before prompting the user for approval to continue the task. Key changes include: - Added `allowedMaxCost` to `globalSettingsSchema` in `@packages/types`. - Implemented cost tracking and limit enforcement in `Task.ts`. - Integrated `allowedMaxCost` into `ClineProvider` and `webviewMessageHandler` for state management. - Created new UI components `CostInput`, `FormattedTextField`, and `MaxLimitInputs` in `webview-ui` to handle cost input and formatting. - Updated `AutoApproveMenu` and `AutoApproveSettings` to include the new cost limit input. - Added new Storybook stories for `AutoApproveMenu` and `AutoApproveSettings`. - Included new test files for `FormattedTextField` and `MaxCostInput`. - Updated i18n localization files across all supported languages to include new strings for the API cost limit setting.
1 parent 74a138e commit eec55b3

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+813
-30
lines changed
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import type { Meta, StoryObj } from "@storybook/react"
2+
import AutoApproveMenu from "../../../webview-ui/src/components/chat/AutoApproveMenu"
3+
4+
const meta: Meta<typeof AutoApproveMenu> = {
5+
title: "Chat/AutoApproveMenu",
6+
component: AutoApproveMenu,
7+
parameters: {
8+
layout: "padded",
9+
},
10+
decorators: [
11+
(Story) => (
12+
<div style={{ maxWidth: "400px", margin: "0 auto" }}>
13+
<Story />
14+
</div>
15+
),
16+
],
17+
}
18+
19+
export default meta
20+
type Story = StoryObj<typeof AutoApproveMenu>
21+
22+
export const Collapsed: Story = {}
23+
24+
export const Expanded: Story = {
25+
args: {
26+
initialExpanded: true,
27+
},
28+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import type { Meta, StoryObj } from "@storybook/react"
2+
import { AutoApproveSettings } from "../../../webview-ui/src/components/settings/AutoApproveSettings"
3+
4+
const meta: Meta<typeof AutoApproveSettings> = {
5+
title: "Settings/AutoApproveSettings",
6+
component: AutoApproveSettings,
7+
decorators: [
8+
(Story) => (
9+
<div style={{ maxWidth: "600px", margin: "0 auto" }}>
10+
<Story />
11+
</div>
12+
),
13+
],
14+
}
15+
16+
export default meta
17+
type Story = StoryObj<typeof AutoApproveSettings>
18+
19+
export const Default: Story = {
20+
args: {},
21+
}

packages/types/src/global-settings.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ export const globalSettingsSchema = z.object({
5353
deniedCommands: z.array(z.string()).optional(),
5454
commandExecutionTimeout: z.number().optional(),
5555
allowedMaxRequests: z.number().nullish(),
56+
allowedMaxCost: z.number().nullish(),
5657
autoCondenseContext: z.boolean().optional(),
5758
autoCondenseContextPercent: z.number().optional(),
5859
maxConcurrentFileReads: z.number().optional(),

src/core/task/Task.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@ export class Task extends EventEmitter<ClineEvents> {
164164
api: ApiHandler
165165
private static lastGlobalApiRequestTime?: number
166166
private consecutiveAutoApprovedRequestsCount: number = 0
167+
private consecutiveAutoApprovedCost: number = 0
167168

168169
/**
169170
* Reset the global API request timestamp. This should only be used for testing.
@@ -1905,6 +1906,22 @@ export class Task extends EventEmitter<ClineEvents> {
19051906
}
19061907
}
19071908

1909+
// Check if we've reached the maximum allowed cost
1910+
const maxCost = state?.allowedMaxCost || Infinity
1911+
const currentCost = getApiMetrics(this.combineMessages(this.clineMessages.slice(1))).totalCost
1912+
this.consecutiveAutoApprovedCost = currentCost
1913+
1914+
if (this.consecutiveAutoApprovedCost > maxCost) {
1915+
const { response } = await this.ask(
1916+
"auto_approval_max_req_reached",
1917+
JSON.stringify({ count: maxCost.toFixed(2) }),
1918+
)
1919+
// If we get past the promise, it means the user approved and did not start a new task
1920+
if (response === "yesButtonClicked") {
1921+
this.consecutiveAutoApprovedCost = 0
1922+
}
1923+
}
1924+
19081925
const metadata: ApiHandlerCreateMessageMetadata = {
19091926
mode: mode,
19101927
taskId: this.taskId,

src/core/webview/ClineProvider.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1437,6 +1437,7 @@ export class ClineProvider
14371437
alwaysAllowSubtasks,
14381438
alwaysAllowUpdateTodoList,
14391439
allowedMaxRequests,
1440+
allowedMaxCost,
14401441
autoCondenseContext,
14411442
autoCondenseContextPercent,
14421443
soundEnabled,
@@ -1533,6 +1534,7 @@ export class ClineProvider
15331534
alwaysAllowSubtasks: alwaysAllowSubtasks ?? true,
15341535
alwaysAllowUpdateTodoList: alwaysAllowUpdateTodoList ?? true,
15351536
allowedMaxRequests,
1537+
allowedMaxCost,
15361538
autoCondenseContext: autoCondenseContext ?? true,
15371539
autoCondenseContextPercent: autoCondenseContextPercent ?? 100,
15381540
uriScheme: vscode.env.uriScheme,
@@ -1715,6 +1717,7 @@ export class ClineProvider
17151717
alwaysAllowUpdateTodoList: stateValues.alwaysAllowUpdateTodoList ?? true, // kilocode_change
17161718
followupAutoApproveTimeoutMs: stateValues.followupAutoApproveTimeoutMs ?? 60000,
17171719
allowedMaxRequests: stateValues.allowedMaxRequests,
1720+
allowedMaxCost: stateValues.allowedMaxCost,
17181721
autoCondenseContext: stateValues.autoCondenseContext ?? true,
17191722
autoCondenseContextPercent: stateValues.autoCondenseContextPercent ?? 100,
17201723
taskHistory: stateValues.taskHistory,

src/core/webview/webviewMessageHandler.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,10 @@ export const webviewMessageHandler = async (
349349
await updateGlobalState("allowedMaxRequests", message.value)
350350
await provider.postStateToWebview()
351351
break
352+
case "allowedMaxCost":
353+
await updateGlobalState("allowedMaxCost", message.value)
354+
await provider.postStateToWebview()
355+
break
352356
case "alwaysAllowSubtasks":
353357
await updateGlobalState("alwaysAllowSubtasks", message.bool)
354358
await provider.postStateToWebview()

src/shared/ExtensionMessage.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,7 @@ export type ExtensionState = Pick<
218218
| "allowedCommands"
219219
| "deniedCommands"
220220
| "allowedMaxRequests"
221+
| "allowedMaxCost"
221222
| "browserToolEnabled"
222223
| "browserViewportSize"
223224
| "showAutoApproveMenu" // kilocode_change

src/shared/WebviewMessage.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ export interface WebviewMessage {
8989
| "alwaysAllowMcp"
9090
| "alwaysAllowModeSwitch"
9191
| "allowedMaxRequests"
92+
| "allowedMaxCost"
9293
| "alwaysAllowSubtasks"
9394
| "alwaysAllowUpdateTodoList"
9495
| "autoCondenseContext"

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

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,25 @@ 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" // kilocode_change
9+
import { MaxLimitInputs } from "../settings/MaxLimitInputs" // kilocode_change
1010
import { StandardTooltip } from "@src/components/ui"
1111
import { useAutoApprovalState } from "@src/hooks/useAutoApprovalState"
1212
import { useAutoApprovalToggles } from "@src/hooks/useAutoApprovalToggles"
1313

1414
interface AutoApproveMenuProps {
1515
style?: React.CSSProperties
16+
initialExpanded?: boolean
1617
}
1718

18-
const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => {
19-
const [isExpanded, setIsExpanded] = useState(false)
19+
const AutoApproveMenu = ({ style, initialExpanded = false }: AutoApproveMenuProps) => {
20+
const [isExpanded, setIsExpanded] = useState(initialExpanded)
2021

2122
const {
2223
autoApprovalEnabled,
2324
setAutoApprovalEnabled,
2425
alwaysApproveResubmit,
2526
allowedMaxRequests,
27+
allowedMaxCost,
2628
setAlwaysAllowReadOnly,
2729
setAlwaysAllowWrite,
2830
setAlwaysAllowExecute,
@@ -34,6 +36,7 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => {
3436
setAlwaysAllowFollowupQuestions,
3537
setAlwaysAllowUpdateTodoList,
3638
setAllowedMaxRequests,
39+
setAllowedMaxCost,
3740
} = useExtensionState()
3841

3942
const { t } = useAppTranslation()
@@ -250,9 +253,11 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => {
250253
<AutoApproveToggle {...toggles} onToggle={onAutoApproveToggle} />
251254

252255
{/* kilocode_change start */}
253-
<MaxRequestsInput
256+
<MaxLimitInputs
254257
allowedMaxRequests={allowedMaxRequests ?? undefined}
255-
onValueChange={(value) => setAllowedMaxRequests(value)}
258+
allowedMaxCost={allowedMaxCost ?? undefined}
259+
onMaxRequestsChange={(value) => setAllowedMaxRequests(value)}
260+
onMaxCostChange={(value) => setAllowedMaxCost(value)}
256261
/>
257262
{/* kilocode_change end */}
258263
</div>
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
// kilocode_change - new file
2+
import { useCallback } from "react"
3+
import { FormattedTextField, currencyFormatter } from "./FormattedTextField"
4+
5+
interface CostInputProps {
6+
value?: number | undefined
7+
onValueChange: (value: number | undefined) => void
8+
placeholder?: string
9+
className?: string
10+
style?: React.CSSProperties
11+
"data-testid"?: string
12+
label?: string
13+
description?: string
14+
icon?: string
15+
}
16+
17+
export function CostInput({
18+
value,
19+
onValueChange,
20+
placeholder,
21+
className,
22+
style,
23+
"data-testid": dataTestId,
24+
label,
25+
description,
26+
icon = "codicon-credit-card",
27+
}: CostInputProps) {
28+
const handleValueChange = useCallback(
29+
(newValue: number | undefined) => {
30+
onValueChange(newValue)
31+
},
32+
[onValueChange],
33+
)
34+
35+
if (label || description) {
36+
return (
37+
<div className={`flex flex-col gap-3 pl-3 border-l-2 border-vscode-button-background ${className || ""}`}>
38+
{label && (
39+
<div className="flex items-center gap-4 font-bold">
40+
<span className={`codicon ${icon}`} />
41+
<div>{label}</div>
42+
</div>
43+
)}
44+
<div className="flex items-center gap-2">
45+
<span className="text-vscode-descriptionForeground">$</span>
46+
<FormattedTextField
47+
value={value}
48+
onValueChange={handleValueChange}
49+
formatter={currencyFormatter}
50+
placeholder={placeholder || "0.00"}
51+
style={style || { flex: 1, maxWidth: "200px" }}
52+
data-testid={dataTestId}
53+
/>
54+
</div>
55+
{description && <div className="text-vscode-descriptionForeground text-sm">{description}</div>}
56+
</div>
57+
)
58+
}
59+
60+
return (
61+
<div className="flex items-center gap-2">
62+
<span className="text-vscode-descriptionForeground">$</span>
63+
<FormattedTextField
64+
value={value}
65+
onValueChange={handleValueChange}
66+
formatter={currencyFormatter}
67+
placeholder={placeholder || "0.00"}
68+
className={className}
69+
style={style}
70+
data-testid={dataTestId}
71+
/>
72+
</div>
73+
)
74+
}

0 commit comments

Comments
 (0)