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
This commit is contained in:
Roo Code 2025-09-12 03:19:21 +00:00
parent 08d7f80e22
commit 9a2eee3ca3
3 changed files with 134 additions and 1 deletions

View file

@ -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 = ({
<span style={{ color: successColor, fontWeight: "bold" }}>{t("chat:taskCompleted")}</span>,
]
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) => (
<div
@ -1275,6 +1277,14 @@ export const ChatRowContent = ({
</div>
</>
)
case "api_req_retry_delayed":
// Display the rate limit countdown component
return (
<RateLimitCountdown
message={message.text || ""}
isRetrying={message.text?.includes("Retry attempt")}
/>
)
case "shell_integration_warning":
return <CommandExecutionError />
case "checkpoint_saved":

View file

@ -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<RateLimitCountdownProps> = ({ 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 (
<div
style={{
display: "flex",
flexDirection: "column",
gap: "12px",
padding: "12px 16px",
backgroundColor: "var(--vscode-editor-background)",
border: "1px solid var(--vscode-editorWidget-border)",
borderRadius: "4px",
marginBottom: "8px",
}}>
{/* Header with spinner and title */}
<div
style={{
display: "flex",
alignItems: "center",
gap: "10px",
color: "var(--vscode-foreground)",
}}>
<ProgressIndicator />
<span style={{ fontWeight: "bold", fontSize: "var(--vscode-font-size)" }}>
{isRetrying || attemptInfo ? t("chat:rateLimitRetry.title") : t("chat:rateLimit.title")}
</span>
</div>
{/* Status message */}
<div
style={{
color: "var(--vscode-descriptionForeground)",
fontSize: "var(--vscode-font-size)",
lineHeight: "1.5",
}}>
{isRetryingNow ? (
<span>{t("chat:rateLimit.retryingNow")}</span>
) : seconds !== null ? (
<div>
{attemptInfo && (
<div style={{ marginBottom: "4px" }}>
<span style={{ color: "var(--vscode-foreground)" }}>{attemptInfo}</span>
</div>
)}
<span>{t("chat:rateLimit.retryingIn", { seconds })}</span>
</div>
) : (
<span>{t("chat:rateLimit.pleaseWait")}</span>
)}
</div>
{/* Optional error details if present in the message */}
{message.includes("\n\n") && !isRetryingNow && (
<details
style={{
marginTop: "4px",
cursor: "pointer",
}}>
<summary
style={{
color: "var(--vscode-textLink-foreground)",
fontSize: "calc(var(--vscode-font-size) - 1px)",
userSelect: "none",
}}>
{t("chat:rateLimit.showDetails")}
</summary>
<pre
style={{
marginTop: "8px",
padding: "8px",
backgroundColor: "var(--vscode-textCodeBlock-background)",
borderRadius: "4px",
fontSize: "calc(var(--vscode-font-size) - 1px)",
color: "var(--vscode-descriptionForeground)",
whiteSpace: "pre-wrap",
wordBreak: "break-word",
overflowWrap: "anywhere",
maxHeight: "200px",
overflowY: "auto",
}}>
{message.split("\n\n")[0]}
</pre>
</details>
)}
</div>
)
}

View file

@ -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 <settingsLink>settings</settingsLink> and restart your task.",