feat(rate-limit): better feedback in chat

This commit is contained in:
elianiva 2025-10-04 17:42:13 +07:00 committed by daniel-lxs
parent 5c738de7ce
commit d452b0a50e
No known key found for this signature in database
GPG key ID: 21C74479048B3AA6
24 changed files with 618 additions and 27 deletions

View file

@ -196,6 +196,21 @@ export const contextCondenseSchema = z.object({
export type ContextCondense = z.infer<typeof contextCondenseSchema>
/**
* 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<typeof rateLimitRetrySchema>
/**
* ClineMessage
*/
@ -223,6 +238,7 @@ export const clineMessageSchema = z.object({
previous_response_id: z.string().optional(),
})
.optional(),
rateLimitRetry: rateLimitRetrySchema.optional(),
})
.optional(),
})

View file

@ -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<TaskEvents> 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<TaskEvents> 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<TaskEvents> implements TaskLike {
images,
checkpoint,
contextCondense,
metadata: options.metadata,
})
}
}
@ -2655,6 +2677,124 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
let rateLimitDelay = 0
const sendRateLimitUpdate = async (payload: RateLimitRetryPayload, isPartial: boolean): Promise<void> => {
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<boolean> => {
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<TaskEvents> 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<TaskEvents> 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<TaskEvents> 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<TaskEvents> 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<void> => {
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
}
}
}

View file

@ -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 = ({
<span style={{ color: successColor, fontWeight: "bold" }}>{t("chat:taskCompleted")}</span>,
]
case "api_req_retry_delayed":
return []
return [null, null]
case "api_req_started":
const getIconSpan = (iconName: string, color: string) => (
<div
@ -1172,6 +1174,11 @@ export const ChatRowContent = ({
</div>
</>
)
case "api_req_retry_delayed":
// Prevent multiple blocks returning, we only need a single block
// that's constantly updated
if (!isLast) return null
return <RateLimitRetryRow metadata={message.metadata?.rateLimitRetry} />
case "shell_integration_warning":
return <CommandExecutionError />
case "checkpoint_saved":

View file

@ -427,9 +427,14 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
// Don't want to reset since there could be a "say" after
// an "ask" while ask is waiting for response.
switch (lastMessage.say) {
case "api_req_retry_delayed":
setSendingDisabled(true)
case "api_req_retry_delayed": {
if (lastMessage.metadata?.rateLimitRetry?.status === "cancelled") {
setSendingDisabled(false)
} else {
setSendingDisabled(true)
}
break
}
case "api_req_started":
if (secondLastMessage?.ask === "command_output") {
setSendingDisabled(true)

View file

@ -0,0 +1,77 @@
import React, { useMemo } from "react"
import { useTranslation } from "react-i18next"
import type { RateLimitRetryMetadata } from "@roo-code/types"
import { ProgressIndicator } from "./ProgressIndicator"
export interface RateLimitRetryRowProps {
metadata?: RateLimitRetryMetadata
}
export const RateLimitRetryRow = ({ metadata }: RateLimitRetryRowProps) => {
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" ? (
<span className="codicon codicon-circle-slash text-vscode-descriptionForeground" aria-hidden="true"></span>
) : (
<ProgressIndicator />
)
return (
<div role="status" aria-live="polite" className="mt-1">
<div className="flex items-start gap-3 rounded-md border border-vscode-editorGroup-border/60 bg-vscode-editor-background px-4 py-3">
<div className="mt-0.5">{iconNode}</div>
<div className="flex flex-col gap-1 text-sm leading-5">
<span className="font-semibold text-vscode-foreground">{t("chat:rateLimitRetry.title")}</span>
{(description || detail) && (
<span className="text-vscode-descriptionForeground">
{description}
{detail ? (
<>
{" — "}
{detail}
</>
) : null}
</span>
)}
</div>
</div>
</div>
)
}

View file

@ -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 + persecond 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(<RateLimitRetryRow metadata={metadata} />)
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(<RateLimitRetryRow metadata={metadata} />)
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(<RateLimitRetryRow metadata={metadata} />)
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(<RateLimitRetryRow />)
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(<RateLimitRetryRow metadata={initialMetadata} />)
// 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(<RateLimitRetryRow metadata={updatedMetadata} />)
// 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(<RateLimitRetryRow metadata={metadata1} />)
// Initial countdown
expect(screen.getByText("chat:rateLimitRetry.waitingWithAttemptMax")).toBeInTheDocument()
// Update countdown
const metadata2: RateLimitRetryMetadata = {
...metadata1,
remainingSeconds: 5,
}
rerender(<RateLimitRetryRow metadata={metadata2} />)
// Should still show the same text key but with updated seconds
expect(screen.getByText("chat:rateLimitRetry.waitingWithAttemptMax")).toBeInTheDocument()
})
})

View file

@ -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 <settingsLink>configuració</settingsLink> i reiniciar la teva tasca.",

View file

@ -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 <settingsLink>Einstellungen</settingsLink> deaktivieren und deine Aufgabe neu starten.",

View file

@ -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 <settingsLink>settings</settingsLink> and restart your task.",

View file

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

View file

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

View file

@ -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": "चेकपॉइंट अभी भी आरंभ हो रहा है... अगर यह बहुत समय ले रहा है, तो आप <settingsLink>सेटिंग्स</settingsLink> में चेकपॉइंट को अक्षम कर सकते हैं और अपने कार्य को पुनः आरंभ कर सकते हैं।",

View file

@ -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 <settingsLink>pengaturan</settingsLink> dan restart tugas.",

View file

@ -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 <settingsLink>impostazioni</settingsLink> e riavviare l'attività.",

View file

@ -143,6 +143,14 @@
"cancelled": "APIリクエストキャンセル",
"streamingFailed": "APIストリーミング失敗"
},
"rateLimitRetry": {
"title": "レート制限がトリガーされました — しばらくお待ちください。",
"waiting": "{{seconds}}秒後に再試行",
"waitingWithAttempt": "{{seconds}}秒後に再試行({{attempt}}回目)",
"waitingWithAttemptMax": "{{seconds}}秒後に再試行({{attempt}}/{{maxAttempts}}回目)",
"retrying": "再試行中…",
"cancelled": "再試行キャンセル"
},
"checkpoint": {
"regular": "チェックポイント",
"initializingWarning": "チェックポイントの初期化中... 時間がかかりすぎる場合は、<settingsLink>設定</settingsLink>でチェックポイントを無効にしてタスクを再開できます。",

View file

@ -143,6 +143,14 @@
"cancelled": "API 요청 취소됨",
"streamingFailed": "API 스트리밍 실패"
},
"rateLimitRetry": {
"title": "속도 제한이 트리거되었습니다 — 잠시 기다려주세요.",
"waiting": "{{seconds}}초 후에 다시 시도",
"waitingWithAttempt": "{{seconds}}초 후에 다시 시도 ({{attempt}}번째 시도)",
"waitingWithAttemptMax": "{{seconds}}초 후에 다시 시도 ({{attempt}}/{{maxAttempts}}번째 시도)",
"retrying": "지금 다시 시도 중…",
"cancelled": "다시 시도 취소됨"
},
"checkpoint": {
"regular": "체크포인트",
"initializingWarning": "체크포인트 초기화 중... 시간이 너무 오래 걸리면 <settingsLink>설정</settingsLink>에서 체크포인트를 비활성화하고 작업을 다시 시작할 수 있습니다.",

View file

@ -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 <settingsLink>instellingen</settingsLink> en je taak opnieuw starten.",

View file

@ -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 <settingsLink>ustawieniach</settingsLink> i uruchomić zadanie ponownie.",

View file

@ -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 <settingsLink>configurações</settingsLink> e reiniciar sua tarefa.",

View file

@ -138,6 +138,14 @@
"cancelled": "API-запрос отменен",
"streamingFailed": "Ошибка потокового API-запроса"
},
"rateLimitRetry": {
"title": "Превышен лимит запросов — пожалуйста, подождите.",
"waiting": "Повторная попытка через {{seconds}}с",
"waitingWithAttempt": "Повторная попытка через {{seconds}}с (попытка {{attempt}})",
"waitingWithAttemptMax": "Повторная попытка через {{seconds}}с (попытка {{attempt}}/{{maxAttempts}})",
"retrying": "Повторная попытка сейчас…",
"cancelled": "Повторная попытка отменена"
},
"checkpoint": {
"regular": "Точка сохранения",
"initializingWarning": "Точка сохранения еще инициализируется... Если это занимает слишком много времени, вы можете отключить точки сохранения в <settingsLink>настройках</settingsLink> и перезапустить задачу.",

View file

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

View file

@ -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 <settingsLink>cài đặt</settingsLink> và khởi động lại tác vụ của bạn.",

View file

@ -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": "正在初始化检查点...如果耗时过长,你可以在<settingsLink>设置</settingsLink>中禁用检查点并重新启动任务。",

View file

@ -149,6 +149,14 @@
"cancelled": "API 請求已取消",
"streamingFailed": "API 串流處理失敗"
},
"rateLimitRetry": {
"title": "已觸發速率限制 — 請稍候。",
"waiting": "正在 {{seconds}} 秒後重試",
"waitingWithAttempt": "正在 {{seconds}} 秒後重試 (第 {{attempt}} 次嘗試)",
"waitingWithAttemptMax": "正在 {{seconds}} 秒後重試 (第 {{attempt}}/{{maxAttempts}} 次嘗試)",
"retrying": "正在立即重試…",
"cancelled": "重試已取消"
},
"checkpoint": {
"regular": "檢查點",
"initializingWarning": "正在初始化檢查點... 如果耗時過長,您可以在<settingsLink>設定</settingsLink>中停用檢查點並重新啟動工作。",