Skip to content

Commit 3b2d6bc

Browse files
authored
Merge pull request RooCodeInc#1564 from cannuri/cannuri/fix_subtask_cancel_resume_bug
fix: Preserve parent-child relationship when cancelling subtasks
2 parents 99258e4 + 6ce8e56 commit 3b2d6bc

File tree

2 files changed

+132
-2
lines changed

2 files changed

+132
-2
lines changed

e2e/src/suite/task.test.ts

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import * as assert from "assert"
2+
import * as vscode from "vscode"
23

34
suite("Roo Code Task", () => {
45
test("Should handle prompt and response correctly", async function () {
@@ -48,4 +49,113 @@ suite("Roo Code Task", () => {
4849
"Did not receive expected response containing 'My name is Roo'",
4950
)
5051
})
52+
53+
test("Should handle subtask cancellation and resumption correctly", async function () {
54+
this.timeout(60000) // Increase timeout for this test
55+
const interval = 1000
56+
57+
if (!globalThis.extension) {
58+
assert.fail("Extension not found")
59+
}
60+
61+
// Ensure the webview is launched
62+
await ensureWebviewLaunched(30000, interval)
63+
64+
// Set up required global state
65+
await globalThis.provider.updateGlobalState("mode", "Code")
66+
await globalThis.provider.updateGlobalState("alwaysAllowModeSwitch", true)
67+
await globalThis.provider.updateGlobalState("alwaysAllowSubtasks", true)
68+
await globalThis.provider.updateGlobalState("autoApprovalEnabled", true)
69+
70+
// 1. Start a parent task that will create a subtask
71+
await globalThis.api.startNewTask(
72+
"You are the parent task. Create a subtask by using the new_task tool with the message 'You are the subtask'. " +
73+
"After creating the subtask, wait for it to complete and then respond with 'Parent task resumed'.",
74+
)
75+
76+
// Wait for the parent task to use the new_task tool
77+
await waitForToolUse("new_task", 30000, interval)
78+
79+
// Wait for the subtask to be created and start responding
80+
await waitForMessage("You are the subtask", 10000, interval)
81+
82+
// 3. Cancel the current task (which should be the subtask)
83+
await globalThis.provider.cancelTask()
84+
85+
// 4. Check if the parent task is still waiting (not resumed)
86+
// We need to wait a bit to ensure any task resumption would have happened
87+
await new Promise((resolve) => setTimeout(resolve, 5000))
88+
89+
// The parent task should not have resumed yet, so we shouldn't see "Parent task resumed"
90+
assert.ok(
91+
!globalThis.provider.messages.some(
92+
({ type, text }) => type === "say" && text?.includes("Parent task resumed"),
93+
),
94+
"Parent task should not have resumed after subtask cancellation",
95+
)
96+
97+
// 5. Start a new task with the same message as the subtask
98+
await globalThis.api.startNewTask("You are the subtask")
99+
100+
// Wait for the subtask to complete
101+
await waitForMessage("Task complete", 20000, interval)
102+
103+
// 6. Verify that the parent task is still not resumed
104+
// We need to wait a bit to ensure any task resumption would have happened
105+
await new Promise((resolve) => setTimeout(resolve, 5000))
106+
107+
// The parent task should still not have resumed
108+
assert.ok(
109+
!globalThis.provider.messages.some(
110+
({ type, text }) => type === "say" && text?.includes("Parent task resumed"),
111+
),
112+
"Parent task should not have resumed after subtask completion",
113+
)
114+
115+
// Clean up - cancel all tasks
116+
await globalThis.provider.cancelTask()
117+
})
51118
})
119+
120+
// Helper functions
121+
async function ensureWebviewLaunched(timeout: number, interval: number): Promise<void> {
122+
const startTime = Date.now()
123+
while (Date.now() - startTime < timeout) {
124+
if (globalThis.provider.viewLaunched) {
125+
return
126+
}
127+
await new Promise((resolve) => setTimeout(resolve, interval))
128+
}
129+
throw new Error("Webview failed to launch within timeout")
130+
}
131+
132+
async function waitForToolUse(toolName: string, timeout: number, interval: number): Promise<void> {
133+
const startTime = Date.now()
134+
while (Date.now() - startTime < timeout) {
135+
const messages = globalThis.provider.messages
136+
if (
137+
messages.some(
138+
(message) =>
139+
message.type === "say" && message.say === "tool" && message.text && message.text.includes(toolName),
140+
)
141+
) {
142+
return
143+
}
144+
await new Promise((resolve) => setTimeout(resolve, interval))
145+
}
146+
throw new Error(`Tool ${toolName} was not used within timeout`)
147+
}
148+
149+
async function waitForMessage(messageContent: string, timeout: number, interval: number): Promise<void> {
150+
const startTime = Date.now()
151+
while (Date.now() - startTime < timeout) {
152+
const messages = globalThis.provider.messages
153+
if (
154+
messages.some((message) => message.type === "say" && message.text && message.text.includes(messageContent))
155+
) {
156+
return
157+
}
158+
await new Promise((resolve) => setTimeout(resolve, interval))
159+
}
160+
throw new Error(`Message containing "${messageContent}" not found within timeout`)
161+
}

src/core/webview/ClineProvider.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2034,8 +2034,16 @@ export class ClineProvider implements vscode.WebviewViewProvider {
20342034

20352035
async cancelTask() {
20362036
if (this.getCurrentCline()) {
2037-
const { historyItem } = await this.getTaskWithId(this.getCurrentCline()!.taskId)
2038-
this.getCurrentCline()!.abortTask()
2037+
const currentCline = this.getCurrentCline()!
2038+
const { historyItem } = await this.getTaskWithId(currentCline.taskId)
2039+
2040+
// Store parent task information if this is a subtask
2041+
// Check if this is a subtask by seeing if it has a parent task
2042+
const parentTask = currentCline.getParentTask()
2043+
const isSubTask = parentTask !== undefined
2044+
const rootTask = isSubTask ? currentCline.getRootTask() : undefined
2045+
2046+
currentCline.abortTask()
20392047

20402048
await pWaitFor(
20412049
() =>
@@ -2062,6 +2070,18 @@ export class ClineProvider implements vscode.WebviewViewProvider {
20622070

20632071
// Clears task again, so we need to abortTask manually above.
20642072
await this.initClineWithHistoryItem(historyItem)
2073+
2074+
// Restore parent-child relationship if this was a subtask
2075+
if (isSubTask && this.getCurrentCline() && parentTask) {
2076+
this.getCurrentCline()!.setSubTask()
2077+
this.getCurrentCline()!.setParentTask(parentTask)
2078+
if (rootTask) {
2079+
this.getCurrentCline()!.setRootTask(rootTask)
2080+
}
2081+
this.log(
2082+
`[subtasks] Restored parent-child relationship for task: ${this.getCurrentCline()!.getTaskNumber()}`,
2083+
)
2084+
}
20652085
}
20662086
}
20672087

0 commit comments

Comments
 (0)