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.",