feat: show rate limiting countdown as informational message instead of error

- Create RateLimitCountdown component with informational styling
- Detect rate limiting messages in ChatRow and render new component
- Add translation key for rate limiting countdown message
- Add tests for RateLimitCountdown component

Fixes #10202
This commit is contained in:
Roo Code 2025-12-30 15:53:27 +00:00
parent 6d8fa39319
commit b17bed1438
4 changed files with 96 additions and 0 deletions

View file

@ -25,6 +25,7 @@ import { ReasoningBlock } from "./ReasoningBlock"
import Thumbnails from "../common/Thumbnails"
import ImageBlock from "../common/ImageBlock"
import ErrorRow from "./ErrorRow"
import { RateLimitCountdown } from "./RateLimitCountdown"
import McpResourceRow from "../mcp/McpResourceRow"
@ -1087,6 +1088,14 @@ export const ChatRowContent = ({
</>
)
case "api_req_retry_delayed":
// Check if this is user-configured rate limiting (not an API error)
if (message.text?.startsWith("Rate limiting for")) {
// Extract countdown from message text: "Rate limiting for X seconds..."
const rateLimitMatch = message.text.match(/Rate limiting for (\d+) seconds/)
const countdown = rateLimitMatch ? parseInt(rateLimitMatch[1], 10) : 0
return <RateLimitCountdown seconds={countdown} />
}
let body = t(`chat:apiRequest.failed`)
let retryInfo, rawError, code, docsURL
if (message.text !== undefined) {

View file

@ -0,0 +1,30 @@
import React, { memo } from "react"
import { useTranslation } from "react-i18next"
import { Timer } from "lucide-react"
export interface RateLimitCountdownProps {
seconds: number
}
/**
* Displays a user-configured rate limiting countdown as an informational message.
* This is NOT an error state - it's expected behavior based on user settings.
*
* Uses neutral/informational styling instead of error styling.
*/
export const RateLimitCountdown = memo(({ seconds }: RateLimitCountdownProps) => {
const { t } = useTranslation()
return (
<div className="flex items-center gap-2 text-vscode-descriptionForeground">
<Timer className="size-4 shrink-0" strokeWidth={1.5} />
<span className="text-sm">
{t("chat:rateLimit.countdown", { seconds, defaultValue: `Rate limiting: ${seconds}s` })}
</span>
</div>
)
})
RateLimitCountdown.displayName = "RateLimitCountdown"
export default RateLimitCountdown

View file

@ -0,0 +1,54 @@
import React from "react"
import { render, screen } from "@/utils/test-utils"
import { RateLimitCountdown } from "../RateLimitCountdown"
// Mock i18n
vi.mock("react-i18next", () => ({
useTranslation: () => ({
t: (key: string, params?: { seconds?: number }) => {
if (key === "chat:rateLimit.countdown") {
return `Rate limiting: ${params?.seconds}s`
}
return key
},
}),
initReactI18next: {
type: "3rdParty",
init: vi.fn(),
},
}))
describe("RateLimitCountdown", () => {
it("renders with countdown seconds", () => {
render(<RateLimitCountdown seconds={5} />)
expect(screen.getByText("Rate limiting: 5s")).toBeInTheDocument()
})
it("renders with zero seconds", () => {
render(<RateLimitCountdown seconds={0} />)
expect(screen.getByText("Rate limiting: 0s")).toBeInTheDocument()
})
it("uses informational styling (not error styling)", () => {
const { container } = render(<RateLimitCountdown seconds={10} />)
// Check that the component has the expected informational styling class
const rootDiv = container.firstChild as HTMLElement
expect(rootDiv).toHaveClass("text-vscode-descriptionForeground")
// Verify it does NOT have error-related styling
expect(rootDiv).not.toHaveClass("text-vscode-errorForeground")
})
it("renders the Timer icon", () => {
const { container } = render(<RateLimitCountdown seconds={5} />)
// Lucide icons render as SVG elements
const svgIcon = container.querySelector("svg")
expect(svgIcon).toBeInTheDocument()
})
})

View file

@ -470,5 +470,8 @@
"updated": "Updated the to-do list",
"completed": "Completed",
"started": "Started"
},
"rateLimit": {
"countdown": "Rate limiting: {{seconds}}s"
}
}