diff --git a/src/core/Cline.ts b/src/core/Cline.ts index 397d3aca80..f1b4d2f4d4 100644 --- a/src/core/Cline.ts +++ b/src/core/Cline.ts @@ -92,6 +92,7 @@ export class Cline { private isSubTask: boolean = false // a flag that indicated if this Cline instance is paused (waiting for provider to resume it after subtask completion) private isPaused: boolean = false + private pausedModeSlug: string = defaultModeSlug api: ApiHandler private terminalManager: TerminalManager private urlContentFetcher: UrlContentFetcher @@ -2642,15 +2643,24 @@ export class Cline { break } + // before switching roo mode (currently a global settings), save the current mode so we can + // resume the parent task (this Cline instance) later with the same mode + const currentMode = + (await this.providerRef.deref()?.getState())?.mode ?? defaultModeSlug + this.pausedModeSlug = currentMode + // Switch mode first, then create new task instance const provider = this.providerRef.deref() if (provider) { await provider.handleModeSwitch(mode) + this.providerRef + .deref() + ?.log(`[subtasks] Task: ${this.taskNumber} creating new task in '${mode}' mode`) await provider.initClineWithSubTask(message) pushToolResult( `Successfully created new task in ${targetMode.name} mode with message: ${message}`, ) - // pasue the current task and start the new task + // set the isPaused flag to true so the parent task can wait for the sub-task to finish this.isPaused = true } else { pushToolResult( @@ -2899,7 +2909,20 @@ export class Cline { // in this Cline request loop, we need to check if this cline (Task) instance has been asked to wait // for a sub-task (it has launched) to finish before continuing if (this.isPaused) { + this.providerRef.deref()?.log(`[subtasks] Task: ${this.taskNumber} has paused`) await this.waitForResume() + this.providerRef.deref()?.log(`[subtasks] Task: ${this.taskNumber} has resumed`) + // waiting for resume is done, resume the task mode + const currentMode = (await this.providerRef.deref()?.getState())?.mode ?? defaultModeSlug + if (currentMode !== this.pausedModeSlug) { + // the mode has changed, we need to switch back to the paused mode + await this.providerRef.deref()?.handleModeSwitch(this.pausedModeSlug) + this.providerRef + .deref() + ?.log( + `[subtasks] Task: ${this.taskNumber} has switched back to mode: '${this.pausedModeSlug}' from mode: '${currentMode}'`, + ) + } } // getting verbose details is an expensive operation, it uses globby to top-down build file structure of project which for large projects can take a few seconds diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 4fa9595ba6..7f412eefde 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -94,7 +94,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { // Adds a new Cline instance to clineStack, marking the start of a new task. // The instance is pushed to the top of the stack (LIFO order). // When the task is completed, the top instance is removed, reactivating the previous task. - addClineToStack(cline: Cline): void { + async addClineToStack(cline: Cline) { // if cline.getTaskNumber() is -1, it means it is a new task if (cline.getTaskNumber() === -1) { // increase last cline number by 1 @@ -107,6 +107,10 @@ export class ClineProvider implements vscode.WebviewViewProvider { } // push the cline instance to the stack this.clineStack.push(cline) + // get the current mode + const currentMode = (await this.getState()).mode + // log the task number and the mode + this.log(`[subtasks] Task: ${cline.getTaskNumber()} started at '${currentMode}' mode`) } // Removes and destroys the top Cline instance (the current finished task), activating the previous one (resuming the parent task). @@ -114,9 +118,11 @@ export class ClineProvider implements vscode.WebviewViewProvider { // pop the top Cline instance from the stack var clineToBeRemoved = this.clineStack.pop() if (clineToBeRemoved) { + const removedTaskNumber = clineToBeRemoved.getTaskNumber() await clineToBeRemoved.abortTask() // make sure no reference kept, once promises end it will be garbage collected clineToBeRemoved = undefined + this.log(`[subtasks] Task: ${removedTaskNumber} stopped`) } // if the stack is empty, reset the last task number if (this.clineStack.length === 0) { @@ -417,7 +423,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { images, experiments, }) - this.addClineToStack(newCline) + await this.addClineToStack(newCline) } public async initClineWithHistoryItem(historyItem: HistoryItem) { @@ -449,7 +455,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { }) // get this cline task number id from the history item and set it to newCline newCline.setTaskNumber(historyItem.number) - this.addClineToStack(newCline) + await this.addClineToStack(newCline) } public async postMessageToWebview(message: ExtensionMessage) { diff --git a/src/core/webview/__tests__/ClineProvider.test.ts b/src/core/webview/__tests__/ClineProvider.test.ts index 90d2da45f4..bb646ccef8 100644 --- a/src/core/webview/__tests__/ClineProvider.test.ts +++ b/src/core/webview/__tests__/ClineProvider.test.ts @@ -415,7 +415,7 @@ describe("ClineProvider", () => { const mockCline = new Cline() // Create a new mocked instance // add the mock object to the stack - provider.addClineToStack(mockCline) + await provider.addClineToStack(mockCline) // get the stack size before the abort call const stackSizeBeforeAbort = provider.getClineStackSize() @@ -433,7 +433,7 @@ describe("ClineProvider", () => { expect(stackSizeBeforeAbort - stackSizeAfterAbort).toBe(1) }) - test("addClineToStack adds multiple Cline instances to the stack", () => { + test("addClineToStack adds multiple Cline instances to the stack", async () => { // Setup Cline instance with auto-mock from the top of the file const { Cline } = require("../../Cline") // Get the mocked class const mockCline1 = new Cline() // Create a new mocked instance @@ -442,8 +442,8 @@ describe("ClineProvider", () => { Object.defineProperty(mockCline2, "taskId", { value: "test-task-id-2", writable: true }) // add Cline instances to the stack - provider.addClineToStack(mockCline1) - provider.addClineToStack(mockCline2) + await provider.addClineToStack(mockCline1) + await provider.addClineToStack(mockCline2) // verify cline instances were added to the stack expect(provider.getClineStackSize()).toBe(2) @@ -847,7 +847,7 @@ describe("ClineProvider", () => { const mockCline = new Cline() // Create a new mocked instance mockCline.clineMessages = mockMessages // Set test-specific messages mockCline.apiConversationHistory = mockApiHistory // Set API history - provider.addClineToStack(mockCline) // Add the mocked instance to the stack + await provider.addClineToStack(mockCline) // Add the mocked instance to the stack // Mock getTaskWithId ;(provider as any).getTaskWithId = jest.fn().mockResolvedValue({ @@ -894,7 +894,7 @@ describe("ClineProvider", () => { const mockCline = new Cline() // Create a new mocked instance mockCline.clineMessages = mockMessages mockCline.apiConversationHistory = mockApiHistory - provider.addClineToStack(mockCline) + await provider.addClineToStack(mockCline) // Mock getTaskWithId ;(provider as any).getTaskWithId = jest.fn().mockResolvedValue({ @@ -921,7 +921,7 @@ describe("ClineProvider", () => { const mockCline = new Cline() // Create a new mocked instance mockCline.clineMessages = [{ ts: 1000 }, { ts: 2000 }] mockCline.apiConversationHistory = [{ ts: 1000 }, { ts: 2000 }] - provider.addClineToStack(mockCline) + await provider.addClineToStack(mockCline) // Trigger message deletion const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0] @@ -1424,7 +1424,7 @@ describe("ClineProvider", () => { // Setup Cline instance with auto-mock from the top of the file const { Cline } = require("../../Cline") // Get the mocked class const mockCline = new Cline() // Create a new mocked instance - provider.addClineToStack(mockCline) + await provider.addClineToStack(mockCline) const testApiConfig = { apiProvider: "anthropic" as const,