@@ -1273,7 +1273,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
12731273 }
12741274 }
12751275
1276- public async abortTask ( isAbandoned = false ) {
1276+ public async abortTask ( isAbandoned = false , skipSave = false ) {
12771277 console . log ( `[subtasks] aborting task ${ this . taskId } .${ this . instanceId } ` )
12781278
12791279 // Will stop any autonomously running promises.
@@ -1290,15 +1290,74 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
12901290 console . error ( `Error during task ${ this . taskId } .${ this . instanceId } disposal:` , error )
12911291 // Don't rethrow - we want abort to always succeed
12921292 }
1293- // Save the countdown message in the automatic retry or other content.
1294- try {
1295- // Save the countdown message in the automatic retry or other content.
1296- await this . saveClineMessages ( )
1297- } catch ( error ) {
1298- console . error ( `Error saving messages during abort for task ${ this . taskId } .${ this . instanceId } :` , error )
1293+
1294+ // Only save messages if not skipping (e.g., during user cancellation where messages are already saved)
1295+ if ( ! skipSave ) {
1296+ try {
1297+ // Save the countdown message in the automatic retry or other content.
1298+ await this . saveClineMessages ( )
1299+ } catch ( error ) {
1300+ console . error ( `Error saving messages during abort for task ${ this . taskId } .${ this . instanceId } :` , error )
1301+ }
12991302 }
13001303 }
13011304
1305+ /**
1306+ * Reset the task to a resumable state without recreating the instance.
1307+ * This is used when canceling a task to avoid unnecessary rerenders.
1308+ */
1309+ public async resetToResumableState ( ) {
1310+ console . log ( `[subtasks] resetting task ${ this . taskId } .${ this . instanceId } to resumable state` )
1311+
1312+ // Reset abort flags
1313+ this . abort = false
1314+ this . abandoned = false
1315+
1316+ // Reset streaming state
1317+ this . isStreaming = false
1318+ this . isWaitingForFirstChunk = false
1319+ this . didFinishAbortingStream = true
1320+ this . didCompleteReadingStream = false
1321+
1322+ // Clear streaming content
1323+ this . currentStreamingContentIndex = 0
1324+ this . currentStreamingDidCheckpoint = false
1325+ this . assistantMessageContent = [ ]
1326+ this . userMessageContent = [ ]
1327+ this . userMessageContentReady = false
1328+ this . didRejectTool = false
1329+ this . didAlreadyUseTool = false
1330+ this . presentAssistantMessageLocked = false
1331+ this . presentAssistantMessageHasPendingUpdates = false
1332+
1333+ // Reset API state
1334+ this . consecutiveMistakeCount = 0
1335+
1336+ // Reset ask response state to allow new messages
1337+ this . askResponse = undefined
1338+ this . askResponseText = undefined
1339+ this . askResponseImages = undefined
1340+ this . blockingAsk = undefined
1341+
1342+ // Reset parser if exists
1343+ if ( this . assistantMessageParser ) {
1344+ this . assistantMessageParser . reset ( )
1345+ }
1346+
1347+ // Only reset diff view if it's actively editing
1348+ // This avoids unnecessary operations when diff view is not in use
1349+ if ( this . diffViewProvider && this . diffViewProvider . isEditing ) {
1350+ await this . diffViewProvider . reset ( )
1351+ }
1352+
1353+ // The task is now ready to be resumed
1354+ // The API request status has already been updated by abortStream
1355+ // We don't add the resume_task message here because ask() will add it
1356+
1357+ // Keep messages and history intact for resumption
1358+ // The task is now ready to be resumed without recreation
1359+ }
1360+
13021361 // Used when a sub-task is launched and the parent task is waiting for it to
13031362 // finish.
13041363 // TBD: The 1s should be added to the settings, also should add a timeout to
@@ -1502,20 +1561,22 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
15021561 } satisfies ClineApiReqInfo )
15031562 }
15041563
1505- const abortStream = async ( cancelReason : ClineApiReqCancelReason , streamingFailedMessage ?: string ) => {
1564+ const abortStream = async (
1565+ cancelReason : ClineApiReqCancelReason ,
1566+ streamingFailedMessage ?: string ,
1567+ skipUIUpdates : boolean = false ,
1568+ ) => {
15061569 if ( this . diffViewProvider . isEditing ) {
15071570 await this . diffViewProvider . revertChanges ( ) // closes diff view
15081571 }
15091572
1510- // if last message is a partial we need to update and save it
1573+ // if last message is a partial we need to update it
15111574 const lastMessage = this . clineMessages . at ( - 1 )
15121575
15131576 if ( lastMessage && lastMessage . partial ) {
15141577 // lastMessage.ts = Date.now() DO NOT update ts since it is used as a key for virtuoso list
15151578 lastMessage . partial = false
1516- // instead of streaming partialMessage events, we do a save and post like normal to persist to disk
15171579 console . log ( "updating partial message" , lastMessage )
1518- // await this.saveClineMessages()
15191580 }
15201581
15211582 // Let assistant know their response was interrupted for when task is resumed
@@ -1538,8 +1599,15 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
15381599 // Update `api_req_started` to have cancelled and cost, so that
15391600 // we can display the cost of the partial stream.
15401601 updateApiReqMsg ( cancelReason , streamingFailedMessage )
1602+
1603+ // Always save messages to ensure the API request status is updated
15411604 await this . saveClineMessages ( )
15421605
1606+ // Only post to webview if we're not skipping UI updates
1607+ if ( ! skipUIUpdates ) {
1608+ await this . providerRef . deref ( ) ?. postStateToWebview ( )
1609+ }
1610+
15431611 // Signals to provider that it can retrieve the saved messages
15441612 // from disk, as abortTask can not be awaited on in nature.
15451613 this . didFinishAbortingStream = true
@@ -1622,7 +1690,8 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
16221690 // isn't abandoned (sometimes OpenRouter stream
16231691 // hangs, in which case this would affect future
16241692 // instances of Cline).
1625- await abortStream ( "user_cancelled" )
1693+ // Don't skip UI updates - we need to update the API request status
1694+ await abortStream ( "user_cancelled" , undefined , false )
16261695 }
16271696
16281697 break // Aborts the stream.
@@ -1659,24 +1728,11 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
16591728 // may have executed), so we just resort to replicating a
16601729 // cancel task.
16611730
1662- // Check if this was a user-initiated cancellation BEFORE calling abortTask
1663- // If this.abort is already true, it means the user clicked cancel, so we should
1664- // treat this as "user_cancelled" rather than "streaming_failed"
1665- const cancelReason = this . abort ? "user_cancelled" : "streaming_failed"
1731+ const streamingFailedMessage = error . message ?? JSON . stringify ( serializeError ( error ) , null , 2 )
16661732
1667- const streamingFailedMessage = this . abort
1668- ? undefined
1669- : ( error . message ?? JSON . stringify ( serializeError ( error ) , null , 2 ) )
1670-
1671- // Now call abortTask after determining the cancel reason.
1672- await this . abortTask ( )
1673- await abortStream ( cancelReason , streamingFailedMessage )
1674-
1675- const history = await provider ?. getTaskWithId ( this . taskId )
1676-
1677- if ( history ) {
1678- await provider ?. initClineWithHistoryItem ( history . historyItem )
1679- }
1733+ // For streaming failures, use the original abort flow
1734+ await this . abortTask ( false , false )
1735+ await abortStream ( "streaming_failed" , streamingFailedMessage , false )
16801736 }
16811737 } finally {
16821738 this . isStreaming = false
@@ -1702,6 +1758,31 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
17021758
17031759 // Need to call here in case the stream was aborted.
17041760 if ( this . abort || this . abandoned ) {
1761+ // If this was a user cancellation, reset and show resume prompt
1762+ if ( this . abort && ! this . abandoned ) {
1763+ // Reset the task to a resumable state
1764+ this . abort = false
1765+ await this . resetToResumableState ( )
1766+
1767+ // Show the resume prompt
1768+ const { response, text, images } = await this . ask ( "resume_task" )
1769+
1770+ if ( response === "messageResponse" ) {
1771+ await this . say ( "user_feedback" , text , images )
1772+ // Continue with the new user input
1773+ const newUserContent : Anthropic . Messages . ContentBlockParam [ ] = [ ]
1774+ if ( text ) {
1775+ newUserContent . push ( { type : "text" , text } )
1776+ }
1777+ if ( images && images . length > 0 ) {
1778+ newUserContent . push ( ...formatResponse . imageBlocks ( images ) )
1779+ }
1780+ // Recursively continue with the new content
1781+ return await this . recursivelyMakeClineRequests ( newUserContent )
1782+ }
1783+ // If not messageResponse, the task will end
1784+ return true
1785+ }
17051786 throw new Error ( `[RooCode#recursivelyMakeRooRequests] task ${ this . taskId } .${ this . instanceId } aborted` )
17061787 }
17071788
0 commit comments