From b17bed14380e2ba98d1060609a6c021e3f4f6cc6 Mon Sep 17 00:00:00 2001 From: Roo Code Date: Tue, 30 Dec 2025 15:53:27 +0000 Subject: [PATCH] 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 --- webview-ui/src/components/chat/ChatRow.tsx | 9 ++++ .../components/chat/RateLimitCountdown.tsx | 30 +++++++++++ .../__tests__/RateLimitCountdown.spec.tsx | 54 +++++++++++++++++++ webview-ui/src/i18n/locales/en/chat.json | 3 ++ 4 files changed, 96 insertions(+) create mode 100644 webview-ui/src/components/chat/RateLimitCountdown.tsx create mode 100644 webview-ui/src/components/chat/__tests__/RateLimitCountdown.spec.tsx diff --git a/webview-ui/src/components/chat/ChatRow.tsx b/webview-ui/src/components/chat/ChatRow.tsx index fee93f68a6..6ac9288923 100644 --- a/webview-ui/src/components/chat/ChatRow.tsx +++ b/webview-ui/src/components/chat/ChatRow.tsx @@ -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 + } + let body = t(`chat:apiRequest.failed`) let retryInfo, rawError, code, docsURL if (message.text !== undefined) { diff --git a/webview-ui/src/components/chat/RateLimitCountdown.tsx b/webview-ui/src/components/chat/RateLimitCountdown.tsx new file mode 100644 index 0000000000..e9ae99db56 --- /dev/null +++ b/webview-ui/src/components/chat/RateLimitCountdown.tsx @@ -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 ( +
+ + + {t("chat:rateLimit.countdown", { seconds, defaultValue: `Rate limiting: ${seconds}s` })} + +
+ ) +}) + +RateLimitCountdown.displayName = "RateLimitCountdown" + +export default RateLimitCountdown diff --git a/webview-ui/src/components/chat/__tests__/RateLimitCountdown.spec.tsx b/webview-ui/src/components/chat/__tests__/RateLimitCountdown.spec.tsx new file mode 100644 index 0000000000..883dbcc846 --- /dev/null +++ b/webview-ui/src/components/chat/__tests__/RateLimitCountdown.spec.tsx @@ -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() + + expect(screen.getByText("Rate limiting: 5s")).toBeInTheDocument() + }) + + it("renders with zero seconds", () => { + render() + + expect(screen.getByText("Rate limiting: 0s")).toBeInTheDocument() + }) + + it("uses informational styling (not error styling)", () => { + const { container } = render() + + // 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() + + // Lucide icons render as SVG elements + const svgIcon = container.querySelector("svg") + expect(svgIcon).toBeInTheDocument() + }) +}) diff --git a/webview-ui/src/i18n/locales/en/chat.json b/webview-ui/src/i18n/locales/en/chat.json index 2d71f43b2b..cd48380c73 100644 --- a/webview-ui/src/i18n/locales/en/chat.json +++ b/webview-ui/src/i18n/locales/en/chat.json @@ -470,5 +470,8 @@ "updated": "Updated the to-do list", "completed": "Completed", "started": "Started" + }, + "rateLimit": { + "countdown": "Rate limiting: {{seconds}}s" } }