Skip to content

Commit 77971a5

Browse files
authored
Revert "fix: prevent UI flicker and enable resumption after task cancellation" (#9032)
1 parent f717863 commit 77971a5

File tree

7 files changed

+56
-411
lines changed

7 files changed

+56
-411
lines changed

src/core/task/Task.ts

Lines changed: 7 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -737,8 +737,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
737737
// deallocated. (Although we set Cline = undefined in provider, that
738738
// simply removes the reference to this instance, but the instance is
739739
// still alive until this promise resolves or rejects.)
740-
// Exception: Allow resume asks even when aborted for soft-interrupt UX
741-
if (this.abort && type !== "resume_task" && type !== "resume_completed_task") {
740+
if (this.abort) {
742741
throw new Error(`[RooCode#ask] task ${this.taskId}.${this.instanceId} aborted`)
743742
}
744743

@@ -1256,7 +1255,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
12561255
])
12571256
}
12581257

1259-
public async resumeTaskFromHistory() {
1258+
private async resumeTaskFromHistory() {
12601259
if (this.enableBridge) {
12611260
try {
12621261
await BridgeOrchestrator.subscribeToTask(this)
@@ -1348,13 +1347,6 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
13481347

13491348
const { response, text, images } = await this.ask(askType) // Calls `postStateToWebview`.
13501349

1351-
// Reset abort flags AFTER user responds to resume ask.
1352-
// This is critical for the cancel → resume flow: when a task is soft-aborted
1353-
// (abandoned = false), we keep the instance alive but set abort = true.
1354-
// We only clear these flags after the user confirms they want to resume,
1355-
// preventing the old stream from continuing if abort was set.
1356-
this.resetAbortAndStreamingState()
1357-
13581350
let responseText: string | undefined
13591351
let responseImages: string[] | undefined
13601352

@@ -1533,86 +1525,6 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
15331525
await this.initiateTaskLoop(newUserContent)
15341526
}
15351527

1536-
/**
1537-
* Resets abort flags and streaming state to allow task resumption.
1538-
* Centralizes the state reset logic used after user confirms task resumption.
1539-
*
1540-
* @private
1541-
*/
1542-
private resetAbortAndStreamingState(): void {
1543-
this.abort = false
1544-
this.abandoned = false
1545-
this.abortReason = undefined
1546-
this.didFinishAbortingStream = false
1547-
this.isStreaming = false
1548-
1549-
// Reset streaming-local fields to avoid stale state from previous stream
1550-
this.currentStreamingContentIndex = 0
1551-
this.currentStreamingDidCheckpoint = false
1552-
this.assistantMessageContent = []
1553-
this.didCompleteReadingStream = false
1554-
this.userMessageContent = []
1555-
this.userMessageContentReady = false
1556-
this.didRejectTool = false
1557-
this.didAlreadyUseTool = false
1558-
this.presentAssistantMessageLocked = false
1559-
this.presentAssistantMessageHasPendingUpdates = false
1560-
this.assistantMessageParser.reset()
1561-
}
1562-
1563-
/**
1564-
* Present a resumable ask on an aborted task without rehydrating.
1565-
* Used by soft-interrupt (cancelTask) to show Resume/Terminate UI.
1566-
* Selects the appropriate ask type based on the last relevant message.
1567-
* If the user clicks Resume, resets abort flags and continues the task loop.
1568-
*/
1569-
public async presentResumableAsk(): Promise<void> {
1570-
const lastClineMessage = this.clineMessages
1571-
.slice()
1572-
.reverse()
1573-
.find((m) => !(m.ask === "resume_task" || m.ask === "resume_completed_task"))
1574-
1575-
let askType: ClineAsk
1576-
if (lastClineMessage?.ask === "completion_result") {
1577-
askType = "resume_completed_task"
1578-
} else {
1579-
askType = "resume_task"
1580-
}
1581-
1582-
const { response, text, images } = await this.ask(askType)
1583-
1584-
// If user clicked Resume (not Terminate), reset abort flags and continue
1585-
if (response === "yesButtonClicked" || response === "messageResponse") {
1586-
// Reset abort flags to allow the loop to continue
1587-
this.resetAbortAndStreamingState()
1588-
1589-
// Prepare content for resuming the task loop
1590-
let userContent: Anthropic.Messages.ContentBlockParam[] = []
1591-
1592-
if (response === "messageResponse" && text) {
1593-
// User provided additional instructions
1594-
await this.say("user_feedback", text, images)
1595-
userContent.push({
1596-
type: "text",
1597-
text: `\n\nNew instructions for task continuation:\n<user_message>\n${text}\n</user_message>`,
1598-
})
1599-
if (images && images.length > 0) {
1600-
userContent.push(...formatResponse.imageBlocks(images))
1601-
}
1602-
} else {
1603-
// Simple resume with no new instructions
1604-
userContent.push({
1605-
type: "text",
1606-
text: "[TASK RESUMPTION] Resuming task...",
1607-
})
1608-
}
1609-
1610-
// Continue the task loop
1611-
await this.initiateTaskLoop(userContent)
1612-
}
1613-
// If user clicked Terminate (noButtonClicked), do nothing - task stays aborted
1614-
}
1615-
16161528
public async abortTask(isAbandoned = false) {
16171529
// Aborting task
16181530

@@ -1624,17 +1536,12 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
16241536
this.abort = true
16251537
this.emit(RooCodeEventName.TaskAborted)
16261538

1627-
// Only dispose if this is a hard abort (abandoned)
1628-
// For soft abort (user cancel), keep the instance alive so we can present a resumable ask
1629-
if (isAbandoned) {
1630-
try {
1631-
this.dispose() // Call the centralized dispose method
1632-
} catch (error) {
1633-
console.error(`Error during task ${this.taskId}.${this.instanceId} disposal:`, error)
1634-
// Don't rethrow - we want abort to always succeed
1635-
}
1539+
try {
1540+
this.dispose() // Call the centralized dispose method
1541+
} catch (error) {
1542+
console.error(`Error during task ${this.taskId}.${this.instanceId} disposal:`, error)
1543+
// Don't rethrow - we want abort to always succeed
16361544
}
1637-
16381545
// Save the countdown message in the automatic retry or other content.
16391546
try {
16401547
// Save the countdown message in the automatic retry or other content.

src/core/task/__tests__/Task.presentResumableAsk.abort-reset.spec.ts

Lines changed: 0 additions & 146 deletions
This file was deleted.

src/core/task/__tests__/Task.spec.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1722,12 +1722,12 @@ describe("Cline", () => {
17221722
// Mock the dispose method to track cleanup
17231723
const disposeSpy = vi.spyOn(task, "dispose").mockImplementation(() => {})
17241724

1725-
// Call abortTask (soft cancel - same path as UI Cancel button)
1725+
// Call abortTask
17261726
await task.abortTask()
17271727

1728-
// Verify the same behavior as Cancel button: soft abort sets abort flag but does not dispose
1728+
// Verify the same behavior as Cancel button
17291729
expect(task.abort).toBe(true)
1730-
expect(disposeSpy).not.toHaveBeenCalled()
1730+
expect(disposeSpy).toHaveBeenCalled()
17311731
})
17321732

17331733
it("should work with TaskLike interface", async () => {
@@ -1771,8 +1771,8 @@ describe("Cline", () => {
17711771
// Spy on console.error to verify error is logged
17721772
const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {})
17731773

1774-
// abortTask should not throw even if dispose fails (hard abort triggers dispose)
1775-
await expect(task.abortTask(true)).resolves.not.toThrow()
1774+
// abortTask should not throw even if dispose fails
1775+
await expect(task.abortTask()).resolves.not.toThrow()
17761776

17771777
// Verify error was logged
17781778
expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining("Error during task"), mockError)

0 commit comments

Comments
 (0)