Skip to content

Commit b17bed1

Browse files
committed
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
1 parent 6d8fa39 commit b17bed1

File tree

4 files changed

+96
-0
lines changed

4 files changed

+96
-0
lines changed

webview-ui/src/components/chat/ChatRow.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { ReasoningBlock } from "./ReasoningBlock"
2525
import Thumbnails from "../common/Thumbnails"
2626
import ImageBlock from "../common/ImageBlock"
2727
import ErrorRow from "./ErrorRow"
28+
import { RateLimitCountdown } from "./RateLimitCountdown"
2829

2930
import McpResourceRow from "../mcp/McpResourceRow"
3031

@@ -1087,6 +1088,14 @@ export const ChatRowContent = ({
10871088
</>
10881089
)
10891090
case "api_req_retry_delayed":
1091+
// Check if this is user-configured rate limiting (not an API error)
1092+
if (message.text?.startsWith("Rate limiting for")) {
1093+
// Extract countdown from message text: "Rate limiting for X seconds..."
1094+
const rateLimitMatch = message.text.match(/Rate limiting for (\d+) seconds/)
1095+
const countdown = rateLimitMatch ? parseInt(rateLimitMatch[1], 10) : 0
1096+
return <RateLimitCountdown seconds={countdown} />
1097+
}
1098+
10901099
let body = t(`chat:apiRequest.failed`)
10911100
let retryInfo, rawError, code, docsURL
10921101
if (message.text !== undefined) {
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import React, { memo } from "react"
2+
import { useTranslation } from "react-i18next"
3+
import { Timer } from "lucide-react"
4+
5+
export interface RateLimitCountdownProps {
6+
seconds: number
7+
}
8+
9+
/**
10+
* Displays a user-configured rate limiting countdown as an informational message.
11+
* This is NOT an error state - it's expected behavior based on user settings.
12+
*
13+
* Uses neutral/informational styling instead of error styling.
14+
*/
15+
export const RateLimitCountdown = memo(({ seconds }: RateLimitCountdownProps) => {
16+
const { t } = useTranslation()
17+
18+
return (
19+
<div className="flex items-center gap-2 text-vscode-descriptionForeground">
20+
<Timer className="size-4 shrink-0" strokeWidth={1.5} />
21+
<span className="text-sm">
22+
{t("chat:rateLimit.countdown", { seconds, defaultValue: `Rate limiting: ${seconds}s` })}
23+
</span>
24+
</div>
25+
)
26+
})
27+
28+
RateLimitCountdown.displayName = "RateLimitCountdown"
29+
30+
export default RateLimitCountdown
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import React from "react"
2+
3+
import { render, screen } from "@/utils/test-utils"
4+
5+
import { RateLimitCountdown } from "../RateLimitCountdown"
6+
7+
// Mock i18n
8+
vi.mock("react-i18next", () => ({
9+
useTranslation: () => ({
10+
t: (key: string, params?: { seconds?: number }) => {
11+
if (key === "chat:rateLimit.countdown") {
12+
return `Rate limiting: ${params?.seconds}s`
13+
}
14+
return key
15+
},
16+
}),
17+
initReactI18next: {
18+
type: "3rdParty",
19+
init: vi.fn(),
20+
},
21+
}))
22+
23+
describe("RateLimitCountdown", () => {
24+
it("renders with countdown seconds", () => {
25+
render(<RateLimitCountdown seconds={5} />)
26+
27+
expect(screen.getByText("Rate limiting: 5s")).toBeInTheDocument()
28+
})
29+
30+
it("renders with zero seconds", () => {
31+
render(<RateLimitCountdown seconds={0} />)
32+
33+
expect(screen.getByText("Rate limiting: 0s")).toBeInTheDocument()
34+
})
35+
36+
it("uses informational styling (not error styling)", () => {
37+
const { container } = render(<RateLimitCountdown seconds={10} />)
38+
39+
// Check that the component has the expected informational styling class
40+
const rootDiv = container.firstChild as HTMLElement
41+
expect(rootDiv).toHaveClass("text-vscode-descriptionForeground")
42+
43+
// Verify it does NOT have error-related styling
44+
expect(rootDiv).not.toHaveClass("text-vscode-errorForeground")
45+
})
46+
47+
it("renders the Timer icon", () => {
48+
const { container } = render(<RateLimitCountdown seconds={5} />)
49+
50+
// Lucide icons render as SVG elements
51+
const svgIcon = container.querySelector("svg")
52+
expect(svgIcon).toBeInTheDocument()
53+
})
54+
})

webview-ui/src/i18n/locales/en/chat.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -470,5 +470,8 @@
470470
"updated": "Updated the to-do list",
471471
"completed": "Completed",
472472
"started": "Started"
473+
},
474+
"rateLimit": {
475+
"countdown": "Rate limiting: {{seconds}}s"
473476
}
474477
}

0 commit comments

Comments
 (0)