mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
feat(rate-limit): better feedback in chat
This commit is contained in:
parent
5c738de7ce
commit
d452b0a50e
24 changed files with 618 additions and 27 deletions
|
|
@ -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(),
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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":
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
77
webview-ui/src/components/chat/RateLimitRetryRow.tsx
Normal file
77
webview-ui/src/components/chat/RateLimitRetryRow.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,132 @@
|
|||
// npx vitest run src/components/chat/__tests__/RateLimitRetryRow.spec.tsx
|
||||
|
||||
import { render, screen } from "@/utils/test-utils"
|
||||
|
||||
import { RateLimitRetryRow } from "../ChatRow"
|
||||
import type { RateLimitRetryMetadata } from "@roo-code/types"
|
||||
|
||||
vi.mock("@/i18n/TranslationContext", () => ({
|
||||
useAppTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}))
|
||||
|
||||
// Manual trigger instructions (for developer reference):
|
||||
// 1) In Roo Code settings, set your provider Rate limit seconds to a small value (e.g., 5s).
|
||||
// 2) Send a message to start an API request.
|
||||
// 3) Immediately send another message within the configured window.
|
||||
// The pre-request wait will emit `api_req_retry_delayed` with metadata,
|
||||
// rendering a single live status row: spinner + per‑second countdown,
|
||||
// then "Retrying now..." at zero. Input remains disabled during the wait.
|
||||
describe("RateLimitRetryRow", () => {
|
||||
it("renders waiting countdown with attempt and max attempts", () => {
|
||||
const metadata: RateLimitRetryMetadata = {
|
||||
type: "rate_limit_retry",
|
||||
status: "waiting",
|
||||
remainingSeconds: 12,
|
||||
attempt: 2,
|
||||
maxAttempts: 5,
|
||||
origin: "retry_attempt",
|
||||
}
|
||||
|
||||
render(<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()
|
||||
})
|
||||
})
|
||||
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"
|
||||
},
|
||||
"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.",
|
||||
|
|
|
|||
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"
|
||||
},
|
||||
"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.",
|
||||
|
|
|
|||
|
|
@ -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.",
|
||||
|
|
|
|||
8
webview-ui/src/i18n/locales/es/chat.json
generated
8
webview-ui/src/i18n/locales/es/chat.json
generated
|
|
@ -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"
|
||||
},
|
||||
|
|
|
|||
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"
|
||||
},
|
||||
"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",
|
||||
|
|
|
|||
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 स्ट्रीमिंग विफल हुई"
|
||||
},
|
||||
"rateLimitRetry": {
|
||||
"title": "दर सीमा ट्रिगर हुई — कृपया प्रतीक्षा करें।",
|
||||
"waiting": "{{seconds}}s में पुनः प्रयास कर रहा है",
|
||||
"waitingWithAttempt": "{{seconds}}s में पुनः प्रयास कर रहा है (प्रयास {{attempt}})",
|
||||
"waitingWithAttemptMax": "{{seconds}}s में पुनः प्रयास कर रहा है (प्रयास {{attempt}}/{{maxAttempts}})",
|
||||
"retrying": "अभी पुनः प्रयास कर रहा है…",
|
||||
"cancelled": "पुनः प्रयास रद्द किया गया"
|
||||
},
|
||||
"checkpoint": {
|
||||
"regular": "चेकपॉइंट",
|
||||
"initializingWarning": "चेकपॉइंट अभी भी आरंभ हो रहा है... अगर यह बहुत समय ले रहा है, तो आप <settingsLink>सेटिंग्स</settingsLink> में चेकपॉइंट को अक्षम कर सकते हैं और अपने कार्य को पुनः आरंभ कर सकते हैं।",
|
||||
|
|
|
|||
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"
|
||||
},
|
||||
"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.",
|
||||
|
|
|
|||
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"
|
||||
},
|
||||
"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à.",
|
||||
|
|
|
|||
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ストリーミング失敗"
|
||||
},
|
||||
"rateLimitRetry": {
|
||||
"title": "レート制限がトリガーされました — しばらくお待ちください。",
|
||||
"waiting": "{{seconds}}秒後に再試行",
|
||||
"waitingWithAttempt": "{{seconds}}秒後に再試行({{attempt}}回目)",
|
||||
"waitingWithAttemptMax": "{{seconds}}秒後に再試行({{attempt}}/{{maxAttempts}}回目)",
|
||||
"retrying": "再試行中…",
|
||||
"cancelled": "再試行キャンセル"
|
||||
},
|
||||
"checkpoint": {
|
||||
"regular": "チェックポイント",
|
||||
"initializingWarning": "チェックポイントの初期化中... 時間がかかりすぎる場合は、<settingsLink>設定</settingsLink>でチェックポイントを無効にしてタスクを再開できます。",
|
||||
|
|
|
|||
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 스트리밍 실패"
|
||||
},
|
||||
"rateLimitRetry": {
|
||||
"title": "속도 제한이 트리거되었습니다 — 잠시 기다려주세요.",
|
||||
"waiting": "{{seconds}}초 후에 다시 시도",
|
||||
"waitingWithAttempt": "{{seconds}}초 후에 다시 시도 ({{attempt}}번째 시도)",
|
||||
"waitingWithAttemptMax": "{{seconds}}초 후에 다시 시도 ({{attempt}}/{{maxAttempts}}번째 시도)",
|
||||
"retrying": "지금 다시 시도 중…",
|
||||
"cancelled": "다시 시도 취소됨"
|
||||
},
|
||||
"checkpoint": {
|
||||
"regular": "체크포인트",
|
||||
"initializingWarning": "체크포인트 초기화 중... 시간이 너무 오래 걸리면 <settingsLink>설정</settingsLink>에서 체크포인트를 비활성화하고 작업을 다시 시작할 수 있습니다.",
|
||||
|
|
|
|||
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"
|
||||
},
|
||||
"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.",
|
||||
|
|
|
|||
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ę"
|
||||
},
|
||||
"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.",
|
||||
|
|
|
|||
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"
|
||||
},
|
||||
"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.",
|
||||
|
|
|
|||
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-запроса"
|
||||
},
|
||||
"rateLimitRetry": {
|
||||
"title": "Превышен лимит запросов — пожалуйста, подождите.",
|
||||
"waiting": "Повторная попытка через {{seconds}}с",
|
||||
"waitingWithAttempt": "Повторная попытка через {{seconds}}с (попытка {{attempt}})",
|
||||
"waitingWithAttemptMax": "Повторная попытка через {{seconds}}с (попытка {{attempt}}/{{maxAttempts}})",
|
||||
"retrying": "Повторная попытка сейчас…",
|
||||
"cancelled": "Повторная попытка отменена"
|
||||
},
|
||||
"checkpoint": {
|
||||
"regular": "Точка сохранения",
|
||||
"initializingWarning": "Точка сохранения еще инициализируется... Если это занимает слишком много времени, вы можете отключить точки сохранения в <settingsLink>настройках</settingsLink> и перезапустить задачу.",
|
||||
|
|
|
|||
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"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
|
|
|
|||
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"
|
||||
},
|
||||
"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.",
|
||||
|
|
|
|||
8
webview-ui/src/i18n/locales/zh-CN/chat.json
generated
8
webview-ui/src/i18n/locales/zh-CN/chat.json
generated
|
|
@ -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>中禁用检查点并重新启动任务。",
|
||||
|
|
|
|||
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 串流處理失敗"
|
||||
},
|
||||
"rateLimitRetry": {
|
||||
"title": "已觸發速率限制 — 請稍候。",
|
||||
"waiting": "正在 {{seconds}} 秒後重試",
|
||||
"waitingWithAttempt": "正在 {{seconds}} 秒後重試 (第 {{attempt}} 次嘗試)",
|
||||
"waitingWithAttemptMax": "正在 {{seconds}} 秒後重試 (第 {{attempt}}/{{maxAttempts}} 次嘗試)",
|
||||
"retrying": "正在立即重試…",
|
||||
"cancelled": "重試已取消"
|
||||
},
|
||||
"checkpoint": {
|
||||
"regular": "檢查點",
|
||||
"initializingWarning": "正在初始化檢查點... 如果耗時過長,您可以在<settingsLink>設定</settingsLink>中停用檢查點並重新啟動工作。",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue