mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-05 08:10:14 +00:00
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:
parent
6d8fa39319
commit
b17bed1438
4 changed files with 96 additions and 0 deletions
|
|
@ -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) {
|
||||
|
|
|
|||
30
webview-ui/src/components/chat/RateLimitCountdown.tsx
Normal file
30
webview-ui/src/components/chat/RateLimitCountdown.tsx
Normal 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
|
||||
|
|
@ -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()
|
||||
})
|
||||
})
|
||||
|
|
@ -470,5 +470,8 @@
|
|||
"updated": "Updated the to-do list",
|
||||
"completed": "Completed",
|
||||
"started": "Started"
|
||||
},
|
||||
"rateLimit": {
|
||||
"countdown": "Rate limiting: {{seconds}}s"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue