diff --git a/packages/types/src/message.ts b/packages/types/src/message.ts index b02078fc0d..f557aa6879 100644 --- a/packages/types/src/message.ts +++ b/packages/types/src/message.ts @@ -196,6 +196,31 @@ export const contextCondenseSchema = z.object({ export type ContextCondense = z.infer +/** + * RetryStatusMetadata + */ +export const retryStatusSchema = z.object({ + type: z.literal("retry_status"), + status: z.enum(["waiting", "retrying", "cancelled"]), + remainingSeconds: z.number().optional(), + attempt: z.number().optional(), + maxAttempts: z.number().optional(), + origin: z.enum(["pre_request", "retry_attempt"]).optional(), + detail: z.string().optional(), + cause: z.enum(["rate_limit", "backoff"]), + rateLimitSeconds: z.number().optional(), // The original rate limit setting (for displaying "Rate limit set to X seconds") +}) + +export type RetryStatusMetadata = z.infer + +// Keep legacy type for backward compatibility during migration +export const rateLimitRetrySchema = retryStatusSchema.extend({ + type: z.literal("rate_limit_retry"), + cause: z.literal("rate_limit").default("rate_limit"), +}) + +export type RateLimitRetryMetadata = z.infer + /** * ClineMessage */ @@ -223,6 +248,8 @@ export const clineMessageSchema = z.object({ previous_response_id: z.string().optional(), }) .optional(), + retryStatus: retryStatusSchema.optional(), + rateLimitRetry: rateLimitRetrySchema.optional(), // Legacy field for backward compatibility }) .optional(), }) diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index 2fcc92426a..f05ffc29f6 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -123,6 +123,29 @@ const DEFAULT_USAGE_COLLECTION_TIMEOUT_MS = 5000 // 5 seconds const FORCED_CONTEXT_REDUCTION_PERCENT = 75 // Keep 75% of context (remove 25%) on context window errors const MAX_CONTEXT_WINDOW_RETRIES = 3 // Maximum retries for context window errors +interface RetryStatusPayload { + type: "retry_status" + status: "waiting" | "retrying" | "cancelled" + remainingSeconds?: number + attempt?: number + maxAttempts?: number + origin: "pre_request" | "retry_attempt" + detail?: string + cause: "rate_limit" | "backoff" + rateLimitSeconds?: number +} + +// Legacy interface for backward compatibility +interface RateLimitRetryPayload { + type: "rate_limit_retry" + status: "waiting" | "retrying" | "cancelled" + remainingSeconds?: number + attempt?: number + maxAttempts?: number + origin: "pre_request" | "retry_attempt" + detail?: string +} + export interface TaskOptions extends CreateTaskOptions { provider: ClineProvider apiConfiguration: ProviderSettings @@ -1100,8 +1123,14 @@ export class Task extends EventEmitter implements TaskLike { if (partial !== undefined) { const lastMessage = this.clineMessages.at(-1) + const isRateLimitUpdate = + type === "api_req_retry_delayed" && + (options.metadata?.retryStatus !== undefined || options.metadata?.rateLimitRetry !== undefined) const isUpdatingPreviousPartial = - lastMessage && lastMessage.partial && lastMessage.type === "say" && lastMessage.say === type + lastMessage && + lastMessage.type === "say" && + lastMessage.say === type && + (lastMessage.partial || isRateLimitUpdate) if (partial) { if (isUpdatingPreviousPartial) { @@ -1110,6 +1139,13 @@ export class Task extends EventEmitter implements TaskLike { lastMessage.images = images lastMessage.partial = partial lastMessage.progressStatus = progressStatus + if (options.metadata) { + const messageWithMetadata = lastMessage as ClineMessage & ClineMessageWithMetadata + if (!messageWithMetadata.metadata) { + messageWithMetadata.metadata = {} + } + Object.assign(messageWithMetadata.metadata, options.metadata) + } this.updateClineMessage(lastMessage) } else { // This is a new partial message, so add it with partial state. @@ -1197,6 +1233,7 @@ export class Task extends EventEmitter implements TaskLike { images, checkpoint, contextCondense, + metadata: options.metadata, }) } } @@ -2655,6 +2692,143 @@ export class Task extends EventEmitter implements TaskLike { let rateLimitDelay = 0 + const sendRetryStatusUpdate = async (payload: RetryStatusPayload, isPartial: boolean): Promise => { + await this.say("api_req_retry_delayed", undefined, undefined, isPartial, undefined, undefined, { + isNonInteractive: true, + metadata: { + retryStatus: payload, + rateLimitRetry: + payload.cause === "rate_limit" ? { ...payload, type: "rate_limit_retry" as const } : undefined, + }, + }) + } + + const runRateLimitCountdown = async ({ + seconds, + origin, + attempt, + maxAttempts, + detail, + rateLimitSeconds, + }: { + seconds: number + origin: RetryStatusPayload["origin"] + attempt?: number + maxAttempts?: number + detail?: string + rateLimitSeconds?: number + }): Promise => { + const normalizedSeconds = Math.max(0, Math.ceil(seconds)) + + if (normalizedSeconds <= 0) { + if (this.abort) { + await sendRetryStatusUpdate( + { + type: "retry_status", + status: "cancelled", + remainingSeconds: 0, + attempt, + maxAttempts, + origin, + detail, + cause: "rate_limit", + rateLimitSeconds, + }, + false, + ) + return false + } + + await sendRetryStatusUpdate( + { + type: "retry_status", + status: "retrying", + remainingSeconds: 0, + attempt, + maxAttempts, + origin, + detail, + cause: "rate_limit", + rateLimitSeconds, + }, + false, + ) + return true + } + + for (let i = normalizedSeconds; i > 0; i--) { + if (this.abort) { + await sendRetryStatusUpdate( + { + type: "retry_status", + status: "cancelled", + remainingSeconds: i, + attempt, + maxAttempts, + origin, + detail, + cause: "rate_limit", + rateLimitSeconds, + }, + false, + ) + return false + } + + await sendRetryStatusUpdate( + { + type: "retry_status", + status: "waiting", + remainingSeconds: i, + attempt, + maxAttempts, + origin, + detail, + cause: "rate_limit", + rateLimitSeconds, + }, + true, + ) + + await delay(1000) + } + + if (this.abort) { + await sendRetryStatusUpdate( + { + type: "retry_status", + status: "cancelled", + remainingSeconds: 0, + attempt, + maxAttempts, + origin, + detail, + cause: "rate_limit", + rateLimitSeconds, + }, + false, + ) + return false + } + + await sendRetryStatusUpdate( + { + type: "retry_status", + status: "retrying", + remainingSeconds: 0, + attempt, + maxAttempts, + origin, + detail, + cause: "rate_limit", + rateLimitSeconds, + }, + false, + ) + + return true + } + // Use the shared timestamp so that subtasks respect the same rate-limit // window as their parent tasks. if (Task.lastGlobalApiRequestTime) { @@ -2666,11 +2840,18 @@ export class Task extends EventEmitter implements TaskLike { // Only show rate limiting message if we're not retrying. If retrying, we'll include the delay there. if (rateLimitDelay > 0 && retryAttempt === 0) { - // Show countdown timer - for (let i = rateLimitDelay; i > 0; i--) { - const delayMessage = `Rate limiting for ${i} seconds...` - await this.say("api_req_retry_delayed", delayMessage, undefined, true) - await delay(1000) + const rateLimit = apiConfiguration?.rateLimitSeconds || 0 + const countdownCompleted = await runRateLimitCountdown({ + seconds: rateLimitDelay, + origin: "pre_request", + attempt: 1, + rateLimitSeconds: rateLimit, + }) + + if (!countdownCompleted) { + throw new Error( + `[RooCode#attemptApiRequest] task ${this.taskId}.${this.instanceId} aborted during pre-request rate limit wait`, + ) } } @@ -2822,7 +3003,7 @@ export class Task extends EventEmitter implements TaskLike { // note that this api_req_failed ask is unique in that we only present this option if the api hasn't streamed any content yet (ie it fails on the first chunk due), as it would allow them to hit a retry button. However if the api failed mid-stream, it could be in any arbitrary state where some tools may have executed, so that error is handled differently and requires cancelling the task entirely. if (autoApprovalEnabled && alwaysApproveResubmit) { - let errorMsg + let errorMsg: string if (error.error?.metadata?.raw) { errorMsg = JSON.stringify(error.error.metadata.raw, null, 2) @@ -2913,43 +3094,127 @@ export class Task extends EventEmitter implements TaskLike { const finalDelay = Math.max(exponentialDelay, rateLimitDelay) if (finalDelay <= 0) return - // Build header text; fall back to error message if none provided - let headerText = header - if (!headerText) { + // Build detail text; fall back to error message if none provided + let errorMsg = header + if (!errorMsg) { if (error?.error?.metadata?.raw) { - headerText = JSON.stringify(error.error.metadata.raw, null, 2) + errorMsg = JSON.stringify(error.error.metadata.raw, null, 2) } else if (error?.message) { - headerText = error.message + errorMsg = error.message } else { - headerText = "Unknown error" + errorMsg = "Unknown error" } } - headerText = headerText ? `${headerText}\n\n` : "" - // Show countdown timer with exponential backoff + // Sanitize detail for UI display + const sanitizedDetail = (() => { + if (!errorMsg) { + return undefined + } + const firstLine = errorMsg + .split("\n") + .map((line) => line.trim()) + .find((line) => line.length > 0) + if (!firstLine) { + return undefined + } + return firstLine.length > 160 ? `${firstLine.slice(0, 157)}…` : firstLine + })() + + // Helper to send retry status updates with structured metadata + const sendRetryStatusUpdate = async (payload: RetryStatusPayload, isPartial: boolean): Promise => { + await this.say("api_req_retry_delayed", undefined, undefined, isPartial, undefined, undefined, { + isNonInteractive: true, + metadata: { + retryStatus: payload, + rateLimitRetry: + payload.cause === "rate_limit" + ? { ...payload, type: "rate_limit_retry" as const } + : undefined, + }, + }) + } + + // Determine the cause based on error type + const cause: "rate_limit" | "backoff" = error?.status === 429 ? "rate_limit" : "backoff" + + // For rate limit errors, include the rate limit setting + const rateLimitSetting = state?.apiConfiguration?.rateLimitSeconds || 0 + const rateLimitSeconds = cause === "rate_limit" ? rateLimitSetting : undefined + + // Show countdown timer with exponential backoff using structured metadata for (let i = finalDelay; i > 0; i--) { // Check abort flag during countdown to allow early exit if (this.abort) { + await sendRetryStatusUpdate( + { + type: "retry_status", + status: "cancelled", + remainingSeconds: i, + attempt: retryAttempt + 1, + origin: "retry_attempt", + detail: sanitizedDetail, + cause, + rateLimitSeconds, + }, + false, + ) throw new Error(`[Task#${this.taskId}] Aborted during retry countdown`) } - await this.say( - "api_req_retry_delayed", - `${headerText}Retry attempt ${retryAttempt + 1}\nRetrying in ${i} seconds...`, - undefined, + await sendRetryStatusUpdate( + { + type: "retry_status", + status: "waiting", + remainingSeconds: i, + attempt: retryAttempt + 1, + origin: "retry_attempt", + detail: sanitizedDetail, + cause, + rateLimitSeconds, + }, true, ) await delay(1000) } - await this.say( - "api_req_retry_delayed", - `${headerText}Retry attempt ${retryAttempt + 1}\nRetrying now...`, - undefined, + // Final check before retrying + if (this.abort) { + await sendRetryStatusUpdate( + { + type: "retry_status", + status: "cancelled", + remainingSeconds: 0, + attempt: retryAttempt + 1, + origin: "retry_attempt", + detail: sanitizedDetail, + cause, + rateLimitSeconds, + }, + false, + ) + throw new Error(`[Task#${this.taskId}] Aborted during retry countdown`) + } + + await sendRetryStatusUpdate( + { + type: "retry_status", + status: "retrying", + remainingSeconds: 0, + attempt: retryAttempt + 1, + origin: "retry_attempt", + detail: sanitizedDetail, + cause, + rateLimitSeconds, + }, false, ) } catch (err) { console.error("Exponential backoff failed:", err) + // Re-throw if it's an abort error so it propagates correctly + if (err instanceof Error && err.message.includes("Aborted during retry countdown")) { + throw err + } } } diff --git a/webview-ui/src/components/chat/ChatRow.tsx b/webview-ui/src/components/chat/ChatRow.tsx index fd3d480014..bb70d6851e 100644 --- a/webview-ui/src/components/chat/ChatRow.tsx +++ b/webview-ui/src/components/chat/ChatRow.tsx @@ -43,6 +43,10 @@ import CodebaseSearchResultsDisplay from "./CodebaseSearchResultsDisplay" import { appendImages } from "@src/utils/imageUtils" import { McpExecution } from "./McpExecution" import { ChatTextArea } from "./ChatTextArea" +import { RateLimitRetryRow } from "./RateLimitRetryRow" +import { RetryStatusRow } from "./RetryStatusRow" +export { RateLimitRetryRow } from "./RateLimitRetryRow" +export { RetryStatusRow } from "./RetryStatusRow" import { MAX_IMAGES_PER_MESSAGE } from "./ChatView" import { useSelectedModel } from "../ui/hooks/useSelectedModel" import { @@ -263,7 +267,7 @@ export const ChatRowContent = ({ {t("chat:taskCompleted")}, ] case "api_req_retry_delayed": - return [] + return [null, null] case "api_req_started": const getIconSpan = (iconName: string, color: string) => (
) + case "api_req_retry_delayed": + // Prevent multiple blocks returning, we only need a single block + // that's constantly updated + if (!isLast) return null + + // Use new RetryStatusRow if retryStatus metadata is available, fall back to legacy + return message.metadata?.retryStatus ? ( + + ) : ( + + ) case "shell_integration_warning": return case "checkpoint_saved": diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx index 929fa9427a..898d3e1c7f 100644 --- a/webview-ui/src/components/chat/ChatView.tsx +++ b/webview-ui/src/components/chat/ChatView.tsx @@ -427,9 +427,16 @@ const ChatViewComponent: React.ForwardRefRenderFunction { + const { t } = useTranslation() + + const description = useMemo(() => { + if (!metadata) { + return "" + } + + if (metadata.status === "retrying") { + return t("chat:rateLimitRetry.retrying") + } + + if (metadata.status === "cancelled") { + return t("chat:rateLimitRetry.cancelled") + } + + if (typeof metadata.remainingSeconds === "number") { + if (metadata.attempt && metadata.maxAttempts) { + return t("chat:rateLimitRetry.waitingWithAttemptMax", { + seconds: metadata.remainingSeconds, + attempt: metadata.attempt, + maxAttempts: metadata.maxAttempts, + }) + } + + if (metadata.attempt) { + return t("chat:rateLimitRetry.waitingWithAttempt", { + seconds: metadata.remainingSeconds, + attempt: metadata.attempt, + }) + } + + return t("chat:rateLimitRetry.waiting", { seconds: metadata.remainingSeconds }) + } + + return "" + }, [metadata, t]) + + const detail = metadata?.detail + const iconNode = + metadata?.status === "cancelled" ? ( + + ) : ( + + ) + + return ( +
+
+
{iconNode}
+
+ {t("chat:rateLimitRetry.title")} + {(description || detail) && ( + + {description} + {detail ? ( + <> + {" — "} + {detail} + + ) : null} + + )} +
+
+
+ ) +} diff --git a/webview-ui/src/components/chat/RetryStatusRow.tsx b/webview-ui/src/components/chat/RetryStatusRow.tsx new file mode 100644 index 0000000000..177d3de45d --- /dev/null +++ b/webview-ui/src/components/chat/RetryStatusRow.tsx @@ -0,0 +1,95 @@ +import React, { useMemo } from "react" +import { useTranslation } from "react-i18next" +import type { RetryStatusMetadata } from "@roo-code/types" +import { ProgressIndicator } from "./ProgressIndicator" + +export interface RetryStatusRowProps { + metadata?: RetryStatusMetadata +} + +export const RetryStatusRow = ({ metadata }: RetryStatusRowProps) => { + const { t } = useTranslation() + + const title = useMemo(() => { + if (!metadata) { + return "" + } + + const isRateLimit = metadata.cause === "rate_limit" + + if (isRateLimit) { + // For rate limit, show "Rate limit set for Xs" + return t("chat:retryStatus.rateLimit.title", { + rateLimitSeconds: metadata.rateLimitSeconds || 30, // fallback to 30 if not provided + }) + } else { + // For backoff/retry, show the error message if available + return metadata.detail || t("chat:retryStatus.backoff.title") + } + }, [metadata, t]) + + const subtitle = useMemo(() => { + if (!metadata) { + // Default to backoff waiting when no metadata is provided + return t("chat:retryStatus.backoff.waiting") + } + + const isRateLimit = metadata.cause === "rate_limit" + + if (metadata.status === "retrying") { + return isRateLimit ? t("chat:retryStatus.rateLimit.proceeding") : t("chat:retryStatus.backoff.retrying") + } + + if (metadata.status === "cancelled") { + return isRateLimit ? t("chat:retryStatus.rateLimit.cancelled") : t("chat:retryStatus.backoff.cancelled") + } + + if (typeof metadata.remainingSeconds === "number") { + if (isRateLimit) { + // Rate limit: always use simple waiting message (no attempt numbers) + return t("chat:retryStatus.rateLimit.waiting", { seconds: metadata.remainingSeconds }) + } else { + // Retry: "Trying in 22s (attempt #2)" + const baseKey = "chat:retryStatus.backoff" + + if (metadata.attempt && metadata.maxAttempts) { + return t(`${baseKey}.waitingWithAttemptMax`, { + seconds: metadata.remainingSeconds, + attempt: metadata.attempt, + maxAttempts: metadata.maxAttempts, + }) + } + + if (metadata.attempt) { + return t(`${baseKey}.waitingWithAttempt`, { + seconds: metadata.remainingSeconds, + attempt: metadata.attempt, + }) + } + + return t(`${baseKey}.waiting`, { seconds: metadata.remainingSeconds }) + } + } + + return "" + }, [metadata, t]) + + const iconNode = + metadata?.status === "cancelled" ? ( + + ) : ( + + ) + + return ( +
+
+
{iconNode}
+
+ {title} + {subtitle && {subtitle}} +
+
+
+ ) +} diff --git a/webview-ui/src/components/chat/__tests__/RateLimitRetryRow.spec.tsx b/webview-ui/src/components/chat/__tests__/RateLimitRetryRow.spec.tsx new file mode 100644 index 0000000000..6a12f384f2 --- /dev/null +++ b/webview-ui/src/components/chat/__tests__/RateLimitRetryRow.spec.tsx @@ -0,0 +1,138 @@ +// npx vitest run src/components/chat/__tests__/RateLimitRetryRow.spec.tsx + +import { render, screen } from "@/utils/test-utils" + +import { RateLimitRetryRow } from "../ChatRow" +import type { RateLimitRetryMetadata } from "@roo-code/types" + +vi.mock("@/i18n/TranslationContext", () => ({ + useAppTranslation: () => ({ + t: (key: string) => key, + }), +})) + +// Manual trigger instructions (for developer reference): +// 1) In Roo Code settings, set your provider Rate limit seconds to a small value (e.g., 5s). +// 2) Send a message to start an API request. +// 3) Immediately send another message within the configured window. +// The pre-request wait will emit `api_req_retry_delayed` with metadata, +// rendering a single live status row: spinner + per‑second countdown, +// then "Retrying now..." at zero. Input remains disabled during the wait. +describe("RateLimitRetryRow", () => { + it("renders waiting countdown with attempt and max attempts", () => { + const metadata: RateLimitRetryMetadata = { + type: "rate_limit_retry", + status: "waiting", + remainingSeconds: 12, + attempt: 2, + maxAttempts: 5, + origin: "retry_attempt", + cause: "rate_limit", + } + + render() + + expect(screen.getByText("chat:rateLimitRetry.title")).toBeInTheDocument() + expect(screen.getByText("chat:rateLimitRetry.waitingWithAttemptMax")).toBeInTheDocument() + }) + + it("renders retrying state", () => { + const metadata: RateLimitRetryMetadata = { + type: "rate_limit_retry", + status: "retrying", + origin: "retry_attempt", + cause: "rate_limit", + } + + render() + + expect(screen.getByText("chat:rateLimitRetry.title")).toBeInTheDocument() + expect(screen.getByText("chat:rateLimitRetry.retrying")).toBeInTheDocument() + }) + + it("renders cancelled state (neutral)", () => { + const metadata: RateLimitRetryMetadata = { + type: "rate_limit_retry", + status: "cancelled", + origin: "retry_attempt", + cause: "rate_limit", + } + + const { container } = render() + + expect(screen.getByText("chat:rateLimitRetry.title")).toBeInTheDocument() + expect(screen.getByText("chat:rateLimitRetry.cancelled")).toBeInTheDocument() + + // Iconography: ensure neutral cancelled icon is present + const cancelledIcon = container.querySelector(".codicon-circle-slash") + expect(cancelledIcon).not.toBeNull() + }) + + it("renders empty description when metadata is missing", () => { + render() + + expect(screen.getByText("chat:rateLimitRetry.title")).toBeInTheDocument() + // Description should be empty when no metadata is provided + const descriptionElement = screen.queryByText(/./i, { selector: ".text-vscode-descriptionForeground span" }) + expect(descriptionElement).not.toBeInTheDocument() + }) + + it("updates when metadata changes from waiting to retrying", () => { + const initialMetadata: RateLimitRetryMetadata = { + type: "rate_limit_retry", + status: "waiting", + remainingSeconds: 5, + attempt: 1, + maxAttempts: 3, + origin: "retry_attempt", + cause: "rate_limit", + } + + const { rerender } = render() + + // Initial state: waiting + expect(screen.getByText("chat:rateLimitRetry.waitingWithAttemptMax")).toBeInTheDocument() + + // Update to retrying state + const updatedMetadata: RateLimitRetryMetadata = { + type: "rate_limit_retry", + status: "retrying", + origin: "retry_attempt", + cause: "rate_limit", + } + + rerender() + + // Should now show retrying + expect(screen.getByText("chat:rateLimitRetry.retrying")).toBeInTheDocument() + expect(screen.queryByText("chat:rateLimitRetry.waitingWithAttemptMax")).not.toBeInTheDocument() + }) + + it("updates countdown when remainingSeconds changes", () => { + const metadata1: RateLimitRetryMetadata = { + type: "rate_limit_retry", + status: "waiting", + remainingSeconds: 10, + attempt: 1, + maxAttempts: 3, + origin: "retry_attempt", + cause: "rate_limit", + } + + const { rerender } = render() + + // Initial countdown + expect(screen.getByText("chat:rateLimitRetry.waitingWithAttemptMax")).toBeInTheDocument() + + // Update countdown + const metadata2: RateLimitRetryMetadata = { + ...metadata1, + remainingSeconds: 5, + } + + rerender() + + // Should still show the same text key but with updated seconds + expect(screen.getByText("chat:rateLimitRetry.waitingWithAttemptMax")).toBeInTheDocument() + }) +}) diff --git a/webview-ui/src/components/chat/__tests__/RetryStatusRow.spec.tsx b/webview-ui/src/components/chat/__tests__/RetryStatusRow.spec.tsx new file mode 100644 index 0000000000..61826e3efd --- /dev/null +++ b/webview-ui/src/components/chat/__tests__/RetryStatusRow.spec.tsx @@ -0,0 +1,146 @@ +// npx vitest run src/components/chat/__tests__/RetryStatusRow.spec.tsx + +import { render, screen } from "@/utils/test-utils" + +import { RetryStatusRow } from "../ChatRow" +import type { RetryStatusMetadata } from "@roo-code/types" + +vi.mock("@/i18n/TranslationContext", () => ({ + useAppTranslation: () => ({ + t: (key: string) => key, + }), +})) + +// Manual trigger instructions (for developer reference): +// 1) In Roo Code settings, set your provider Rate limit seconds to a small value (e.g., 5s). +// 2) Send a message to start an API request. +// 3) Immediately send another message within the configured window. +// The pre-request wait will emit `api_req_retry_delayed` with metadata, +// rendering a single live status row: spinner + per‑second countdown, +// then "Retrying now..." at zero. Input remains disabled during the wait. +describe("RetryStatusRow", () => { + it("renders waiting countdown for rate limit cause", () => { + const metadata: RetryStatusMetadata = { + type: "retry_status", + status: "waiting", + remainingSeconds: 12, + attempt: 2, + maxAttempts: 5, + origin: "retry_attempt", + cause: "rate_limit", + } + + render() + + expect(screen.getByText("chat:retryStatus.rateLimit.waiting")).toBeInTheDocument() + expect(screen.getByText("chat:retryStatus.rateLimit.title")).toBeInTheDocument() + }) + + it("renders retrying state for backoff cause", () => { + const metadata: RetryStatusMetadata = { + type: "retry_status", + status: "retrying", + origin: "retry_attempt", + cause: "backoff", + } + + render() + + expect(screen.getByText("chat:retryStatus.backoff.retrying")).toBeInTheDocument() + }) + + it("renders proceeding state for rate limit cause", () => { + const metadata: RetryStatusMetadata = { + type: "retry_status", + status: "retrying", + origin: "retry_attempt", + cause: "rate_limit", + } + + render() + + expect(screen.getByText("chat:retryStatus.rateLimit.proceeding")).toBeInTheDocument() + }) + + it("renders cancelled state (neutral)", () => { + const metadata: RetryStatusMetadata = { + type: "retry_status", + status: "cancelled", + origin: "retry_attempt", + cause: "rate_limit", + } + + const { container } = render() + + expect(screen.getByText("chat:retryStatus.rateLimit.cancelled")).toBeInTheDocument() + + // Iconography: ensure neutral cancelled icon is present + const cancelledIcon = container.querySelector(".codicon-circle-slash") + expect(cancelledIcon).not.toBeNull() + }) + + it("renders empty description when metadata is missing", () => { + render() + + expect(screen.getByText("chat:retryStatus.backoff.waiting")).toBeInTheDocument() + }) + + it("updates when metadata changes from waiting to retrying", () => { + const initialMetadata: RetryStatusMetadata = { + type: "retry_status", + status: "waiting", + remainingSeconds: 5, + attempt: 1, + maxAttempts: 3, + origin: "retry_attempt", + cause: "backoff", + } + + const { rerender } = render() + + // Initial state: waiting + expect(screen.getByText("chat:retryStatus.backoff.waitingWithAttemptMax")).toBeInTheDocument() + + // Update to retrying state + const updatedMetadata: RetryStatusMetadata = { + type: "retry_status", + status: "retrying", + origin: "retry_attempt", + cause: "backoff", + } + + rerender() + + // Should now show retrying + expect(screen.getByText("chat:retryStatus.backoff.retrying")).toBeInTheDocument() + expect(screen.queryByText("chat:retryStatus.backoff.waitingWithAttemptMax")).not.toBeInTheDocument() + }) + + it("updates countdown when remainingSeconds changes", () => { + const metadata1: RetryStatusMetadata = { + type: "retry_status", + status: "waiting", + remainingSeconds: 10, + attempt: 1, + maxAttempts: 3, + origin: "retry_attempt", + cause: "rate_limit", + } + + const { rerender } = render() + + // Initial countdown + expect(screen.getByText("chat:retryStatus.rateLimit.waiting")).toBeInTheDocument() + + // Update countdown + const metadata2: RetryStatusMetadata = { + ...metadata1, + remainingSeconds: 5, + } + + rerender() + + // Should still show the same text key but with updated seconds + expect(screen.getByText("chat:retryStatus.rateLimit.waiting")).toBeInTheDocument() + }) +}) diff --git a/webview-ui/src/i18n/locales/ca/chat.json b/webview-ui/src/i18n/locales/ca/chat.json index 425cd9b6a0..bd93e7c11d 100644 --- a/webview-ui/src/i18n/locales/ca/chat.json +++ b/webview-ui/src/i18n/locales/ca/chat.json @@ -143,6 +143,30 @@ "cancelled": "Sol·licitud API cancel·lada", "streamingFailed": "Transmissió API ha fallat" }, + "retryStatus": { + "rateLimit": { + "title": "Límit de taxa establert a {{rateLimitSeconds}} segons", + "waiting": "Esperant {{seconds}} segons", + "proceeding": "Continuant ara...", + "cancelled": "Sol·licitud cancel·lada" + }, + "backoff": { + "title": "La sol·licitud ha fallat", + "waiting": "Intentant en {{seconds}} segons", + "waitingWithAttempt": "Intentant en {{seconds}} segons (intent #{{attempt}})", + "waitingWithAttemptMax": "Intentant en {{seconds}} segons (intent #{{attempt}}/{{maxAttempts}})", + "retrying": "Intentant ara...", + "cancelled": "Reintent cancel·lat" + } + }, + "rateLimitRetry": { + "title": "Límit de peticions assolit — si us plau, espera.", + "waiting": "Reintentant en {{seconds}}s", + "waitingWithAttempt": "Reintentant en {{seconds}}s (intent {{attempt}})", + "waitingWithAttemptMax": "Reintentant en {{seconds}}s (intent {{attempt}}/{{maxAttempts}})", + "retrying": "Reintentant ara…", + "cancelled": "Reintent cancel·lat" + }, "checkpoint": { "regular": "Punt de control", "initializingWarning": "Encara s'està inicialitzant el punt de control... Si això triga massa, pots desactivar els punts de control a la configuració i reiniciar la teva tasca.", diff --git a/webview-ui/src/i18n/locales/de/chat.json b/webview-ui/src/i18n/locales/de/chat.json index c0fd7a98a3..0cab84835a 100644 --- a/webview-ui/src/i18n/locales/de/chat.json +++ b/webview-ui/src/i18n/locales/de/chat.json @@ -143,6 +143,30 @@ "cancelled": "API-Anfrage abgebrochen", "streamingFailed": "API-Streaming fehlgeschlagen" }, + "retryStatus": { + "rateLimit": { + "title": "Ratenlimit auf {{rateLimitSeconds}} Sekunden festgelegt", + "waiting": "Warte {{seconds}} Sekunden", + "proceeding": "Wird jetzt fortgesetzt…", + "cancelled": "Anfrage abgebrochen" + }, + "backoff": { + "title": "Anfrage fehlgeschlagen", + "waiting": "Versuche in {{seconds}} Sekunden", + "waitingWithAttempt": "Versuche in {{seconds}} Sekunden (Versuch #{{attempt}})", + "waitingWithAttemptMax": "Versuche in {{seconds}} Sekunden (Versuch #{{attempt}}/{{maxAttempts}})", + "retrying": "Versuche jetzt…", + "cancelled": "Wiederholung abgebrochen" + } + }, + "rateLimitRetry": { + "title": "Ratenlimit ausgelöst — bitte warten.", + "waiting": "Wiederholung in {{seconds}}s", + "waitingWithAttempt": "Wiederholung in {{seconds}}s (Versuch {{attempt}})", + "waitingWithAttemptMax": "Wiederholung in {{seconds}}s (Versuch {{attempt}}/{{maxAttempts}})", + "retrying": "Wiederholung läuft…", + "cancelled": "Wiederholung abgebrochen" + }, "checkpoint": { "regular": "Checkpoint", "initializingWarning": "Checkpoint wird noch initialisiert... Falls dies zu lange dauert, kannst du Checkpoints in den Einstellungen deaktivieren und deine Aufgabe neu starten.", diff --git a/webview-ui/src/i18n/locales/en/chat.json b/webview-ui/src/i18n/locales/en/chat.json index 706e54c696..bb50cf0819 100644 --- a/webview-ui/src/i18n/locales/en/chat.json +++ b/webview-ui/src/i18n/locales/en/chat.json @@ -149,6 +149,30 @@ "cancelled": "API Request Cancelled", "streamingFailed": "API Streaming Failed" }, + "retryStatus": { + "rateLimit": { + "title": "Rate limit set to {{rateLimitSeconds}} seconds", + "waiting": "Waiting {{seconds}} seconds", + "proceeding": "Proceeding now…", + "cancelled": "Request cancelled" + }, + "backoff": { + "title": "Request failed", + "waiting": "Trying in {{seconds}} seconds", + "waitingWithAttempt": "Trying in {{seconds}} seconds (attempt #{{attempt}})", + "waitingWithAttemptMax": "Trying in {{seconds}} seconds (attempt #{{attempt}}/{{maxAttempts}})", + "retrying": "Trying now…", + "cancelled": "Retry cancelled" + } + }, + "rateLimitRetry": { + "title": "Rate limit triggered — please wait.", + "waiting": "Retrying in {{seconds}}s", + "waitingWithAttempt": "Retrying in {{seconds}}s (attempt {{attempt}})", + "waitingWithAttemptMax": "Retrying in {{seconds}}s (attempt {{attempt}}/{{maxAttempts}})", + "retrying": "Retrying now…", + "cancelled": "Retry cancelled" + }, "checkpoint": { "regular": "Checkpoint", "initializingWarning": "Still initializing checkpoint... If this takes too long, you can disable checkpoints in settings and restart your task.", diff --git a/webview-ui/src/i18n/locales/es/chat.json b/webview-ui/src/i18n/locales/es/chat.json index 0e4953a494..9814288124 100644 --- a/webview-ui/src/i18n/locales/es/chat.json +++ b/webview-ui/src/i18n/locales/es/chat.json @@ -162,6 +162,30 @@ }, "current": "Actual" }, + "retryStatus": { + "rateLimit": { + "title": "Límite de velocidad establecido en {{rateLimitSeconds}} segundos", + "waiting": "Esperando {{seconds}} segundos", + "proceeding": "Continuando ahora…", + "cancelled": "Solicitud cancelada" + }, + "backoff": { + "title": "La solicitud falló", + "waiting": "Intentando en {{seconds}} segundos", + "waitingWithAttempt": "Intentando en {{seconds}} segundos (intento #{{attempt}})", + "waitingWithAttemptMax": "Intentando en {{seconds}} segundos (intento #{{attempt}}/{{maxAttempts}})", + "retrying": "Intentando ahora…", + "cancelled": "Reintento cancelado" + } + }, + "rateLimitRetry": { + "title": "Límite de tasa activado — por favor, espera.", + "waiting": "Reintentando en {{seconds}}s", + "waitingWithAttempt": "Reintentando en {{seconds}}s (intento {{attempt}})", + "waitingWithAttemptMax": "Reintentando en {{seconds}}s (intento {{attempt}}/{{maxAttempts}})", + "retrying": "Reintentando ahora…", + "cancelled": "Reintento cancelado" + }, "instructions": { "wantsToFetch": "Roo quiere obtener instrucciones detalladas para ayudar con la tarea actual" }, diff --git a/webview-ui/src/i18n/locales/fr/chat.json b/webview-ui/src/i18n/locales/fr/chat.json index 15ce79eda3..7be33c7770 100644 --- a/webview-ui/src/i18n/locales/fr/chat.json +++ b/webview-ui/src/i18n/locales/fr/chat.json @@ -162,6 +162,30 @@ }, "current": "Actuel" }, + "retryStatus": { + "rateLimit": { + "title": "Limite de taux fixée à {{rateLimitSeconds}} secondes", + "waiting": "En attente de {{seconds}} secondes", + "proceeding": "En cours…", + "cancelled": "Requête annulée" + }, + "backoff": { + "title": "La requête a échoué", + "waiting": "Nouvel essai dans {{seconds}} secondes", + "waitingWithAttempt": "Nouvel essai dans {{seconds}} secondes (tentative n°{{attempt}})", + "waitingWithAttemptMax": "Nouvel essai dans {{seconds}} secondes (tentative n°{{attempt}}/{{maxAttempts}})", + "retrying": "Nouvel essai en cours…", + "cancelled": "Nouvel essai annulé" + } + }, + "rateLimitRetry": { + "title": "Limite de débit atteinte — veuillez patienter.", + "waiting": "Nouvel essai dans {{seconds}}s", + "waitingWithAttempt": "Nouvel essai dans {{seconds}}s (tentative {{attempt}})", + "waitingWithAttemptMax": "Nouvel essai dans {{seconds}}s (tentative {{attempt}}/{{maxAttempts}})", + "retrying": "Nouvel essai en cours…", + "cancelled": "Nouvel essai annulé" + }, "fileOperations": { "wantsToRead": "Roo veut lire ce fichier", "wantsToReadOutsideWorkspace": "Roo veut lire ce fichier en dehors de l'espace de travail", diff --git a/webview-ui/src/i18n/locales/hi/chat.json b/webview-ui/src/i18n/locales/hi/chat.json index 7dae0e524a..909247012f 100644 --- a/webview-ui/src/i18n/locales/hi/chat.json +++ b/webview-ui/src/i18n/locales/hi/chat.json @@ -143,6 +143,30 @@ "cancelled": "API अनुरोध रद्द किया गया", "streamingFailed": "API स्ट्रीमिंग विफल हुई" }, + "retryStatus": { + "rateLimit": { + "title": "दर सीमा {{rateLimitSeconds}} सेकंड पर सेट है", + "waiting": "{{seconds}} सेकंड प्रतीक्षा कर रहा है", + "proceeding": "अब आगे बढ़ रहा है...", + "cancelled": "अनुरोध रद्द कर दिया गया" + }, + "backoff": { + "title": "अनुरोध विफल", + "waiting": "{{seconds}} सेकंड में पुनः प्रयास कर रहा है", + "waitingWithAttempt": "{{seconds}} सेकंड में पुनः प्रयास कर रहा है (प्रयास #{{attempt}})", + "waitingWithAttemptMax": "{{seconds}} सेकंड में पुनः प्रयास कर रहा है (प्रयास #{{attempt}}/{{maxAttempts}})", + "retrying": "अब कोशिश कर रहा हूँ...", + "cancelled": "पुनः प्रयास रद्द" + } + }, + "rateLimitRetry": { + "title": "दर सीमा ट्रिगर हुई — कृपया प्रतीक्षा करें।", + "waiting": "{{seconds}}s में पुनः प्रयास कर रहा है", + "waitingWithAttempt": "{{seconds}}s में पुनः प्रयास कर रहा है (प्रयास {{attempt}})", + "waitingWithAttemptMax": "{{seconds}}s में पुनः प्रयास कर रहा है (प्रयास {{attempt}}/{{maxAttempts}})", + "retrying": "अभी पुनः प्रयास कर रहा है…", + "cancelled": "पुनः प्रयास रद्द किया गया" + }, "checkpoint": { "regular": "चेकपॉइंट", "initializingWarning": "चेकपॉइंट अभी भी आरंभ हो रहा है... अगर यह बहुत समय ले रहा है, तो आप सेटिंग्स में चेकपॉइंट को अक्षम कर सकते हैं और अपने कार्य को पुनः आरंभ कर सकते हैं।", diff --git a/webview-ui/src/i18n/locales/id/chat.json b/webview-ui/src/i18n/locales/id/chat.json index 6e5590f7f7..19bedbd0dc 100644 --- a/webview-ui/src/i18n/locales/id/chat.json +++ b/webview-ui/src/i18n/locales/id/chat.json @@ -152,6 +152,30 @@ "cancelled": "Permintaan API Dibatalkan", "streamingFailed": "Streaming API Gagal" }, + "retryStatus": { + "rateLimit": { + "title": "Batas tarif diatur ke {{rateLimitSeconds}} detik", + "waiting": "Menunggu {{seconds}} detik", + "proceeding": "Sedang memproses…", + "cancelled": "Permintaan dibatalkan" + }, + "backoff": { + "title": "Permintaan gagal", + "waiting": "Mencoba dalam {{seconds}} detik", + "waitingWithAttempt": "Mencoba dalam {{seconds}} detik (percobaan #{{attempt}})", + "waitingWithAttemptMax": "Mencoba dalam {{seconds}} detik (percobaan #{{attempt}}/{{maxAttempts}})", + "retrying": "Mencoba sekarang…", + "cancelled": "Percobaan ulang dibatalkan" + } + }, + "rateLimitRetry": { + "title": "Batas kecepatan tercapai — mohon tunggu.", + "waiting": "Mencoba lagi dalam {{seconds}}dtk", + "waitingWithAttempt": "Mencoba lagi dalam {{seconds}}dtk (percobaan {{attempt}})", + "waitingWithAttemptMax": "Mencoba lagi dalam {{seconds}}dtk (percobaan {{attempt}}/{{maxAttempts}})", + "retrying": "Mencoba lagi sekarang…", + "cancelled": "Percobaan ulang dibatalkan" + }, "checkpoint": { "regular": "Checkpoint", "initializingWarning": "Masih menginisialisasi checkpoint... Jika ini terlalu lama, kamu bisa menonaktifkan checkpoint di pengaturan dan restart tugas.", diff --git a/webview-ui/src/i18n/locales/it/chat.json b/webview-ui/src/i18n/locales/it/chat.json index db09a561c3..f1d7ccf417 100644 --- a/webview-ui/src/i18n/locales/it/chat.json +++ b/webview-ui/src/i18n/locales/it/chat.json @@ -146,6 +146,30 @@ "cancelled": "Richiesta API annullata", "streamingFailed": "Streaming API fallito" }, + "retryStatus": { + "rateLimit": { + "title": "Limite di velocità impostato a {{rateLimitSeconds}} secondi", + "waiting": "In attesa di {{seconds}} secondi", + "proceeding": "Procedendo ora...", + "cancelled": "Richiesta annullata" + }, + "backoff": { + "title": "Richiesta fallita", + "waiting": "Tento tra {{seconds}} secondi", + "waitingWithAttempt": "Tento tra {{seconds}} secondi (tentativo n. {{attempt}})", + "waitingWithAttemptMax": "Tento tra {{seconds}} secondi (tentativo n. {{attempt}}/{{maxAttempts}})", + "retrying": "Tento ora...", + "cancelled": "Tentativo annullato" + } + }, + "rateLimitRetry": { + "title": "Limite di frequenza raggiunto — attendi.", + "waiting": "Riprovo tra {{seconds}}s", + "waitingWithAttempt": "Riprovo tra {{seconds}}s (tentativo {{attempt}})", + "waitingWithAttemptMax": "Riprovo tra {{seconds}}s (tentativo {{attempt}}/{{maxAttempts}})", + "retrying": "Riprovo ora…", + "cancelled": "Riprova annullata" + }, "checkpoint": { "regular": "Checkpoint", "initializingWarning": "Inizializzazione del checkpoint in corso... Se questa operazione richiede troppo tempo, puoi disattivare i checkpoint nelle impostazioni e riavviare l'attività.", diff --git a/webview-ui/src/i18n/locales/ja/chat.json b/webview-ui/src/i18n/locales/ja/chat.json index 0bb079ffac..ecb583ec7c 100644 --- a/webview-ui/src/i18n/locales/ja/chat.json +++ b/webview-ui/src/i18n/locales/ja/chat.json @@ -143,6 +143,30 @@ "cancelled": "APIリクエストキャンセル", "streamingFailed": "APIストリーミング失敗" }, + "retryStatus": { + "rateLimit": { + "title": "レート制限が{{rateLimitSeconds}}秒に設定されました", + "waiting": "{{seconds}}秒待機中", + "proceeding": "ただいま処理中...", + "cancelled": "リクエストがキャンセルされました" + }, + "backoff": { + "title": "リクエスト失敗", + "waiting": "{{seconds}}秒後に再試行します", + "waitingWithAttempt": "{{seconds}}秒後に再試行します(試行{{attempt}}回目)", + "waitingWithAttemptMax": "{{seconds}}秒後に再試行します(試行{{attempt}}/{{maxAttempts}}回目)", + "retrying": "再試行中...", + "cancelled": "再試行がキャンセルされました" + } + }, + "rateLimitRetry": { + "title": "レート制限がトリガーされました — しばらくお待ちください。", + "waiting": "{{seconds}}秒後に再試行", + "waitingWithAttempt": "{{seconds}}秒後に再試行({{attempt}}回目)", + "waitingWithAttemptMax": "{{seconds}}秒後に再試行({{attempt}}/{{maxAttempts}}回目)", + "retrying": "再試行中…", + "cancelled": "再試行キャンセル" + }, "checkpoint": { "regular": "チェックポイント", "initializingWarning": "チェックポイントの初期化中... 時間がかかりすぎる場合は、設定でチェックポイントを無効にしてタスクを再開できます。", diff --git a/webview-ui/src/i18n/locales/ko/chat.json b/webview-ui/src/i18n/locales/ko/chat.json index d598bab8e0..8cd07b5f30 100644 --- a/webview-ui/src/i18n/locales/ko/chat.json +++ b/webview-ui/src/i18n/locales/ko/chat.json @@ -143,6 +143,30 @@ "cancelled": "API 요청 취소됨", "streamingFailed": "API 스트리밍 실패" }, + "retryStatus": { + "rateLimit": { + "title": "속도 제한이 {{rateLimitSeconds}}초로 설정되었습니다.", + "waiting": "{{seconds}}초 대기 중", + "proceeding": "지금 진행 중...", + "cancelled": "요청이 취소되었습니다." + }, + "backoff": { + "title": "요청 실패", + "waiting": "{{seconds}}초 후에 다시 시도합니다", + "waitingWithAttempt": "{{seconds}}초 후에 다시 시도합니다 (시도 #{{attempt}})", + "waitingWithAttemptMax": "{{seconds}}초 후에 다시 시도합니다 (시도 #{{attempt}}/{{maxAttempts}})", + "retrying": "지금 시도 중...", + "cancelled": "재시도 취소됨" + } + }, + "rateLimitRetry": { + "title": "속도 제한이 트리거되었습니다 — 잠시 기다려주세요.", + "waiting": "{{seconds}}초 후에 다시 시도", + "waitingWithAttempt": "{{seconds}}초 후에 다시 시도 ({{attempt}}번째 시도)", + "waitingWithAttemptMax": "{{seconds}}초 후에 다시 시도 ({{attempt}}/{{maxAttempts}}번째 시도)", + "retrying": "지금 다시 시도 중…", + "cancelled": "다시 시도 취소됨" + }, "checkpoint": { "regular": "체크포인트", "initializingWarning": "체크포인트 초기화 중... 시간이 너무 오래 걸리면 설정에서 체크포인트를 비활성화하고 작업을 다시 시작할 수 있습니다.", diff --git a/webview-ui/src/i18n/locales/nl/chat.json b/webview-ui/src/i18n/locales/nl/chat.json index f6b437bfbb..b971ef99ad 100644 --- a/webview-ui/src/i18n/locales/nl/chat.json +++ b/webview-ui/src/i18n/locales/nl/chat.json @@ -138,6 +138,30 @@ "cancelled": "API-verzoek geannuleerd", "streamingFailed": "API-streaming mislukt" }, + "retryStatus": { + "rateLimit": { + "title": "Snelheidslimiet ingesteld op {{rateLimitSeconds}} seconden", + "waiting": "{{seconds}} seconden wachten", + "proceeding": "Nu bezig...", + "cancelled": "Verzoek geannuleerd" + }, + "backoff": { + "title": "Verzoek mislukt", + "waiting": "Probeert over {{seconds}} seconden", + "waitingWithAttempt": "Probeert over {{seconds}} seconden (poging #{{attempt}})", + "waitingWithAttemptMax": "Probeert over {{seconds}} seconden (poging #{{attempt}}/{{maxAttempts}})", + "retrying": "Nu proberen...", + "cancelled": "Opnieuw proberen geannuleerd" + } + }, + "rateLimitRetry": { + "title": "Snelheidslimiet bereikt — even geduld a.u.b.", + "waiting": "Opnieuw proberen over {{seconds}}s", + "waitingWithAttempt": "Opnieuw proberen over {{seconds}}s (poging {{attempt}})", + "waitingWithAttemptMax": "Opnieuw proberen over {{seconds}}s (poging {{attempt}}/{{maxAttempts}})", + "retrying": "Nu opnieuw proberen…", + "cancelled": "Opnieuw proberen geannuleerd" + }, "checkpoint": { "regular": "Checkpoint", "initializingWarning": "Checkpoint wordt nog steeds geïnitialiseerd... Als dit te lang duurt, kun je checkpoints uitschakelen in de instellingen en je taak opnieuw starten.", diff --git a/webview-ui/src/i18n/locales/pl/chat.json b/webview-ui/src/i18n/locales/pl/chat.json index bc42d249f3..2e6f6389bb 100644 --- a/webview-ui/src/i18n/locales/pl/chat.json +++ b/webview-ui/src/i18n/locales/pl/chat.json @@ -143,6 +143,30 @@ "cancelled": "Zapytanie API anulowane", "streamingFailed": "Strumieniowanie API nie powiodło się" }, + "retryStatus": { + "rateLimit": { + "title": "Limit zapytań ustawiony na {{rateLimitSeconds}} sekund", + "waiting": "Oczekiwanie {{seconds}} sekund", + "proceeding": "Przechodzę teraz...", + "cancelled": "Żądanie anulowane" + }, + "backoff": { + "title": "Żądanie nie powiodło się", + "waiting": "Próbuję za {{seconds}} sekund", + "waitingWithAttempt": "Próbuję za {{seconds}} sekund (próba #{{attempt}})", + "waitingWithAttemptMax": "Próbuję za {{seconds}} sekund (próba #{{attempt}}/{{maxAttempts}})", + "retrying": "Próbuję teraz...", + "cancelled": "Ponowienie anulowane" + } + }, + "rateLimitRetry": { + "title": "Osiągnięto limit szybkości — proszę czekać.", + "waiting": "Ponawianie za {{seconds}}s", + "waitingWithAttempt": "Ponawianie za {{seconds}}s (próba {{attempt}})", + "waitingWithAttemptMax": "Ponawianie za {{seconds}}s (próba {{attempt}}/{{maxAttempts}})", + "retrying": "Ponawianie teraz…", + "cancelled": "Ponawianie anulowane" + }, "checkpoint": { "regular": "Punkt kontrolny", "initializingWarning": "Trwa inicjalizacja punktu kontrolnego... Jeśli to trwa zbyt długo, możesz wyłączyć punkty kontrolne w ustawieniach i uruchomić zadanie ponownie.", diff --git a/webview-ui/src/i18n/locales/pt-BR/chat.json b/webview-ui/src/i18n/locales/pt-BR/chat.json index 82713aac7b..da4251d9be 100644 --- a/webview-ui/src/i18n/locales/pt-BR/chat.json +++ b/webview-ui/src/i18n/locales/pt-BR/chat.json @@ -143,6 +143,30 @@ "cancelled": "Requisição API cancelada", "streamingFailed": "Streaming API falhou" }, + "retryStatus": { + "rateLimit": { + "title": "Limite de taxa definido para {{rateLimitSeconds}} segundos", + "waiting": "Aguardando {{seconds}} segundos", + "proceeding": "Prosseguindo agora...", + "cancelled": "Solicitação cancelada" + }, + "backoff": { + "title": "A solicitação falhou", + "waiting": "Tentando em {{seconds}} segundos", + "waitingWithAttempt": "Tentando em {{seconds}} segundos (tentativa #{{attempt}})", + "waitingWithAttemptMax": "Tentando em {{seconds}} segundos (tentativa #{{attempt}}/{{maxAttempts}})", + "retrying": "Tentando agora...", + "cancelled": "Tentativa cancelada" + } + }, + "rateLimitRetry": { + "title": "Limite de taxa atingido — por favor, aguarde.", + "waiting": "Tentando novamente em {{seconds}}s", + "waitingWithAttempt": "Tentando novamente em {{seconds}}s (tentativa {{attempt}})", + "waitingWithAttemptMax": "Tentando novamente em {{seconds}}s (tentativa {{attempt}}/{{maxAttempts}})", + "retrying": "Tentando novamente agora…", + "cancelled": "Tentativa cancelada" + }, "checkpoint": { "regular": "Ponto de verificação", "initializingWarning": "Ainda inicializando ponto de verificação... Se isso demorar muito, você pode desativar os pontos de verificação nas configurações e reiniciar sua tarefa.", diff --git a/webview-ui/src/i18n/locales/ru/chat.json b/webview-ui/src/i18n/locales/ru/chat.json index 06e880fb9a..ee0123a99b 100644 --- a/webview-ui/src/i18n/locales/ru/chat.json +++ b/webview-ui/src/i18n/locales/ru/chat.json @@ -138,6 +138,30 @@ "cancelled": "API-запрос отменен", "streamingFailed": "Ошибка потокового API-запроса" }, + "retryStatus": { + "rateLimit": { + "title": "Лимит запросов установлен на {{rateLimitSeconds}} секунд", + "waiting": "Ожидание {{seconds}} секунд", + "proceeding": "Продолжаем...", + "cancelled": "Запрос отменен" + }, + "backoff": { + "title": "Запрос не удался", + "waiting": "Попытка через {{seconds}} секунд", + "waitingWithAttempt": "Попытка через {{seconds}} секунд (попытка №{{attempt}})", + "waitingWithAttemptMax": "Попытка через {{seconds}} секунд (попытка №{{attempt}}/{{maxAttempts}})", + "retrying": "Повторяем...", + "cancelled": "Повтор отменен" + } + }, + "rateLimitRetry": { + "title": "Превышен лимит запросов — пожалуйста, подождите.", + "waiting": "Повторная попытка через {{seconds}}с", + "waitingWithAttempt": "Повторная попытка через {{seconds}}с (попытка {{attempt}})", + "waitingWithAttemptMax": "Повторная попытка через {{seconds}}с (попытка {{attempt}}/{{maxAttempts}})", + "retrying": "Повторная попытка сейчас…", + "cancelled": "Повторная попытка отменена" + }, "checkpoint": { "regular": "Точка сохранения", "initializingWarning": "Точка сохранения еще инициализируется... Если это занимает слишком много времени, вы можете отключить точки сохранения в настройках и перезапустить задачу.", diff --git a/webview-ui/src/i18n/locales/tr/chat.json b/webview-ui/src/i18n/locales/tr/chat.json index a62e63a24d..5ccabd4de9 100644 --- a/webview-ui/src/i18n/locales/tr/chat.json +++ b/webview-ui/src/i18n/locales/tr/chat.json @@ -162,6 +162,30 @@ }, "current": "Mevcut" }, + "retryStatus": { + "rateLimit": { + "title": "Hız limiti {{rateLimitSeconds}} saniyeye ayarlandı", + "waiting": "{{seconds}} saniye bekleniyor", + "proceeding": "Şimdi devam ediliyor...", + "cancelled": "İstek iptal edildi" + }, + "backoff": { + "title": "İstek başarısız oldu", + "waiting": "{{seconds}} saniye içinde deneniyor", + "waitingWithAttempt": "{{seconds}} saniye içinde deneniyor (deneme #{{attempt}})", + "waitingWithAttemptMax": "{{seconds}} saniye içinde deneniyor (deneme #{{attempt}}/{{maxAttempts}})", + "retrying": "Şimdi deneniyor...", + "cancelled": "Yeniden deneme iptal edildi" + } + }, + "rateLimitRetry": { + "title": "Hız limiti tetiklendi — lütfen bekleyin.", + "waiting": "{{seconds}}s içinde tekrar deniyor", + "waitingWithAttempt": "{{seconds}}s içinde tekrar deniyor (deneme {{attempt}})", + "waitingWithAttemptMax": "{{seconds}}s içinde tekrar deniyor (deneme {{attempt}}/{{maxAttempts}})", + "retrying": "Şimdi tekrar deniyor…", + "cancelled": "Tekrar deneme iptal edildi" + }, "instructions": { "wantsToFetch": "Roo mevcut göreve yardımcı olmak için ayrıntılı talimatlar almak istiyor" }, diff --git a/webview-ui/src/i18n/locales/vi/chat.json b/webview-ui/src/i18n/locales/vi/chat.json index e418a6f1ca..72e69d59e8 100644 --- a/webview-ui/src/i18n/locales/vi/chat.json +++ b/webview-ui/src/i18n/locales/vi/chat.json @@ -143,6 +143,30 @@ "cancelled": "Yêu cầu API đã hủy", "streamingFailed": "Streaming API thất bại" }, + "retryStatus": { + "rateLimit": { + "title": "Giới hạn tốc độ được đặt thành {{rateLimitSeconds}} giây", + "waiting": "Đang chờ {{seconds}} giây", + "proceeding": "Đang tiến hành...", + "cancelled": "Yêu cầu đã bị hủy" + }, + "backoff": { + "title": "Yêu cầu thất bại", + "waiting": "Đang thử lại sau {{seconds}} giây", + "waitingWithAttempt": "Đang thử lại sau {{seconds}} giây (lần thử #{{attempt}})", + "waitingWithAttemptMax": "Đang thử lại sau {{seconds}} giây (lần thử #{{attempt}}/{{maxAttempts}})", + "retrying": "Đang thử ngay bây giờ...", + "cancelled": "Đã hủy thử lại" + } + }, + "rateLimitRetry": { + "title": "Đã đạt giới hạn tốc độ — vui lòng đợi.", + "waiting": "Đang thử lại sau {{seconds}} giây", + "waitingWithAttempt": "Đang thử lại sau {{seconds}} giây (thử lại lần {{attempt}})", + "waitingWithAttemptMax": "Đang thử lại sau {{seconds}} giây (thử lại lần {{attempt}}/{{maxAttempts}})", + "retrying": "Đang thử lại ngay…", + "cancelled": "Đã hủy thử lại" + }, "checkpoint": { "regular": "Điểm kiểm tra", "initializingWarning": "Đang khởi tạo điểm kiểm tra... Nếu quá trình này mất quá nhiều thời gian, bạn có thể vô hiệu hóa điểm kiểm tra trong cài đặt và khởi động lại tác vụ của bạn.", diff --git a/webview-ui/src/i18n/locales/zh-CN/chat.json b/webview-ui/src/i18n/locales/zh-CN/chat.json index 0e48504a27..f5ab5476f7 100644 --- a/webview-ui/src/i18n/locales/zh-CN/chat.json +++ b/webview-ui/src/i18n/locales/zh-CN/chat.json @@ -143,6 +143,30 @@ "cancelled": "API请求已取消", "streamingFailed": "API流式传输失败" }, + "retryStatus": { + "rateLimit": { + "title": "速率限制设置为 {{rateLimitSeconds}} 秒", + "waiting": "正在等待 {{seconds}} 秒", + "proceeding": "正在继续...", + "cancelled": "请求已取消" + }, + "backoff": { + "title": "请求失败", + "waiting": "将在 {{seconds}} 秒后重试", + "waitingWithAttempt": "将在 {{seconds}} 秒后重试 (第 #{{attempt}} 次)", + "waitingWithAttemptMax": "将在 {{seconds}} 秒后重试 (第 #{{attempt}}/{{maxAttempts}} 次)", + "retrying": "正在重试...", + "cancelled": "重试已取消" + } + }, + "rateLimitRetry": { + "title": "API 请求频率限制已触发 — 请稍候。", + "waiting": "正在 {{seconds}} 秒后重试", + "waitingWithAttempt": "正在 {{seconds}} 秒后重试 (第 {{attempt}} 次尝试)", + "waitingWithAttemptMax": "正在 {{seconds}} 秒后重试 (第 {{attempt}}/{{maxAttempts}} 次尝试)", + "retrying": "正在立即重试…", + "cancelled": "重试已取消" + }, "checkpoint": { "regular": "检查点", "initializingWarning": "正在初始化检查点...如果耗时过长,你可以在设置中禁用检查点并重新启动任务。", diff --git a/webview-ui/src/i18n/locales/zh-TW/chat.json b/webview-ui/src/i18n/locales/zh-TW/chat.json index 0988b42402..1ad48408ea 100644 --- a/webview-ui/src/i18n/locales/zh-TW/chat.json +++ b/webview-ui/src/i18n/locales/zh-TW/chat.json @@ -149,6 +149,30 @@ "cancelled": "API 請求已取消", "streamingFailed": "API 串流處理失敗" }, + "retryStatus": { + "rateLimit": { + "title": "速率限制設定為 {{rateLimitSeconds}} 秒", + "waiting": "正在等待 {{seconds}} 秒", + "proceeding": "正在繼續...", + "cancelled": "請求已取消" + }, + "backoff": { + "title": "請求失敗", + "waiting": "將在 {{seconds}} 秒後重試", + "waitingWithAttempt": "將在 {{seconds}} 秒後重試 (第 #{{attempt}} 次)", + "waitingWithAttemptMax": "將在 {{seconds}} 秒後重試 (第 #{{attempt}}/{{maxAttempts}} 次)", + "retrying": "正在重試...", + "cancelled": "重試已取消" + } + }, + "rateLimitRetry": { + "title": "已觸發速率限制 — 請稍候。", + "waiting": "正在 {{seconds}} 秒後重試", + "waitingWithAttempt": "正在 {{seconds}} 秒後重試 (第 {{attempt}} 次嘗試)", + "waitingWithAttemptMax": "正在 {{seconds}} 秒後重試 (第 {{attempt}}/{{maxAttempts}} 次嘗試)", + "retrying": "正在立即重試…", + "cancelled": "重試已取消" + }, "checkpoint": { "regular": "檢查點", "initializingWarning": "正在初始化檢查點... 如果耗時過長,您可以在設定中停用檢查點並重新啟動工作。",