mirror of
https://github.com/cline/cline.git
synced 2026-09-01 23:19:18 +08:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4f3c671c43 | |||
| 2a300173e0 | |||
| e6d0a832d7 | |||
| 1a773c6538 | |||
| 9069984e49 | |||
| d14b3b2440 | |||
| aef99d4003 | |||
| 77c0c929df | |||
| c15e38fb62 | |||
| 0140a64e5e | |||
| 17a4bad5ff |
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,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()) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
},
|
||||
)
|
||||
Reference in New Issue
Block a user