Add auto-approved cost limits (#6484)

Co-authored-by: Daniel Riccio <ricciodaniel98@gmail.com>
This commit is contained in:
Chris Hasson 2025-07-31 21:33:09 +02:00 committed by GitHub
parent 1a013b4416
commit 1da82b2db0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
57 changed files with 1404 additions and 88 deletions

View file

@ -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(),

View file

@ -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,
}
}
}

View file

@ -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
// Check auto-approval limits
const approvalResult = await this.autoApprovalHandler.checkAutoApprovalLimits(
state,
this.combineMessages(this.clineMessages.slice(1)),
async (type, data) => this.ask(type, data),
)
// Increment the counter for each new API request
this.consecutiveAutoApprovedRequestsCount++
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 = {

View file

@ -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)
})
})
})

View file

@ -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,

View file

@ -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()

View file

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

View file

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

View file

@ -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>

View file

@ -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>
</>

View file

@ -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)

View file

@ -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
},
}

View file

@ -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)
})
})
})

View file

@ -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 */}

View file

@ -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>
)
}

View file

@ -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>
)
}

View file

@ -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>
)
}

View file

@ -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 })

View file

@ -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)
})
})

View file

@ -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)
})
})

View file

@ -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 })),

View file

@ -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": {

View file

@ -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",

View file

@ -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": {

View file

@ -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",

View file

@ -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": {

View file

@ -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"

View file

@ -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": {

View file

@ -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.",

View file

@ -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": {

View file

@ -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.",

View file

@ -314,6 +314,11 @@
"title": "स्वत:-स्वीकृत अनुरोध सीमा पहुंची",
"description": "Roo {{count}} API अनुरोध(धों) की स्वत:-स्वीकृत सीमा तक पहुंच गया है। क्या आप गणना को रीसेट करके कार्य जारी रखना चाहते हैं?",
"button": "रीसेट करें और जारी रखें"
},
"autoApprovedCostLimitReached": {
"title": "स्वत:-अनुमोदित लागत सीमा पहुँच गई",
"button": "रीसेट करें और जारी रखें",
"description": "Roo ने स्वचालित-स्वीकृत लागत सीमा ${{count}} तक पहुंच गई है। क्या आप लागत को रीसेट करके कार्य जारी रखना चाहेंगे?"
}
},
"codebaseSearch": {

View file

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

View file

@ -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": {

View file

@ -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.",

View file

@ -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": {

View file

@ -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.",

View file

@ -314,6 +314,11 @@
"title": "自動承認リクエスト制限に達しました",
"description": "Rooは{{count}}件のAPI自動承認リクエスト制限に達しました。カウントをリセットしてタスクを続行しますか",
"button": "リセットして続行"
},
"autoApprovedCostLimitReached": {
"title": "自動承認コスト制限に達しました",
"description": "Rooは自動承認されたコスト制限の${{count}}に達しました。コストをリセットしてタスクを続行しますか?",
"button": "リセットして続ける"
}
},
"codebaseSearch": {

View file

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

View file

@ -314,6 +314,11 @@
"title": "자동 승인 요청 한도 도달",
"description": "Roo가 {{count}}개의 API 요청(들)에 대한 자동 승인 한도에 도달했습니다. 카운트를 재설정하고 작업을 계속하시겠습니까?",
"button": "재설정 후 계속"
},
"autoApprovedCostLimitReached": {
"description": "Roo가 자동 승인된 비용 한도인 ${{count}}에 도달했습니다. 비용을 초기화하고 작업을 계속하시겠습니까?",
"title": "자동 승인 비용 한도에 도달함",
"button": "재설정 후 계속하기"
}
},
"codebaseSearch": {

View file

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

View file

@ -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": {

View file

@ -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.",

View file

@ -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": {

View file

@ -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.",

View file

@ -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": {

View file

@ -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.",

View file

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

View file

@ -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.",

View file

@ -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": {

View file

@ -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.",

View file

@ -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": {

View file

@ -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.",

View file

@ -314,6 +314,11 @@
"title": "已达自动批准请求限制",
"description": "Roo 已达到 {{count}} 次 API 请求的自动批准限制。您想重置计数并继续任务吗?",
"button": "重置并继续"
},
"autoApprovedCostLimitReached": {
"title": "已达到自动批准的费用限额",
"description": "Roo已经达到了${{count}}的自动批准成本限制。您想重置成本并继续任务吗?",
"button": "重置并继续"
}
},
"codebaseSearch": {

View file

@ -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 上下文",

View file

@ -314,6 +314,11 @@
"title": "已達自動核准請求限制",
"description": "Roo 已達到 {{count}} 次 API 請求的自動核准限制。您想要重設計數並繼續工作嗎?",
"button": "重設並繼續"
},
"autoApprovedCostLimitReached": {
"title": "已达到自动批准成本上限",
"button": "重置并继续",
"description": "Roo已达到自动批准的成本限制${{count}}。您想要重置成本并继续任务吗?"
}
},
"codebaseSearch": {

View file

@ -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 上下文",