Skip to content

Commit 9a2eee3

Browse files
committed
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
1 parent 08d7f80 commit 9a2eee3

File tree

3 files changed

+134
-1
lines changed

3 files changed

+134
-1
lines changed

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

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ import { McpExecution } from "./McpExecution"
4747
import { ChatTextArea } from "./ChatTextArea"
4848
import { MAX_IMAGES_PER_MESSAGE } from "./ChatView"
4949
import { useSelectedModel } from "../ui/hooks/useSelectedModel"
50+
import { RateLimitCountdown } from "./RateLimitCountdown"
5051

5152
interface ChatRowProps {
5253
message: ClineMessage
@@ -262,7 +263,8 @@ export const ChatRowContent = ({
262263
<span style={{ color: successColor, fontWeight: "bold" }}>{t("chat:taskCompleted")}</span>,
263264
]
264265
case "api_req_retry_delayed":
265-
return []
266+
// Don't return empty array, this will be handled in the switch statement
267+
return [null, null]
266268
case "api_req_started":
267269
const getIconSpan = (iconName: string, color: string) => (
268270
<div
@@ -1275,6 +1277,14 @@ export const ChatRowContent = ({
12751277
</div>
12761278
</>
12771279
)
1280+
case "api_req_retry_delayed":
1281+
// Display the rate limit countdown component
1282+
return (
1283+
<RateLimitCountdown
1284+
message={message.text || ""}
1285+
isRetrying={message.text?.includes("Retry attempt")}
1286+
/>
1287+
)
12781288
case "shell_integration_warning":
12791289
return <CommandExecutionError />
12801290
case "checkpoint_saved":
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import React, { useMemo } from "react"
2+
import { useTranslation } from "react-i18next"
3+
import { ProgressIndicator } from "./ProgressIndicator"
4+
5+
interface RateLimitCountdownProps {
6+
message: string
7+
isRetrying?: boolean
8+
}
9+
10+
export const RateLimitCountdown: React.FC<RateLimitCountdownProps> = ({ message, isRetrying = false }) => {
11+
const { t } = useTranslation()
12+
13+
// Parse the message to extract countdown seconds and retry attempt info
14+
const { seconds, attemptInfo } = useMemo(() => {
15+
// Match patterns like "Retrying in X seconds" or "Rate limiting for X seconds"
16+
const secondsMatch = message.match(/(\d+)\s+second/i)
17+
const seconds = secondsMatch ? parseInt(secondsMatch[1], 10) : null
18+
19+
// Match retry attempt pattern like "Retry attempt X"
20+
const attemptMatch = message.match(/Retry attempt (\d+)/i)
21+
const attemptInfo = attemptMatch ? attemptMatch[0] : null
22+
23+
return { seconds, attemptInfo }
24+
}, [message])
25+
26+
// Check if this is the "Retrying now..." message
27+
const isRetryingNow = message.toLowerCase().includes("retrying now")
28+
29+
return (
30+
<div
31+
style={{
32+
display: "flex",
33+
flexDirection: "column",
34+
gap: "12px",
35+
padding: "12px 16px",
36+
backgroundColor: "var(--vscode-editor-background)",
37+
border: "1px solid var(--vscode-editorWidget-border)",
38+
borderRadius: "4px",
39+
marginBottom: "8px",
40+
}}>
41+
{/* Header with spinner and title */}
42+
<div
43+
style={{
44+
display: "flex",
45+
alignItems: "center",
46+
gap: "10px",
47+
color: "var(--vscode-foreground)",
48+
}}>
49+
<ProgressIndicator />
50+
<span style={{ fontWeight: "bold", fontSize: "var(--vscode-font-size)" }}>
51+
{isRetrying || attemptInfo ? t("chat:rateLimitRetry.title") : t("chat:rateLimit.title")}
52+
</span>
53+
</div>
54+
55+
{/* Status message */}
56+
<div
57+
style={{
58+
color: "var(--vscode-descriptionForeground)",
59+
fontSize: "var(--vscode-font-size)",
60+
lineHeight: "1.5",
61+
}}>
62+
{isRetryingNow ? (
63+
<span>{t("chat:rateLimit.retryingNow")}</span>
64+
) : seconds !== null ? (
65+
<div>
66+
{attemptInfo && (
67+
<div style={{ marginBottom: "4px" }}>
68+
<span style={{ color: "var(--vscode-foreground)" }}>{attemptInfo}</span>
69+
</div>
70+
)}
71+
<span>{t("chat:rateLimit.retryingIn", { seconds })}</span>
72+
</div>
73+
) : (
74+
<span>{t("chat:rateLimit.pleaseWait")}</span>
75+
)}
76+
</div>
77+
78+
{/* Optional error details if present in the message */}
79+
{message.includes("\n\n") && !isRetryingNow && (
80+
<details
81+
style={{
82+
marginTop: "4px",
83+
cursor: "pointer",
84+
}}>
85+
<summary
86+
style={{
87+
color: "var(--vscode-textLink-foreground)",
88+
fontSize: "calc(var(--vscode-font-size) - 1px)",
89+
userSelect: "none",
90+
}}>
91+
{t("chat:rateLimit.showDetails")}
92+
</summary>
93+
<pre
94+
style={{
95+
marginTop: "8px",
96+
padding: "8px",
97+
backgroundColor: "var(--vscode-textCodeBlock-background)",
98+
borderRadius: "4px",
99+
fontSize: "calc(var(--vscode-font-size) - 1px)",
100+
color: "var(--vscode-descriptionForeground)",
101+
whiteSpace: "pre-wrap",
102+
wordBreak: "break-word",
103+
overflowWrap: "anywhere",
104+
maxHeight: "200px",
105+
overflowY: "auto",
106+
}}>
107+
{message.split("\n\n")[0]}
108+
</pre>
109+
</details>
110+
)}
111+
</div>
112+
)
113+
}

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,16 @@
149149
"cancelled": "API Request Cancelled",
150150
"streamingFailed": "API Streaming Failed"
151151
},
152+
"rateLimit": {
153+
"title": "Rate limit triggered — please wait",
154+
"pleaseWait": "Please wait while we handle the rate limit...",
155+
"retryingIn": "Retrying in {{seconds}} seconds...",
156+
"retryingNow": "Retrying now...",
157+
"showDetails": "Show error details"
158+
},
159+
"rateLimitRetry": {
160+
"title": "Rate limit triggered — retrying"
161+
},
152162
"checkpoint": {
153163
"regular": "Checkpoint",
154164
"initializingWarning": "Still initializing checkpoint... If this takes too long, you can disable checkpoints in <settingsLink>settings</settingsLink> and restart your task.",

0 commit comments

Comments
 (0)