diff --git a/packages/types/src/global-settings.ts b/packages/types/src/global-settings.ts index 8916263d5d..82f1426349 100644 --- a/packages/types/src/global-settings.ts +++ b/packages/types/src/global-settings.ts @@ -68,6 +68,7 @@ export const globalSettingsSchema = z.object({ commandTimeoutAllowlist: z.array(z.string()).optional(), preventCompletionWithOpenTodos: z.boolean().optional(), allowedMaxRequests: z.number().nullish(), + allowedMaxCost: z.number().nullish(), autoCondenseContext: z.boolean().optional(), autoCondenseContextPercent: z.number().optional(), maxConcurrentFileReads: z.number().optional(), diff --git a/src/core/task/AutoApprovalHandler.ts b/src/core/task/AutoApprovalHandler.ts new file mode 100644 index 0000000000..33821ddfa2 --- /dev/null +++ b/src/core/task/AutoApprovalHandler.ts @@ -0,0 +1,144 @@ +import { GlobalState, ClineMessage, ClineAsk } from "@roo-code/types" +import { getApiMetrics } from "../../shared/getApiMetrics" +import { ClineAskResponse } from "../../shared/WebviewMessage" + +export interface AutoApprovalResult { + shouldProceed: boolean + requiresApproval: boolean + approvalType?: "requests" | "cost" + approvalCount?: number | string +} + +export class AutoApprovalHandler { + private consecutiveAutoApprovedRequestsCount: number = 0 + private consecutiveAutoApprovedCost: number = 0 + + /** + * Check if auto-approval limits have been reached and handle user approval if needed + */ + async checkAutoApprovalLimits( + state: GlobalState | undefined, + messages: ClineMessage[], + askForApproval: ( + type: ClineAsk, + data: string, + ) => Promise<{ response: ClineAskResponse; text?: string; images?: string[] }>, + ): Promise { + // Check request count limit + const requestResult = await this.checkRequestLimit(state, askForApproval) + if (!requestResult.shouldProceed || requestResult.requiresApproval) { + return requestResult + } + + // Check cost limit + const costResult = await this.checkCostLimit(state, messages, askForApproval) + return costResult + } + + /** + * Increment the request counter and check if limit is exceeded + */ + private async checkRequestLimit( + state: GlobalState | undefined, + askForApproval: ( + type: ClineAsk, + data: string, + ) => Promise<{ response: ClineAskResponse; text?: string; images?: string[] }>, + ): Promise { + const maxRequests = state?.allowedMaxRequests || Infinity + + // Increment the counter for each new API request + this.consecutiveAutoApprovedRequestsCount++ + + if (this.consecutiveAutoApprovedRequestsCount > maxRequests) { + const { response } = await askForApproval( + "auto_approval_max_req_reached", + JSON.stringify({ count: maxRequests, type: "requests" }), + ) + + // If we get past the promise, it means the user approved and did not start a new task + if (response === "yesButtonClicked") { + this.consecutiveAutoApprovedRequestsCount = 0 + return { + shouldProceed: true, + requiresApproval: true, + approvalType: "requests", + approvalCount: maxRequests, + } + } + + return { + shouldProceed: false, + requiresApproval: true, + approvalType: "requests", + approvalCount: maxRequests, + } + } + + return { shouldProceed: true, requiresApproval: false } + } + + /** + * Calculate current cost and check if limit is exceeded + */ + private async checkCostLimit( + state: GlobalState | undefined, + messages: ClineMessage[], + askForApproval: ( + type: ClineAsk, + data: string, + ) => Promise<{ response: ClineAskResponse; text?: string; images?: string[] }>, + ): Promise { + const maxCost = state?.allowedMaxCost || Infinity + + // Calculate total cost from messages + this.consecutiveAutoApprovedCost = getApiMetrics(messages).totalCost + + // Use epsilon for floating-point comparison to avoid precision issues + const EPSILON = 0.0001 + if (this.consecutiveAutoApprovedCost > maxCost + EPSILON) { + const { response } = await askForApproval( + "auto_approval_max_req_reached", + JSON.stringify({ count: maxCost.toFixed(2), type: "cost" }), + ) + + // If we get past the promise, it means the user approved and did not start a new task + if (response === "yesButtonClicked") { + // Note: We don't reset the cost to 0 here because the actual cost + // is calculated from the messages. This is different from the request count. + return { + shouldProceed: true, + requiresApproval: true, + approvalType: "cost", + approvalCount: maxCost.toFixed(2), + } + } + + return { + shouldProceed: false, + requiresApproval: true, + approvalType: "cost", + approvalCount: maxCost.toFixed(2), + } + } + + return { shouldProceed: true, requiresApproval: false } + } + + /** + * Reset the request counter (typically called when starting a new task) + */ + resetRequestCount(): void { + this.consecutiveAutoApprovedRequestsCount = 0 + } + + /** + * Get current approval state for debugging/testing + */ + getApprovalState(): { requestCount: number; currentCost: number } { + return { + requestCount: this.consecutiveAutoApprovedRequestsCount, + currentCost: this.consecutiveAutoApprovedCost, + } + } +} diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index 9d68640fd8..9df9a225d1 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -92,6 +92,7 @@ import { ApiMessage } from "../task-persistence/apiMessages" import { getMessagesSinceLastSummary, summarizeConversation } from "../condense" import { maybeRemoveImageBlocks } from "../../api/transform/image-cleaning" import { restoreTodoListForTask } from "../tools/updateTodoListTool" +import { AutoApprovalHandler } from "./AutoApprovalHandler" const MAX_EXPONENTIAL_BACKOFF_SECONDS = 600 // 10 minutes @@ -199,7 +200,7 @@ export class Task extends EventEmitter { readonly apiConfiguration: ProviderSettings api: ApiHandler private static lastGlobalApiRequestTime?: number - private consecutiveAutoApprovedRequestsCount: number = 0 + private autoApprovalHandler: AutoApprovalHandler /** * Reset the global API request timestamp. This should only be used for testing. @@ -302,6 +303,7 @@ export class Task extends EventEmitter { this.apiConfiguration = apiConfiguration this.api = buildApiHandler(apiConfiguration) + this.autoApprovalHandler = new AutoApprovalHandler() this.urlContentFetcher = new UrlContentFetcher(provider.context) this.browserSession = new BrowserSession(provider.context) @@ -1968,18 +1970,16 @@ export class Task extends EventEmitter { ({ role, content }) => ({ role, content }), ) - // Check if we've reached the maximum number of auto-approved requests - const maxRequests = state?.allowedMaxRequests || Infinity - - // Increment the counter for each new API request - this.consecutiveAutoApprovedRequestsCount++ + // Check auto-approval limits + const approvalResult = await this.autoApprovalHandler.checkAutoApprovalLimits( + state, + this.combineMessages(this.clineMessages.slice(1)), + async (type, data) => this.ask(type, data), + ) - if (this.consecutiveAutoApprovedRequestsCount > maxRequests) { - const { response } = await this.ask("auto_approval_max_req_reached", JSON.stringify({ count: maxRequests })) - // If we get past the promise, it means the user approved and did not start a new task - if (response === "yesButtonClicked") { - this.consecutiveAutoApprovedRequestsCount = 0 - } + if (!approvalResult.shouldProceed) { + // User did not approve, task should be aborted + throw new Error("Auto-approval limit reached and user did not approve continuation") } const metadata: ApiHandlerCreateMessageMetadata = { diff --git a/src/core/task/__tests__/AutoApprovalHandler.spec.ts b/src/core/task/__tests__/AutoApprovalHandler.spec.ts new file mode 100644 index 0000000000..e200948a33 --- /dev/null +++ b/src/core/task/__tests__/AutoApprovalHandler.spec.ts @@ -0,0 +1,249 @@ +import { describe, it, expect, vi, beforeEach } from "vitest" +import { AutoApprovalHandler } from "../AutoApprovalHandler" +import { GlobalState, ClineMessage } from "@roo-code/types" + +// Mock getApiMetrics +vi.mock("../../../shared/getApiMetrics", () => ({ + getApiMetrics: vi.fn(), +})) + +import { getApiMetrics } from "../../../shared/getApiMetrics" + +describe("AutoApprovalHandler", () => { + let handler: AutoApprovalHandler + let mockAskForApproval: any + let mockState: GlobalState + const mockGetApiMetrics = getApiMetrics as any + + beforeEach(() => { + handler = new AutoApprovalHandler() + mockAskForApproval = vi.fn() + mockState = {} as GlobalState + vi.clearAllMocks() + + // Default mock for getApiMetrics + mockGetApiMetrics.mockReturnValue({ totalCost: 0 }) + }) + + describe("checkAutoApprovalLimits", () => { + it("should proceed when no limits are set", async () => { + const messages: ClineMessage[] = [] + const result = await handler.checkAutoApprovalLimits(mockState, messages, mockAskForApproval) + + expect(result.shouldProceed).toBe(true) + expect(result.requiresApproval).toBe(false) + expect(mockAskForApproval).not.toHaveBeenCalled() + }) + + it("should check request limit before cost limit", async () => { + mockState.allowedMaxRequests = 1 + mockState.allowedMaxCost = 10 + const messages: ClineMessage[] = [] + + // First call should be under limit + const result1 = await handler.checkAutoApprovalLimits(mockState, messages, mockAskForApproval) + expect(result1.shouldProceed).toBe(true) + expect(result1.requiresApproval).toBe(false) + + // Second call should trigger request limit + mockAskForApproval.mockResolvedValue({ response: "yesButtonClicked" }) + const result2 = await handler.checkAutoApprovalLimits(mockState, messages, mockAskForApproval) + + expect(mockAskForApproval).toHaveBeenCalledWith( + "auto_approval_max_req_reached", + JSON.stringify({ count: 1, type: "requests" }), + ) + expect(result2.shouldProceed).toBe(true) + expect(result2.requiresApproval).toBe(true) + expect(result2.approvalType).toBe("requests") + }) + }) + + describe("request limit handling", () => { + beforeEach(() => { + mockState.allowedMaxRequests = 3 + }) + + it("should increment request count on each check", async () => { + const messages: ClineMessage[] = [] + + // Check state after each call + for (let i = 1; i <= 3; i++) { + await handler.checkAutoApprovalLimits(mockState, messages, mockAskForApproval) + const state = handler.getApprovalState() + expect(state.requestCount).toBe(i) + } + }) + + it("should ask for approval when limit is exceeded", async () => { + const messages: ClineMessage[] = [] + + // Make 3 requests (within limit) + for (let i = 0; i < 3; i++) { + await handler.checkAutoApprovalLimits(mockState, messages, mockAskForApproval) + } + expect(mockAskForApproval).not.toHaveBeenCalled() + + // 4th request should trigger approval + mockAskForApproval.mockResolvedValue({ response: "yesButtonClicked" }) + const result = await handler.checkAutoApprovalLimits(mockState, messages, mockAskForApproval) + + expect(mockAskForApproval).toHaveBeenCalledWith( + "auto_approval_max_req_reached", + JSON.stringify({ count: 3, type: "requests" }), + ) + expect(result.shouldProceed).toBe(true) + expect(result.requiresApproval).toBe(true) + }) + + it("should reset count when user approves", async () => { + const messages: ClineMessage[] = [] + + // Exceed limit + for (let i = 0; i < 3; i++) { + await handler.checkAutoApprovalLimits(mockState, messages, mockAskForApproval) + } + + // 4th request should trigger approval and reset + mockAskForApproval.mockResolvedValue({ response: "yesButtonClicked" }) + await handler.checkAutoApprovalLimits(mockState, messages, mockAskForApproval) + + // Count should be reset + const state = handler.getApprovalState() + expect(state.requestCount).toBe(0) + }) + + it("should not proceed when user rejects", async () => { + const messages: ClineMessage[] = [] + + // Exceed limit + for (let i = 0; i < 3; i++) { + await handler.checkAutoApprovalLimits(mockState, messages, mockAskForApproval) + } + + // 4th request with rejection + mockAskForApproval.mockResolvedValue({ response: "noButtonClicked" }) + const result = await handler.checkAutoApprovalLimits(mockState, messages, mockAskForApproval) + + expect(result.shouldProceed).toBe(false) + expect(result.requiresApproval).toBe(true) + }) + }) + + describe("cost limit handling", () => { + beforeEach(() => { + mockState.allowedMaxCost = 5.0 + }) + + it("should calculate cost from messages", async () => { + const messages: ClineMessage[] = [] + + mockGetApiMetrics.mockReturnValue({ totalCost: 3.5 }) + const result = await handler.checkAutoApprovalLimits(mockState, messages, mockAskForApproval) + + expect(mockGetApiMetrics).toHaveBeenCalledWith(messages) + expect(result.shouldProceed).toBe(true) + expect(result.requiresApproval).toBe(false) + }) + + it("should ask for approval when cost limit is exceeded", async () => { + const messages: ClineMessage[] = [] + + mockGetApiMetrics.mockReturnValue({ totalCost: 5.5 }) + mockAskForApproval.mockResolvedValue({ response: "yesButtonClicked" }) + + const result = await handler.checkAutoApprovalLimits(mockState, messages, mockAskForApproval) + + expect(mockAskForApproval).toHaveBeenCalledWith( + "auto_approval_max_req_reached", + JSON.stringify({ count: "5.00", type: "cost" }), + ) + expect(result.shouldProceed).toBe(true) + expect(result.requiresApproval).toBe(true) + expect(result.approvalType).toBe("cost") + }) + + it("should handle floating-point precision correctly", async () => { + const messages: ClineMessage[] = [] + + // Test edge case where cost is exactly at limit (should not trigger) + mockGetApiMetrics.mockReturnValue({ totalCost: 5.0 }) + const result1 = await handler.checkAutoApprovalLimits(mockState, messages, mockAskForApproval) + expect(result1.requiresApproval).toBe(false) + + // Test with slight floating-point error (should not trigger) + mockGetApiMetrics.mockReturnValue({ totalCost: 5.00009 }) + const result2 = await handler.checkAutoApprovalLimits(mockState, messages, mockAskForApproval) + expect(result2.requiresApproval).toBe(false) + + // Test when actually exceeded (should trigger) + mockGetApiMetrics.mockReturnValue({ totalCost: 5.001 }) + mockAskForApproval.mockResolvedValue({ response: "yesButtonClicked" }) + const result3 = await handler.checkAutoApprovalLimits(mockState, messages, mockAskForApproval) + expect(result3.requiresApproval).toBe(true) + }) + + it("should not reset cost to zero on approval", async () => { + const messages: ClineMessage[] = [] + + mockGetApiMetrics.mockReturnValue({ totalCost: 6.0 }) + mockAskForApproval.mockResolvedValue({ response: "yesButtonClicked" }) + + await handler.checkAutoApprovalLimits(mockState, messages, mockAskForApproval) + + // Cost should still be calculated from messages, not reset + const state = handler.getApprovalState() + expect(state.currentCost).toBe(6.0) + }) + }) + + describe("combined limits", () => { + it("should handle both request and cost limits", async () => { + mockState.allowedMaxRequests = 2 + mockState.allowedMaxCost = 10.0 + const messages: ClineMessage[] = [] + + mockGetApiMetrics.mockReturnValue({ totalCost: 3.0 }) + + // First two requests should pass + for (let i = 0; i < 2; i++) { + const result = await handler.checkAutoApprovalLimits(mockState, messages, mockAskForApproval) + expect(result.shouldProceed).toBe(true) + expect(result.requiresApproval).toBe(false) + } + + // Third request should trigger request limit (not cost limit) + mockAskForApproval.mockResolvedValue({ response: "yesButtonClicked" }) + const result = await handler.checkAutoApprovalLimits(mockState, messages, mockAskForApproval) + + expect(mockAskForApproval).toHaveBeenCalledWith( + "auto_approval_max_req_reached", + JSON.stringify({ count: 2, type: "requests" }), + ) + expect(result.shouldProceed).toBe(true) + expect(result.requiresApproval).toBe(true) + expect(result.approvalType).toBe("requests") + }) + }) + + describe("resetRequestCount", () => { + it("should reset the request counter", async () => { + mockState.allowedMaxRequests = 5 + const messages: ClineMessage[] = [] + + // Make some requests + for (let i = 0; i < 3; i++) { + await handler.checkAutoApprovalLimits(mockState, messages, mockAskForApproval) + } + + let state = handler.getApprovalState() + expect(state.requestCount).toBe(3) + + // Reset + handler.resetRequestCount() + + state = handler.getApprovalState() + expect(state.requestCount).toBe(0) + }) + }) +}) diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index e013525e06..94a84d6d21 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -1446,6 +1446,7 @@ export class ClineProvider alwaysAllowSubtasks, alwaysAllowUpdateTodoList, allowedMaxRequests, + allowedMaxCost, autoCondenseContext, autoCondenseContextPercent, soundEnabled, @@ -1541,6 +1542,7 @@ export class ClineProvider alwaysAllowSubtasks: alwaysAllowSubtasks ?? false, alwaysAllowUpdateTodoList: alwaysAllowUpdateTodoList ?? false, allowedMaxRequests, + allowedMaxCost, autoCondenseContext: autoCondenseContext ?? true, autoCondenseContextPercent: autoCondenseContextPercent ?? 100, uriScheme: vscode.env.uriScheme, @@ -1737,6 +1739,7 @@ export class ClineProvider followupAutoApproveTimeoutMs: stateValues.followupAutoApproveTimeoutMs ?? 60000, diagnosticsEnabled: stateValues.diagnosticsEnabled ?? true, allowedMaxRequests: stateValues.allowedMaxRequests, + allowedMaxCost: stateValues.allowedMaxCost, autoCondenseContext: stateValues.autoCondenseContext ?? true, autoCondenseContextPercent: stateValues.autoCondenseContextPercent ?? 100, taskHistory: stateValues.taskHistory, diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index b1b62229c9..cabd5a1677 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -332,6 +332,10 @@ export const webviewMessageHandler = async ( await updateGlobalState("allowedMaxRequests", message.value) await provider.postStateToWebview() break + case "allowedMaxCost": + await updateGlobalState("allowedMaxCost", message.value) + await provider.postStateToWebview() + break case "alwaysAllowSubtasks": await updateGlobalState("alwaysAllowSubtasks", message.bool) await provider.postStateToWebview() diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index 1e562bb9ee..e81ab9659e 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -222,6 +222,7 @@ export type ExtensionState = Pick< | "allowedCommands" | "deniedCommands" | "allowedMaxRequests" + | "allowedMaxCost" | "browserToolEnabled" | "browserViewportSize" | "screenshotQuality" diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index 0b0cc06880..cb8759d851 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -80,6 +80,7 @@ export interface WebviewMessage { | "alwaysAllowMcp" | "alwaysAllowModeSwitch" | "allowedMaxRequests" + | "allowedMaxCost" | "alwaysAllowSubtasks" | "alwaysAllowUpdateTodoList" | "autoCondenseContext" diff --git a/webview-ui/src/components/chat/AutoApproveMenu.tsx b/webview-ui/src/components/chat/AutoApproveMenu.tsx index 0feafae15d..c97af13593 100644 --- a/webview-ui/src/components/chat/AutoApproveMenu.tsx +++ b/webview-ui/src/components/chat/AutoApproveMenu.tsx @@ -1,11 +1,12 @@ import { memo, useCallback, useMemo, useState } from "react" import { Trans } from "react-i18next" -import { VSCodeCheckbox, VSCodeLink, VSCodeTextField } from "@vscode/webview-ui-toolkit/react" +import { VSCodeCheckbox, VSCodeLink } from "@vscode/webview-ui-toolkit/react" import { vscode } from "@src/utils/vscode" import { useExtensionState } from "@src/context/ExtensionStateContext" import { useAppTranslation } from "@src/i18n/TranslationContext" import { AutoApproveToggle, AutoApproveSetting, autoApproveSettingsConfig } from "../settings/AutoApproveToggle" +import { MaxLimitInputs } from "../settings/MaxLimitInputs" import { StandardTooltip } from "@src/components/ui" import { useAutoApprovalState } from "@src/hooks/useAutoApprovalState" import { useAutoApprovalToggles } from "@src/hooks/useAutoApprovalToggles" @@ -22,6 +23,7 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => { setAutoApprovalEnabled, alwaysApproveResubmit, allowedMaxRequests, + allowedMaxCost, setAlwaysAllowReadOnly, setAlwaysAllowWrite, setAlwaysAllowExecute, @@ -33,6 +35,7 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => { setAlwaysAllowFollowupQuestions, setAlwaysAllowUpdateTodoList, setAllowedMaxRequests, + setAllowedMaxCost, } = useExtensionState() const { t } = useAppTranslation() @@ -243,42 +246,12 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => { - {/* Auto-approve API request count limit input row inspired by Cline */} -
- - : - - { - const input = e.target as HTMLInputElement - // Remove any non-numeric characters - input.value = input.value.replace(/[^0-9]/g, "") - const value = parseInt(input.value) - const parsedValue = !isNaN(value) && value > 0 ? value : undefined - setAllowedMaxRequests(parsedValue) - vscode.postMessage({ type: "allowedMaxRequests", value: parsedValue }) - }} - style={{ flex: 1 }} - /> -
-
- -
+ setAllowedMaxRequests(value)} + onMaxCostChange={(value) => setAllowedMaxCost(value)} + /> )} diff --git a/webview-ui/src/components/chat/AutoApprovedRequestLimitWarning.tsx b/webview-ui/src/components/chat/AutoApprovedRequestLimitWarning.tsx index 1c454e0082..6f019a84c8 100644 --- a/webview-ui/src/components/chat/AutoApprovedRequestLimitWarning.tsx +++ b/webview-ui/src/components/chat/AutoApprovedRequestLimitWarning.tsx @@ -12,18 +12,29 @@ type AutoApprovedRequestLimitWarningProps = { export const AutoApprovedRequestLimitWarning = memo(({ message }: AutoApprovedRequestLimitWarningProps) => { const [buttonClicked, setButtonClicked] = useState(false) - const { count } = JSON.parse(message.text ?? "{}") + const { count, type = "requests" } = JSON.parse(message.text ?? "{}") if (buttonClicked) { return null } + const isCostLimit = type === "cost" + const titleKey = isCostLimit + ? "ask.autoApprovedCostLimitReached.title" + : "ask.autoApprovedRequestLimitReached.title" + const descriptionKey = isCostLimit + ? "ask.autoApprovedCostLimitReached.description" + : "ask.autoApprovedRequestLimitReached.description" + const buttonKey = isCostLimit + ? "ask.autoApprovedCostLimitReached.button" + : "ask.autoApprovedRequestLimitReached.button" + return ( <>
- +
@@ -37,7 +48,7 @@ export const AutoApprovedRequestLimitWarning = memo(({ message }: AutoApprovedRe justifyContent: "center", }}>
- +
- + diff --git a/webview-ui/src/components/common/DecoratedVSCodeTextField.tsx b/webview-ui/src/components/common/DecoratedVSCodeTextField.tsx new file mode 100644 index 0000000000..5e03525ccb --- /dev/null +++ b/webview-ui/src/components/common/DecoratedVSCodeTextField.tsx @@ -0,0 +1,92 @@ +import { cn } from "@/lib/utils" +import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react" +import { forwardRef, useCallback, useRef, ReactNode, ComponentRef, ComponentProps } from "react" + +// Type for web components that have shadow DOM +interface WebComponentWithShadowRoot extends HTMLElement { + shadowRoot: ShadowRoot | null +} + +export interface VSCodeTextFieldWithNodesProps extends ComponentProps { + leftNodes?: ReactNode[] + rightNodes?: ReactNode[] +} + +function VSCodeTextFieldWithNodesInner( + props: VSCodeTextFieldWithNodesProps, + forwardedRef: React.Ref, +) { + const { className, style, "data-testid": dataTestId, leftNodes, rightNodes, ...restProps } = props + + const inputRef = useRef(null) + + // Callback ref to get access to the underlying input element. + // VSCodeTextField doesn't expose this directly so we have to query for it! + const handleVSCodeFieldRef = useCallback( + (element: ComponentRef) => { + if (!element) return + + const webComponent = element as unknown as WebComponentWithShadowRoot + const inputElement = + webComponent.shadowRoot?.querySelector?.("input") || webComponent.querySelector?.("input") + if (inputElement && inputElement instanceof HTMLInputElement) { + inputRef.current = inputElement + if (typeof forwardedRef === "function") { + forwardedRef?.(inputElement) + } else if (forwardedRef) { + ;(forwardedRef as React.MutableRefObject).current = inputElement + } + } + }, + [forwardedRef], + ) + + const focusInput = useCallback(async () => { + if (inputRef.current && document.activeElement !== inputRef.current) { + setTimeout(() => { + inputRef.current?.focus() + }) + } + }, []) + + const hasLeftNodes = leftNodes && leftNodes.filter(Boolean).length > 0 + const hasRightNodes = rightNodes && rightNodes.filter(Boolean).length > 0 + + return ( +
+ {hasLeftNodes && ( +
{leftNodes}
+ )} + + + + {hasRightNodes && ( +
{rightNodes}
+ )} + + {/* Absolutely positioned focus border overlay */} +
+
+ ) +} + +export const DecoratedVSCodeTextField = forwardRef(VSCodeTextFieldWithNodesInner) diff --git a/webview-ui/src/components/common/FormattedTextField.tsx b/webview-ui/src/components/common/FormattedTextField.tsx new file mode 100644 index 0000000000..6e952bd769 --- /dev/null +++ b/webview-ui/src/components/common/FormattedTextField.tsx @@ -0,0 +1,119 @@ +import { useCallback, forwardRef, useState, useEffect } from "react" +import { DecoratedVSCodeTextField, VSCodeTextFieldWithNodesProps } from "./DecoratedVSCodeTextField" + +export interface InputFormatter { + /** + * Parse the raw input string into the typed value + */ + parse: (input: string) => T | undefined + + /** + * Format the typed value for display in the input field + */ + format: (value: T | undefined) => string + + /** + * Filter/transform the input as the user types (optional) + */ + filter?: (input: string) => string +} + +interface FormattedTextFieldProps extends Omit { + value: T | undefined + onValueChange: (value: T | undefined) => void + formatter: InputFormatter +} + +function FormattedTextFieldInner( + { value, onValueChange, formatter, ...restProps }: FormattedTextFieldProps, + forwardedRef: React.Ref, +) { + const [rawInput, setRawInput] = useState("") + const [isTyping, setIsTyping] = useState(false) + + // Update raw input when external value changes (but not when we're actively typing) + useEffect(() => { + if (!isTyping) { + setRawInput(formatter.format(value)) + } + }, [value, formatter, isTyping]) + + const handleInput = useCallback( + (e: React.FormEvent) => { + const input = e.target as HTMLInputElement + setIsTyping(true) + + let filteredValue = input.value + if (formatter.filter) { + filteredValue = formatter.filter(input.value) + input.value = filteredValue + } + + setRawInput(filteredValue) + const parsedValue = formatter.parse(filteredValue) + onValueChange(parsedValue) + }, + [formatter, onValueChange], + ) + + const handleBlur = useCallback(() => { + setIsTyping(false) + // On blur, format the value properly + setRawInput(formatter.format(value)) + }, [formatter, value]) + + const displayValue = isTyping ? rawInput : formatter.format(value) + + return ( + + ) +} + +export const FormattedTextField = forwardRef(FormattedTextFieldInner as any) as ( + props: FormattedTextFieldProps & { ref?: React.Ref }, +) => React.ReactElement + +// Common formatters for reuse +export const unlimitedIntegerFormatter: InputFormatter = { + parse: (input: string) => { + if (input.trim() === "") return undefined + const value = parseInt(input) + return !isNaN(value) && value > 0 ? value : undefined + }, + format: (value: number | undefined) => { + return value === undefined || value === Infinity ? "" : value.toString() + }, + filter: (input: string) => input.replace(/[^0-9]/g, ""), +} + +export const unlimitedDecimalFormatter: InputFormatter = { + parse: (input: string) => { + if (input.trim() === "") return undefined + const value = parseFloat(input) + return !isNaN(value) && value >= 0 ? value : undefined + }, + format: (value: number | undefined) => { + return value === undefined || value === Infinity ? "" : value.toString() + }, + filter: (input: string) => { + // Remove all non-numeric and non-dot characters + let cleanValue = input.replace(/[^0-9.]/g, "") + + // Handle multiple dots - keep only the first one + const firstDotIndex = cleanValue.indexOf(".") + if (firstDotIndex !== -1) { + // Keep everything up to and including the first dot, then remove any additional dots + const beforeDot = cleanValue.substring(0, firstDotIndex + 1) + const afterDot = cleanValue.substring(firstDotIndex + 1).replace(/\./g, "") + cleanValue = beforeDot + afterDot + } + + return cleanValue + }, +} diff --git a/webview-ui/src/components/common/__tests__/FormattedTextField.spec.tsx b/webview-ui/src/components/common/__tests__/FormattedTextField.spec.tsx new file mode 100644 index 0000000000..637a0b5061 --- /dev/null +++ b/webview-ui/src/components/common/__tests__/FormattedTextField.spec.tsx @@ -0,0 +1,219 @@ +import React from "react" +import { describe, it, expect, vi } from "vitest" +import { render, screen, fireEvent } from "@testing-library/react" +import { FormattedTextField, unlimitedIntegerFormatter, unlimitedDecimalFormatter } from "../FormattedTextField" + +// Mock VSCodeTextField to render as regular HTML input for testing +vi.mock("@vscode/webview-ui-toolkit/react", () => ({ + VSCodeTextField: ({ value, onInput, onBlur, placeholder, "data-testid": dataTestId }: any) => ( + onInput({ target: { value: e.target.value } })} + onBlur={onBlur} + placeholder={placeholder} + data-testid={dataTestId} + /> + ), +})) + +describe("FormattedTextField", () => { + describe("unlimitedIntegerFormatter", () => { + it("should parse valid integers", () => { + expect(unlimitedIntegerFormatter.parse("123")).toBe(123) + expect(unlimitedIntegerFormatter.parse("1")).toBe(1) + }) + + it("should return undefined for empty input (unlimited)", () => { + expect(unlimitedIntegerFormatter.parse("")).toBeUndefined() + expect(unlimitedIntegerFormatter.parse(" ")).toBeUndefined() + }) + + it("should return undefined for invalid inputs", () => { + expect(unlimitedIntegerFormatter.parse("0")).toBeUndefined() + expect(unlimitedIntegerFormatter.parse("-5")).toBeUndefined() + expect(unlimitedIntegerFormatter.parse("abc")).toBeUndefined() + }) + + it("should format numbers correctly, treating undefined/Infinity as empty", () => { + expect(unlimitedIntegerFormatter.format(123)).toBe("123") + expect(unlimitedIntegerFormatter.format(undefined)).toBe("") + expect(unlimitedIntegerFormatter.format(Infinity)).toBe("") + }) + + it("should filter non-numeric characters", () => { + expect(unlimitedIntegerFormatter.filter?.("123abc")).toBe("123") + expect(unlimitedIntegerFormatter.filter?.("a1b2c3")).toBe("123") + }) + }) + + describe("FormattedTextField component", () => { + it("should render with correct initial value", () => { + const mockOnChange = vi.fn() + render( + , + ) + + const input = screen.getByTestId("test-input") as HTMLInputElement + expect(input.value).toBe("123") + }) + + it("should render as HTML input (mock verification)", () => { + const mockOnChange = vi.fn() + render( + , + ) + + const input = screen.getByTestId("test-input") + expect(input.tagName).toBe("INPUT") + expect(input).toHaveAttribute("type", "text") + }) + + it("should call onValueChange when input changes", () => { + const mockOnChange = vi.fn() + render( + , + ) + + const input = screen.getByTestId("test-input") + fireEvent.change(input, { target: { value: "456" } }) + expect(mockOnChange).toHaveBeenCalledWith(456) + }) + + it("should apply input filtering", () => { + const mockOnChange = vi.fn() + render( + , + ) + + const input = screen.getByTestId("test-input") as HTMLInputElement + fireEvent.change(input, { target: { value: "123abc" } }) + expect(mockOnChange).toHaveBeenCalledWith(123) + }) + }) + + describe("unlimitedDecimalFormatter", () => { + it("should parse valid decimal numbers", () => { + expect(unlimitedDecimalFormatter.parse("123.45")).toBe(123.45) + expect(unlimitedDecimalFormatter.parse("0.5")).toBe(0.5) + expect(unlimitedDecimalFormatter.parse("1")).toBe(1) + expect(unlimitedDecimalFormatter.parse("0")).toBe(0) + }) + + it("should return undefined for empty input (unlimited)", () => { + expect(unlimitedDecimalFormatter.parse("")).toBeUndefined() + expect(unlimitedDecimalFormatter.parse(" ")).toBeUndefined() + }) + + it("should return undefined for invalid inputs", () => { + expect(unlimitedDecimalFormatter.parse("-5")).toBeUndefined() + expect(unlimitedDecimalFormatter.parse("abc")).toBeUndefined() + }) + + it("should format numbers correctly, treating undefined/Infinity as empty", () => { + expect(unlimitedDecimalFormatter.format(123.45)).toBe("123.45") + expect(unlimitedDecimalFormatter.format(0)).toBe("0") + expect(unlimitedDecimalFormatter.format(undefined)).toBe("") + expect(unlimitedDecimalFormatter.format(Infinity)).toBe("") + }) + + it("should filter non-numeric characters except dots", () => { + expect(unlimitedDecimalFormatter.filter?.("123.45abc")).toBe("123.45") + expect(unlimitedDecimalFormatter.filter?.("a1b2.c3")).toBe("12.3") + }) + + it("should handle multiple dots by keeping only the first one", () => { + expect(unlimitedDecimalFormatter.filter?.("1.2.3.4")).toBe("1.234") + expect(unlimitedDecimalFormatter.filter?.("..123")).toBe(".123") + expect(unlimitedDecimalFormatter.filter?.("1..2")).toBe("1.2") + }) + + it("should preserve trailing dots during typing", () => { + const mockOnChange = vi.fn() + render( + , + ) + + const input = screen.getByTestId("decimal-input") as HTMLInputElement + + // Type "1." + fireEvent.change(input, { target: { value: "1." } }) + + // The input should show "1." (preserving the dot) + expect(input.value).toBe("1.") + // But the parsed value should be 1 + expect(mockOnChange).toHaveBeenCalledWith(1) + }) + + it("should format properly on blur", async () => { + const mockOnChange = vi.fn() + render( + , + ) + + const input = screen.getByTestId("decimal-input") as HTMLInputElement + + // Initially shows formatted value + expect(input.value).toBe("1") + + // Type "1." + fireEvent.change(input, { target: { value: "1." } }) + expect(input.value).toBe("1.") + + // On blur, should format back to "1" + fireEvent.blur(input) + + // Wait for state update + await new Promise((resolve) => setTimeout(resolve, 0)) + expect(input.value).toBe("1") + }) + }) + + describe("FormattedTextField with decimal formatter", () => { + it("should handle decimal input correctly", () => { + const mockOnChange = vi.fn() + render( + , + ) + + const input = screen.getByTestId("test-input") + fireEvent.change(input, { target: { value: "12.34" } }) + expect(mockOnChange).toHaveBeenCalledWith(12.34) + }) + }) +}) diff --git a/webview-ui/src/components/settings/AutoApproveSettings.tsx b/webview-ui/src/components/settings/AutoApproveSettings.tsx index 1a93fee01a..95c311422a 100644 --- a/webview-ui/src/components/settings/AutoApproveSettings.tsx +++ b/webview-ui/src/components/settings/AutoApproveSettings.tsx @@ -10,6 +10,7 @@ import { SetCachedStateField } from "./types" import { SectionHeader } from "./SectionHeader" import { Section } from "./Section" import { AutoApproveToggle } from "./AutoApproveToggle" +import { MaxLimitInputs } from "./MaxLimitInputs" import { useExtensionState } from "@/context/ExtensionStateContext" import { useAutoApprovalState } from "@/hooks/useAutoApprovalState" import { useAutoApprovalToggles } from "@/hooks/useAutoApprovalToggles" @@ -31,6 +32,8 @@ type AutoApproveSettingsProps = HTMLAttributes & { alwaysAllowUpdateTodoList?: boolean followupAutoApproveTimeoutMs?: number allowedCommands?: string[] + allowedMaxRequests?: number | undefined + allowedMaxCost?: number | undefined deniedCommands?: string[] setCachedStateField: SetCachedStateField< | "alwaysAllowReadOnly" @@ -48,6 +51,8 @@ type AutoApproveSettingsProps = HTMLAttributes & { | "alwaysAllowFollowupQuestions" | "followupAutoApproveTimeoutMs" | "allowedCommands" + | "allowedMaxRequests" + | "allowedMaxCost" | "deniedCommands" | "alwaysAllowUpdateTodoList" > @@ -70,6 +75,8 @@ export const AutoApproveSettings = ({ followupAutoApproveTimeoutMs = 60000, alwaysAllowUpdateTodoList, allowedCommands, + allowedMaxRequests, + allowedMaxCost, deniedCommands, setCachedStateField, ...props @@ -152,6 +159,12 @@ export const AutoApproveSettings = ({ alwaysAllowUpdateTodoList={alwaysAllowUpdateTodoList} onToggle={(key, value) => setCachedStateField(key, value)} /> + setCachedStateField("allowedMaxRequests", value)} + onMaxCostChange={(value) => setCachedStateField("allowedMaxCost", value)} + /> {/* ADDITIONAL SETTINGS */} diff --git a/webview-ui/src/components/settings/MaxCostInput.tsx b/webview-ui/src/components/settings/MaxCostInput.tsx new file mode 100644 index 0000000000..369d1bfe35 --- /dev/null +++ b/webview-ui/src/components/settings/MaxCostInput.tsx @@ -0,0 +1,41 @@ +import { useTranslation } from "react-i18next" +import { vscode } from "@/utils/vscode" +import { useCallback } from "react" +import { FormattedTextField, unlimitedDecimalFormatter } from "../common/FormattedTextField" + +interface MaxCostInputProps { + allowedMaxCost?: number + onValueChange: (value: number | undefined) => void +} + +export function MaxCostInput({ allowedMaxCost, onValueChange }: MaxCostInputProps) { + const { t } = useTranslation() + + const handleValueChange = useCallback( + (value: number | undefined) => { + onValueChange(value) + vscode.postMessage({ type: "allowedMaxCost", value }) + }, + [onValueChange], + ) + + return ( +
+
+ +
{t("settings:autoApprove.apiCostLimit.title")}
+
+
+ $]} + /> +
+
+ ) +} diff --git a/webview-ui/src/components/settings/MaxLimitInputs.tsx b/webview-ui/src/components/settings/MaxLimitInputs.tsx new file mode 100644 index 0000000000..0508843180 --- /dev/null +++ b/webview-ui/src/components/settings/MaxLimitInputs.tsx @@ -0,0 +1,32 @@ +import React from "react" +import { useTranslation } from "react-i18next" +import { MaxRequestsInput } from "./MaxRequestsInput" +import { MaxCostInput } from "./MaxCostInput" + +export interface MaxLimitInputsProps { + allowedMaxRequests?: number + allowedMaxCost?: number + onMaxRequestsChange: (value: number | undefined) => void + onMaxCostChange: (value: number | undefined) => void +} + +export const MaxLimitInputs: React.FC = ({ + allowedMaxRequests, + allowedMaxCost, + onMaxRequestsChange, + onMaxCostChange, +}) => { + const { t } = useTranslation() + + return ( +
+
+ + +
+
+ {t("settings:autoApprove.maxLimits.description")} +
+
+ ) +} diff --git a/webview-ui/src/components/settings/MaxRequestsInput.tsx b/webview-ui/src/components/settings/MaxRequestsInput.tsx new file mode 100644 index 0000000000..d0609f4e8e --- /dev/null +++ b/webview-ui/src/components/settings/MaxRequestsInput.tsx @@ -0,0 +1,40 @@ +import { useTranslation } from "react-i18next" +import { vscode } from "@/utils/vscode" +import { useCallback } from "react" +import { FormattedTextField, unlimitedIntegerFormatter } from "../common/FormattedTextField" + +interface MaxRequestsInputProps { + allowedMaxRequests?: number + onValueChange: (value: number | undefined) => void +} + +export function MaxRequestsInput({ allowedMaxRequests, onValueChange }: MaxRequestsInputProps) { + const { t } = useTranslation() + + const handleValueChange = useCallback( + (value: number | undefined) => { + onValueChange(value) + vscode.postMessage({ type: "allowedMaxRequests", value }) + }, + [onValueChange], + ) + + return ( +
+
+ +
{t("settings:autoApprove.apiRequestLimit.title")}
+
+
+ +
+
+ ) +} diff --git a/webview-ui/src/components/settings/SettingsView.tsx b/webview-ui/src/components/settings/SettingsView.tsx index 9cfd9b64e5..630b59485d 100644 --- a/webview-ui/src/components/settings/SettingsView.tsx +++ b/webview-ui/src/components/settings/SettingsView.tsx @@ -125,6 +125,7 @@ const SettingsView = forwardRef(({ onDone, t allowedCommands, deniedCommands, allowedMaxRequests, + allowedMaxCost, language, alwaysAllowBrowser, alwaysAllowExecute, @@ -291,6 +292,7 @@ const SettingsView = forwardRef(({ onDone, t vscode.postMessage({ type: "allowedCommands", commands: allowedCommands ?? [] }) vscode.postMessage({ type: "deniedCommands", commands: deniedCommands ?? [] }) vscode.postMessage({ type: "allowedMaxRequests", value: allowedMaxRequests ?? undefined }) + vscode.postMessage({ type: "allowedMaxCost", value: allowedMaxCost ?? undefined }) vscode.postMessage({ type: "autoCondenseContext", bool: autoCondenseContext }) vscode.postMessage({ type: "autoCondenseContextPercent", value: autoCondenseContextPercent }) vscode.postMessage({ type: "browserToolEnabled", bool: browserToolEnabled }) diff --git a/webview-ui/src/components/settings/__tests__/MaxCostInput.spec.tsx b/webview-ui/src/components/settings/__tests__/MaxCostInput.spec.tsx new file mode 100644 index 0000000000..b57d1cba6c --- /dev/null +++ b/webview-ui/src/components/settings/__tests__/MaxCostInput.spec.tsx @@ -0,0 +1,84 @@ +import { render, screen, fireEvent } from "@testing-library/react" +import { vi } from "vitest" +import { MaxCostInput } from "../MaxCostInput" + +vi.mock("@/utils/vscode", () => ({ + vscode: { postMessage: vi.fn() }, +})) + +vi.mock("react-i18next", () => ({ + useTranslation: () => { + const translations: Record = { + "settings:autoApprove.apiCostLimit.title": "Max API Cost", + "settings:autoApprove.apiCostLimit.unlimited": "Unlimited", + } + return { t: (key: string) => translations[key] || key } + }, +})) + +describe("MaxCostInput", () => { + const mockOnValueChange = vi.fn() + + beforeEach(() => { + mockOnValueChange.mockClear() + }) + + it("shows empty input when allowedMaxCost is undefined", () => { + render() + + const input = screen.getByPlaceholderText("Unlimited") + expect(input).toHaveValue("") + }) + + it("shows formatted cost value when allowedMaxCost is provided", () => { + render() + + const input = screen.getByPlaceholderText("Unlimited") + expect(input).toHaveValue("5.5") + }) + + it("calls onValueChange when input changes", () => { + render() + + const input = screen.getByPlaceholderText("Unlimited") + fireEvent.input(input, { target: { value: "10.25" } }) + + expect(mockOnValueChange).toHaveBeenCalledWith(10.25) + }) + + it("calls onValueChange with undefined when input is cleared", () => { + render() + + const input = screen.getByPlaceholderText("Unlimited") + fireEvent.input(input, { target: { value: "" } }) + + expect(mockOnValueChange).toHaveBeenCalledWith(undefined) + }) + + it("handles decimal input correctly", () => { + render() + + const input = screen.getByPlaceholderText("Unlimited") + fireEvent.input(input, { target: { value: "2.99" } }) + + expect(mockOnValueChange).toHaveBeenCalledWith(2.99) + }) + + it("accepts zero as a valid value", () => { + render() + + const input = screen.getByPlaceholderText("Unlimited") + fireEvent.input(input, { target: { value: "0" } }) + + expect(mockOnValueChange).toHaveBeenCalledWith(0) + }) + + it("allows typing decimal values starting with zero", () => { + render() + + const input = screen.getByPlaceholderText("Unlimited") + fireEvent.input(input, { target: { value: "0.15" } }) + + expect(mockOnValueChange).toHaveBeenCalledWith(0.15) + }) +}) diff --git a/webview-ui/src/components/settings/__tests__/MaxRequestsInput.spec.tsx b/webview-ui/src/components/settings/__tests__/MaxRequestsInput.spec.tsx new file mode 100644 index 0000000000..94940e4569 --- /dev/null +++ b/webview-ui/src/components/settings/__tests__/MaxRequestsInput.spec.tsx @@ -0,0 +1,87 @@ +import { render, screen, fireEvent } from "@testing-library/react" +import { vi } from "vitest" +import { MaxRequestsInput } from "../MaxRequestsInput" + +vi.mock("@/utils/vscode", () => ({ + vscode: { postMessage: vi.fn() }, +})) + +vi.mock("react-i18next", () => ({ + useTranslation: () => { + const translations: Record = { + "settings:autoApprove.apiRequestLimit.title": "Max Count", + "settings:autoApprove.apiRequestLimit.unlimited": "Unlimited", + } + return { t: (key: string) => translations[key] || key } + }, +})) + +describe("MaxRequestsInput", () => { + const mockOnValueChange = vi.fn() + + beforeEach(() => { + mockOnValueChange.mockClear() + }) + + it("shows empty input when allowedMaxRequests is undefined", () => { + render() + + const input = screen.getByPlaceholderText("Unlimited") + expect(input).toHaveValue("") + }) + + it("shows formatted request value when allowedMaxRequests is provided", () => { + render() + + const input = screen.getByPlaceholderText("Unlimited") + expect(input).toHaveValue("10") + }) + + it("calls onValueChange when input changes", () => { + render() + + const input = screen.getByPlaceholderText("Unlimited") + fireEvent.input(input, { target: { value: "5" } }) + + expect(mockOnValueChange).toHaveBeenCalledWith(5) + }) + + it("calls onValueChange with undefined when input is cleared", () => { + render() + + const input = screen.getByPlaceholderText("Unlimited") + fireEvent.input(input, { target: { value: "" } }) + + expect(mockOnValueChange).toHaveBeenCalledWith(undefined) + }) + + it("handles integer input correctly", () => { + render() + + const input = screen.getByPlaceholderText("Unlimited") + fireEvent.input(input, { target: { value: "25" } }) + + expect(mockOnValueChange).toHaveBeenCalledWith(25) + }) + + it("rejects zero and negative values", () => { + render() + + const input = screen.getByPlaceholderText("Unlimited") + + fireEvent.input(input, { target: { value: "0" } }) + expect(mockOnValueChange).toHaveBeenCalledWith(undefined) + + fireEvent.input(input, { target: { value: "-5" } }) + expect(mockOnValueChange).toHaveBeenCalledWith(undefined) + }) + + it("filters non-numeric characters", () => { + render() + + const input = screen.getByPlaceholderText("Unlimited") + fireEvent.input(input, { target: { value: "123abc" } }) + + expect(mockOnValueChange).toHaveBeenCalledWith(123) + }) +}) diff --git a/webview-ui/src/context/ExtensionStateContext.tsx b/webview-ui/src/context/ExtensionStateContext.tsx index 33537b58ac..da7ab63358 100644 --- a/webview-ui/src/context/ExtensionStateContext.tsx +++ b/webview-ui/src/context/ExtensionStateContext.tsx @@ -71,6 +71,7 @@ export interface ExtensionStateContextType extends ExtensionState { setAllowedCommands: (value: string[]) => void setDeniedCommands: (value: string[]) => void setAllowedMaxRequests: (value: number | undefined) => void + setAllowedMaxCost: (value: number | undefined) => void setSoundEnabled: (value: boolean) => void setSoundVolume: (value: number) => void terminalShellIntegrationTimeout?: number @@ -429,6 +430,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode setAllowedCommands: (value) => setState((prevState) => ({ ...prevState, allowedCommands: value })), setDeniedCommands: (value) => setState((prevState) => ({ ...prevState, deniedCommands: value })), setAllowedMaxRequests: (value) => setState((prevState) => ({ ...prevState, allowedMaxRequests: value })), + setAllowedMaxCost: (value) => setState((prevState) => ({ ...prevState, allowedMaxCost: value })), setSoundEnabled: (value) => setState((prevState) => ({ ...prevState, soundEnabled: value })), setSoundVolume: (value) => setState((prevState) => ({ ...prevState, soundVolume: value })), setTtsEnabled: (value) => setState((prevState) => ({ ...prevState, ttsEnabled: value })), diff --git a/webview-ui/src/i18n/locales/ca/chat.json b/webview-ui/src/i18n/locales/ca/chat.json index da0eb00a44..2188e9b706 100644 --- a/webview-ui/src/i18n/locales/ca/chat.json +++ b/webview-ui/src/i18n/locales/ca/chat.json @@ -314,6 +314,11 @@ "title": "S'ha arribat al límit de sol·licituds aprovades automàticament", "description": "Roo ha arribat al límit aprovat automàticament de {{count}} sol·licitud(s) d'API. Vols reiniciar el comptador i continuar amb la tasca?", "button": "Reiniciar i continuar" + }, + "autoApprovedCostLimitReached": { + "title": "S'ha arribat al límit de cost d'aprovació automàtica", + "button": "Restableix i continua", + "description": "Roo ha arribat al límit de cost aprovat automàticament de ${{count}}. Vols restablir el cost i continuar amb la tasca?" } }, "codebaseSearch": { diff --git a/webview-ui/src/i18n/locales/ca/settings.json b/webview-ui/src/i18n/locales/ca/settings.json index 3da1f5e50e..eb26482d96 100644 --- a/webview-ui/src/i18n/locales/ca/settings.json +++ b/webview-ui/src/i18n/locales/ca/settings.json @@ -197,7 +197,14 @@ "description": "Fes aquesta quantitat de sol·licituds API automàticament abans de demanar aprovació per continuar amb la tasca.", "unlimited": "Il·limitat" }, - "selectOptionsFirst": "Seleccioneu almenys una opció a continuació per activar l'aprovació automàtica" + "selectOptionsFirst": "Seleccioneu almenys una opció a continuació per activar l'aprovació automàtica", + "apiCostLimit": { + "title": "Cost Màxim", + "unlimited": "Il·limitat" + }, + "maxLimits": { + "description": "Fes sol·licituds automàticament fins a aquests límits abans de demanar aprovació per continuar." + } }, "providers": { "providerDocumentation": "Documentació de {{provider}}", @@ -308,7 +315,6 @@ "cacheUsageNote": "Nota: Si no veieu l'ús de la caché, proveu de seleccionar un model diferent i després tornar a seleccionar el model desitjat.", "vscodeLmModel": "Model de llenguatge", "vscodeLmWarning": "Nota: Aquesta és una integració molt experimental i el suport del proveïdor variarà. Si rebeu un error sobre un model no compatible, és un problema del proveïdor.", - "geminiParameters": { "urlContext": { "title": "Activa el context d'URL", diff --git a/webview-ui/src/i18n/locales/de/chat.json b/webview-ui/src/i18n/locales/de/chat.json index 36c03c9309..a9c2a385f9 100644 --- a/webview-ui/src/i18n/locales/de/chat.json +++ b/webview-ui/src/i18n/locales/de/chat.json @@ -314,6 +314,11 @@ "title": "Limit für automatisch genehmigte Anfragen erreicht", "description": "Roo hat das automatisch genehmigte Limit von {{count}} API-Anfrage(n) erreicht. Möchtest du den Zähler zurücksetzen und mit der Aufgabe fortfahren?", "button": "Zurücksetzen und fortfahren" + }, + "autoApprovedCostLimitReached": { + "description": "Roo hat das automatisch genehmigte Kostenlimit von ${{count}} erreicht. Möchten Sie die Kosten zurücksetzen und mit der Aufgabe fortfahren?", + "title": "Kostengrenze für automatische Genehmigung erreicht", + "button": "Zurücksetzen und Fortfahren" } }, "codebaseSearch": { diff --git a/webview-ui/src/i18n/locales/de/settings.json b/webview-ui/src/i18n/locales/de/settings.json index a96b41f0c3..1915b67433 100644 --- a/webview-ui/src/i18n/locales/de/settings.json +++ b/webview-ui/src/i18n/locales/de/settings.json @@ -197,7 +197,14 @@ "description": "Automatisch so viele API-Anfragen stellen, bevor du um die Erlaubnis gebeten wirst, mit der Aufgabe fortzufahren.", "unlimited": "Unbegrenzt" }, - "selectOptionsFirst": "Wähle mindestens eine Option unten aus, um die automatische Genehmigung zu aktivieren" + "selectOptionsFirst": "Wähle mindestens eine Option unten aus, um die automatische Genehmigung zu aktivieren", + "apiCostLimit": { + "title": "Maximale Kosten", + "unlimited": "Unbegrenzt" + }, + "maxLimits": { + "description": "Anfragen bis zu diesen Grenzwerten automatisch stellen, bevor um Genehmigung zur Fortsetzung gebeten wird." + } }, "providers": { "providerDocumentation": "{{provider}}-Dokumentation", @@ -308,7 +315,6 @@ "cacheUsageNote": "Hinweis: Wenn Sie keine Cache-Nutzung sehen, versuchen Sie ein anderes Modell auszuwählen und dann Ihr gewünschtes Modell erneut auszuwählen.", "vscodeLmModel": "Sprachmodell", "vscodeLmWarning": "Hinweis: Dies ist eine sehr experimentelle Integration und die Anbieterunterstützung variiert. Wenn Sie einen Fehler über ein nicht unterstütztes Modell erhalten, liegt das Problem auf Anbieterseite.", - "geminiParameters": { "urlContext": { "title": "URL-Kontext aktivieren", diff --git a/webview-ui/src/i18n/locales/en/chat.json b/webview-ui/src/i18n/locales/en/chat.json index b33ddd4ab4..48d55172a5 100644 --- a/webview-ui/src/i18n/locales/en/chat.json +++ b/webview-ui/src/i18n/locales/en/chat.json @@ -332,6 +332,11 @@ "title": "Auto-Approved Request Limit Reached", "description": "Roo has reached the auto-approved limit of {{count}} API request(s). Would you like to reset the count and proceed with the task?", "button": "Reset and Continue" + }, + "autoApprovedCostLimitReached": { + "title": "Auto-Approved Cost Limit Reached", + "description": "Roo has reached the auto-approved cost limit of ${{count}}. Would you like to reset the cost and proceed with the task?", + "button": "Reset and Continue" } }, "indexingStatus": { diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index 5f67268bd9..019d49bc63 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -191,10 +191,16 @@ "description": "Automatically update the to-do list without requiring approval" }, "apiRequestLimit": { - "title": "Max Requests", - "description": "Automatically make this many API requests before asking for approval to continue with the task.", + "title": "Max Count", "unlimited": "Unlimited" }, + "apiCostLimit": { + "title": "Max Cost", + "unlimited": "Unlimited" + }, + "maxLimits": { + "description": "Automatically make requests up to these limits before asking for approval to continue." + }, "toggleAriaLabel": "Toggle auto-approval", "disabledAriaLabel": "Auto-approval disabled - select options first", "selectOptionsFirst": "Select at least one option below to enable auto-approval" diff --git a/webview-ui/src/i18n/locales/es/chat.json b/webview-ui/src/i18n/locales/es/chat.json index 156af85012..e1cd0b262a 100644 --- a/webview-ui/src/i18n/locales/es/chat.json +++ b/webview-ui/src/i18n/locales/es/chat.json @@ -314,6 +314,11 @@ "title": "Límite de Solicitudes Auto-aprobadas Alcanzado", "description": "Roo ha alcanzado el límite auto-aprobado de {{count}} solicitud(es) API. ¿Deseas reiniciar el contador y continuar con la tarea?", "button": "Reiniciar y Continuar" + }, + "autoApprovedCostLimitReached": { + "title": "Límite de Costo Auto-Aprobado Alcanzado", + "description": "Roo ha alcanzado el límite de costo autoaprobado de ${{count}}. ¿Le gustaría reiniciar el costo y continuar con la tarea?", + "button": "Reiniciar y continuar" } }, "codebaseSearch": { diff --git a/webview-ui/src/i18n/locales/es/settings.json b/webview-ui/src/i18n/locales/es/settings.json index c4c7fb39c0..31f12e59c0 100644 --- a/webview-ui/src/i18n/locales/es/settings.json +++ b/webview-ui/src/i18n/locales/es/settings.json @@ -197,7 +197,14 @@ "description": "Realizar automáticamente esta cantidad de solicitudes a la API antes de pedir aprobación para continuar con la tarea.", "unlimited": "Ilimitado" }, - "selectOptionsFirst": "Selecciona al menos una opción a continuación para habilitar la aprobación automática" + "selectOptionsFirst": "Selecciona al menos una opción a continuación para habilitar la aprobación automática", + "apiCostLimit": { + "title": "Costo Máximo", + "unlimited": "Ilimitado" + }, + "maxLimits": { + "description": "Realizar automáticamente solicitudes hasta estos límites antes de pedir aprobación para continuar." + } }, "providers": { "providerDocumentation": "Documentación de {{provider}}", @@ -318,7 +325,6 @@ "description": "Permite que Gemini busque en Google información actual y fundamente las respuestas en datos en tiempo real. Útil para consultas que requieren información actualizada." } }, - "googleCloudSetup": { "title": "Para usar Google Cloud Vertex AI, necesita:", "step1": "1. Crear una cuenta de Google Cloud, habilitar la API de Vertex AI y habilitar los modelos Claude deseados.", diff --git a/webview-ui/src/i18n/locales/fr/chat.json b/webview-ui/src/i18n/locales/fr/chat.json index 959db06b39..b06ed48ea9 100644 --- a/webview-ui/src/i18n/locales/fr/chat.json +++ b/webview-ui/src/i18n/locales/fr/chat.json @@ -314,6 +314,11 @@ "title": "Limite de requêtes auto-approuvées atteinte", "description": "Roo a atteint la limite auto-approuvée de {{count}} requête(s) API. Souhaitez-vous réinitialiser le compteur et poursuivre la tâche ?", "button": "Réinitialiser et continuer" + }, + "autoApprovedCostLimitReached": { + "title": "Limite de coût en auto-approbation atteinte", + "description": "Roo a atteint la limite de coût auto-approuvée de ${{count}}. Souhaitez-vous réinitialiser le coût et poursuivre la tâche ?", + "button": "Réinitialiser et Continuer" } }, "codebaseSearch": { diff --git a/webview-ui/src/i18n/locales/fr/settings.json b/webview-ui/src/i18n/locales/fr/settings.json index e00b0c0559..439560d0e9 100644 --- a/webview-ui/src/i18n/locales/fr/settings.json +++ b/webview-ui/src/i18n/locales/fr/settings.json @@ -197,6 +197,13 @@ "title": "Requêtes maximales", "description": "Effectuer automatiquement ce nombre de requêtes API avant de demander l'approbation pour continuer la tâche.", "unlimited": "Illimité" + }, + "apiCostLimit": { + "unlimited": "Illimité", + "title": "Coût maximum" + }, + "maxLimits": { + "description": "Effectuer automatiquement des requêtes jusqu'à ces limites avant de demander une autorisation pour continuer." } }, "providers": { @@ -318,7 +325,6 @@ "description": "Permet à Gemini d'effectuer des recherches sur Google pour obtenir des informations actuelles et fonder les réponses sur des données en temps réel. Utile pour les requêtes nécessitant des informations à jour." } }, - "googleCloudSetup": { "title": "Pour utiliser Google Cloud Vertex AI, vous devez :", "step1": "1. Créer un compte Google Cloud, activer l'API Vertex AI et activer les modèles Claude souhaités.", diff --git a/webview-ui/src/i18n/locales/hi/chat.json b/webview-ui/src/i18n/locales/hi/chat.json index 21d81e4b88..1c912c3d70 100644 --- a/webview-ui/src/i18n/locales/hi/chat.json +++ b/webview-ui/src/i18n/locales/hi/chat.json @@ -314,6 +314,11 @@ "title": "स्वत:-स्वीकृत अनुरोध सीमा पहुंची", "description": "Roo {{count}} API अनुरोध(धों) की स्वत:-स्वीकृत सीमा तक पहुंच गया है। क्या आप गणना को रीसेट करके कार्य जारी रखना चाहते हैं?", "button": "रीसेट करें और जारी रखें" + }, + "autoApprovedCostLimitReached": { + "title": "स्वत:-अनुमोदित लागत सीमा पहुँच गई", + "button": "रीसेट करें और जारी रखें", + "description": "Roo ने स्वचालित-स्वीकृत लागत सीमा ${{count}} तक पहुंच गई है। क्या आप लागत को रीसेट करके कार्य जारी रखना चाहेंगे?" } }, "codebaseSearch": { diff --git a/webview-ui/src/i18n/locales/hi/settings.json b/webview-ui/src/i18n/locales/hi/settings.json index 2497ffe6da..2429ddaa94 100644 --- a/webview-ui/src/i18n/locales/hi/settings.json +++ b/webview-ui/src/i18n/locales/hi/settings.json @@ -197,7 +197,14 @@ "description": "कार्य जारी रखने के लिए अनुमति मांगने से पहले स्वचालित रूप से इतने API अनुरोध करें।", "unlimited": "असीमित" }, - "selectOptionsFirst": "स्वतः-अनुमोदन सक्षम करने के लिए नीचे से कम से कम एक विकल्प चुनें" + "selectOptionsFirst": "स्वतः-अनुमोदन सक्षम करने के लिए नीचे से कम से कम एक विकल्प चुनें", + "apiCostLimit": { + "unlimited": "असीमित", + "title": "अधिकतम लागत" + }, + "maxLimits": { + "description": "स्वचालित रूप से जारी रखने के लिए अनुमोदन माँगने से पहले इन सीमाओं तक अनुरोध करें।" + } }, "providers": { "providerDocumentation": "{{provider}} दस्तावेज़ीकरण", @@ -318,7 +325,6 @@ "description": "Gemini को वास्तविक समय के डेटा पर आधारित उत्तर प्रदान करने के लिए Google पर जानकारी खोजने और उत्तरों को ग्राउंड करने की अनुमति देता है। अद्यतित जानकारी की आवश्यकता वाली क्वेरीज़ के लिए उपयोगी।" } }, - "googleCloudSetup": { "title": "Google Cloud Vertex AI का उपयोग करने के लिए, आपको आवश्यकता है:", "step1": "1. Google Cloud खाता बनाएं, Vertex AI API सक्षम करें और वांछित Claude मॉडल सक्षम करें।", diff --git a/webview-ui/src/i18n/locales/id/chat.json b/webview-ui/src/i18n/locales/id/chat.json index fd594480a8..2a4345191e 100644 --- a/webview-ui/src/i18n/locales/id/chat.json +++ b/webview-ui/src/i18n/locales/id/chat.json @@ -335,6 +335,11 @@ "title": "Batas Permintaan yang Disetujui Otomatis Tercapai", "description": "Roo telah mencapai batas {{count}} permintaan API yang disetujui otomatis. Apakah kamu ingin mengatur ulang hitungan dan melanjutkan tugas?", "button": "Atur Ulang dan Lanjutkan" + }, + "autoApprovedCostLimitReached": { + "description": "Roo telah mencapai batas biaya yang disetujui secara otomatis sebesar ${{count}}. Apakah Anda ingin mengatur ulang biaya dan melanjutkan tugas ini?", + "button": "Reset dan Lanjutkan", + "title": "Batas Biaya Otomatis-Disetujui Tercapai" } }, "indexingStatus": { diff --git a/webview-ui/src/i18n/locales/id/settings.json b/webview-ui/src/i18n/locales/id/settings.json index 3665c99c1b..5c85ec3856 100644 --- a/webview-ui/src/i18n/locales/id/settings.json +++ b/webview-ui/src/i18n/locales/id/settings.json @@ -201,7 +201,14 @@ "description": "Secara otomatis membuat sejumlah permintaan API ini sebelum meminta persetujuan untuk melanjutkan tugas.", "unlimited": "Tidak terbatas" }, - "selectOptionsFirst": "Pilih setidaknya satu opsi di bawah ini untuk mengaktifkan persetujuan otomatis" + "selectOptionsFirst": "Pilih setidaknya satu opsi di bawah ini untuk mengaktifkan persetujuan otomatis", + "apiCostLimit": { + "title": "Biaya Maksimal", + "unlimited": "Tidak Terbatas" + }, + "maxLimits": { + "description": "Secara otomatis membuat permintaan hingga batas ini sebelum meminta persetujuan untuk melanjutkan." + } }, "providers": { "providerDocumentation": "Dokumentasi {{provider}}", @@ -322,7 +329,6 @@ "description": "Memungkinkan Gemini mencari informasi terkini di Google dan mendasarkan respons pada data waktu nyata. Berguna untuk kueri yang memerlukan informasi terkini." } }, - "googleCloudSetup": { "title": "Untuk menggunakan Google Cloud Vertex AI, kamu perlu:", "step1": "1. Buat akun Google Cloud, aktifkan Vertex AI API & aktifkan model Claude yang diinginkan.", diff --git a/webview-ui/src/i18n/locales/it/chat.json b/webview-ui/src/i18n/locales/it/chat.json index 5b92e06322..d36a20f3da 100644 --- a/webview-ui/src/i18n/locales/it/chat.json +++ b/webview-ui/src/i18n/locales/it/chat.json @@ -314,6 +314,11 @@ "title": "Limite di Richieste Auto-approvate Raggiunto", "description": "Roo ha raggiunto il limite auto-approvato di {{count}} richiesta/e API. Vuoi reimpostare il contatore e procedere con l'attività?", "button": "Reimposta e Continua" + }, + "autoApprovedCostLimitReached": { + "title": "Limite di costo auto-approvato raggiunto", + "button": "Reimposta e Continua", + "description": "Roo ha raggiunto il limite di costo approvato automaticamente di ${{count}}. Vuoi reimpostare il costo e procedere con l'attività?" } }, "codebaseSearch": { diff --git a/webview-ui/src/i18n/locales/it/settings.json b/webview-ui/src/i18n/locales/it/settings.json index 056b0f9124..90b95ac5e5 100644 --- a/webview-ui/src/i18n/locales/it/settings.json +++ b/webview-ui/src/i18n/locales/it/settings.json @@ -197,7 +197,14 @@ "description": "Esegui automaticamente questo numero di richieste API prima di chiedere l'approvazione per continuare con l'attività.", "unlimited": "Illimitato" }, - "selectOptionsFirst": "Seleziona almeno un'opzione qui sotto per abilitare l'approvazione automatica" + "selectOptionsFirst": "Seleziona almeno un'opzione qui sotto per abilitare l'approvazione automatica", + "apiCostLimit": { + "unlimited": "Illimitato", + "title": "Costo massimo" + }, + "maxLimits": { + "description": "Esegui automaticamente richieste fino a questi limiti prima di chiedere l'approvazione per continuare." + } }, "providers": { "providerDocumentation": "Documentazione {{provider}}", @@ -318,7 +325,6 @@ "description": "Consente a Gemini di cercare informazioni aggiornate su Google e basare le risposte su dati in tempo reale. Utile per query che richiedono informazioni aggiornate." } }, - "googleCloudSetup": { "title": "Per utilizzare Google Cloud Vertex AI, è necessario:", "step1": "1. Creare un account Google Cloud, abilitare l'API Vertex AI e abilitare i modelli Claude desiderati.", diff --git a/webview-ui/src/i18n/locales/ja/chat.json b/webview-ui/src/i18n/locales/ja/chat.json index 0ed516f2b7..1b268f007d 100644 --- a/webview-ui/src/i18n/locales/ja/chat.json +++ b/webview-ui/src/i18n/locales/ja/chat.json @@ -314,6 +314,11 @@ "title": "自動承認リクエスト制限に達しました", "description": "Rooは{{count}}件のAPI自動承認リクエスト制限に達しました。カウントをリセットしてタスクを続行しますか?", "button": "リセットして続行" + }, + "autoApprovedCostLimitReached": { + "title": "自動承認コスト制限に達しました", + "description": "Rooは自動承認されたコスト制限の${{count}}に達しました。コストをリセットしてタスクを続行しますか?", + "button": "リセットして続ける" } }, "codebaseSearch": { diff --git a/webview-ui/src/i18n/locales/ja/settings.json b/webview-ui/src/i18n/locales/ja/settings.json index 3b38277b86..5370d00688 100644 --- a/webview-ui/src/i18n/locales/ja/settings.json +++ b/webview-ui/src/i18n/locales/ja/settings.json @@ -197,7 +197,14 @@ "description": "タスクを続行するための承認を求める前に、自動的にこの数のAPIリクエストを行います。", "unlimited": "無制限" }, - "selectOptionsFirst": "自動承認を有効にするには、以下のオプションを少なくとも1つ選択してください" + "selectOptionsFirst": "自動承認を有効にするには、以下のオプションを少なくとも1つ選択してください", + "apiCostLimit": { + "unlimited": "無制限", + "title": "最大料金" + }, + "maxLimits": { + "description": "これらの上限まで自動的にリクエストを行い、その後継続の承認を求めます。" + } }, "providers": { "providerDocumentation": "{{provider}}のドキュメント", @@ -318,7 +325,6 @@ "description": "GeminiがGoogleを検索して最新情報を取得し、リアルタイムデータに基づいて応答をグラウンディングできるようにします。最新情報が必要なクエリに便利です。" } }, - "googleCloudSetup": { "title": "Google Cloud Vertex AIを使用するには:", "step1": "1. Google Cloudアカウントを作成し、Vertex AI APIを有効にして、希望するClaudeモデルを有効にします。", diff --git a/webview-ui/src/i18n/locales/ko/chat.json b/webview-ui/src/i18n/locales/ko/chat.json index 95f783b085..147ed8fa5b 100644 --- a/webview-ui/src/i18n/locales/ko/chat.json +++ b/webview-ui/src/i18n/locales/ko/chat.json @@ -314,6 +314,11 @@ "title": "자동 승인 요청 한도 도달", "description": "Roo가 {{count}}개의 API 요청(들)에 대한 자동 승인 한도에 도달했습니다. 카운트를 재설정하고 작업을 계속하시겠습니까?", "button": "재설정 후 계속" + }, + "autoApprovedCostLimitReached": { + "description": "Roo가 자동 승인된 비용 한도인 ${{count}}에 도달했습니다. 비용을 초기화하고 작업을 계속하시겠습니까?", + "title": "자동 승인 비용 한도에 도달함", + "button": "재설정 후 계속하기" } }, "codebaseSearch": { diff --git a/webview-ui/src/i18n/locales/ko/settings.json b/webview-ui/src/i18n/locales/ko/settings.json index 89739f4c4a..1f1bf869d2 100644 --- a/webview-ui/src/i18n/locales/ko/settings.json +++ b/webview-ui/src/i18n/locales/ko/settings.json @@ -197,7 +197,14 @@ "description": "작업을 계속하기 위한 승인을 요청하기 전에 자동으로 이 수의 API 요청을 수행합니다.", "unlimited": "무제한" }, - "selectOptionsFirst": "자동 승인을 활성화하려면 아래에서 하나 이상의 옵션을 선택하세요" + "selectOptionsFirst": "자동 승인을 활성화하려면 아래에서 하나 이상의 옵션을 선택하세요", + "apiCostLimit": { + "unlimited": "무제한", + "title": "최대 비용" + }, + "maxLimits": { + "description": "이러한 한도까지 자동으로 요청을 수행한 후, 계속 진행하기 위한 승인을 요청합니다." + } }, "providers": { "providerDocumentation": "{{provider}} 문서", @@ -318,7 +325,6 @@ "description": "Gemini가 최신 정보를 얻기 위해 Google을 검색하고 응답을 실시간 데이터에 근거하도록 합니다. 최신 정보가 필요한 쿼리에 유용합니다." } }, - "googleCloudSetup": { "title": "Google Cloud Vertex AI를 사용하려면:", "step1": "1. Google Cloud 계정을 만들고, Vertex AI API를 활성화하고, 원하는 Claude 모델을 활성화하세요.", diff --git a/webview-ui/src/i18n/locales/nl/chat.json b/webview-ui/src/i18n/locales/nl/chat.json index 4bfaf467f6..9789f634a5 100644 --- a/webview-ui/src/i18n/locales/nl/chat.json +++ b/webview-ui/src/i18n/locales/nl/chat.json @@ -314,6 +314,11 @@ "title": "Limiet voor automatisch goedgekeurde verzoeken bereikt", "description": "Roo heeft de automatisch goedgekeurde limiet van {{count}} API-verzoek(en) bereikt. Wil je de teller resetten en doorgaan met de taak?", "button": "Resetten en doorgaan" + }, + "autoApprovedCostLimitReached": { + "title": "Limiet voor automatisch goedgekeurde kosten bereikt", + "button": "Resetten en doorgaan", + "description": "Roo heeft de automatisch goedgekeurde kostenlimiet van ${{count}} bereikt. Wilt u de kosten resetten en doorgaan met de taak?" } }, "codebaseSearch": { diff --git a/webview-ui/src/i18n/locales/nl/settings.json b/webview-ui/src/i18n/locales/nl/settings.json index 7d8721ffd8..d026540e67 100644 --- a/webview-ui/src/i18n/locales/nl/settings.json +++ b/webview-ui/src/i18n/locales/nl/settings.json @@ -197,7 +197,14 @@ "description": "Voer automatisch dit aantal API-verzoeken uit voordat om goedkeuring wordt gevraagd om door te gaan met de taak.", "unlimited": "Onbeperkt" }, - "selectOptionsFirst": "Selecteer ten minste één optie hieronder om automatische goedkeuring in te schakelen" + "selectOptionsFirst": "Selecteer ten minste één optie hieronder om automatische goedkeuring in te schakelen", + "apiCostLimit": { + "title": "Max kosten", + "unlimited": "Onbeperkt" + }, + "maxLimits": { + "description": "Automatisch verzoeken indienen tot aan deze limieten voordat om goedkeuring wordt gevraagd om door te gaan." + } }, "providers": { "providerDocumentation": "{{provider}} documentatie", @@ -318,7 +325,6 @@ "description": "Staat Gemini toe om Google te doorzoeken voor actuele informatie en antwoorden op realtime gegevens te baseren. Handig voor vragen die actuele informatie vereisen." } }, - "googleCloudSetup": { "title": "Om Google Cloud Vertex AI te gebruiken, moet je:", "step1": "1. Maak een Google Cloud-account aan, schakel de Vertex AI API in en activeer de gewenste Claude-modellen.", diff --git a/webview-ui/src/i18n/locales/pl/chat.json b/webview-ui/src/i18n/locales/pl/chat.json index 10ee653582..ff1c1dbe89 100644 --- a/webview-ui/src/i18n/locales/pl/chat.json +++ b/webview-ui/src/i18n/locales/pl/chat.json @@ -314,6 +314,11 @@ "title": "Osiągnięto limit automatycznie zatwierdzonych żądań", "description": "Roo osiągnął automatycznie zatwierdzony limit {{count}} żądania/żądań API. Czy chcesz zresetować licznik i kontynuować zadanie?", "button": "Zresetuj i kontynuuj" + }, + "autoApprovedCostLimitReached": { + "button": "Zresetuj i Kontynuuj", + "title": "Osiągnięto limit kosztów z automatycznym zatwierdzaniem", + "description": "Roo osiągnął automatycznie zatwierdzony limit kosztów wynoszący ${{count}}. Czy chcesz zresetować koszt i kontynuować zadanie?" } }, "codebaseSearch": { diff --git a/webview-ui/src/i18n/locales/pl/settings.json b/webview-ui/src/i18n/locales/pl/settings.json index 5a23d2137d..b4d64b1e65 100644 --- a/webview-ui/src/i18n/locales/pl/settings.json +++ b/webview-ui/src/i18n/locales/pl/settings.json @@ -197,7 +197,14 @@ "description": "Automatycznie wykonaj tyle żądań API przed poproszeniem o zgodę na kontynuowanie zadania.", "unlimited": "Bez limitu" }, - "selectOptionsFirst": "Wybierz co najmniej jedną opcję poniżej, aby włączyć automatyczne zatwierdzanie" + "selectOptionsFirst": "Wybierz co najmniej jedną opcję poniżej, aby włączyć automatyczne zatwierdzanie", + "apiCostLimit": { + "title": "Maksymalny koszt", + "unlimited": "Bez limitu" + }, + "maxLimits": { + "description": "Automatycznie składaj zapytania do tych limitów przed poproszeniem o zgodę na kontynuowanie." + } }, "providers": { "providerDocumentation": "Dokumentacja {{provider}}", @@ -318,7 +325,6 @@ "description": "Pozwala Gemini przeszukiwać Google w celu uzyskania aktualnych informacji i opierać odpowiedzi na danych w czasie rzeczywistym. Przydatne w zapytaniach wymagających najnowszych informacji." } }, - "googleCloudSetup": { "title": "Aby korzystać z Google Cloud Vertex AI, potrzebujesz:", "step1": "1. Utworzyć konto Google Cloud, włączyć API Vertex AI i włączyć żądane modele Claude.", diff --git a/webview-ui/src/i18n/locales/pt-BR/chat.json b/webview-ui/src/i18n/locales/pt-BR/chat.json index b286f5c0ad..69f197ad2e 100644 --- a/webview-ui/src/i18n/locales/pt-BR/chat.json +++ b/webview-ui/src/i18n/locales/pt-BR/chat.json @@ -314,6 +314,11 @@ "title": "Limite de Solicitações Auto-aprovadas Atingido", "description": "Roo atingiu o limite auto-aprovado de {{count}} solicitação(ões) de API. Deseja redefinir a contagem e prosseguir com a tarefa?", "button": "Redefinir e Continuar" + }, + "autoApprovedCostLimitReached": { + "title": "Limite de Custo com Aprovação Automática Atingido", + "description": "Roo atingiu o limite de custo com aprovação automática de US${{count}}. Você gostaria de redefinir o custo e prosseguir com a tarefa?", + "button": "Redefinir e Continuar" } }, "codebaseSearch": { diff --git a/webview-ui/src/i18n/locales/pt-BR/settings.json b/webview-ui/src/i18n/locales/pt-BR/settings.json index 2e39982d27..b117212e6d 100644 --- a/webview-ui/src/i18n/locales/pt-BR/settings.json +++ b/webview-ui/src/i18n/locales/pt-BR/settings.json @@ -197,7 +197,14 @@ "description": "Fazer automaticamente este número de requisições à API antes de pedir aprovação para continuar com a tarefa.", "unlimited": "Ilimitado" }, - "selectOptionsFirst": "Selecione pelo menos uma opção abaixo para habilitar a aprovação automática" + "selectOptionsFirst": "Selecione pelo menos uma opção abaixo para habilitar a aprovação automática", + "apiCostLimit": { + "title": "Custo máximo", + "unlimited": "Ilimitado" + }, + "maxLimits": { + "description": "Fazer solicitações automaticamente até estes limites antes de pedir aprovação para continuar." + } }, "providers": { "providerDocumentation": "Documentação do {{provider}}", @@ -318,7 +325,6 @@ "description": "Permite que o Gemini pesquise informações atuais no Google e fundamente as respostas em dados em tempo real. Útil para consultas que requerem informações atualizadas." } }, - "googleCloudSetup": { "title": "Para usar o Google Cloud Vertex AI, você precisa:", "step1": "1. Criar uma conta Google Cloud, ativar a API Vertex AI e ativar os modelos Claude desejados.", diff --git a/webview-ui/src/i18n/locales/ru/chat.json b/webview-ui/src/i18n/locales/ru/chat.json index af3e9aadf8..579d688a5a 100644 --- a/webview-ui/src/i18n/locales/ru/chat.json +++ b/webview-ui/src/i18n/locales/ru/chat.json @@ -314,6 +314,11 @@ "title": "Достигнут лимит автоматически одобренных запросов", "description": "Roo достиг автоматически одобренного лимита в {{count}} API-запрос(ов). Хотите сбросить счетчик и продолжить задачу?", "button": "Сбросить и продолжить" + }, + "autoApprovedCostLimitReached": { + "title": "Достигнут лимит автоматически одобряемых расходов", + "button": "Сбросить и продолжить", + "description": "Ру достиг автоматически утвержденного лимита расходов в размере ${{count}}. Хотите сбросить расходы и продолжить выполнение задачи?" } }, "codebaseSearch": { diff --git a/webview-ui/src/i18n/locales/ru/settings.json b/webview-ui/src/i18n/locales/ru/settings.json index 76c3877e10..cf657948be 100644 --- a/webview-ui/src/i18n/locales/ru/settings.json +++ b/webview-ui/src/i18n/locales/ru/settings.json @@ -197,7 +197,14 @@ "description": "Автоматически выполнять это количество API-запросов перед запросом разрешения на продолжение задачи.", "unlimited": "Без ограничений" }, - "selectOptionsFirst": "Выберите хотя бы один вариант ниже, чтобы включить автоодобрение" + "selectOptionsFirst": "Выберите хотя бы один вариант ниже, чтобы включить автоодобрение", + "apiCostLimit": { + "title": "Максимальная стоимость", + "unlimited": "Безлимитный" + }, + "maxLimits": { + "description": "Автоматически выполнять запросы до указанных лимитов, прежде чем запрашивать разрешение на продолжение." + } }, "providers": { "providerDocumentation": "Документация {{provider}}", @@ -318,7 +325,6 @@ "description": "Позволяет Gemini искать актуальную информацию в Google и основывать ответы на данных в реальном времени. Полезно для запросов, требующих актуальной информации." } }, - "googleCloudSetup": { "title": "Для использования Google Cloud Vertex AI необходимо:", "step1": "1. Создайте аккаунт Google Cloud, включите Vertex AI API и нужные модели Claude.", diff --git a/webview-ui/src/i18n/locales/tr/chat.json b/webview-ui/src/i18n/locales/tr/chat.json index e6868b5db1..a9ffb31f90 100644 --- a/webview-ui/src/i18n/locales/tr/chat.json +++ b/webview-ui/src/i18n/locales/tr/chat.json @@ -314,6 +314,11 @@ "title": "Otomatik Onaylanan İstek Limiti Aşıldı", "description": "Roo, {{count}} API isteği/istekleri için otomatik onaylanan limite ulaştı. Sayacı sıfırlamak ve göreve devam etmek istiyor musunuz?", "button": "Sıfırla ve Devam Et" + }, + "autoApprovedCostLimitReached": { + "title": "Otomatik Onaylanan Maliyet Sınırına Ulaşıldı", + "description": "Roo otomatik olarak onaylanmış ${{count}} maliyet sınırına ulaştı. Maliyeti sıfırlamak ve göreve devam etmek ister misiniz?", + "button": "Sıfırla ve Devam Et" } }, "codebaseSearch": { diff --git a/webview-ui/src/i18n/locales/tr/settings.json b/webview-ui/src/i18n/locales/tr/settings.json index 26a295ffae..216da83dff 100644 --- a/webview-ui/src/i18n/locales/tr/settings.json +++ b/webview-ui/src/i18n/locales/tr/settings.json @@ -197,7 +197,14 @@ "description": "Göreve devam etmek için onay istemeden önce bu sayıda API isteği otomatik olarak yap.", "unlimited": "Sınırsız" }, - "selectOptionsFirst": "Otomatik onayı etkinleştirmek için aşağıdan en az bir seçenek seçin" + "selectOptionsFirst": "Otomatik onayı etkinleştirmek için aşağıdan en az bir seçenek seçin", + "apiCostLimit": { + "unlimited": "Sınırsız", + "title": "Maksimum Maliyet" + }, + "maxLimits": { + "description": "Bu sınırlara ulaşana kadar otomatik olarak istekleri yap, sonrasında devam etmek için onay iste." + } }, "providers": { "providerDocumentation": "{{provider}} Dokümantasyonu", @@ -318,7 +325,6 @@ "description": "Gemini'nin güncel bilgileri almak için Google'da arama yapmasına ve yanıtları gerçek zamanlı verilere dayandırmasına izin verir. Güncel bilgi gerektiren sorgular için kullanışlıdır." } }, - "googleCloudSetup": { "title": "Google Cloud Vertex AI'yi kullanmak için şunları yapmanız gerekir:", "step1": "1. Google Cloud hesabı oluşturun, Vertex AI API'sini etkinleştirin ve istediğiniz Claude modellerini etkinleştirin.", diff --git a/webview-ui/src/i18n/locales/vi/chat.json b/webview-ui/src/i18n/locales/vi/chat.json index 2b86060ceb..dc24e40122 100644 --- a/webview-ui/src/i18n/locales/vi/chat.json +++ b/webview-ui/src/i18n/locales/vi/chat.json @@ -314,6 +314,11 @@ "title": "Đã Đạt Giới Hạn Yêu Cầu Tự Động Phê Duyệt", "description": "Roo đã đạt đến giới hạn tự động phê duyệt là {{count}} yêu cầu API. Bạn có muốn đặt lại bộ đếm và tiếp tục nhiệm vụ không?", "button": "Đặt lại và Tiếp tục" + }, + "autoApprovedCostLimitReached": { + "button": "Đặt lại và Tiếp tục", + "title": "Đã Đạt Giới Hạn Chi Phí Tự Động Phê Duyệt", + "description": "Roo đã đạt đến giới hạn chi phí tự động phê duyệt là ${{count}}. Bạn có muốn đặt lại chi phí và tiếp tục với nhiệm vụ không?" } }, "codebaseSearch": { diff --git a/webview-ui/src/i18n/locales/vi/settings.json b/webview-ui/src/i18n/locales/vi/settings.json index 48f63bdf42..6a12c91200 100644 --- a/webview-ui/src/i18n/locales/vi/settings.json +++ b/webview-ui/src/i18n/locales/vi/settings.json @@ -197,7 +197,14 @@ "description": "Tự động thực hiện số lượng API request này trước khi yêu cầu phê duyệt để tiếp tục với nhiệm vụ.", "unlimited": "Không giới hạn" }, - "selectOptionsFirst": "Chọn ít nhất một tùy chọn bên dưới để bật tự động phê duyệt" + "selectOptionsFirst": "Chọn ít nhất một tùy chọn bên dưới để bật tự động phê duyệt", + "apiCostLimit": { + "title": "Chi phí tối đa", + "unlimited": "Không giới hạn" + }, + "maxLimits": { + "description": "Tự động thực hiện các yêu cầu lên đến các giới hạn này trước khi xin phê duyệt để tiếp tục." + } }, "providers": { "providerDocumentation": "Tài liệu {{provider}}", @@ -318,7 +325,6 @@ "description": "Cho phép Gemini tìm kiếm trên Google để lấy thông tin mới nhất và căn cứ phản hồi dựa trên dữ liệu thời gian thực. Hữu ích cho các truy vấn yêu cầu thông tin cập nhật." } }, - "googleCloudSetup": { "title": "Để sử dụng Google Cloud Vertex AI, bạn cần:", "step1": "1. Tạo tài khoản Google Cloud, kích hoạt Vertex AI API và kích hoạt các mô hình Claude mong muốn.", diff --git a/webview-ui/src/i18n/locales/zh-CN/chat.json b/webview-ui/src/i18n/locales/zh-CN/chat.json index dc21acee0b..6035ff78bf 100644 --- a/webview-ui/src/i18n/locales/zh-CN/chat.json +++ b/webview-ui/src/i18n/locales/zh-CN/chat.json @@ -314,6 +314,11 @@ "title": "已达自动批准请求限制", "description": "Roo 已达到 {{count}} 次 API 请求的自动批准限制。您想重置计数并继续任务吗?", "button": "重置并继续" + }, + "autoApprovedCostLimitReached": { + "title": "已达到自动批准的费用限额", + "description": "Roo已经达到了${{count}}的自动批准成本限制。您想重置成本并继续任务吗?", + "button": "重置并继续" } }, "codebaseSearch": { diff --git a/webview-ui/src/i18n/locales/zh-CN/settings.json b/webview-ui/src/i18n/locales/zh-CN/settings.json index 6d46e337c5..52b8802bc6 100644 --- a/webview-ui/src/i18n/locales/zh-CN/settings.json +++ b/webview-ui/src/i18n/locales/zh-CN/settings.json @@ -197,7 +197,14 @@ "description": "在请求批准以继续执行任务之前,自动发出此数量的 API 请求。", "unlimited": "无限制" }, - "selectOptionsFirst": "请至少选择以下一个选项以启用自动批准" + "selectOptionsFirst": "请至少选择以下一个选项以启用自动批准", + "apiCostLimit": { + "title": "最高费用", + "unlimited": "无限" + }, + "maxLimits": { + "description": "在请求批准继续之前,自动发出请求,最多不超过这些限制。" + } }, "providers": { "providerDocumentation": "{{provider}} 文档", @@ -308,7 +315,6 @@ "cacheUsageNote": "提示:若未显示缓存使用情况,请切换模型后重新选择", "vscodeLmModel": "VSCode LM 模型", "vscodeLmWarning": "注意:这是一个非常实验性的集成,提供商支持会有所不同。如果您收到有关不支持模型的错误,则这是提供商方面的问题。", - "geminiParameters": { "urlContext": { "title": "启用 URL 上下文", diff --git a/webview-ui/src/i18n/locales/zh-TW/chat.json b/webview-ui/src/i18n/locales/zh-TW/chat.json index fc38009186..63dfd06f1d 100644 --- a/webview-ui/src/i18n/locales/zh-TW/chat.json +++ b/webview-ui/src/i18n/locales/zh-TW/chat.json @@ -314,6 +314,11 @@ "title": "已達自動核准請求限制", "description": "Roo 已達到 {{count}} 次 API 請求的自動核准限制。您想要重設計數並繼續工作嗎?", "button": "重設並繼續" + }, + "autoApprovedCostLimitReached": { + "title": "已达到自动批准成本上限", + "button": "重置并继续", + "description": "Roo已达到自动批准的成本限制${{count}}。您想要重置成本并继续任务吗?" } }, "codebaseSearch": { diff --git a/webview-ui/src/i18n/locales/zh-TW/settings.json b/webview-ui/src/i18n/locales/zh-TW/settings.json index ffd852397f..c90080cb3a 100644 --- a/webview-ui/src/i18n/locales/zh-TW/settings.json +++ b/webview-ui/src/i18n/locales/zh-TW/settings.json @@ -197,7 +197,14 @@ "description": "在請求批准以繼續執行工作之前,自動發出此數量的 API 請求。", "unlimited": "無限制" }, - "selectOptionsFirst": "請至少選擇以下一個選項以啟用自動核准" + "selectOptionsFirst": "請至少選擇以下一個選項以啟用自動核准", + "apiCostLimit": { + "unlimited": "无限", + "title": "最高费用" + }, + "maxLimits": { + "description": "在请求获得继续操作的批准前,自动发送请求直至达到这些限制。" + } }, "providers": { "providerDocumentation": "{{provider}} 文件", @@ -308,7 +315,6 @@ "cacheUsageNote": "注意:如果您沒有看到快取使用情況,請嘗試選擇其他模型,然後重新選擇您想要的模型。", "vscodeLmModel": "語言模型", "vscodeLmWarning": "注意:此整合功能仍處於實驗階段,各供應商的支援程度可能不同。如果出現模型不支援的錯誤,通常是供應商方面的問題。", - "geminiParameters": { "urlContext": { "title": "啟用 URL 上下文",