diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index 9c44bbb956..03dc3784e2 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -1353,24 +1353,7 @@ export class Task extends EventEmitter implements TaskLike { // (abandoned = false), we keep the instance alive but set abort = true. // We only clear these flags after the user confirms they want to resume, // preventing the old stream from continuing if abort was set. - this.abort = false - this.abandoned = false - this.abortReason = undefined - this.didFinishAbortingStream = false - this.isStreaming = false - - // Reset streaming-local fields to avoid stale state from previous stream - this.currentStreamingContentIndex = 0 - this.currentStreamingDidCheckpoint = false - this.assistantMessageContent = [] - this.didCompleteReadingStream = false - this.userMessageContent = [] - this.userMessageContentReady = false - this.didRejectTool = false - this.didAlreadyUseTool = false - this.presentAssistantMessageLocked = false - this.presentAssistantMessageHasPendingUpdates = false - this.assistantMessageParser.reset() + this.resetAbortAndStreamingState() let responseText: string | undefined let responseImages: string[] | undefined @@ -1550,6 +1533,33 @@ export class Task extends EventEmitter implements TaskLike { await this.initiateTaskLoop(newUserContent) } + /** + * Resets abort flags and streaming state to allow task resumption. + * Centralizes the state reset logic used after user confirms task resumption. + * + * @private + */ + private resetAbortAndStreamingState(): void { + this.abort = false + this.abandoned = false + this.abortReason = undefined + this.didFinishAbortingStream = false + this.isStreaming = false + + // Reset streaming-local fields to avoid stale state from previous stream + this.currentStreamingContentIndex = 0 + this.currentStreamingDidCheckpoint = false + this.assistantMessageContent = [] + this.didCompleteReadingStream = false + this.userMessageContent = [] + this.userMessageContentReady = false + this.didRejectTool = false + this.didAlreadyUseTool = false + this.presentAssistantMessageLocked = false + this.presentAssistantMessageHasPendingUpdates = false + this.assistantMessageParser.reset() + } + /** * Present a resumable ask on an aborted task without rehydrating. * Used by soft-interrupt (cancelTask) to show Resume/Terminate UI. @@ -1574,24 +1584,7 @@ export class Task extends EventEmitter implements TaskLike { // If user clicked Resume (not Terminate), reset abort flags and continue if (response === "yesButtonClicked" || response === "messageResponse") { // Reset abort flags to allow the loop to continue - this.abort = false - this.abandoned = false - this.abortReason = undefined - this.didFinishAbortingStream = false - this.isStreaming = false - - // Reset streaming-local fields to avoid stale state from previous stream - this.currentStreamingContentIndex = 0 - this.currentStreamingDidCheckpoint = false - this.assistantMessageContent = [] - this.didCompleteReadingStream = false - this.userMessageContent = [] - this.userMessageContentReady = false - this.didRejectTool = false - this.didAlreadyUseTool = false - this.presentAssistantMessageLocked = false - this.presentAssistantMessageHasPendingUpdates = false - this.assistantMessageParser.reset() + this.resetAbortAndStreamingState() // Prepare content for resuming the task loop let userContent: Anthropic.Messages.ContentBlockParam[] = [] diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 8878ba35af..684c51764c 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -50,6 +50,7 @@ import { Package } from "../../shared/package" import { findLast } from "../../shared/array" import { supportPrompt } from "../../shared/support-prompt" import { GlobalFileNames } from "../../shared/globalFileNames" +import { safeJsonParse } from "../../shared/safeJsonParse" import type { ExtensionMessage, ExtensionState, MarketplaceInstalledMetadata } from "../../shared/ExtensionMessage" import { Mode, defaultModeSlug, getModeBySlug } from "../../shared/modes" import { experimentDefault } from "../../shared/experiments" @@ -2665,9 +2666,12 @@ export class ClineProvider if (lastApiReqStartedIndex !== -1) { const lastApiReqStarted = task.clineMessages[lastApiReqStartedIndex] - const apiReqInfo = JSON.parse(lastApiReqStarted.text || "{}") + const apiReqInfo = safeJsonParse<{ cost?: number; cancelReason?: string }>( + lastApiReqStarted.text || "{}", + {}, + ) - if (apiReqInfo.cost === undefined && apiReqInfo.cancelReason === undefined) { + if (apiReqInfo && apiReqInfo.cost === undefined && apiReqInfo.cancelReason === undefined) { apiReqInfo.cancelReason = "user_cancelled" lastApiReqStarted.text = JSON.stringify(apiReqInfo) await task.overwriteClineMessages([...task.clineMessages])