Skip to content

Commit 6ce8e56

Browse files
committed
fix: Preserve parent-child relationship when cancelling subtasks
This commit fixes an issue where subtasks weren't properly reporting back to parent tasks when cancelled and resumed. Previously, when a subtask was cancelled and a new task was started with the same message, the parent task would incorrectly resume, causing unexpected behavior. The fix: 1. Stores parent-child relationship information before cancelling a task 2. Restores this relationship after task reinitialization 3. Ensures parent tasks only resume when explicitly instructed to do so This approach maintains the correct task hierarchy throughout the cancellation and resumption process, preventing parent tasks from automatically resuming when unrelated tasks with similar messages are started.
1 parent 9dfdc42 commit 6ce8e56

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
@@ -1933,8 +1933,16 @@ export class ClineProvider implements vscode.WebviewViewProvider {
19331933

19341934
async cancelTask() {
19351935
if (this.getCurrentCline()) {
1936-
const { historyItem } = await this.getTaskWithId(this.getCurrentCline()!.taskId)
1937-
this.getCurrentCline()!.abortTask()
1936+
const currentCline = this.getCurrentCline()!
1937+
const { historyItem } = await this.getTaskWithId(currentCline.taskId)
1938+
1939+
// Store parent task information if this is a subtask
1940+
// Check if this is a subtask by seeing if it has a parent task
1941+
const parentTask = currentCline.getParentTask()
1942+
const isSubTask = parentTask !== undefined
1943+
const rootTask = isSubTask ? currentCline.getRootTask() : undefined
1944+
1945+
currentCline.abortTask()
19381946

19391947
await pWaitFor(
19401948
() =>
@@ -1961,6 +1969,18 @@ export class ClineProvider implements vscode.WebviewViewProvider {
19611969

19621970
// Clears task again, so we need to abortTask manually above.
19631971
await this.initClineWithHistoryItem(historyItem)
1972+
1973+
// Restore parent-child relationship if this was a subtask
1974+
if (isSubTask && this.getCurrentCline() && parentTask) {
1975+
this.getCurrentCline()!.setSubTask()
1976+
this.getCurrentCline()!.setParentTask(parentTask)
1977+
if (rootTask) {
1978+
this.getCurrentCline()!.setRootTask(rootTask)
1979+
}
1980+
this.log(
1981+
`[subtasks] Restored parent-child relationship for task: ${this.getCurrentCline()!.getTaskNumber()}`,
1982+
)
1983+
}
19641984
}
19651985
}
19661986

0 commit comments

Comments
 (0)