Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/evals/src/cli/runTask.ts
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,7 @@ export const runTask = async ({ run, task, publish, logger, jobToken }: RunTaskO
"condense_context",
"condense_context_error",
"api_req_retry_delayed",
"api_req_rate_limited",
"api_req_retried",
]

Expand Down
2 changes: 2 additions & 0 deletions packages/types/src/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ export function isNonBlockingAsk(ask: ClineAsk): ask is NonBlockingAsk {
* - `api_req_finished`: Indicates an API request has completed successfully
* - `api_req_retried`: Indicates an API request is being retried after a failure
* - `api_req_retry_delayed`: Indicates an API request retry has been delayed
* - `api_req_rate_limited`: Indicates user-configured rate limiting countdown (not an error)
* - `api_req_deleted`: Indicates an API request has been deleted/cancelled
* - `text`: General text message or assistant response
* - `reasoning`: Assistant's reasoning or thought process (often hidden from user)
Expand All @@ -155,6 +156,7 @@ export const clineSays = [
"api_req_finished",
"api_req_retried",
"api_req_retry_delayed",
"api_req_rate_limited",
"api_req_deleted",
"text",
"image",
Expand Down
5 changes: 2 additions & 3 deletions src/core/task/Task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3706,10 +3706,9 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {

// Only show rate limiting message if we're not retrying. If retrying, we'll include the delay there.
if (rateLimitDelay > 0 && retryAttempt === 0) {
// Show countdown timer
// Show countdown timer using dedicated rate limit message type
for (let i = rateLimitDelay; i > 0; i--) {
const delayMessage = `Rate limiting for ${i} seconds...`
await this.say("api_req_retry_delayed", delayMessage, undefined, true)
await this.say("api_req_rate_limited", String(i), undefined, true)
await delay(1000)
}
}
Expand Down
5 changes: 5 additions & 0 deletions webview-ui/src/components/chat/ChatRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -1086,6 +1087,10 @@ export const ChatRowContent = ({
)}
</>
)
case "api_req_rate_limited":
// User-configured rate limiting countdown (not an API error)
const rateLimitSeconds = parseInt(message.text || "0", 10)
return <RateLimitCountdown seconds={rateLimitSeconds} />
case "api_req_retry_delayed":
let body = t(`chat:apiRequest.failed`)
let retryInfo, rawError, code, docsURL
Expand Down
30 changes: 30 additions & 0 deletions webview-ui/src/components/chat/RateLimitCountdown.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="flex items-center gap-2 text-vscode-descriptionForeground">
<Timer className="size-4 shrink-0" strokeWidth={1.5} />
<span className="text-sm">
{t("chat:rateLimit.countdown", { seconds, defaultValue: `Rate limiting: ${seconds}s` })}
</span>
</div>
)
})

RateLimitCountdown.displayName = "RateLimitCountdown"

export default RateLimitCountdown
Original file line number Diff line number Diff line change
@@ -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(<RateLimitCountdown seconds={5} />)

expect(screen.getByText("Rate limiting: 5s")).toBeInTheDocument()
})

it("renders with zero seconds", () => {
render(<RateLimitCountdown seconds={0} />)

expect(screen.getByText("Rate limiting: 0s")).toBeInTheDocument()
})

it("uses informational styling (not error styling)", () => {
const { container } = render(<RateLimitCountdown seconds={10} />)

// 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(<RateLimitCountdown seconds={5} />)

// Lucide icons render as SVG elements
const svgIcon = container.querySelector("svg")
expect(svgIcon).toBeInTheDocument()
})
})
3 changes: 3 additions & 0 deletions webview-ui/src/i18n/locales/en/chat.json
Original file line number Diff line number Diff line change
Expand Up @@ -470,5 +470,8 @@
"updated": "Updated the to-do list",
"completed": "Completed",
"started": "Started"
},
"rateLimit": {
"countdown": "Rate limiting: {{seconds}}s"
}
}
Loading