mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-07 08:26:51 +00:00
feat(retry-status): generalize rate-limit UI to generic retry/backoff with cause
- Replace rateLimitRetrySchema with retryStatusSchema including cause and rateLimitSeconds fields
- Add new RetryStatusRow component with differentiated messaging:
- Rate limit: 'Rate limit set to {{rateLimitSeconds}} seconds' (title) + 'Waiting {{seconds}} seconds' (subtitle)
- Retry: Error message (title) + 'Trying in {{seconds}} seconds (attempt #{{attempt}})' (subtitle)
- Update Task.say emitters to use retryStatus metadata with appropriate cause:
- 'rate_limit' for provider window waits and 429 errors (includes rateLimitSeconds)
- 'backoff' for exponential backoff retries
- Migrate i18n keys from rateLimitRetry to retryStatus with new nested structure in English
- Maintain backward compatibility with legacy rateLimitRetry metadata
- Update tests to cover new RetryStatusRow component and metadata structure
This commit is contained in:
parent
7b46d60901
commit
1a41005466
25 changed files with 523 additions and 33 deletions
|
|
@ -197,16 +197,26 @@ export const contextCondenseSchema = z.object({
|
|||
export type ContextCondense = z.infer<typeof contextCondenseSchema>
|
||||
|
||||
/**
|
||||
* 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<typeof retryStatusSchema>
|
||||
|
||||
// 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<typeof rateLimitRetrySchema>
|
||||
|
|
@ -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(),
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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<TaskEvents> 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<TaskEvents> implements TaskLike {
|
|||
|
||||
let rateLimitDelay = 0
|
||||
|
||||
const sendRateLimitUpdate = async (payload: RateLimitRetryPayload, isPartial: boolean): Promise<void> => {
|
||||
const sendRetryStatusUpdate = async (payload: RetryStatusPayload, isPartial: boolean): Promise<void> => {
|
||||
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<void> => {
|
||||
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<boolean> => {
|
||||
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<TaskEvents> 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<TaskEvents> 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<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) {
|
||||
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<TaskEvents> 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<void> => {
|
||||
// Helper to send retry status updates with structured metadata
|
||||
const sendRetryStatusUpdate = async (payload: RetryStatusPayload, isPartial: boolean): Promise<void> => {
|
||||
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<TaskEvents> 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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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 <RateLimitRetryRow metadata={message.metadata?.rateLimitRetry} />
|
||||
|
||||
// Use new RetryStatusRow if retryStatus metadata is available, fall back to legacy
|
||||
return message.metadata?.retryStatus ? (
|
||||
<RetryStatusRow metadata={message.metadata.retryStatus} />
|
||||
) : (
|
||||
<RateLimitRetryRow metadata={message.metadata?.rateLimitRetry} />
|
||||
)
|
||||
case "shell_integration_warning":
|
||||
return <CommandExecutionError />
|
||||
case "checkpoint_saved":
|
||||
|
|
|
|||
|
|
@ -428,7 +428,9 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|||
// an "ask" while ask is waiting for response.
|
||||
switch (lastMessage.say) {
|
||||
case "api_req_retry_delayed": {
|
||||
if (lastMessage.metadata?.rateLimitRetry?.status === "cancelled") {
|
||||
const retryStatus =
|
||||
lastMessage.metadata?.retryStatus || lastMessage.metadata?.rateLimitRetry
|
||||
if (retryStatus?.status === "cancelled") {
|
||||
setSendingDisabled(false)
|
||||
} else {
|
||||
setSendingDisabled(true)
|
||||
|
|
|
|||
96
webview-ui/src/components/chat/RetryStatusRow.tsx
Normal file
96
webview-ui/src/components/chat/RetryStatusRow.tsx
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
import React, { useMemo } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import type { RetryStatusMetadata } from "@roo-code/types"
|
||||
import { ProgressIndicator } from "./ProgressIndicator"
|
||||
|
||||
export interface RetryStatusRowProps {
|
||||
metadata?: RetryStatusMetadata
|
||||
}
|
||||
|
||||
export const RetryStatusRow = ({ metadata }: RetryStatusRowProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const title = useMemo(() => {
|
||||
if (!metadata) {
|
||||
return ""
|
||||
}
|
||||
|
||||
const isRateLimit = metadata.cause === "rate_limit"
|
||||
|
||||
if (isRateLimit) {
|
||||
// For rate limit, show "Rate limit set for Xs"
|
||||
return t("chat:retryStatus.rateLimit.title", {
|
||||
rateLimitSeconds: metadata.rateLimitSeconds || 30, // fallback to 30 if not provided
|
||||
})
|
||||
} else {
|
||||
// For backoff/retry, show the error message if available
|
||||
return metadata.detail || t("chat:retryStatus.backoff.title")
|
||||
}
|
||||
}, [metadata, t])
|
||||
|
||||
const subtitle = useMemo(() => {
|
||||
if (!metadata) {
|
||||
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" ? (
|
||||
<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">{title}</span>
|
||||
{subtitle && <span className="text-vscode-descriptionForeground">{subtitle}</span>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -27,6 +27,7 @@ describe("RateLimitRetryRow", () => {
|
|||
attempt: 2,
|
||||
maxAttempts: 5,
|
||||
origin: "retry_attempt",
|
||||
cause: "rate_limit",
|
||||
}
|
||||
|
||||
render(<RateLimitRetryRow metadata={metadata} />)
|
||||
|
|
@ -40,6 +41,7 @@ describe("RateLimitRetryRow", () => {
|
|||
type: "rate_limit_retry",
|
||||
status: "retrying",
|
||||
origin: "retry_attempt",
|
||||
cause: "rate_limit",
|
||||
}
|
||||
|
||||
render(<RateLimitRetryRow metadata={metadata} />)
|
||||
|
|
@ -53,6 +55,7 @@ describe("RateLimitRetryRow", () => {
|
|||
type: "rate_limit_retry",
|
||||
status: "cancelled",
|
||||
origin: "retry_attempt",
|
||||
cause: "rate_limit",
|
||||
}
|
||||
|
||||
const { container } = render(<RateLimitRetryRow metadata={metadata} />)
|
||||
|
|
@ -82,6 +85,7 @@ describe("RateLimitRetryRow", () => {
|
|||
attempt: 1,
|
||||
maxAttempts: 3,
|
||||
origin: "retry_attempt",
|
||||
cause: "rate_limit",
|
||||
}
|
||||
|
||||
const { rerender } = render(<RateLimitRetryRow metadata={initialMetadata} />)
|
||||
|
|
@ -94,6 +98,7 @@ describe("RateLimitRetryRow", () => {
|
|||
type: "rate_limit_retry",
|
||||
status: "retrying",
|
||||
origin: "retry_attempt",
|
||||
cause: "rate_limit",
|
||||
}
|
||||
|
||||
rerender(<RateLimitRetryRow metadata={updatedMetadata} />)
|
||||
|
|
@ -111,6 +116,7 @@ describe("RateLimitRetryRow", () => {
|
|||
attempt: 1,
|
||||
maxAttempts: 3,
|
||||
origin: "retry_attempt",
|
||||
cause: "rate_limit",
|
||||
}
|
||||
|
||||
const { rerender } = render(<RateLimitRetryRow metadata={metadata1} />)
|
||||
|
|
|
|||
133
webview-ui/src/components/chat/__tests__/RetryStatusRow.spec.tsx
Normal file
133
webview-ui/src/components/chat/__tests__/RetryStatusRow.spec.tsx
Normal file
|
|
@ -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(<RetryStatusRow metadata={metadata} />)
|
||||
|
||||
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(<RetryStatusRow metadata={metadata} />)
|
||||
|
||||
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(<RetryStatusRow metadata={metadata} />)
|
||||
|
||||
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(<RetryStatusRow />)
|
||||
|
||||
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(<RetryStatusRow metadata={initialMetadata} />)
|
||||
|
||||
// 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(<RetryStatusRow metadata={updatedMetadata} />)
|
||||
|
||||
// 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(<RetryStatusRow metadata={metadata1} />)
|
||||
|
||||
// Initial countdown
|
||||
expect(screen.getByText("chat:retryStatus.rateLimit.waitingWithAttemptMax")).toBeInTheDocument()
|
||||
|
||||
// Update countdown
|
||||
const metadata2: RetryStatusMetadata = {
|
||||
...metadata1,
|
||||
remainingSeconds: 5,
|
||||
}
|
||||
|
||||
rerender(<RetryStatusRow metadata={metadata2} />)
|
||||
|
||||
// Should still show the same text key but with updated seconds
|
||||
expect(screen.getByText("chat:retryStatus.rateLimit.waitingWithAttemptMax")).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
8
webview-ui/src/i18n/locales/ca/chat.json
generated
8
webview-ui/src/i18n/locales/ca/chat.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
8
webview-ui/src/i18n/locales/de/chat.json
generated
8
webview-ui/src/i18n/locales/de/chat.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
18
webview-ui/src/i18n/locales/es/chat.json
generated
18
webview-ui/src/i18n/locales/es/chat.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
8
webview-ui/src/i18n/locales/fr/chat.json
generated
8
webview-ui/src/i18n/locales/fr/chat.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
8
webview-ui/src/i18n/locales/hi/chat.json
generated
8
webview-ui/src/i18n/locales/hi/chat.json
generated
|
|
@ -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 में पुनः प्रयास कर रहा है",
|
||||
|
|
|
|||
8
webview-ui/src/i18n/locales/id/chat.json
generated
8
webview-ui/src/i18n/locales/id/chat.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
8
webview-ui/src/i18n/locales/it/chat.json
generated
8
webview-ui/src/i18n/locales/it/chat.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
8
webview-ui/src/i18n/locales/ja/chat.json
generated
8
webview-ui/src/i18n/locales/ja/chat.json
generated
|
|
@ -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}}秒後に再試行",
|
||||
|
|
|
|||
8
webview-ui/src/i18n/locales/ko/chat.json
generated
8
webview-ui/src/i18n/locales/ko/chat.json
generated
|
|
@ -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}}초 후에 다시 시도",
|
||||
|
|
|
|||
8
webview-ui/src/i18n/locales/nl/chat.json
generated
8
webview-ui/src/i18n/locales/nl/chat.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
8
webview-ui/src/i18n/locales/pl/chat.json
generated
8
webview-ui/src/i18n/locales/pl/chat.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
8
webview-ui/src/i18n/locales/pt-BR/chat.json
generated
8
webview-ui/src/i18n/locales/pt-BR/chat.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
8
webview-ui/src/i18n/locales/ru/chat.json
generated
8
webview-ui/src/i18n/locales/ru/chat.json
generated
|
|
@ -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}}с",
|
||||
|
|
|
|||
8
webview-ui/src/i18n/locales/tr/chat.json
generated
8
webview-ui/src/i18n/locales/tr/chat.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
8
webview-ui/src/i18n/locales/vi/chat.json
generated
8
webview-ui/src/i18n/locales/vi/chat.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
18
webview-ui/src/i18n/locales/zh-CN/chat.json
generated
18
webview-ui/src/i18n/locales/zh-CN/chat.json
generated
|
|
@ -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}} 秒后重试",
|
||||
|
|
|
|||
8
webview-ui/src/i18n/locales/zh-TW/chat.json
generated
8
webview-ui/src/i18n/locales/zh-TW/chat.json
generated
|
|
@ -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}} 秒後重試",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue