Skip to content

Commit c93d690

Browse files
daniel-lxsroomote
authored andcommitted
fix: eliminate UI rerender during task cancellation
- Add resetToResumableState() method to Task class to reset internal state without recreation - Simplify cancellation flow to handle everything within Task class - Update abortStream() to properly mark API requests as cancelled - Remove initClineWithHistoryItem() call from ClineProvider.cancelTask() - Task instance now persists through cancellation, preventing UI flicker The task now handles its own cancellation and resumption internally, maintaining the same instance throughout. This provides a seamless user experience with no visual disruption when cancelling and resuming tasks.
1 parent aee531a commit c93d690

File tree

2 files changed

+72
-40
lines changed

2 files changed

+72
-40
lines changed

src/core/task/Task.ts

Lines changed: 68 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1489,8 +1489,8 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
14891489
}
14901490
}
14911491

1492-
public async abortTask(isAbandoned = false) {
1493-
// Aborting task
1492+
public async abortTask(isAbandoned = false, skipSave = false) {
1493+
console.log(`[subtasks] aborting task ${this.taskId}.${this.instanceId}`)
14941494

14951495
// Will stop any autonomously running promises.
14961496
if (isAbandoned) {
@@ -1506,15 +1506,74 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
15061506
console.error(`Error during task ${this.taskId}.${this.instanceId} disposal:`, error)
15071507
// Don't rethrow - we want abort to always succeed
15081508
}
1509-
// Save the countdown message in the automatic retry or other content.
1510-
try {
1511-
// Save the countdown message in the automatic retry or other content.
1512-
await this.saveClineMessages()
1513-
} catch (error) {
1514-
console.error(`Error saving messages during abort for task ${this.taskId}.${this.instanceId}:`, error)
1509+
1510+
// Only save messages if not skipping (e.g., during user cancellation where messages are already saved)
1511+
if (!skipSave) {
1512+
try {
1513+
// Save the countdown message in the automatic retry or other content.
1514+
await this.saveClineMessages()
1515+
} catch (error) {
1516+
console.error(`Error saving messages during abort for task ${this.taskId}.${this.instanceId}:`, error)
1517+
}
15151518
}
15161519
}
15171520

1521+
/**
1522+
* Reset the task to a resumable state without recreating the instance.
1523+
* This is used when canceling a task to avoid unnecessary rerenders.
1524+
*/
1525+
public async resetToResumableState() {
1526+
console.log(`[subtasks] resetting task ${this.taskId}.${this.instanceId} to resumable state`)
1527+
1528+
// Reset abort flags
1529+
this.abort = false
1530+
this.abandoned = false
1531+
1532+
// Reset streaming state
1533+
this.isStreaming = false
1534+
this.isWaitingForFirstChunk = false
1535+
this.didFinishAbortingStream = true
1536+
this.didCompleteReadingStream = false
1537+
1538+
// Clear streaming content
1539+
this.currentStreamingContentIndex = 0
1540+
this.currentStreamingDidCheckpoint = false
1541+
this.assistantMessageContent = []
1542+
this.userMessageContent = []
1543+
this.userMessageContentReady = false
1544+
this.didRejectTool = false
1545+
this.didAlreadyUseTool = false
1546+
this.presentAssistantMessageLocked = false
1547+
this.presentAssistantMessageHasPendingUpdates = false
1548+
1549+
// Reset API state
1550+
this.consecutiveMistakeCount = 0
1551+
1552+
// Reset ask response state to allow new messages
1553+
this.askResponse = undefined
1554+
this.askResponseText = undefined
1555+
this.askResponseImages = undefined
1556+
this.blockingAsk = undefined
1557+
1558+
// Reset parser if exists
1559+
if (this.assistantMessageParser) {
1560+
this.assistantMessageParser.reset()
1561+
}
1562+
1563+
// Only reset diff view if it's actively editing
1564+
// This avoids unnecessary operations when diff view is not in use
1565+
if (this.diffViewProvider && this.diffViewProvider.isEditing) {
1566+
await this.diffViewProvider.reset()
1567+
}
1568+
1569+
// The task is now ready to be resumed
1570+
// The API request status has already been updated by abortStream
1571+
// We don't add the resume_task message here because ask() will add it
1572+
1573+
// Keep messages and history intact for resumption
1574+
// The task is now ready to be resumed without recreation
1575+
}
1576+
15181577
// Used when a sub-task is launched and the parent task is waiting for it to
15191578
// finish.
15201579
// TBD: The 1s should be added to the settings, also should add a timeout to
@@ -1584,7 +1643,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
15841643
const currentUserContent = currentItem.userContent
15851644
const currentIncludeFileDetails = currentItem.includeFileDetails
15861645

1587-
if (this.abort) {
1646+
if (this.abort) {
15881647
throw new Error(`[RooCode#recursivelyMakeRooRequests] task ${this.taskId}.${this.instanceId} aborted`)
15891648
}
15901649

src/core/webview/ClineProvider.ts

Lines changed: 4 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1279,38 +1279,11 @@ export class ClineProvider
12791279

12801280
console.log(`[subtasks] cancelling task ${cline.taskId}.${cline.instanceId}`)
12811281

1282-
const { historyItem } = await this.getTaskWithId(cline.taskId)
1283-
// Preserve parent and root task information for history item.
1284-
const rootTask = cline.rootTask
1285-
const parentTask = cline.parentTask
1286-
1287-
cline.abortTask()
1288-
1289-
await pWaitFor(
1290-
() =>
1291-
this.getCurrentTask()! === undefined ||
1292-
this.getCurrentTask()!.isStreaming === false ||
1293-
this.getCurrentTask()!.didFinishAbortingStream ||
1294-
// If only the first chunk is processed, then there's no
1295-
// need to wait for graceful abort (closes edits, browser,
1296-
// etc).
1297-
this.getCurrentTask()!.isWaitingForFirstChunk,
1298-
{
1299-
timeout: 3_000,
1300-
},
1301-
).catch(() => {
1302-
console.error("Failed to abort task")
1303-
})
1304-
1305-
if (this.getCurrentTask()) {
1306-
// 'abandoned' will prevent this Cline instance from affecting
1307-
// future Cline instances. This may happen if its hanging on a
1308-
// streaming request.
1309-
this.getCurrentTask()!.abandoned = true
1310-
}
1282+
// Just set the abort flag - the task will handle its own resumption
1283+
cline.abort = true
13111284

1312-
// Clears task again, so we need to abortTask manually above.
1313-
await this.createTaskWithHistoryItem({ ...historyItem, rootTask, parentTask })
1285+
// The task's streaming loop will detect the abort flag and handle the resumption
1286+
// No need to wait or do anything else here
13141287
}
13151288

13161289
async updateCustomInstructions(instructions?: string) {

0 commit comments

Comments
 (0)