feat: add full error details to streaming failure dialog (#10131)

Co-authored-by: Roo Code <roomote@roocode.com>
Co-authored-by: cte <cestreich@gmail.com>
This commit is contained in:
roomote[bot] 2025-12-16 10:34:16 -08:00 committed by GitHub
parent a7b192adca
commit caf6142201
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 13 additions and 30 deletions

View file

@ -2169,7 +2169,6 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
break
} else {
const modelInfo = this.api.getModel().info
const state = await this.providerRef.deref()?.getState()
const toolProtocol = resolveToolProtocol(this.apiConfiguration, modelInfo)
nextUserContent = [{ type: "text", text: formatResponse.noToolsUsed(toolProtocol) }]
this.consecutiveMistakeCount++
@ -2960,11 +2959,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
// Apply exponential backoff similar to first-chunk errors when auto-resubmit is enabled
const stateForBackoff = await this.providerRef.deref()?.getState()
if (stateForBackoff?.autoApprovalEnabled) {
await this.backoffAndAnnounce(
currentItem.retryAttempt ?? 0,
error,
streamingFailedMessage,
)
await this.backoffAndAnnounce(currentItem.retryAttempt ?? 0, error)
// Check if task was aborted during the backoff
if (this.abort) {
@ -3066,10 +3061,10 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
// Need to save assistant responses to file before proceeding to
// tool use since user can exit at any moment and we wouldn't be
// able to save the assistant's response.
let didEndLoop = false
// Check if we have any content to process (text or tool uses)
const hasTextContent = assistantMessage.length > 0
const hasToolUses = this.assistantMessageContent.some(
(block) => block.type === "tool_use" || block.type === "mcp_tool_use",
)
@ -3139,10 +3134,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
}
await this.addToApiConversationHistory(
{
role: "assistant",
content: assistantContent,
},
{ role: "assistant", content: assistantContent },
reasoningMessage || undefined,
)
@ -3191,7 +3183,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
// Add periodic yielding to prevent blocking
await new Promise((resolve) => setImmediate(resolve))
}
// Continue to next iteration instead of setting didEndLoop from recursive call
continue
} else {
// If there's no assistant_responses, that means we got no text
@ -3218,13 +3210,11 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
// Reuse the state variable from above
if (state?.autoApprovalEnabled) {
// Auto-retry with backoff - don't persist failure message when retrying
const errorMsg =
"Unexpected API Response: The language model did not provide any assistant messages. This may indicate an issue with the API or the model's output."
await this.backoffAndAnnounce(
currentItem.retryAttempt ?? 0,
new Error("Empty assistant response"),
errorMsg,
new Error(
"Unexpected API Response: The language model did not provide any assistant messages. This may indicate an issue with the API or the model's output.",
),
)
// Check if task was aborted during the backoff
@ -3811,18 +3801,8 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
// 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.
if (autoApprovalEnabled) {
let errorMsg
if (error.error?.metadata?.raw) {
errorMsg = JSON.stringify(error.error.metadata.raw, null, 2)
} else if (error.message) {
errorMsg = error.message
} else {
errorMsg = "Unknown error"
}
// Apply shared exponential backoff and countdown UX
await this.backoffAndAnnounce(retryAttempt, error, errorMsg)
await this.backoffAndAnnounce(retryAttempt, error)
// CRITICAL: Check if task was aborted during the backoff countdown
// This prevents infinite loops when users cancel during auto-retry
@ -3870,7 +3850,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
}
// Shared exponential backoff for retries (first-chunk and mid-stream)
private async backoffAndAnnounce(retryAttempt: number, error: any, header?: string): Promise<void> {
private async backoffAndAnnounce(retryAttempt: number, error: any): Promise<void> {
try {
const state = await this.providerRef.deref()?.getState()
const baseDelay = state?.requestDelaySeconds || 5
@ -3900,7 +3880,9 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
}
const finalDelay = Math.max(exponentialDelay, rateLimitDelay)
if (finalDelay <= 0) return
if (finalDelay <= 0) {
return
}
// Build header text; fall back to error message if none provided
let headerText

View file

@ -1081,6 +1081,7 @@ export const ChatRowContent = ({
? "https://github.com/cline/cline/wiki/TroubleShooting-%E2%80%90-%22PowerShell-is-not-recognized-as-an-internal-or-external-command%22"
: undefined
}
errorDetails={apiReqStreamingFailedMessage}
/>
)}
</>