Parcourir la source

Add auto-approved cost limits (#6484)

Co-authored-by: Daniel Riccio <[email protected]>
Chris Hasson il y a 5 mois
Parent
commit
1da82b2db0
57 fichiers modifiés avec 1404 ajouts et 88 suppressions
  1. 1 0
      packages/types/src/global-settings.ts
  2. 144 0
      src/core/task/AutoApprovalHandler.ts
  3. 12 12
      src/core/task/Task.ts
  4. 249 0
      src/core/task/__tests__/AutoApprovalHandler.spec.ts
  5. 3 0
      src/core/webview/ClineProvider.ts
  6. 4 0
      src/core/webview/webviewMessageHandler.ts
  7. 1 0
      src/shared/ExtensionMessage.ts
  8. 1 0
      src/shared/WebviewMessage.ts
  9. 10 37
      webview-ui/src/components/chat/AutoApproveMenu.tsx
  10. 15 4
      webview-ui/src/components/chat/AutoApprovedRequestLimitWarning.tsx
  11. 92 0
      webview-ui/src/components/common/DecoratedVSCodeTextField.tsx
  12. 119 0
      webview-ui/src/components/common/FormattedTextField.tsx
  13. 219 0
      webview-ui/src/components/common/__tests__/FormattedTextField.spec.tsx
  14. 13 0
      webview-ui/src/components/settings/AutoApproveSettings.tsx
  15. 41 0
      webview-ui/src/components/settings/MaxCostInput.tsx
  16. 32 0
      webview-ui/src/components/settings/MaxLimitInputs.tsx
  17. 40 0
      webview-ui/src/components/settings/MaxRequestsInput.tsx
  18. 2 0
      webview-ui/src/components/settings/SettingsView.tsx
  19. 84 0
      webview-ui/src/components/settings/__tests__/MaxCostInput.spec.tsx
  20. 87 0
      webview-ui/src/components/settings/__tests__/MaxRequestsInput.spec.tsx
  21. 2 0
      webview-ui/src/context/ExtensionStateContext.tsx
  22. 5 0
      webview-ui/src/i18n/locales/ca/chat.json
  23. 8 2
      webview-ui/src/i18n/locales/ca/settings.json
  24. 5 0
      webview-ui/src/i18n/locales/de/chat.json
  25. 8 2
      webview-ui/src/i18n/locales/de/settings.json
  26. 5 0
      webview-ui/src/i18n/locales/en/chat.json
  27. 8 2
      webview-ui/src/i18n/locales/en/settings.json
  28. 5 0
      webview-ui/src/i18n/locales/es/chat.json
  29. 8 2
      webview-ui/src/i18n/locales/es/settings.json
  30. 5 0
      webview-ui/src/i18n/locales/fr/chat.json
  31. 7 1
      webview-ui/src/i18n/locales/fr/settings.json
  32. 5 0
      webview-ui/src/i18n/locales/hi/chat.json
  33. 8 2
      webview-ui/src/i18n/locales/hi/settings.json
  34. 5 0
      webview-ui/src/i18n/locales/id/chat.json
  35. 8 2
      webview-ui/src/i18n/locales/id/settings.json
  36. 5 0
      webview-ui/src/i18n/locales/it/chat.json
  37. 8 2
      webview-ui/src/i18n/locales/it/settings.json
  38. 5 0
      webview-ui/src/i18n/locales/ja/chat.json
  39. 8 2
      webview-ui/src/i18n/locales/ja/settings.json
  40. 5 0
      webview-ui/src/i18n/locales/ko/chat.json
  41. 8 2
      webview-ui/src/i18n/locales/ko/settings.json
  42. 5 0
      webview-ui/src/i18n/locales/nl/chat.json
  43. 8 2
      webview-ui/src/i18n/locales/nl/settings.json
  44. 5 0
      webview-ui/src/i18n/locales/pl/chat.json
  45. 8 2
      webview-ui/src/i18n/locales/pl/settings.json
  46. 5 0
      webview-ui/src/i18n/locales/pt-BR/chat.json
  47. 8 2
      webview-ui/src/i18n/locales/pt-BR/settings.json
  48. 5 0
      webview-ui/src/i18n/locales/ru/chat.json
  49. 8 2
      webview-ui/src/i18n/locales/ru/settings.json
  50. 5 0
      webview-ui/src/i18n/locales/tr/chat.json
  51. 8 2
      webview-ui/src/i18n/locales/tr/settings.json
  52. 5 0
      webview-ui/src/i18n/locales/vi/chat.json
  53. 8 2
      webview-ui/src/i18n/locales/vi/settings.json
  54. 5 0
      webview-ui/src/i18n/locales/zh-CN/chat.json
  55. 8 2
      webview-ui/src/i18n/locales/zh-CN/settings.json
  56. 5 0
      webview-ui/src/i18n/locales/zh-TW/chat.json
  57. 8 2
      webview-ui/src/i18n/locales/zh-TW/settings.json

+ 1 - 0
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(),

+ 144 - 0
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<AutoApprovalResult> {
+		// 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<AutoApprovalResult> {
+		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<AutoApprovalResult> {
+		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,
+		}
+	}
+}

+ 12 - 12
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<TaskEvents> {
 	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<TaskEvents> {
 
 		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<TaskEvents> {
 			({ 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 = {

+ 249 - 0
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)
+		})
+	})
+})

+ 3 - 0
src/core/webview/ClineProvider.ts

@@ -1467,6 +1467,7 @@ export class ClineProvider
 			alwaysAllowSubtasks,
 			alwaysAllowUpdateTodoList,
 			allowedMaxRequests,
+			allowedMaxCost,
 			autoCondenseContext,
 			autoCondenseContextPercent,
 			soundEnabled,
@@ -1562,6 +1563,7 @@ export class ClineProvider
 			alwaysAllowSubtasks: alwaysAllowSubtasks ?? false,
 			alwaysAllowUpdateTodoList: alwaysAllowUpdateTodoList ?? false,
 			allowedMaxRequests,
+			allowedMaxCost,
 			autoCondenseContext: autoCondenseContext ?? true,
 			autoCondenseContextPercent: autoCondenseContextPercent ?? 100,
 			uriScheme: vscode.env.uriScheme,
@@ -1758,6 +1760,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,

+ 4 - 0
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()

+ 1 - 0
src/shared/ExtensionMessage.ts

@@ -222,6 +222,7 @@ export type ExtensionState = Pick<
 	| "allowedCommands"
 	| "deniedCommands"
 	| "allowedMaxRequests"
+	| "allowedMaxCost"
 	| "browserToolEnabled"
 	| "browserViewportSize"
 	| "screenshotQuality"

+ 1 - 0
src/shared/WebviewMessage.ts

@@ -80,6 +80,7 @@ export interface WebviewMessage {
 		| "alwaysAllowMcp"
 		| "alwaysAllowModeSwitch"
 		| "allowedMaxRequests"
+		| "allowedMaxCost"
 		| "alwaysAllowSubtasks"
 		| "alwaysAllowUpdateTodoList"
 		| "autoCondenseContext"

+ 10 - 37
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) => {
 
 					<AutoApproveToggle {...toggles} onToggle={onAutoApproveToggle} />
 
-					{/* Auto-approve API request count limit input row inspired by Cline */}
-					<div
-						style={{
-							display: "flex",
-							alignItems: "center",
-							gap: "8px",
-							marginTop: "10px",
-							marginBottom: "8px",
-							color: "var(--vscode-descriptionForeground)",
-						}}>
-						<span style={{ flexShrink: 1, minWidth: 0 }}>
-							<Trans i18nKey="settings:autoApprove.apiRequestLimit.title" />:
-						</span>
-						<VSCodeTextField
-							placeholder={t("settings:autoApprove.apiRequestLimit.unlimited")}
-							value={(allowedMaxRequests ?? Infinity) === Infinity ? "" : allowedMaxRequests?.toString()}
-							onInput={(e) => {
-								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 }}
-						/>
-					</div>
-					<div
-						style={{
-							color: "var(--vscode-descriptionForeground)",
-							fontSize: "12px",
-							marginBottom: "10px",
-						}}>
-						<Trans i18nKey="settings:autoApprove.apiRequestLimit.description" />
-					</div>
+					<MaxLimitInputs
+						allowedMaxRequests={allowedMaxRequests ?? undefined}
+						allowedMaxCost={allowedMaxCost ?? undefined}
+						onMaxRequestsChange={(value) => setAllowedMaxRequests(value)}
+						onMaxCostChange={(value) => setAllowedMaxCost(value)}
+					/>
 				</div>
 			)}
 		</div>

+ 15 - 4
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 (
 		<>
 			<div style={{ display: "flex", alignItems: "center", gap: "8px", color: "var(--vscode-foreground)" }}>
 				<span className="codicon codicon-warning" />
 				<span style={{ fontWeight: "bold" }}>
-					<Trans i18nKey="ask.autoApprovedRequestLimitReached.title" ns="chat" />
+					<Trans i18nKey={titleKey} ns="chat" />
 				</span>
 			</div>
 
@@ -37,7 +48,7 @@ export const AutoApprovedRequestLimitWarning = memo(({ message }: AutoApprovedRe
 					justifyContent: "center",
 				}}>
 				<div className="flex justify-between items-center">
-					<Trans i18nKey="ask.autoApprovedRequestLimitReached.description" ns="chat" values={{ count }} />
+					<Trans i18nKey={descriptionKey} ns="chat" values={{ count }} />
 				</div>
 				<VSCodeButton
 					style={{ width: "100%", padding: "6px", borderRadius: "4px" }}
@@ -46,7 +57,7 @@ export const AutoApprovedRequestLimitWarning = memo(({ message }: AutoApprovedRe
 						setButtonClicked(true)
 						vscode.postMessage({ type: "askResponse", askResponse: "yesButtonClicked" })
 					}}>
-					<Trans i18nKey="ask.autoApprovedRequestLimitReached.button" ns="chat" />
+					<Trans i18nKey={buttonKey} ns="chat" />
 				</VSCodeButton>
 			</div>
 		</>

+ 92 - 0
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<typeof VSCodeTextField> {
+	leftNodes?: ReactNode[]
+	rightNodes?: ReactNode[]
+}
+
+function VSCodeTextFieldWithNodesInner(
+	props: VSCodeTextFieldWithNodesProps,
+	forwardedRef: React.Ref<HTMLInputElement>,
+) {
+	const { className, style, "data-testid": dataTestId, leftNodes, rightNodes, ...restProps } = props
+
+	const inputRef = useRef<HTMLInputElement | null>(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<typeof VSCodeTextField>) => {
+			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<HTMLInputElement | null>).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 (
+		<div
+			className={cn(
+				`group`,
+				`relative flex items-center cursor-text`,
+				`bg-[var(--input-background)] text-[var(--input-foreground)]`,
+				`rounded-[calc(var(--corner-radius-round)*1px)]`,
+				className,
+			)}
+			style={style}
+			onMouseDown={focusInput}>
+			{hasLeftNodes && (
+				<div className="absolute left-2 z-10 flex items-center gap-1 pointer-events-none">{leftNodes}</div>
+			)}
+
+			<VSCodeTextField
+				data-testid={dataTestId}
+				ref={handleVSCodeFieldRef}
+				style={{
+					flex: 1,
+					paddingLeft: hasLeftNodes ? "24px" : undefined,
+					paddingRight: hasRightNodes ? "24px" : undefined,
+				}}
+				className="[--border-width:0]"
+				{...restProps}
+			/>
+
+			{hasRightNodes && (
+				<div className="absolute right-2 z-10 flex items-center gap-1 pointer-events-none">{rightNodes}</div>
+			)}
+
+			{/* Absolutely positioned focus border overlay */}
+			<div className="absolute top-0 left-0 size-full border border-vscode-input-border group-focus-within:border-[var(--focus-border)] rounded"></div>
+		</div>
+	)
+}
+
+export const DecoratedVSCodeTextField = forwardRef(VSCodeTextFieldWithNodesInner)

+ 119 - 0
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<T> {
+	/**
+	 * 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<T> extends Omit<VSCodeTextFieldWithNodesProps, "value" | "onInput"> {
+	value: T | undefined
+	onValueChange: (value: T | undefined) => void
+	formatter: InputFormatter<T>
+}
+
+function FormattedTextFieldInner<T>(
+	{ value, onValueChange, formatter, ...restProps }: FormattedTextFieldProps<T>,
+	forwardedRef: React.Ref<HTMLInputElement>,
+) {
+	const [rawInput, setRawInput] = useState<string>("")
+	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<HTMLInputElement>) => {
+			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 (
+		<DecoratedVSCodeTextField
+			{...restProps}
+			value={displayValue}
+			onInput={handleInput}
+			onBlur={handleBlur}
+			ref={forwardedRef}
+		/>
+	)
+}
+
+export const FormattedTextField = forwardRef(FormattedTextFieldInner as any) as <T>(
+	props: FormattedTextFieldProps<T> & { ref?: React.Ref<HTMLInputElement> },
+) => React.ReactElement
+
+// Common formatters for reuse
+export const unlimitedIntegerFormatter: InputFormatter<number> = {
+	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<number> = {
+	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
+	},
+}

+ 219 - 0
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) => (
+		<input
+			type="text"
+			value={value}
+			onChange={(e) => 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(
+				<FormattedTextField
+					value={123}
+					onValueChange={mockOnChange}
+					formatter={unlimitedIntegerFormatter}
+					data-testid="test-input"
+				/>,
+			)
+
+			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(
+				<FormattedTextField
+					value={123}
+					onValueChange={mockOnChange}
+					formatter={unlimitedIntegerFormatter}
+					data-testid="test-input"
+				/>,
+			)
+
+			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(
+				<FormattedTextField
+					value={undefined}
+					onValueChange={mockOnChange}
+					formatter={unlimitedIntegerFormatter}
+					data-testid="test-input"
+				/>,
+			)
+
+			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(
+				<FormattedTextField
+					value={undefined}
+					onValueChange={mockOnChange}
+					formatter={unlimitedIntegerFormatter}
+					data-testid="test-input"
+				/>,
+			)
+
+			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(
+				<FormattedTextField
+					value={undefined}
+					onValueChange={mockOnChange}
+					formatter={unlimitedDecimalFormatter}
+					data-testid="decimal-input"
+				/>,
+			)
+
+			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(
+				<FormattedTextField
+					value={1}
+					onValueChange={mockOnChange}
+					formatter={unlimitedDecimalFormatter}
+					data-testid="decimal-input"
+				/>,
+			)
+
+			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(
+				<FormattedTextField
+					value={undefined}
+					onValueChange={mockOnChange}
+					formatter={unlimitedDecimalFormatter}
+					data-testid="test-input"
+				/>,
+			)
+
+			const input = screen.getByTestId("test-input")
+			fireEvent.change(input, { target: { value: "12.34" } })
+			expect(mockOnChange).toHaveBeenCalledWith(12.34)
+		})
+	})
+})

+ 13 - 0
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<HTMLDivElement> & {
 	alwaysAllowUpdateTodoList?: boolean
 	followupAutoApproveTimeoutMs?: number
 	allowedCommands?: string[]
+	allowedMaxRequests?: number | undefined
+	allowedMaxCost?: number | undefined
 	deniedCommands?: string[]
 	setCachedStateField: SetCachedStateField<
 		| "alwaysAllowReadOnly"
@@ -48,6 +51,8 @@ type AutoApproveSettingsProps = HTMLAttributes<HTMLDivElement> & {
 		| "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)}
 				/>
+				<MaxLimitInputs
+					allowedMaxRequests={allowedMaxRequests}
+					allowedMaxCost={allowedMaxCost}
+					onMaxRequestsChange={(value) => setCachedStateField("allowedMaxRequests", value)}
+					onMaxCostChange={(value) => setCachedStateField("allowedMaxCost", value)}
+				/>
 
 				{/* ADDITIONAL SETTINGS */}
 

+ 41 - 0
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 (
+		<div className="flex flex-col gap-3 pl-3 flex-auto">
+			<div className="flex items-center gap-4 font-bold">
+				<span className="codicon codicon-credit-card" />
+				<div>{t("settings:autoApprove.apiCostLimit.title")}</div>
+			</div>
+			<div className="flex items-center">
+				<FormattedTextField
+					value={allowedMaxCost}
+					onValueChange={handleValueChange}
+					formatter={unlimitedDecimalFormatter}
+					placeholder={t("settings:autoApprove.apiCostLimit.unlimited")}
+					style={{ flex: 1, maxWidth: "200px" }}
+					data-testid="max-cost-input"
+					leftNodes={[<span key="dollar">$</span>]}
+				/>
+			</div>
+		</div>
+	)
+}

+ 32 - 0
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<MaxLimitInputsProps> = ({
+	allowedMaxRequests,
+	allowedMaxCost,
+	onMaxRequestsChange,
+	onMaxCostChange,
+}) => {
+	const { t } = useTranslation()
+
+	return (
+		<div className="space-y-2">
+			<div className="flex justify-stretch">
+				<MaxRequestsInput allowedMaxRequests={allowedMaxRequests} onValueChange={onMaxRequestsChange} />
+				<MaxCostInput allowedMaxCost={allowedMaxCost} onValueChange={onMaxCostChange} />
+			</div>
+			<div className="text-xs text-vscode-descriptionForeground">
+				{t("settings:autoApprove.maxLimits.description")}
+			</div>
+		</div>
+	)
+}

+ 40 - 0
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 (
+		<div className="flex flex-col gap-3 pl-3 flex-auto">
+			<div className="flex items-center gap-4 font-bold">
+				<span className="codicon codicon-pulse" />
+				<div>{t("settings:autoApprove.apiRequestLimit.title")}</div>
+			</div>
+			<div className="flex items-center gap-2">
+				<FormattedTextField
+					value={allowedMaxRequests}
+					onValueChange={handleValueChange}
+					formatter={unlimitedIntegerFormatter}
+					placeholder={t("settings:autoApprove.apiRequestLimit.unlimited")}
+					style={{ flex: 1, maxWidth: "200px" }}
+					data-testid="max-requests-input"
+				/>
+			</div>
+		</div>
+	)
+}

+ 2 - 0
webview-ui/src/components/settings/SettingsView.tsx

@@ -125,6 +125,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
 		allowedCommands,
 		deniedCommands,
 		allowedMaxRequests,
+		allowedMaxCost,
 		language,
 		alwaysAllowBrowser,
 		alwaysAllowExecute,
@@ -291,6 +292,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ 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 })

+ 84 - 0
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<string, string> = {
+			"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(<MaxCostInput allowedMaxCost={undefined} onValueChange={mockOnValueChange} />)
+
+		const input = screen.getByPlaceholderText("Unlimited")
+		expect(input).toHaveValue("")
+	})
+
+	it("shows formatted cost value when allowedMaxCost is provided", () => {
+		render(<MaxCostInput allowedMaxCost={5.5} onValueChange={mockOnValueChange} />)
+
+		const input = screen.getByPlaceholderText("Unlimited")
+		expect(input).toHaveValue("5.5")
+	})
+
+	it("calls onValueChange when input changes", () => {
+		render(<MaxCostInput allowedMaxCost={undefined} onValueChange={mockOnValueChange} />)
+
+		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(<MaxCostInput allowedMaxCost={5.0} onValueChange={mockOnValueChange} />)
+
+		const input = screen.getByPlaceholderText("Unlimited")
+		fireEvent.input(input, { target: { value: "" } })
+
+		expect(mockOnValueChange).toHaveBeenCalledWith(undefined)
+	})
+
+	it("handles decimal input correctly", () => {
+		render(<MaxCostInput allowedMaxCost={undefined} onValueChange={mockOnValueChange} />)
+
+		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(<MaxCostInput allowedMaxCost={undefined} onValueChange={mockOnValueChange} />)
+
+		const input = screen.getByPlaceholderText("Unlimited")
+		fireEvent.input(input, { target: { value: "0" } })
+
+		expect(mockOnValueChange).toHaveBeenCalledWith(0)
+	})
+
+	it("allows typing decimal values starting with zero", () => {
+		render(<MaxCostInput allowedMaxCost={undefined} onValueChange={mockOnValueChange} />)
+
+		const input = screen.getByPlaceholderText("Unlimited")
+		fireEvent.input(input, { target: { value: "0.15" } })
+
+		expect(mockOnValueChange).toHaveBeenCalledWith(0.15)
+	})
+})

+ 87 - 0
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<string, string> = {
+			"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(<MaxRequestsInput allowedMaxRequests={undefined} onValueChange={mockOnValueChange} />)
+
+		const input = screen.getByPlaceholderText("Unlimited")
+		expect(input).toHaveValue("")
+	})
+
+	it("shows formatted request value when allowedMaxRequests is provided", () => {
+		render(<MaxRequestsInput allowedMaxRequests={10} onValueChange={mockOnValueChange} />)
+
+		const input = screen.getByPlaceholderText("Unlimited")
+		expect(input).toHaveValue("10")
+	})
+
+	it("calls onValueChange when input changes", () => {
+		render(<MaxRequestsInput allowedMaxRequests={undefined} onValueChange={mockOnValueChange} />)
+
+		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(<MaxRequestsInput allowedMaxRequests={5} onValueChange={mockOnValueChange} />)
+
+		const input = screen.getByPlaceholderText("Unlimited")
+		fireEvent.input(input, { target: { value: "" } })
+
+		expect(mockOnValueChange).toHaveBeenCalledWith(undefined)
+	})
+
+	it("handles integer input correctly", () => {
+		render(<MaxRequestsInput allowedMaxRequests={undefined} onValueChange={mockOnValueChange} />)
+
+		const input = screen.getByPlaceholderText("Unlimited")
+		fireEvent.input(input, { target: { value: "25" } })
+
+		expect(mockOnValueChange).toHaveBeenCalledWith(25)
+	})
+
+	it("rejects zero and negative values", () => {
+		render(<MaxRequestsInput allowedMaxRequests={undefined} onValueChange={mockOnValueChange} />)
+
+		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(<MaxRequestsInput allowedMaxRequests={undefined} onValueChange={mockOnValueChange} />)
+
+		const input = screen.getByPlaceholderText("Unlimited")
+		fireEvent.input(input, { target: { value: "123abc" } })
+
+		expect(mockOnValueChange).toHaveBeenCalledWith(123)
+	})
+})

+ 2 - 0
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 })),

+ 5 - 0
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": {

+ 8 - 2
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",

+ 5 - 0
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": {

+ 8 - 2
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",

+ 5 - 0
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": {

+ 8 - 2
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"

+ 5 - 0
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": {

+ 8 - 2
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.",

+ 5 - 0
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": {

+ 7 - 1
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.",

+ 5 - 0
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": {

+ 8 - 2
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 मॉडल सक्षम करें।",

+ 5 - 0
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": {

+ 8 - 2
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.",

+ 5 - 0
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": {

+ 8 - 2
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.",

+ 5 - 0
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": {

+ 8 - 2
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モデルを有効にします。",

+ 5 - 0
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": {

+ 8 - 2
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 모델을 활성화하세요.",

+ 5 - 0
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": {

+ 8 - 2
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.",

+ 5 - 0
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": {

+ 8 - 2
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.",

+ 5 - 0
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": {

+ 8 - 2
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.",

+ 5 - 0
webview-ui/src/i18n/locales/ru/chat.json

@@ -314,6 +314,11 @@
 			"title": "Достигнут лимит автоматически одобренных запросов",
 			"description": "Roo достиг автоматически одобренного лимита в {{count}} API-запрос(ов). Хотите сбросить счетчик и продолжить задачу?",
 			"button": "Сбросить и продолжить"
+		},
+		"autoApprovedCostLimitReached": {
+			"title": "Достигнут лимит автоматически одобряемых расходов",
+			"button": "Сбросить и продолжить",
+			"description": "Ру достиг автоматически утвержденного лимита расходов в размере ${{count}}. Хотите сбросить расходы и продолжить выполнение задачи?"
 		}
 	},
 	"codebaseSearch": {

+ 8 - 2
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.",

+ 5 - 0
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": {

+ 8 - 2
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.",

+ 5 - 0
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": {

+ 8 - 2
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.",

+ 5 - 0
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": {

+ 8 - 2
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 上下文",

+ 5 - 0
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": {

+ 8 - 2
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 上下文",