refactor: centralize abort/streaming state reset logic and use safe JSON parsing

- Extract duplicated abort/streaming state reset logic into private helper method resetAbortAndStreamingState()
- Update resumeTaskFromHistory() and presentResumableAsk() to use the helper method
- Replace unsafe JSON.parse() with safeJsonParse() in ClineProvider.ts for better error handling

Addresses review feedback from @mrubens on PR #8986
This commit is contained in:
daniel-lxs 2025-11-03 15:23:27 -05:00
parent ac57bd0209
commit 58cbc690e4
No known key found for this signature in database
GPG key ID: 21C74479048B3AA6
2 changed files with 35 additions and 38 deletions

View file

@ -1353,24 +1353,7 @@ export class Task extends EventEmitter<TaskEvents> 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<TaskEvents> 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<TaskEvents> 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[] = []

View file

@ -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])