From 9a2eee3ca325a399731bd0c2ef714513d706d175 Mon Sep 17 00:00:00 2001 From: Roo Code Date: Fri, 12 Sep 2025 03:19:21 +0000 Subject: [PATCH] feat: add rate limit countdown UI with neutral loading state - Created RateLimitCountdown component to display rate limit messages - Shows spinner with countdown timer and retry attempt info - Uses neutral styling instead of error colors - Added translations for rate limit messages - Updated ChatRow to properly handle api_req_retry_delayed messages Fixes #7922 --- webview-ui/src/components/chat/ChatRow.tsx | 12 +- .../components/chat/RateLimitCountdown.tsx | 113 ++++++++++++++++++ webview-ui/src/i18n/locales/en/chat.json | 10 ++ 3 files changed, 134 insertions(+), 1 deletion(-) create mode 100644 webview-ui/src/components/chat/RateLimitCountdown.tsx diff --git a/webview-ui/src/components/chat/ChatRow.tsx b/webview-ui/src/components/chat/ChatRow.tsx index 23ec50af37..6be78b1c8b 100644 --- a/webview-ui/src/components/chat/ChatRow.tsx +++ b/webview-ui/src/components/chat/ChatRow.tsx @@ -47,6 +47,7 @@ import { McpExecution } from "./McpExecution" import { ChatTextArea } from "./ChatTextArea" import { MAX_IMAGES_PER_MESSAGE } from "./ChatView" import { useSelectedModel } from "../ui/hooks/useSelectedModel" +import { RateLimitCountdown } from "./RateLimitCountdown" interface ChatRowProps { message: ClineMessage @@ -262,7 +263,8 @@ export const ChatRowContent = ({ {t("chat:taskCompleted")}, ] case "api_req_retry_delayed": - return [] + // Don't return empty array, this will be handled in the switch statement + return [null, null] case "api_req_started": const getIconSpan = (iconName: string, color: string) => (
) + case "api_req_retry_delayed": + // Display the rate limit countdown component + return ( + + ) case "shell_integration_warning": return case "checkpoint_saved": diff --git a/webview-ui/src/components/chat/RateLimitCountdown.tsx b/webview-ui/src/components/chat/RateLimitCountdown.tsx new file mode 100644 index 0000000000..c8b989b483 --- /dev/null +++ b/webview-ui/src/components/chat/RateLimitCountdown.tsx @@ -0,0 +1,113 @@ +import React, { useMemo } from "react" +import { useTranslation } from "react-i18next" +import { ProgressIndicator } from "./ProgressIndicator" + +interface RateLimitCountdownProps { + message: string + isRetrying?: boolean +} + +export const RateLimitCountdown: React.FC = ({ message, isRetrying = false }) => { + const { t } = useTranslation() + + // Parse the message to extract countdown seconds and retry attempt info + const { seconds, attemptInfo } = useMemo(() => { + // Match patterns like "Retrying in X seconds" or "Rate limiting for X seconds" + const secondsMatch = message.match(/(\d+)\s+second/i) + const seconds = secondsMatch ? parseInt(secondsMatch[1], 10) : null + + // Match retry attempt pattern like "Retry attempt X" + const attemptMatch = message.match(/Retry attempt (\d+)/i) + const attemptInfo = attemptMatch ? attemptMatch[0] : null + + return { seconds, attemptInfo } + }, [message]) + + // Check if this is the "Retrying now..." message + const isRetryingNow = message.toLowerCase().includes("retrying now") + + return ( +
+ {/* Header with spinner and title */} +
+ + + {isRetrying || attemptInfo ? t("chat:rateLimitRetry.title") : t("chat:rateLimit.title")} + +
+ + {/* Status message */} +
+ {isRetryingNow ? ( + {t("chat:rateLimit.retryingNow")} + ) : seconds !== null ? ( +
+ {attemptInfo && ( +
+ {attemptInfo} +
+ )} + {t("chat:rateLimit.retryingIn", { seconds })} +
+ ) : ( + {t("chat:rateLimit.pleaseWait")} + )} +
+ + {/* Optional error details if present in the message */} + {message.includes("\n\n") && !isRetryingNow && ( +
+ + {t("chat:rateLimit.showDetails")} + +
+						{message.split("\n\n")[0]}
+					
+
+ )} +
+ ) +} diff --git a/webview-ui/src/i18n/locales/en/chat.json b/webview-ui/src/i18n/locales/en/chat.json index 38448ffee0..b7caa466b5 100644 --- a/webview-ui/src/i18n/locales/en/chat.json +++ b/webview-ui/src/i18n/locales/en/chat.json @@ -149,6 +149,16 @@ "cancelled": "API Request Cancelled", "streamingFailed": "API Streaming Failed" }, + "rateLimit": { + "title": "Rate limit triggered — please wait", + "pleaseWait": "Please wait while we handle the rate limit...", + "retryingIn": "Retrying in {{seconds}} seconds...", + "retryingNow": "Retrying now...", + "showDetails": "Show error details" + }, + "rateLimitRetry": { + "title": "Rate limit triggered — retrying" + }, "checkpoint": { "regular": "Checkpoint", "initializingWarning": "Still initializing checkpoint... If this takes too long, you can disable checkpoints in settings and restart your task.",