From d452b0a50e7fc8af557e45c5dbbf50bed415a2be Mon Sep 17 00:00:00 2001 From: elianiva Date: Sat, 4 Oct 2025 17:42:13 +0700 Subject: [PATCH] feat(rate-limit): better feedback in chat --- packages/types/src/message.ts | 16 ++ src/core/task/Task.ts | 258 ++++++++++++++++-- webview-ui/src/components/chat/ChatRow.tsx | 9 +- webview-ui/src/components/chat/ChatView.tsx | 9 +- .../src/components/chat/RateLimitRetryRow.tsx | 77 ++++++ .../chat/__tests__/RateLimitRetryRow.spec.tsx | 132 +++++++++ webview-ui/src/i18n/locales/ca/chat.json | 8 + webview-ui/src/i18n/locales/de/chat.json | 8 + webview-ui/src/i18n/locales/en/chat.json | 8 + webview-ui/src/i18n/locales/es/chat.json | 8 + webview-ui/src/i18n/locales/fr/chat.json | 8 + webview-ui/src/i18n/locales/hi/chat.json | 8 + webview-ui/src/i18n/locales/id/chat.json | 8 + webview-ui/src/i18n/locales/it/chat.json | 8 + webview-ui/src/i18n/locales/ja/chat.json | 8 + webview-ui/src/i18n/locales/ko/chat.json | 8 + webview-ui/src/i18n/locales/nl/chat.json | 8 + webview-ui/src/i18n/locales/pl/chat.json | 8 + webview-ui/src/i18n/locales/pt-BR/chat.json | 8 + webview-ui/src/i18n/locales/ru/chat.json | 8 + webview-ui/src/i18n/locales/tr/chat.json | 8 + webview-ui/src/i18n/locales/vi/chat.json | 8 + webview-ui/src/i18n/locales/zh-CN/chat.json | 8 + webview-ui/src/i18n/locales/zh-TW/chat.json | 8 + 24 files changed, 618 insertions(+), 27 deletions(-) create mode 100644 webview-ui/src/components/chat/RateLimitRetryRow.tsx create mode 100644 webview-ui/src/components/chat/__tests__/RateLimitRetryRow.spec.tsx diff --git a/packages/types/src/message.ts b/packages/types/src/message.ts index b02078fc0d..fa6c0e0e8f 100644 --- a/packages/types/src/message.ts +++ b/packages/types/src/message.ts @@ -196,6 +196,21 @@ export const contextCondenseSchema = z.object({ export type ContextCondense = z.infer +/** + * RateLimitRetryMetadata + */ +export const rateLimitRetrySchema = z.object({ + type: z.literal("rate_limit_retry"), + 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(), +}) + +export type RateLimitRetryMetadata = z.infer + /** * ClineMessage */ @@ -223,6 +238,7 @@ export const clineMessageSchema = z.object({ previous_response_id: z.string().optional(), }) .optional(), + rateLimitRetry: rateLimitRetrySchema.optional(), }) .optional(), }) diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index 2fcc92426a..cb7576befd 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -123,6 +123,16 @@ 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 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 +1110,12 @@ 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?.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 +1124,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 +1218,7 @@ export class Task extends EventEmitter implements TaskLike { images, checkpoint, contextCondense, + metadata: options.metadata, }) } } @@ -2655,6 +2677,124 @@ export class Task extends EventEmitter implements TaskLike { let rateLimitDelay = 0 + const sendRateLimitUpdate = async (payload: RateLimitRetryPayload, isPartial: boolean): Promise => { + await this.say("api_req_retry_delayed", undefined, undefined, isPartial, undefined, undefined, { + metadata: { rateLimitRetry: payload }, + }) + } + + const runRateLimitCountdown = async ({ + seconds, + origin, + attempt, + maxAttempts, + detail, + }: { + seconds: number + origin: RateLimitRetryPayload["origin"] + attempt?: number + maxAttempts?: number + detail?: string + }): Promise => { + const normalizedSeconds = Math.max(0, Math.ceil(seconds)) + + if (normalizedSeconds <= 0) { + if (this.abort) { + await sendRateLimitUpdate( + { + type: "rate_limit_retry", + status: "cancelled", + remainingSeconds: 0, + attempt, + maxAttempts, + origin, + detail, + }, + false, + ) + return false + } + + await sendRateLimitUpdate( + { + type: "rate_limit_retry", + status: "retrying", + remainingSeconds: 0, + attempt, + maxAttempts, + origin, + detail, + }, + false, + ) + return true + } + + for (let i = normalizedSeconds; i > 0; i--) { + if (this.abort) { + await sendRateLimitUpdate( + { + type: "rate_limit_retry", + status: "cancelled", + remainingSeconds: i, + attempt, + maxAttempts, + origin, + detail, + }, + false, + ) + return false + } + + await sendRateLimitUpdate( + { + type: "rate_limit_retry", + status: "waiting", + remainingSeconds: i, + attempt, + maxAttempts, + origin, + detail, + }, + true, + ) + + await delay(1000) + } + + if (this.abort) { + await sendRateLimitUpdate( + { + type: "rate_limit_retry", + status: "cancelled", + remainingSeconds: 0, + attempt, + maxAttempts, + origin, + detail, + }, + false, + ) + return false + } + + await sendRateLimitUpdate( + { + type: "rate_limit_retry", + status: "retrying", + remainingSeconds: 0, + attempt, + maxAttempts, + origin, + detail, + }, + 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 +2806,16 @@ 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 countdownCompleted = await runRateLimitCountdown({ + seconds: rateLimitDelay, + origin: "pre_request", + attempt: 1, + }) + + if (!countdownCompleted) { + throw new Error( + `[RooCode#attemptApiRequest] task ${this.taskId}.${this.instanceId} aborted during pre-request rate limit wait`, + ) } } @@ -2822,7 +2967,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) @@ -2843,7 +2988,7 @@ export class Task extends EventEmitter implements TaskLike { `[Task#attemptApiRequest] task ${this.taskId}.${this.instanceId} aborted during retry`, ) } - + // Delegate generator output from the recursive call with // incremented retry count. yield* this.attemptApiRequest(retryAttempt + 1) @@ -2913,43 +3058,108 @@ 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 rate limit updates with structured metadata + const sendRateLimitUpdate = async ( + payload: RateLimitRetryPayload, + isPartial: boolean, + ): Promise => { + await this.say("api_req_retry_delayed", undefined, undefined, isPartial, undefined, undefined, { + metadata: { rateLimitRetry: payload }, + }) + } + + // 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 sendRateLimitUpdate( + { + type: "rate_limit_retry", + status: "cancelled", + remainingSeconds: i, + attempt: retryAttempt + 1, + origin: "retry_attempt", + detail: sanitizedDetail, + }, + 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 sendRateLimitUpdate( + { + type: "rate_limit_retry", + status: "waiting", + remainingSeconds: i, + attempt: retryAttempt + 1, + origin: "retry_attempt", + detail: sanitizedDetail, + }, 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 sendRateLimitUpdate( + { + type: "rate_limit_retry", + status: "cancelled", + remainingSeconds: 0, + attempt: retryAttempt + 1, + origin: "retry_attempt", + detail: sanitizedDetail, + }, + false, + ) + throw new Error(`[Task#${this.taskId}] Aborted during retry countdown`) + } + + await sendRateLimitUpdate( + { + type: "rate_limit_retry", + status: "retrying", + remainingSeconds: 0, + attempt: retryAttempt + 1, + origin: "retry_attempt", + detail: sanitizedDetail, + }, 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..ebf8f88d30 100644 --- a/webview-ui/src/components/chat/ChatRow.tsx +++ b/webview-ui/src/components/chat/ChatRow.tsx @@ -43,6 +43,8 @@ import CodebaseSearchResultsDisplay from "./CodebaseSearchResultsDisplay" import { appendImages } from "@src/utils/imageUtils" import { McpExecution } from "./McpExecution" import { ChatTextArea } from "./ChatTextArea" +import { RateLimitRetryRow } from "./RateLimitRetryRow" +export { RateLimitRetryRow } from "./RateLimitRetryRow" import { MAX_IMAGES_PER_MESSAGE } from "./ChatView" import { useSelectedModel } from "../ui/hooks/useSelectedModel" import { @@ -263,7 +265,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 + return 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..b90e139893 100644 --- a/webview-ui/src/components/chat/ChatView.tsx +++ b/webview-ui/src/components/chat/ChatView.tsx @@ -427,9 +427,14 @@ 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/__tests__/RateLimitRetryRow.spec.tsx b/webview-ui/src/components/chat/__tests__/RateLimitRetryRow.spec.tsx new file mode 100644 index 0000000000..9fc2477ada --- /dev/null +++ b/webview-ui/src/components/chat/__tests__/RateLimitRetryRow.spec.tsx @@ -0,0 +1,132 @@ +// 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", + } + + 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", + } + + 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", + } + + 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", + } + + 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", + } + + 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", + } + + 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/i18n/locales/ca/chat.json b/webview-ui/src/i18n/locales/ca/chat.json index 425cd9b6a0..6fd0fc998a 100644 --- a/webview-ui/src/i18n/locales/ca/chat.json +++ b/webview-ui/src/i18n/locales/ca/chat.json @@ -143,6 +143,14 @@ "cancelled": "Sol·licitud API cancel·lada", "streamingFailed": "Transmissió API ha fallat" }, + "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..ca08339a83 100644 --- a/webview-ui/src/i18n/locales/de/chat.json +++ b/webview-ui/src/i18n/locales/de/chat.json @@ -143,6 +143,14 @@ "cancelled": "API-Anfrage abgebrochen", "streamingFailed": "API-Streaming fehlgeschlagen" }, + "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..181f40bbff 100644 --- a/webview-ui/src/i18n/locales/en/chat.json +++ b/webview-ui/src/i18n/locales/en/chat.json @@ -149,6 +149,14 @@ "cancelled": "API Request Cancelled", "streamingFailed": "API Streaming Failed" }, + "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..d9344fa05f 100644 --- a/webview-ui/src/i18n/locales/es/chat.json +++ b/webview-ui/src/i18n/locales/es/chat.json @@ -162,6 +162,14 @@ }, "current": "Actual" }, + "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..7a37b1fe0f 100644 --- a/webview-ui/src/i18n/locales/fr/chat.json +++ b/webview-ui/src/i18n/locales/fr/chat.json @@ -162,6 +162,14 @@ }, "current": "Actuel" }, + "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..723d9b3987 100644 --- a/webview-ui/src/i18n/locales/hi/chat.json +++ b/webview-ui/src/i18n/locales/hi/chat.json @@ -143,6 +143,14 @@ "cancelled": "API अनुरोध रद्द किया गया", "streamingFailed": "API स्ट्रीमिंग विफल हुई" }, + "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..1ab123f9d1 100644 --- a/webview-ui/src/i18n/locales/id/chat.json +++ b/webview-ui/src/i18n/locales/id/chat.json @@ -152,6 +152,14 @@ "cancelled": "Permintaan API Dibatalkan", "streamingFailed": "Streaming API Gagal" }, + "rateLimitRetry": { + "title": "Batas kecepatan tercapai — mohon tunggu.", + "waiting": "Mencoba lagi dalam {{seconds}}d", + "waitingWithAttempt": "Mencoba lagi dalam {{seconds}}d (percobaan {{attempt}})", + "waitingWithAttemptMax": "Mencoba lagi dalam {{seconds}}d (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..6f1242ec18 100644 --- a/webview-ui/src/i18n/locales/it/chat.json +++ b/webview-ui/src/i18n/locales/it/chat.json @@ -146,6 +146,14 @@ "cancelled": "Richiesta API annullata", "streamingFailed": "Streaming API fallito" }, + "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..e93d5074ee 100644 --- a/webview-ui/src/i18n/locales/ja/chat.json +++ b/webview-ui/src/i18n/locales/ja/chat.json @@ -143,6 +143,14 @@ "cancelled": "APIリクエストキャンセル", "streamingFailed": "APIストリーミング失敗" }, + "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..5207a5da5c 100644 --- a/webview-ui/src/i18n/locales/ko/chat.json +++ b/webview-ui/src/i18n/locales/ko/chat.json @@ -143,6 +143,14 @@ "cancelled": "API 요청 취소됨", "streamingFailed": "API 스트리밍 실패" }, + "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..b8f474d2fd 100644 --- a/webview-ui/src/i18n/locales/nl/chat.json +++ b/webview-ui/src/i18n/locales/nl/chat.json @@ -138,6 +138,14 @@ "cancelled": "API-verzoek geannuleerd", "streamingFailed": "API-streaming mislukt" }, + "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..22b9fe2685 100644 --- a/webview-ui/src/i18n/locales/pl/chat.json +++ b/webview-ui/src/i18n/locales/pl/chat.json @@ -143,6 +143,14 @@ "cancelled": "Zapytanie API anulowane", "streamingFailed": "Strumieniowanie API nie powiodło się" }, + "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..974170bbff 100644 --- a/webview-ui/src/i18n/locales/pt-BR/chat.json +++ b/webview-ui/src/i18n/locales/pt-BR/chat.json @@ -143,6 +143,14 @@ "cancelled": "Requisição API cancelada", "streamingFailed": "Streaming API falhou" }, + "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..d19546a43d 100644 --- a/webview-ui/src/i18n/locales/ru/chat.json +++ b/webview-ui/src/i18n/locales/ru/chat.json @@ -138,6 +138,14 @@ "cancelled": "API-запрос отменен", "streamingFailed": "Ошибка потокового API-запроса" }, + "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..a3173531d1 100644 --- a/webview-ui/src/i18n/locales/tr/chat.json +++ b/webview-ui/src/i18n/locales/tr/chat.json @@ -162,6 +162,14 @@ }, "current": "Mevcut" }, + "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..faf3048192 100644 --- a/webview-ui/src/i18n/locales/vi/chat.json +++ b/webview-ui/src/i18n/locales/vi/chat.json @@ -143,6 +143,14 @@ "cancelled": "Yêu cầu API đã hủy", "streamingFailed": "Streaming API thất bại" }, + "rateLimitRetry": { + "title": "Đã đạt giới hạn tốc độ — vui lòng đợi.", + "waiting": "Đang thử lại sau {{seconds}}s", + "waitingWithAttempt": "Đang thử lại sau {{seconds}}s (thử lại lần {{attempt}})", + "waitingWithAttemptMax": "Đang thử lại sau {{seconds}}s (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..f5a523d267 100644 --- a/webview-ui/src/i18n/locales/zh-CN/chat.json +++ b/webview-ui/src/i18n/locales/zh-CN/chat.json @@ -143,6 +143,14 @@ "cancelled": "API请求已取消", "streamingFailed": "API流式传输失败" }, + "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..2776a06300 100644 --- a/webview-ui/src/i18n/locales/zh-TW/chat.json +++ b/webview-ui/src/i18n/locales/zh-TW/chat.json @@ -149,6 +149,14 @@ "cancelled": "API 請求已取消", "streamingFailed": "API 串流處理失敗" }, + "rateLimitRetry": { + "title": "已觸發速率限制 — 請稍候。", + "waiting": "正在 {{seconds}} 秒後重試", + "waitingWithAttempt": "正在 {{seconds}} 秒後重試 (第 {{attempt}} 次嘗試)", + "waitingWithAttemptMax": "正在 {{seconds}} 秒後重試 (第 {{attempt}}/{{maxAttempts}} 次嘗試)", + "retrying": "正在立即重試…", + "cancelled": "重試已取消" + }, "checkpoint": { "regular": "檢查點", "initializingWarning": "正在初始化檢查點... 如果耗時過長,您可以在設定中停用檢查點並重新啟動工作。",