diff --git a/packages/types/src/message.ts b/packages/types/src/message.ts index fa6c0e0e8f..f557aa6879 100644 --- a/packages/types/src/message.ts +++ b/packages/types/src/message.ts @@ -197,16 +197,26 @@ export const contextCondenseSchema = z.object({ export type ContextCondense = z.infer /** - * RateLimitRetryMetadata + * RetryStatusMetadata */ -export const rateLimitRetrySchema = z.object({ - type: z.literal("rate_limit_retry"), +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 @@ -238,7 +248,8 @@ export const clineMessageSchema = z.object({ previous_response_id: z.string().optional(), }) .optional(), - rateLimitRetry: rateLimitRetrySchema.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 01ea935d82..496ca038a3 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -123,6 +123,19 @@ 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" @@ -1110,7 +1123,9 @@ 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 isRateLimitUpdate = + type === "api_req_retry_delayed" && + (options.metadata?.retryStatus !== undefined || options.metadata?.rateLimitRetry !== undefined) const isUpdatingPreviousPartial = lastMessage && lastMessage.type === "say" && @@ -2677,54 +2692,70 @@ export class Task extends EventEmitter implements TaskLike { let rateLimitDelay = 0 - const sendRateLimitUpdate = async (payload: RateLimitRetryPayload, isPartial: boolean): Promise => { + const sendRetryStatusUpdate = async (payload: RetryStatusPayload, isPartial: boolean): Promise => { await this.say("api_req_retry_delayed", undefined, undefined, isPartial, undefined, undefined, { isNonInteractive: true, - metadata: { rateLimitRetry: payload }, + metadata: { + retryStatus: payload, + rateLimitRetry: + payload.cause === "rate_limit" ? { ...payload, type: "rate_limit_retry" as const } : undefined, + }, }) } + // Legacy function for backward compatibility + const sendRateLimitUpdate = async (payload: RateLimitRetryPayload, isPartial: boolean): Promise => { + const retryPayload: RetryStatusPayload = { ...payload, type: "retry_status", cause: "rate_limit" } + await sendRetryStatusUpdate(retryPayload, isPartial) + } + const runRateLimitCountdown = async ({ seconds, origin, attempt, maxAttempts, detail, + rateLimitSeconds, }: { seconds: number - origin: RateLimitRetryPayload["origin"] + 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 sendRateLimitUpdate( + await sendRetryStatusUpdate( { - type: "rate_limit_retry", + type: "retry_status", status: "cancelled", remainingSeconds: 0, attempt, maxAttempts, origin, detail, + cause: "rate_limit", + rateLimitSeconds, }, false, ) return false } - await sendRateLimitUpdate( + await sendRetryStatusUpdate( { - type: "rate_limit_retry", + type: "retry_status", status: "retrying", remainingSeconds: 0, attempt, maxAttempts, origin, detail, + cause: "rate_limit", + rateLimitSeconds, }, false, ) @@ -2733,30 +2764,34 @@ export class Task extends EventEmitter implements TaskLike { for (let i = normalizedSeconds; i > 0; i--) { if (this.abort) { - await sendRateLimitUpdate( + await sendRetryStatusUpdate( { - type: "rate_limit_retry", + type: "retry_status", status: "cancelled", remainingSeconds: i, attempt, maxAttempts, origin, detail, + cause: "rate_limit", + rateLimitSeconds, }, false, ) return false } - await sendRateLimitUpdate( + await sendRetryStatusUpdate( { - type: "rate_limit_retry", + type: "retry_status", status: "waiting", remainingSeconds: i, attempt, maxAttempts, origin, detail, + cause: "rate_limit", + rateLimitSeconds, }, true, ) @@ -2765,30 +2800,34 @@ export class Task extends EventEmitter implements TaskLike { } if (this.abort) { - await sendRateLimitUpdate( + await sendRetryStatusUpdate( { - type: "rate_limit_retry", + type: "retry_status", status: "cancelled", remainingSeconds: 0, attempt, maxAttempts, origin, detail, + cause: "rate_limit", + rateLimitSeconds, }, false, ) return false } - await sendRateLimitUpdate( + await sendRetryStatusUpdate( { - type: "rate_limit_retry", + type: "retry_status", status: "retrying", remainingSeconds: 0, attempt, maxAttempts, origin, detail, + cause: "rate_limit", + rateLimitSeconds, }, false, ) @@ -2807,10 +2846,12 @@ 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) { + const rateLimit = apiConfiguration?.rateLimitSeconds || 0 const countdownCompleted = await runRateLimitCountdown({ seconds: rateLimitDelay, origin: "pre_request", attempt: 1, + rateLimitSeconds: rateLimit, }) if (!countdownCompleted) { @@ -3086,40 +3127,57 @@ export class Task extends EventEmitter implements TaskLike { 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 => { + // 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: { rateLimitRetry: payload }, + 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 sendRateLimitUpdate( + await sendRetryStatusUpdate( { - type: "rate_limit_retry", + 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 sendRateLimitUpdate( + await sendRetryStatusUpdate( { - type: "rate_limit_retry", + type: "retry_status", status: "waiting", remainingSeconds: i, attempt: retryAttempt + 1, origin: "retry_attempt", detail: sanitizedDetail, + cause, + rateLimitSeconds, }, true, ) @@ -3128,28 +3186,32 @@ export class Task extends EventEmitter implements TaskLike { // Final check before retrying if (this.abort) { - await sendRateLimitUpdate( + await sendRetryStatusUpdate( { - type: "rate_limit_retry", + 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 sendRateLimitUpdate( + await sendRetryStatusUpdate( { - type: "rate_limit_retry", + type: "retry_status", status: "retrying", remainingSeconds: 0, attempt: retryAttempt + 1, origin: "retry_attempt", detail: sanitizedDetail, + cause, + rateLimitSeconds, }, false, ) diff --git a/webview-ui/src/components/chat/ChatRow.tsx b/webview-ui/src/components/chat/ChatRow.tsx index ebf8f88d30..bb70d6851e 100644 --- a/webview-ui/src/components/chat/ChatRow.tsx +++ b/webview-ui/src/components/chat/ChatRow.tsx @@ -44,7 +44,9 @@ 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 { @@ -1178,7 +1180,13 @@ export const ChatRowContent = ({ // Prevent multiple blocks returning, we only need a single block // that's constantly updated if (!isLast) return null - return + + // 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 b90e139893..898d3e1c7f 100644 --- a/webview-ui/src/components/chat/ChatView.tsx +++ b/webview-ui/src/components/chat/ChatView.tsx @@ -428,7 +428,9 @@ const ChatViewComponent: React.ForwardRefRenderFunction { + 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) { + return "" + } + + 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: just "Waiting 22s" (no attempt number) + 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 index 9fc2477ada..6a12f384f2 100644 --- a/webview-ui/src/components/chat/__tests__/RateLimitRetryRow.spec.tsx +++ b/webview-ui/src/components/chat/__tests__/RateLimitRetryRow.spec.tsx @@ -27,6 +27,7 @@ describe("RateLimitRetryRow", () => { attempt: 2, maxAttempts: 5, origin: "retry_attempt", + cause: "rate_limit", } render() @@ -40,6 +41,7 @@ describe("RateLimitRetryRow", () => { type: "rate_limit_retry", status: "retrying", origin: "retry_attempt", + cause: "rate_limit", } render() @@ -53,6 +55,7 @@ describe("RateLimitRetryRow", () => { type: "rate_limit_retry", status: "cancelled", origin: "retry_attempt", + cause: "rate_limit", } const { container } = render() @@ -82,6 +85,7 @@ describe("RateLimitRetryRow", () => { attempt: 1, maxAttempts: 3, origin: "retry_attempt", + cause: "rate_limit", } const { rerender } = render() @@ -94,6 +98,7 @@ describe("RateLimitRetryRow", () => { type: "rate_limit_retry", status: "retrying", origin: "retry_attempt", + cause: "rate_limit", } rerender() @@ -111,6 +116,7 @@ describe("RateLimitRetryRow", () => { attempt: 1, maxAttempts: 3, origin: "retry_attempt", + cause: "rate_limit", } const { rerender } = render() 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..236d8a2745 --- /dev/null +++ b/webview-ui/src/components/chat/__tests__/RetryStatusRow.spec.tsx @@ -0,0 +1,133 @@ +// 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 with attempt and max attempts 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.waitingWithAttemptMax")).toBeInTheDocument() + expect(screen.getByText("chat:retryStatus.rateLimit.description")).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 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.waitingWithAttemptMax")).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.waitingWithAttemptMax")).toBeInTheDocument() + }) +}) diff --git a/webview-ui/src/i18n/locales/ca/chat.json b/webview-ui/src/i18n/locales/ca/chat.json index 6fd0fc998a..6ede837c1b 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" }, + "retryStatus": { + "title": "Sol·licitud endarrerida — 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" + }, "rateLimitRetry": { "title": "Límit de peticions assolit — si us plau, espera.", "waiting": "Reintentant en {{seconds}}s", diff --git a/webview-ui/src/i18n/locales/de/chat.json b/webview-ui/src/i18n/locales/de/chat.json index ca08339a83..c93de46e00 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" }, + "retryStatus": { + "title": "Anfrage verzögert — bitte warten.", + "waiting": "Wiederholung in {{seconds}}s", + "waitingWithAttempt": "Wiederholung in {{seconds}}s (Versuch {{attempt}})", + "waitingWithAttemptMax": "Wiederholung in {{seconds}}s (Versuch {{attempt}}/{{maxAttempts}})", + "retrying": "Wird wiederholt…", + "cancelled": "Wiederholung abgebrochen" + }, "rateLimitRetry": { "title": "Ratenlimit ausgelöst — bitte warten.", "waiting": "Wiederholung in {{seconds}}s", diff --git a/webview-ui/src/i18n/locales/en/chat.json b/webview-ui/src/i18n/locales/en/chat.json index 181f40bbff..bb50cf0819 100644 --- a/webview-ui/src/i18n/locales/en/chat.json +++ b/webview-ui/src/i18n/locales/en/chat.json @@ -149,6 +149,22 @@ "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", diff --git a/webview-ui/src/i18n/locales/es/chat.json b/webview-ui/src/i18n/locales/es/chat.json index d9344fa05f..0336920c75 100644 --- a/webview-ui/src/i18n/locales/es/chat.json +++ b/webview-ui/src/i18n/locales/es/chat.json @@ -162,6 +162,24 @@ }, "current": "Actual" }, + "retryStatus": { + "rateLimit": { + "title": "Retraso por límite de velocidad — por favor, espera.", + "waiting": "Continuando en {{seconds}}s", + "waitingWithAttempt": "Continuando en {{seconds}}s (intento {{attempt}})", + "waitingWithAttemptMax": "Continuando en {{seconds}}s (intento {{attempt}}/{{maxAttempts}})", + "proceeding": "Continuando ahora…", + "cancelled": "Solicitud cancelada" + }, + "backoff": { + "title": "Solicitud retrasada — reintentando.", + "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" + } + }, "rateLimitRetry": { "title": "Límite de tasa activado — por favor, espera.", "waiting": "Reintentando en {{seconds}}s", diff --git a/webview-ui/src/i18n/locales/fr/chat.json b/webview-ui/src/i18n/locales/fr/chat.json index 7a37b1fe0f..f3251e25c4 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" }, + "retryStatus": { + "title": "Requête retardée — veuillez patienter.", + "waiting": "Nouvelle tentative dans {{seconds}}s", + "waitingWithAttempt": "Nouvelle tentative dans {{seconds}}s (tentative {{attempt}})", + "waitingWithAttemptMax": "Nouvelle tentative dans {{seconds}}s (tentative {{attempt}}/{{maxAttempts}})", + "retrying": "Nouvelle tentative en cours…", + "cancelled": "Nouvelle tentative annulée" + }, "rateLimitRetry": { "title": "Limite de débit atteinte — veuillez patienter.", "waiting": "Nouvel essai dans {{seconds}}s", diff --git a/webview-ui/src/i18n/locales/hi/chat.json b/webview-ui/src/i18n/locales/hi/chat.json index 723d9b3987..b060d64fae 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 स्ट्रीमिंग विफल हुई" }, + "retryStatus": { + "title": "अनुरोध में देरी — कृपया प्रतीक्षा करें।", + "waiting": "{{seconds}}s में पुनः प्रयास", + "waitingWithAttempt": "{{seconds}}s में पुनः प्रयास (प्रयास {{attempt}})", + "waitingWithAttemptMax": "{{seconds}}s में पुनः प्रयास (प्रयास {{attempt}}/{{maxAttempts}})", + "retrying": "अभी पुनः प्रयास कर रहे हैं…", + "cancelled": "पुनः प्रयास रद्द" + }, "rateLimitRetry": { "title": "दर सीमा ट्रिगर हुई — कृपया प्रतीक्षा करें।", "waiting": "{{seconds}}s में पुनः प्रयास कर रहा है", diff --git a/webview-ui/src/i18n/locales/id/chat.json b/webview-ui/src/i18n/locales/id/chat.json index 6453910d56..50d8983d3d 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" }, + "retryStatus": { + "title": "Permintaan tertunda — mohon tunggu.", + "waiting": "Mencoba lagi dalam {{seconds}}s", + "waitingWithAttempt": "Mencoba lagi dalam {{seconds}}s (percobaan {{attempt}})", + "waitingWithAttemptMax": "Mencoba lagi dalam {{seconds}}s (percobaan {{attempt}}/{{maxAttempts}})", + "retrying": "Mencoba sekarang…", + "cancelled": "Percobaan ulang dibatalkan" + }, "rateLimitRetry": { "title": "Batas kecepatan tercapai — mohon tunggu.", "waiting": "Mencoba lagi dalam {{seconds}}dtk", diff --git a/webview-ui/src/i18n/locales/it/chat.json b/webview-ui/src/i18n/locales/it/chat.json index 6f1242ec18..99247bdfbb 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" }, + "retryStatus": { + "title": "Richiesta ritardata — attendi.", + "waiting": "Riprovando tra {{seconds}}s", + "waitingWithAttempt": "Riprovando tra {{seconds}}s (tentativo {{attempt}})", + "waitingWithAttemptMax": "Riprovando tra {{seconds}}s (tentativo {{attempt}}/{{maxAttempts}})", + "retrying": "Riprovando ora…", + "cancelled": "Tentativo cancellato" + }, "rateLimitRetry": { "title": "Limite di frequenza raggiunto — attendi.", "waiting": "Riprovo tra {{seconds}}s", diff --git a/webview-ui/src/i18n/locales/ja/chat.json b/webview-ui/src/i18n/locales/ja/chat.json index e93d5074ee..3bd9e79c40 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ストリーミング失敗" }, + "retryStatus": { + "title": "リクエストが遅延しています — しばらくお待ちください。", + "waiting": "{{seconds}}秒後に再試行", + "waitingWithAttempt": "{{seconds}}秒後に再試行({{attempt}}回目の試行)", + "waitingWithAttemptMax": "{{seconds}}秒後に再試行({{attempt}}/{{maxAttempts}}回目の試行)", + "retrying": "再試行中…", + "cancelled": "再試行がキャンセルされました" + }, "rateLimitRetry": { "title": "レート制限がトリガーされました — しばらくお待ちください。", "waiting": "{{seconds}}秒後に再試行", diff --git a/webview-ui/src/i18n/locales/ko/chat.json b/webview-ui/src/i18n/locales/ko/chat.json index 5207a5da5c..c4f7ee8247 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 스트리밍 실패" }, + "retryStatus": { + "title": "요청이 지연되었습니다 — 잠시 기다려주세요.", + "waiting": "{{seconds}}초 후 재시도", + "waitingWithAttempt": "{{seconds}}초 후 재시도 (시도 {{attempt}})", + "waitingWithAttemptMax": "{{seconds}}초 후 재시도 (시도 {{attempt}}/{{maxAttempts}})", + "retrying": "재시도 중…", + "cancelled": "재시도 취소됨" + }, "rateLimitRetry": { "title": "속도 제한이 트리거되었습니다 — 잠시 기다려주세요.", "waiting": "{{seconds}}초 후에 다시 시도", diff --git a/webview-ui/src/i18n/locales/nl/chat.json b/webview-ui/src/i18n/locales/nl/chat.json index b8f474d2fd..d0c31cc677 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" }, + "retryStatus": { + "title": "Verzoek vertraagd — 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": "Herpoging geannuleerd" + }, "rateLimitRetry": { "title": "Snelheidslimiet bereikt — even geduld a.u.b.", "waiting": "Opnieuw proberen over {{seconds}}s", diff --git a/webview-ui/src/i18n/locales/pl/chat.json b/webview-ui/src/i18n/locales/pl/chat.json index 22b9fe2685..da12ee8e29 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ę" }, + "retryStatus": { + "title": "Żądanie opóźnione — proszę czekać.", + "waiting": "Ponowna próba za {{seconds}}s", + "waitingWithAttempt": "Ponowna próba za {{seconds}}s (próba {{attempt}})", + "waitingWithAttemptMax": "Ponowna próba za {{seconds}}s (próba {{attempt}}/{{maxAttempts}})", + "retrying": "Ponawiam teraz…", + "cancelled": "Ponowna próba anulowana" + }, "rateLimitRetry": { "title": "Osiągnięto limit szybkości — proszę czekać.", "waiting": "Ponawianie za {{seconds}}s", diff --git a/webview-ui/src/i18n/locales/pt-BR/chat.json b/webview-ui/src/i18n/locales/pt-BR/chat.json index 974170bbff..149aff381f 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" }, + "retryStatus": { + "title": "Solicitação atrasada — 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 agora…", + "cancelled": "Tentativa cancelada" + }, "rateLimitRetry": { "title": "Limite de taxa atingido — por favor, aguarde.", "waiting": "Tentando novamente em {{seconds}}s", diff --git a/webview-ui/src/i18n/locales/ru/chat.json b/webview-ui/src/i18n/locales/ru/chat.json index d19546a43d..2ffa4fcd63 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-запроса" }, + "retryStatus": { + "title": "Запрос задержан — пожалуйста, подождите.", + "waiting": "Повтор через {{seconds}}с", + "waitingWithAttempt": "Повтор через {{seconds}}с (попытка {{attempt}})", + "waitingWithAttemptMax": "Повтор через {{seconds}}с (попытка {{attempt}}/{{maxAttempts}})", + "retrying": "Повторяем…", + "cancelled": "Повтор отменен" + }, "rateLimitRetry": { "title": "Превышен лимит запросов — пожалуйста, подождите.", "waiting": "Повторная попытка через {{seconds}}с", diff --git a/webview-ui/src/i18n/locales/tr/chat.json b/webview-ui/src/i18n/locales/tr/chat.json index a3173531d1..ca5ff11467 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" }, + "retryStatus": { + "title": "İstek gecikti — lütfen bekleyin.", + "waiting": "{{seconds}}s içinde yeniden deneniyor", + "waitingWithAttempt": "{{seconds}}s içinde yeniden deneniyor (deneme {{attempt}})", + "waitingWithAttemptMax": "{{seconds}}s içinde yeniden deneniyor (deneme {{attempt}}/{{maxAttempts}})", + "retrying": "Şimdi yeniden deneniyor…", + "cancelled": "Yeniden deneme iptal edildi" + }, "rateLimitRetry": { "title": "Hız limiti tetiklendi — lütfen bekleyin.", "waiting": "{{seconds}}s içinde tekrar deniyor", diff --git a/webview-ui/src/i18n/locales/vi/chat.json b/webview-ui/src/i18n/locales/vi/chat.json index faf3048192..2763517d22 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" }, + "retryStatus": { + "title": "Yêu cầu bị trì hoãn — vui lòng đợi.", + "waiting": "Thử lại sau {{seconds}}s", + "waitingWithAttempt": "Thử lại sau {{seconds}}s (lần thử {{attempt}})", + "waitingWithAttemptMax": "Thử lại sau {{seconds}}s (lần thử {{attempt}}/{{maxAttempts}})", + "retrying": "Đang thử lại…", + "cancelled": "Thử lại đã bị hủy" + }, "rateLimitRetry": { "title": "Đã đạt giới hạn tốc độ — vui lòng đợi.", "waiting": "Đang thử lại sau {{seconds}}s", diff --git a/webview-ui/src/i18n/locales/zh-CN/chat.json b/webview-ui/src/i18n/locales/zh-CN/chat.json index f5a523d267..e89f8f78b0 100644 --- a/webview-ui/src/i18n/locales/zh-CN/chat.json +++ b/webview-ui/src/i18n/locales/zh-CN/chat.json @@ -143,6 +143,24 @@ "cancelled": "API请求已取消", "streamingFailed": "API流式传输失败" }, + "retryStatus": { + "rateLimit": { + "title": "速率限制延迟 — 请稍候。", + "waiting": "{{seconds}}秒后继续", + "waitingWithAttempt": "{{seconds}}秒后继续(第 {{attempt}} 次尝试)", + "waitingWithAttemptMax": "{{seconds}}秒后继续(第 {{attempt}}/{{maxAttempts}} 次尝试)", + "proceeding": "正在继续…", + "cancelled": "请求已取消" + }, + "backoff": { + "title": "请求已延迟 — 重试中。", + "waiting": "{{seconds}}秒后重试", + "waitingWithAttempt": "{{seconds}}秒后重试(第 {{attempt}} 次尝试)", + "waitingWithAttemptMax": "{{seconds}}秒后重试(第 {{attempt}}/{{maxAttempts}} 次尝试)", + "retrying": "正在重试…", + "cancelled": "重试已取消" + } + }, "rateLimitRetry": { "title": "API 请求频率限制已触发 — 请稍候。", "waiting": "正在 {{seconds}} 秒后重试", diff --git a/webview-ui/src/i18n/locales/zh-TW/chat.json b/webview-ui/src/i18n/locales/zh-TW/chat.json index 2776a06300..dbb6fa0717 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 串流處理失敗" }, + "retryStatus": { + "title": "請求已延遲 — 請稍候。", + "waiting": "{{seconds}}秒後重試", + "waitingWithAttempt": "{{seconds}}秒後重試(第 {{attempt}} 次嘗試)", + "waitingWithAttemptMax": "{{seconds}}秒後重試(第 {{attempt}}/{{maxAttempts}} 次嘗試)", + "retrying": "正在重試…", + "cancelled": "重試已取消" + }, "rateLimitRetry": { "title": "已觸發速率限制 — 請稍候。", "waiting": "正在 {{seconds}} 秒後重試",