Compare commits

...

11 Commits

Author SHA1 Message Date
abeatrix 4f3c671c43 Merge branch 'main' into bee/clineerror 2025-07-14 19:31:33 -07:00
abeatrix 2a300173e0 clean up 2025-07-14 18:46:42 -07:00
abeatrix e6d0a832d7 ErrorRow 2025-07-14 17:13:14 -07:00
abeatrix 1a773c6538 fix 2025-07-14 17:05:45 -07:00
abeatrix 9069984e49 apply feedback and clean up 2025-07-14 15:15:18 -07:00
abeatrix d14b3b2440 backward comp 2025-07-14 15:02:37 -07:00
abeatrix aef99d4003 clean up 2025-07-14 14:46:33 -07:00
abeatrix 77c0c929df Merge branch 'main' into bee/clineerror 2025-07-14 14:43:44 -07:00
abeatrix c15e38fb62 pass error 2025-07-14 13:01:44 -07:00
abeatrix 0140a64e5e Improve error reporting and handling with ClineError
Introduces a new `ClineError` class and enhances error handling throughout the Cline extension to provide more informative error messages to the user and improve debugging.

Key changes:

- Introduces `ClineError` class to encapsulate error information, including request ID, error details, and stack traces.
- Creates `src/utils/error.ts` to handle error serialization and create safe error messages for UI display.
- Modifies Cline API calls to throw `ClineError` instead of generic errors, including the request ID.
- Updates the UI to display the request ID along with error messages, aiding in debugging.
- Implements structured error handling in `ChatRowContent` to display credit limit and rate limit errors more effectively.
- Adds error serialization and deserialization to proto conversions to preserve error information across the extension.
- Updates telemetry to log `ClineError` objects, providing more context for debugging.
- Updates `formatErrorWithStatusCode` to include the request ID in the error message.

These changes provide more context for error messages, making it easier for users and developers to understand and resolve issues.
2025-07-14 11:20:25 -07:00
abeatrix 17a4bad5ff Adds request ID to Cline API error message
Improves error handling for the Cline API:
2025-07-11 13:42:22 -07:00
12 changed files with 443 additions and 122 deletions
+11
View File
@@ -177,6 +177,15 @@ message ApiReqRetryStatus {
string error_snippet = 4;
}
// Message for ClineError
message ClineError {
string message = 1;
string title = 2;
string request_id = 3;
string error_details = 4; // JSON string of error details
string stack = 5;
}
// Message for ClineApiReqInfo
message ClineApiReqInfo {
string request = 1;
@@ -188,6 +197,7 @@ message ClineApiReqInfo {
ClineApiReqCancelReason cancel_reason = 7;
string streaming_failed_message = 8;
ApiReqRetryStatus retry_status = 9;
ClineError error = 10;
}
// Main ClineMessage type
@@ -216,6 +226,7 @@ message ClineMessage {
ClineAskQuestion ask_question = 20;
ClineAskNewTask ask_new_task = 21;
ClineApiReqInfo api_req_info = 22;
ClineError error = 23;
}
// UiService provides methods for managing UI interactions
+4 -8
View File
@@ -9,6 +9,7 @@ import { OpenRouterErrorResponse } from "./types"
import { withRetry } from "../retry"
import { AuthService } from "@/services/auth/AuthService"
import OpenAI from "openai"
import { ClineError } from "@/services/error/ClineError"
import { version as extensionVersion } from "../../../package.json"
import { shouldSkipReasoningForModel } from "@utils/model-utils"
@@ -183,13 +184,8 @@ export class ClineHandler implements ApiHandler {
}
}
} catch (error) {
if (error.code === "ERR_BAD_REQUEST" || error.status === 401) {
throw new Error("Unauthorized: Please sign in to Cline before trying again.") // match with webview-ui/src/components/chat/ChatRow.tsx
} else if (error.code === "insufficient_credits" || error.status === 402) {
throw new Error(error.error ? JSON.stringify(error.error) : "Insufficient credits or unknown error.")
}
console.error("Cline API Error:", error)
throw error instanceof Error ? error : new Error(String(error))
console.error("Cline API Error", error)
throw new ClineError(error, (error as any).request_id)
}
}
@@ -230,7 +226,7 @@ export class ClineHandler implements ApiHandler {
}
}
} catch (error) {
// ignore if fails
// Log the error but don't throw - this is a fallback method
console.error("Error fetching cline generation details:", error)
}
}
+9 -3
View File
@@ -82,7 +82,8 @@ import { MessageStateHandler } from "./message-state"
import { TaskState } from "./TaskState"
import { ToolExecutor } from "./ToolExecutor"
import { formatErrorWithStatusCode, updateApiReqMsg } from "./utils"
import { createDiffViewProvider, getHostBridgeProvider } from "@/hosts/host-providers"
import { ClineError } from "@/services/error/ClineError"
import { createDiffViewProvider } from "@/hosts/host-providers"
export const USE_EXPERIMENTAL_CLAUDE4_FEATURES = false
@@ -1694,6 +1695,7 @@ export class Task {
} else {
// request failed after retrying automatically once, ask user if they want to retry again
// note that this api_req_failed ask is unique in that we only present this option if the api hasn't streamed any content yet (ie it fails on the first chunk due), as it would allow them to hit a retry button. However if the api failed mid-stream, it could be in any arbitrary state where some tools may have executed, so that error is handled differently and requires cancelling the task entirely.
const requestId = error.request_id
if (isOpenRouterContextWindowError || isAnthropicContextWindowError) {
const truncatedConversationHistory = this.contextManager.getTruncatedMessages(
@@ -1704,7 +1706,10 @@ export class Task {
// If the conversation has more than 3 messages, we can truncate again. If not, then the conversation is bricked.
// ToDo: Allow the user to change their input if this is the case.
if (truncatedConversationHistory.length > 3) {
error = new Error("Context window exceeded. Click retry to truncate the conversation and try again.")
error = new ClineError(
"Context window exceeded. Click retry to truncate the conversation and try again.",
requestId,
)
this.taskState.didAutomaticallyRetryFailedApiRequest = false
}
}
@@ -1727,6 +1732,7 @@ export class Task {
// cancelReason: "retries_exhausted", // Indicate that automatic retries failed
streamingFailedMessage: errorMessage,
} satisfies ClineApiReqInfo),
error: new ClineError(error, requestId),
})
// this.ask will trigger postStateToWebview, so this change should be picked up.
}
@@ -1735,7 +1741,7 @@ export class Task {
if (response !== "yesButtonClicked") {
// this will never happen since if noButtonClicked, we will clear current task, aborting this instance
throw new Error("API request failed")
throw new ClineError("API request failed", requestId)
}
await this.say("api_req_retried")
+5 -1
View File
@@ -4,10 +4,12 @@ import { serializeError } from "serialize-error"
import { MessageStateHandler } from "./message-state"
import { calculateApiCostAnthropic } from "@/utils/cost"
import { ApiHandler } from "@/api"
import { ClineError } from "@/services/error/ClineError"
export function formatErrorWithStatusCode(error: any): string {
const statusCode = error.status || error.statusCode || (error.response && error.response.status)
const message = error.message ?? JSON.stringify(serializeError(error), null, 2)
const requestId = error.request_id ? ` - ${error.request_id}` : ""
const message = (error.message ?? JSON.stringify(serializeError(error), null, 2)) + requestId
// Only prepend the statusCode if it's not already part of the message
return statusCode && !message.includes(statusCode.toString()) ? `${statusCode} - ${message}` : message
@@ -37,6 +39,7 @@ type UpdateApiReqMsgParams = {
api: ApiHandler
cancelReason?: ClineApiReqCancelReason
streamingFailedMessage?: string
error?: ClineError
}
// update api_req_started. we can't use api_req_finished anymore since it's a unique case where it could come after a streaming message (ie in the middle of being updated or executed)
@@ -66,5 +69,6 @@ export const updateApiReqMsg = async (params: UpdateApiReqMsgParams) => {
cancelReason: params.cancelReason,
streamingFailedMessage: params.streamingFailedMessage,
} satisfies ClineApiReqInfo),
error: params.error,
})
}
+8 -7
View File
@@ -104,13 +104,14 @@ export class ClineAccountService {
// Throw error if insufficient credits (balance <= 0)
if (currentBalance <= 0) {
throw new Error(
JSON.stringify({
code: "insufficient_credits",
current_balance: currentBalance,
message: "Not enough credits available",
}),
)
const insufficientCreditsError = new Error("Not enough credits available")
// Add properties that will be extracted by ClineError's extractSafeErrorDetails
;(insufficientCreditsError as any).error = {
code: "insufficient_credits",
current_balance: currentBalance,
message: "Not enough credits available",
}
throw insufficientCreditsError
}
} catch (error) {
console.error("Invalid Cline API request:", error)
+184
View File
@@ -0,0 +1,184 @@
export class ClineError extends Error {
public readonly title = "ClineError"
public readonly originalError?: Error
public readonly errorDetails: {
message: string
code?: string
status?: number
details?: any
}
constructor(error: Error, request_id?: string)
constructor(message: string, request_id?: string)
constructor(
_error: unknown,
public readonly request_id?: string,
) {
// Safely serialize the error to avoid circular references
const serializedError = serializeError(_error)
super(serializedError.message)
this.errorDetails = serializedError
this.request_id = request_id || serializedError.request_id
if (_error instanceof Error) {
this.originalError = _error
this.stack = _error.stack
this.name = _error.name
// Copy any additional enumerable properties from the original error
Object.keys(_error).forEach((key) => {
if (key !== "message" && key !== "stack" && key !== "name") {
;(this as any)[key] = (_error as any)[key]
}
})
}
this.errorDetails.message = createSafeErrorMessage(_error) || serializedError.message
}
/**
* Get a JSON-serializable representation of the error
*/
toJSON() {
return {
message: this.message,
title: this.title,
request_id: this.request_id,
errorDetails: this.errorDetails,
stack: this.stack,
}
}
}
export function isClineError(error: unknown): error is ClineError {
return error instanceof ClineError
}
/**
* Safely serialize an error object to avoid circular reference issues
*/
function serializeError(error: unknown): {
message: string
code?: string
status?: number
request_id?: string
details?: any
} {
if (error instanceof Error) {
const errorDetails = (error as any).details?.error || extractSafeErrorDetails(error)
return {
message: error.message,
code: (error as any).code,
status: (error as any).status,
request_id: (error as any).request_id,
details: errorDetails,
}
}
// Handle axios errors
if (error && typeof error === "object" && "isAxiosError" in error) {
const axiosError = error as any
return {
message: axiosError.message || "Network request failed",
code: axiosError.code,
status: axiosError.response?.status,
request_id: axiosError.request_id,
details: {
url: axiosError.config?.url,
method: axiosError.config?.method,
statusText: axiosError.response?.statusText,
},
}
}
// Handle other error-like objects
if (error && typeof error === "object") {
return {
message: (error as any).message || String(error),
code: (error as any).code,
status: (error as any).status,
request_id: (error as any).request_id,
details: extractSafeErrorDetails(error),
}
}
return {
message: String(error),
}
}
/**
* Extract safe error details that can be serialized without circular references
*/
function extractSafeErrorDetails(error: any): any {
const safeDetails: any = {}
// Extract common error properties that are safe to serialize
const safeProperties = [
"name",
"code",
"status",
"statusText",
"url",
"method",
"response",
"request_id",
"error",
"metadata",
"current_balance", // Add this for insufficient credits errors
"total_spent", // Add this for insufficient credits errors
"total_promotions", // Add this for insufficient credits errors
"buy_credits_url", // Add this for insufficient credits errors
]
for (const prop of safeProperties) {
if (error[prop] !== undefined) {
// For response objects, only extract safe properties
if (prop === "response" && error[prop] && typeof error[prop] === "object") {
safeDetails[prop] = {
status: error[prop].status,
statusText: error[prop].statusText,
data: error[prop].data,
}
} else {
safeDetails[prop] = error[prop]
}
}
}
return safeDetails
}
/**
* Create a safe error message that can be displayed in the UI
*/
export function createSafeErrorMessage(error: unknown): string {
const serialized = serializeError(error)
// Handle specific error types
if (serialized?.details?.code === "ERR_BAD_REQUEST" || serialized?.details?.status === 401) {
return "Unauthorized: Please sign in to Cline before trying again."
}
if (serialized?.details?.code === "insufficient_credits" && serialized?.details?.status === 402) {
try {
// Return the serialized details directly for insufficient credits
return JSON.stringify(serialized.details)
} catch {
return "Insufficient credits. Please add more credits to continue."
}
}
// Handle network errors
if (serialized?.details?.code === "ECONNREFUSED" || serialized?.details?.code === "ENOTFOUND") {
return "Network connection failed. Please check your internet connection and try again."
}
if (serialized?.details?.code === "ETIMEDOUT") {
return "Request timed out. Please try again."
}
// Return the main error message
return serialized?.details?.message || "An unexpected error occurred."
}
+2 -1
View File
@@ -2,6 +2,7 @@ import * as Sentry from "@sentry/browser"
import * as vscode from "vscode"
import { telemetryService } from "@/services/posthog/telemetry/TelemetryService"
import * as pkg from "../../../package.json"
import { ClineError } from "./ClineError"
let telemetryLevel = vscode.workspace.getConfiguration("telemetry").get<string>("telemetryLevel", "all")
let isTelemetryEnabled = ["all", "error"].includes(telemetryLevel)
@@ -65,7 +66,7 @@ export class ErrorService {
}
}
static logException(error: Error): void {
static logException(error: Error | ClineError): void {
// Don't log if telemetry is off
const isUserManuallyOptedIn = telemetryService.isTelemetryEnabled()
if (!isUserManuallyOptedIn || !ErrorService.isEnabled()) {
+4 -1
View File
@@ -7,7 +7,8 @@ import { HistoryItem } from "./HistoryItem"
import { TelemetrySetting } from "./TelemetrySetting"
import { ClineRulesToggles } from "./cline-rules"
import { UserInfo } from "./UserInfo"
import { McpDisplayMode, DEFAULT_MCP_DISPLAY_MODE } from "./McpDisplayMode"
import { ClineError } from "../services/error/ClineError"
import { McpDisplayMode } from "./McpDisplayMode"
// webview will hold state
export interface ExtensionMessage {
@@ -77,6 +78,7 @@ export interface ClineMessage {
isOperationOutsideWorkspace?: boolean
conversationHistoryIndex?: number
conversationHistoryDeletedRange?: [number, number] // for when conversation history is truncated for API requests
error?: ClineError
}
export type ClineAsk =
@@ -200,6 +202,7 @@ export interface ClineApiReqInfo {
delaySec: number
errorSnippet?: string
}
error?: ClineError
}
export type ClineApiReqCancelReason = "streaming_failed" | "user_cancelled" | "retries_exhausted"
@@ -184,6 +184,15 @@ export function convertClineMessageToProto(message: AppClineMessage): ProtoCline
endIndex: message.conversationHistoryDeletedRange[1],
}
: undefined,
error: message.error
? {
message: message.error.message,
title: message.error.title,
requestId: message.error.request_id ?? "",
errorDetails: JSON.stringify(message.error.errorDetails),
stack: message.error.stack ?? "",
}
: undefined,
}
return protoMessage
@@ -251,5 +260,29 @@ export function convertProtoToClineMessage(protoMessage: ProtoClineMessage): App
]
}
// Convert error field
if (protoMessage.error) {
try {
const errorDetails = JSON.parse(protoMessage.error.errorDetails)
message.error = {
message: protoMessage.error.message,
title: protoMessage.error.title,
request_id: protoMessage.error.requestId,
errorDetails,
stack: protoMessage.error.stack,
} as any // Cast to any since we're creating a ClineError-like object
} catch (e) {
console.error("Failed to parse error details:", e)
// Fallback to basic error info
message.error = {
message: protoMessage.error.message,
title: protoMessage.error.title,
request_id: protoMessage.error.requestId,
errorDetails: { message: protoMessage.error.message },
stack: protoMessage.error.stack,
} as any
}
}
return message
}
+29 -100
View File
@@ -1,10 +1,9 @@
import { VSCodeBadge, VSCodeButton, VSCodeProgressRing } from "@vscode/webview-ui-toolkit/react"
import { VSCodeBadge, VSCodeProgressRing } from "@vscode/webview-ui-toolkit/react"
import deepEqual from "fast-deep-equal"
import React, { memo, MouseEvent, useCallback, useEffect, useMemo, useRef, useState } from "react"
import styled from "styled-components"
import { useSize } from "react-use"
import CreditLimitError from "@/components/chat/CreditLimitError"
import { OptionsButtons } from "@/components/chat/OptionsButtons"
import TaskFeedbackButtons from "@/components/chat/TaskFeedbackButtons"
import { CheckmarkControl } from "@/components/common/CheckmarkControl"
@@ -37,6 +36,7 @@ import ReportBugPreview from "./ReportBugPreview"
import UserMessage from "./UserMessage"
import QuoteButton from "./QuoteButton"
import { useClineAuth } from "@/context/ClineAuthContext"
import { ErrorRow } from "./ErrorRow"
const normalColor = "var(--vscode-foreground)"
const errorColor = "var(--vscode-errorForeground)"
@@ -195,13 +195,13 @@ export const ChatRowContent = memo(
selectedText: "",
})
const contentRef = useRef<HTMLDivElement>(null)
const [cost, apiReqCancelReason, apiReqStreamingFailedMessage, retryStatus] = useMemo(() => {
const [cost, apiReqCancelReason, apiReqStreamingFailedMessage, retryStatus, error] = useMemo(() => {
if (message.text != null && message.say === "api_req_started") {
const info: ClineApiReqInfo = JSON.parse(message.text)
return [info.cost, info.cancelReason, info.streamingFailedMessage, info.retryStatus]
return [info.cost, info.cancelReason, info.streamingFailedMessage, info.retryStatus, message.error]
}
return [undefined, undefined, undefined, undefined]
}, [message.text, message.say])
return [undefined, undefined, undefined, undefined, message.error]
}, [message.text, message.say, message.error])
// when resuming task last won't be api_req_failed but a resume_task message so api_req_started will show loading spinner. that's why we just remove the last api_req_started that failed without streaming anything
const apiRequestFailedMessage =
@@ -413,20 +413,29 @@ export const ChatRowContent = memo(
<ProgressIndicator />
),
(() => {
const requestId = error?.request_id ? ` - ${error.request_id}` : ""
if (apiReqCancelReason != null) {
return apiReqCancelReason === "user_cancelled" ? (
<span style={{ color: normalColor, fontWeight: "bold" }}>API Request Cancelled</span>
<span className="text-[var(--vscode-foreground)] font-bold">
API Request Cancelled {requestId}
</span>
) : (
<span style={{ color: errorColor, fontWeight: "bold" }}>API Streaming Failed</span>
<span className="text-[var(--vscode-errorForeground)] font-bold">
API Streaming Failed {requestId}
</span>
)
}
if (cost != null) {
return <span style={{ color: normalColor, fontWeight: "bold" }}>API Request</span>
return <span className="text-[var(--vscode-foreground)] font-bold">API Request {requestId}</span>
}
if (apiRequestFailedMessage) {
return <span style={{ color: errorColor, fontWeight: "bold" }}>API Request Failed</span>
if (apiRequestFailedMessage || error) {
return (
<span className="text-[var(--vscode-errorForeground)] font-bold">
API Request Failed {requestId}
</span>
)
}
// New: Check for retryStatus to modify the title
if (retryStatus && cost == null && !apiReqCancelReason) {
@@ -440,7 +449,7 @@ export const ChatRowContent = memo(
)
}
return <span style={{ color: normalColor, fontWeight: "bold" }}>API Request...</span>
return <span className="text-[var(--vscode-foreground)] font-bold">API Request...</span>
})(),
]
case "followup":
@@ -941,94 +950,14 @@ export const ChatRowContent = memo(
<span className={`codicon codicon-chevron-${isExpanded ? "up" : "down"}`}></span>
</div>
{((cost == null && apiRequestFailedMessage) || apiReqStreamingFailedMessage) && (
<>
{(() => {
// Try to parse the error message as JSON for credit limit error
const errorData = parseErrorText(
apiRequestFailedMessage || apiReqStreamingFailedMessage,
)
if (errorData) {
if (
errorData.code === "insufficient_credits" &&
typeof errorData.current_balance === "number"
) {
return (
<CreditLimitError
currentBalance={errorData.current_balance}
totalSpent={errorData.total_spent}
totalPromotions={errorData.total_promotions}
message={errorData.message}
buyCreditsUrl={errorData.buy_credits_url}
/>
)
}
}
// Check for rate limit errors (status code 429)
const isRateLimitError =
apiRequestFailedMessage?.includes("status code 429") ||
apiRequestFailedMessage?.toLowerCase().includes("rate limit") ||
apiRequestFailedMessage?.toLowerCase().includes("too many requests") ||
apiRequestFailedMessage?.toLowerCase().includes("quota exceeded") ||
apiRequestFailedMessage?.toLowerCase().includes("resource exhausted")
if (isRateLimitError) {
return (
<p
style={{
...pStyle,
color: "var(--vscode-errorForeground)",
}}>
{apiRequestFailedMessage || apiReqStreamingFailedMessage}
</p>
)
}
// Default error display
return (
<p
style={{
...pStyle,
color: "var(--vscode-errorForeground)",
}}>
{apiRequestFailedMessage || apiReqStreamingFailedMessage}
{apiRequestFailedMessage?.toLowerCase().includes("powershell") && (
<>
<br />
<br />
It seems like you're having Windows PowerShell issues, please see this{" "}
<a
href="https://github.com/cline/cline/wiki/TroubleShooting-%E2%80%90-%22PowerShell-is-not-recognized-as-an-internal-or-external-command%22"
style={{
color: "inherit",
textDecoration: "underline",
}}>
troubleshooting guide
</a>
.
</>
)}
{apiRequestFailedMessage?.includes(
"Unauthorized: Please sign in to Cline before trying again.", // match with cline.ts (TODO: remove after some time)
) && (
<>
<br />
<br />
{clineUser ? (
<span style={{ color: "var(--vscode-descriptionForeground)" }}>
(Click "Retry" below)
</span>
) : (
<VSCodeButton onClick={handleSignIn} className="w-full mb-4">
Sign in to Cline
</VSCodeButton>
)}
</>
)}
</p>
)
})()}
</>
<ErrorRow
error={error}
apiRequestFailedMessage={apiRequestFailedMessage}
apiReqStreamingFailedMessage={apiReqStreamingFailedMessage}
clineUser={clineUser}
handleSignIn={handleSignIn}
pStyle={pStyle}
/>
)}
{isExpanded && (
@@ -22,7 +22,8 @@ const CreditLimitError: React.FC<CreditLimitErrorProps> = ({
// We have to divide because the balance is stored in microcredits
return (
<div className="p-2 border-none rounded-md mb-2 bg-[var(--vscode-textBlockQuote-background)]">
<div className="mb-2">{message}</div>
{/* Remove dollar sign from error message sent back from server */}
<div className="mb-2">{message?.replace(/^\$/, "")}</div>
<div className="mb-3">
<div className="text-[var(--vscode-foreground)]">
Current Balance: <span className="font-bold">${(currentBalance / 1000000).toFixed(4)}</span>
+152
View File
@@ -0,0 +1,152 @@
import { memo, useMemo } from "react"
import CreditLimitError from "./CreditLimitError"
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
interface ErrorInfo {
details: any
message: string
isStructured: boolean
}
interface ErrorRowProps {
error: any
apiRequestFailedMessage: string | undefined
apiReqStreamingFailedMessage: string | undefined
clineUser: any
handleSignIn: () => void
pStyle: React.CSSProperties
}
const parseErrorText = (text: string | undefined): any => {
if (!text) return undefined
const startIndex = text.indexOf("{")
const endIndex = text.lastIndexOf("}")
if (startIndex === -1 || endIndex === -1) return undefined
try {
return JSON.parse(text.substring(startIndex, endIndex + 1))
} catch {
return undefined
}
}
const getErrorInfo = (
error: any,
apiRequestFailedMessage: string | undefined,
apiReqStreamingFailedMessage: string | undefined,
): ErrorInfo => {
// Handle structured error first
if (error) {
const errorDetails =
error.errorDetails?.details?.error || parseErrorText(error.errorDetails?.details?.message || error.message) || error
return {
details: errorDetails,
message: errorDetails?.message || error.message || "An unknown error occurred.",
isStructured: true,
}
}
// Handle text-based errors
const errorMessage = apiRequestFailedMessage || apiReqStreamingFailedMessage || ""
const errorData = parseErrorText(errorMessage)
return {
details: errorData,
message: errorMessage,
isStructured: false,
}
}
const checkErrorType = (errorInfo: ErrorInfo) => {
const message = errorInfo.message.toLowerCase()
const details = errorInfo.details
const errorDetails = details?.errorDetails
return {
isRateLimit:
details?.status === 429 ||
message.includes("rate limit") ||
message.includes("too many requests") ||
message.includes("quota exceeded") ||
message.includes("resource exhausted"),
isCreditLimit:
details?.code === "insufficient_credits" ||
(errorDetails?.code === "insufficient_credits" && typeof errorDetails?.current_balance === "number"),
isAuth:
details?.status === 401 ||
(errorDetails?.status === 401 &&
errorDetails?.message?.includes("Unauthorized: Please sign in to Cline before trying again.")),
isPowerShell: message.includes("powershell") || errorDetails?.message?.toLowerCase().includes("powershell"),
}
}
export const ErrorRow = memo<ErrorRowProps>(
({ error, apiRequestFailedMessage, apiReqStreamingFailedMessage, clineUser, handleSignIn, pStyle }) => {
console.error("ErrorRow", { error, apiRequestFailedMessage, apiReqStreamingFailedMessage })
const errorInfo = useMemo(
() => getErrorInfo(error, apiRequestFailedMessage, apiReqStreamingFailedMessage),
[error, apiRequestFailedMessage, apiReqStreamingFailedMessage],
)
const errorTypes = useMemo(() => checkErrorType(errorInfo), [errorInfo])
// Handle credit limit error with dedicated component
if (errorTypes.isCreditLimit) {
const details = errorInfo.details
// Use details directly if available, otherwise fall back to errorDetails
const creditDetails = details?.code === "insufficient_credits" ? details : details?.errorDetails
return (
<CreditLimitError
currentBalance={creditDetails?.current_balance}
totalSpent={creditDetails?.total_spent}
totalPromotions={creditDetails?.total_promotions}
message={creditDetails?.message || "Not enough credits available"}
buyCreditsUrl={creditDetails?.buy_credits_url}
/>
)
}
console.log(clineUser, "clineUser")
const displayMessage = errorInfo.details?.errorDetails?.message || errorInfo.message
return (
<p style={{ ...pStyle, color: "var(--vscode-errorForeground)" }}>
{displayMessage}
{errorTypes.isAuth && (
<>
<br />
<br />
{clineUser && !displayMessage?.includes("Unauthorized: Please sign in to Cline before trying again.") ? (
<span style={{ color: "var(--vscode-descriptionForeground)" }}>(Click "Retry" below)</span>
) : (
<VSCodeButton onClick={handleSignIn} className="w-full mb-4">
Sign in to Cline
</VSCodeButton>
)}
</>
)}
{errorTypes.isPowerShell && (
<>
<br />
<br />
It seems like you're having Windows PowerShell issues, please see this{" "}
<a
href="https://github.com/cline/cline/wiki/TroubleShooting-%E2%80%90-%22PowerShell-is-not-recognized-as-an-internal-or-external-command%22"
style={{ color: "inherit", textDecoration: "underline" }}>
troubleshooting guide
</a>
.
</>
)}
</p>
)
},
)