Skip to content

Commit ef2d4cc

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 bbb25c3 commit ef2d4cc

Some content is hidden

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

45 files changed

+828
-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
@@ -51,6 +51,7 @@ export const globalSettingsSchema = z.object({
5151
alwaysAllowUpdateTodoList: z.boolean().optional(),
5252
allowedCommands: z.array(z.string()).optional(),
5353
allowedMaxRequests: z.number().nullish(),
54+
allowedMaxCost: z.number().nullish(),
5455
autoCondenseContext: z.boolean().optional(),
5556
autoCondenseContextPercent: z.number().optional(),
5657
maxConcurrentFileReads: z.number().optional(),

src/core/task/Task.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ export class Task extends EventEmitter<ClineEvents> {
162162
api: ApiHandler
163163
private static lastGlobalApiRequestTime?: number
164164
private consecutiveAutoApprovedRequestsCount: number = 0
165+
private consecutiveAutoApprovedCost: number = 0
165166

166167
/**
167168
* Reset the global API request timestamp. This should only be used for testing.
@@ -1894,6 +1895,22 @@ export class Task extends EventEmitter<ClineEvents> {
18941895
}
18951896
}
18961897

1898+
// Check if we've reached the maximum allowed cost
1899+
const maxCost = state?.allowedMaxCost || Infinity
1900+
const currentCost = getApiMetrics(this.combineMessages(this.clineMessages.slice(1))).totalCost
1901+
this.consecutiveAutoApprovedCost = currentCost
1902+
1903+
if (this.consecutiveAutoApprovedCost > maxCost) {
1904+
const { response } = await this.ask(
1905+
"auto_approval_max_req_reached",
1906+
JSON.stringify({ count: maxCost.toFixed(2) }),
1907+
)
1908+
// If we get past the promise, it means the user approved and did not start a new task
1909+
if (response === "yesButtonClicked") {
1910+
this.consecutiveAutoApprovedCost = 0
1911+
}
1912+
}
1913+
18971914
const metadata: ApiHandlerCreateMessageMetadata = {
18981915
mode: mode,
18991916
taskId: this.taskId,

src/core/webview/ClineProvider.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1410,6 +1410,7 @@ export class ClineProvider
14101410
alwaysAllowSubtasks,
14111411
alwaysAllowUpdateTodoList,
14121412
allowedMaxRequests,
1413+
allowedMaxCost,
14131414
autoCondenseContext,
14141415
autoCondenseContextPercent,
14151416
soundEnabled,
@@ -1505,6 +1506,7 @@ export class ClineProvider
15051506
alwaysAllowSubtasks: alwaysAllowSubtasks ?? true,
15061507
alwaysAllowUpdateTodoList: alwaysAllowUpdateTodoList ?? true,
15071508
allowedMaxRequests,
1509+
allowedMaxCost,
15081510
autoCondenseContext: autoCondenseContext ?? true,
15091511
autoCondenseContextPercent: autoCondenseContextPercent ?? 100,
15101512
uriScheme: vscode.env.uriScheme,
@@ -1682,6 +1684,7 @@ export class ClineProvider
16821684
alwaysAllowUpdateTodoList: stateValues.alwaysAllowUpdateTodoList ?? true, // kilocode_change
16831685
followupAutoApproveTimeoutMs: stateValues.followupAutoApproveTimeoutMs ?? 60000,
16841686
allowedMaxRequests: stateValues.allowedMaxRequests,
1687+
allowedMaxCost: stateValues.allowedMaxCost,
16851688
autoCondenseContext: stateValues.autoCondenseContext ?? true,
16861689
autoCondenseContextPercent: stateValues.autoCondenseContextPercent ?? 100,
16871690
taskHistory: stateValues.taskHistory,

src/core/webview/webviewMessageHandler.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -391,6 +391,10 @@ export const webviewMessageHandler = async (
391391
await updateGlobalState("allowedMaxRequests", message.value)
392392
await provider.postStateToWebview()
393393
break
394+
case "allowedMaxCost":
395+
await updateGlobalState("allowedMaxCost", message.value)
396+
await provider.postStateToWebview()
397+
break
394398
case "alwaysAllowSubtasks":
395399
await updateGlobalState("alwaysAllowSubtasks", message.bool)
396400
await provider.postStateToWebview()

src/shared/ExtensionMessage.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,7 @@ export type ExtensionState = Pick<
213213
| "alwaysAllowUpdateTodoList"
214214
| "allowedCommands"
215215
| "allowedMaxRequests"
216+
| "allowedMaxCost"
216217
| "browserToolEnabled"
217218
| "browserViewportSize"
218219
| "showAutoApproveMenu" // kilocode_change

src/shared/WebviewMessage.ts

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

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

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,15 @@ 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"
9+
import { MaxLimitInputs } from "../settings/MaxLimitInputs"
1010

1111
interface AutoApproveMenuProps {
1212
style?: React.CSSProperties
13+
initialExpanded?: boolean
1314
}
1415

15-
const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => {
16-
const [isExpanded, setIsExpanded] = useState(false)
16+
const AutoApproveMenu = ({ style, initialExpanded = false }: AutoApproveMenuProps) => {
17+
const [isExpanded, setIsExpanded] = useState(initialExpanded)
1718

1819
const {
1920
autoApprovalEnabled,
@@ -29,6 +30,7 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => {
2930
alwaysAllowFollowupQuestions,
3031
alwaysAllowUpdateTodoList,
3132
allowedMaxRequests,
33+
allowedMaxCost,
3234
setAlwaysAllowReadOnly,
3335
setAlwaysAllowWrite,
3436
setAlwaysAllowExecute,
@@ -40,6 +42,7 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => {
4042
setAlwaysAllowFollowupQuestions,
4143
setAlwaysAllowUpdateTodoList,
4244
setAllowedMaxRequests,
45+
setAllowedMaxCost,
4346
} = useExtensionState()
4447

4548
const { t } = useAppTranslation()
@@ -219,9 +222,11 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => {
219222
<AutoApproveToggle {...toggles} onToggle={onAutoApproveToggle} />
220223

221224
{/* kilocode_change start */}
222-
<MaxRequestsInput
225+
<MaxLimitInputs
223226
allowedMaxRequests={allowedMaxRequests ?? undefined}
224-
onValueChange={(value) => setAllowedMaxRequests(value)}
227+
allowedMaxCost={allowedMaxCost ?? undefined}
228+
onMaxRequestsChange={(value) => setAllowedMaxRequests(value)}
229+
onMaxCostChange={(value) => setAllowedMaxCost(value)}
225230
/>
226231
{/* kilocode_change end */}
227232
</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)