Ver código fonte

subtasks alpha version (still in development)

ShayBC 10 meses atrás
pai
commit
c3c3874593

+ 1 - 1
src/activate/registerCommands.ts

@@ -20,7 +20,7 @@ export const registerCommands = (options: RegisterCommandOptions) => {
 const getCommandsMap = ({ context, outputChannel, provider }: RegisterCommandOptions) => {
 	return {
 		"roo-cline.plusButtonClicked": async () => {
-			await provider.clearTask()
+			await provider.removeClineFromStack()
 			await provider.postStateToWebview()
 			await provider.postMessageToWebview({ type: "action", action: "chatButtonClicked" })
 		},

+ 78 - 1
src/core/Cline.ts

@@ -75,6 +75,10 @@ type UserContent = Array<
 
 export class Cline {
 	readonly taskId: string
+	// a flag that indicated if this Cline instance is a subtask (on finish return control to parent task)
+	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
 	api: ApiHandler
 	private terminalManager: TerminalManager
 	private urlContentFetcher: UrlContentFetcher
@@ -160,6 +164,12 @@ export class Cline {
 		}
 	}
 
+	// a helper function to set the private member isSubTask to true
+	// and by that set this Cline instance to be a subtask (on finish return control to parent task)
+	setSubTask() {
+		this.isSubTask = true
+	}
+
 	// Add method to update diffStrategy
 	async updateDiffStrategy(experimentalDiffStrategy?: boolean) {
 		// If not provided, get from current state
@@ -480,6 +490,43 @@ export class Cline {
 		])
 	}
 
+	async resumePausedTask() {
+		// release this Cline instance from paused state
+		this.isPaused = false
+
+		// Clear any existing ask state and simulate a completed ask response
+		// this.askResponse = "messageResponse";
+		// this.askResponseText = "Sub Task finished Successfully!\nthere is no need to perform this task again, please continue to the next task.";
+		// this.askResponseImages = undefined;
+		// this.lastMessageTs = Date.now();
+
+		// This adds the completion message to conversation history
+		await this.say(
+			"text",
+			"Sub Task finished Successfully!\nthere is no need to perform this task again, please continue to the next task.",
+		)
+
+		// this.userMessageContent.push({
+		// 	type: "text",
+		// 	text: `${"Result:\\n\\nSub Task finished Successfully!\nthere is no need to perform this task again, please continue to the next task."}`,
+		// })
+
+		try {
+			// Resume parent task
+			await this.ask("resume_task")
+		} catch (error) {
+			if (error.message === "Current ask promise was ignored") {
+				// ignore the ignored promise, since it was performed by launching a subtask and it probably took more then 1 sec,
+				// also set the didAlreadyUseTool flag to indicate that the tool was already used, and there is no need to relaunch it
+				this.didAlreadyUseTool = true
+			} else {
+				// Handle error appropriately
+				console.error("Failed to resume task:", error)
+				throw error
+			}
+		}
+	}
+
 	private async resumeTaskFromHistory() {
 		const modifiedClineMessages = await this.getSavedClineMessages()
 
@@ -2553,10 +2600,12 @@ export class Cline {
 								const provider = this.providerRef.deref()
 								if (provider) {
 									await provider.handleModeSwitch(mode)
-									await provider.initClineWithTask(message)
+									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
+									this.isPaused = true
 								} else {
 									pushToolResult(
 										formatResponse.toolError("Failed to create new task: provider not available"),
@@ -2648,6 +2697,10 @@ export class Cline {
 									if (lastMessage && lastMessage.ask !== "command") {
 										// havent sent a command message yet so first send completion_result then command
 										await this.say("completion_result", result, undefined, false)
+										if (this.isSubTask) {
+											// tell the provider to remove the current subtask and resume the previous task in the stack
+											this.providerRef.deref()?.finishSubTask()
+										}
 									}
 
 									// complete command message
@@ -2665,6 +2718,10 @@ export class Cline {
 									commandResult = execCommandResult
 								} else {
 									await this.say("completion_result", result, undefined, false)
+									if (this.isSubTask) {
+										// tell the provider to remove the current subtask and resume the previous task in the stack
+										this.providerRef.deref()?.finishSubTask()
+									}
 								}
 
 								// we already sent completion_result says, an empty string asks relinquishes control over button and field
@@ -2740,6 +2797,20 @@ export class Cline {
 		}
 	}
 
+	// this function checks if this Cline instance is set to pause state and wait for being resumed,
+	// this is used when a sub-task is launched and the parent task is waiting for it to finish
+	async waitForResume() {
+		// wait until isPaused is false
+		await new Promise<void>((resolve) => {
+			const interval = setInterval(() => {
+				if (!this.isPaused) {
+					clearInterval(interval)
+					resolve()
+				}
+			}, 1000) // TBD: the 1 sec should be added to the settings, also should add a timeout to prevent infinit wait
+		})
+	}
+
 	async recursivelyMakeClineRequests(
 		userContent: UserContent,
 		includeFileDetails: boolean = false,
@@ -2779,6 +2850,12 @@ export class Cline {
 			await this.checkpointSave({ isFirst: true })
 		}
 
+		// 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) {
+			await this.waitForResume()
+		}
+
 		// 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
 		// for the best UX we show a placeholder api_req_started message with a loading spinner as this happens
 		await this.say(

+ 137 - 74
src/core/webview/ClineProvider.ts

@@ -147,7 +147,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 	private disposables: vscode.Disposable[] = []
 	private view?: vscode.WebviewView | vscode.WebviewPanel
 	private isViewLaunched = false
-	private cline?: Cline
+	private clineStack: Cline[] = []
 	private workspaceTracker?: WorkspaceTracker
 	protected mcpHub?: McpHub // Change from private to protected
 	private latestAnnouncementId = "jan-21-2025-custom-modes" // update to some unique identifier when we add a new announcement
@@ -176,6 +176,55 @@ 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 {
+		this.clineStack.push(cline)
+	}
+
+	// Removes and destroys the top Cline instance (the current finished task), activating the previous one (resuming the parent task).
+	async removeClineFromStack() {
+		// pop the top Cline instance from the stack
+		var clineToBeRemoved = this.clineStack.pop()
+		if (clineToBeRemoved) {
+			await clineToBeRemoved.abortTask()
+			// make sure no reference kept, once promises end it will be garbage collected
+			clineToBeRemoved = undefined
+		}
+	}
+
+	// remove the cline object with the received clineId, and all the cline objects bove it in the stack
+	// for each cline object removed, pop it from the stack, abort the task and set it to undefined
+	async removeClineWithIdFromStack(clineId: string) {
+		const index = this.clineStack.findIndex((c) => c.taskId === clineId)
+		if (index === -1) {
+			return
+		}
+		for (let i = this.clineStack.length - 1; i >= index; i--) {
+			this.removeClineFromStack()
+		}
+	}
+
+	// returns the current cline object in the stack (the top one)
+	// if the stack is empty, returns undefined
+	getCurrentCline(): Cline | undefined {
+		if (this.clineStack.length === 0) {
+			return undefined
+		}
+		return this.clineStack[this.clineStack.length - 1]
+	}
+
+	// remove the current task/cline instance (at the top of the stack), ao this task is finished
+	// and resume the previous task/cline instance (if it exists)
+	// this is used when a sub task is finished and the parent task needs to be resumed
+	async finishSubTask() {
+		// remove the last cline instance from the stack (this is the finished sub task)
+		await this.removeClineFromStack()
+		// resume the last cline instance in the stack (if it exists - this is the 'parnt' calling task)
+		this.getCurrentCline()?.resumePausedTask()
+	}
+
 	/*
 	VSCode extensions use the disposable pattern to clean up resources when the sidebar/editor tab is closed by the user or system. This applies to event listening, commands, interacting with the UI, etc.
 	- https://vscode-docs.readthedocs.io/en/stable/extensions/patterns-and-principles/
@@ -183,7 +232,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 	*/
 	async dispose() {
 		this.outputChannel.appendLine("Disposing ClineProvider...")
-		await this.clearTask()
+		await this.removeClineFromStack()
 		this.outputChannel.appendLine("Cleared task")
 		if (this.view && "dispose" in this.view) {
 			this.view.dispose()
@@ -236,7 +285,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 			return false
 		}
 
-		if (visibleProvider.cline) {
+		// check if there is a cline instance in the stack (if this provider has an active task)
+		if (visibleProvider.getCurrentCline()) {
 			return true
 		}
 
@@ -267,7 +317,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 			return
 		}
 
-		if (visibleProvider.cline && command.endsWith("InCurrentTask")) {
+		if (visibleProvider.getCurrentCline() && command.endsWith("InCurrentTask")) {
 			await visibleProvider.postMessageToWebview({
 				type: "invoke",
 				invoke: "sendMessage",
@@ -303,7 +353,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 			return
 		}
 
-		if (visibleProvider.cline && command.endsWith("InCurrentTask")) {
+		if (visibleProvider.getCurrentCline() && command.endsWith("InCurrentTask")) {
 			await visibleProvider.postMessageToWebview({
 				type: "invoke",
 				invoke: "sendMessage",
@@ -392,13 +442,21 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 		)
 
 		// if the extension is starting a new session, clear previous task state
-		this.clearTask()
+		await this.removeClineFromStack()
 
 		this.outputChannel.appendLine("Webview view resolved")
 	}
 
+	// a wrapper that inits a new Cline instance (Task) ans setting it as a sub task of the current task
+	public async initClineWithSubTask(task?: string, images?: string[]) {
+		await this.initClineWithTask(task, images)
+		this.getCurrentCline()?.setSubTask()
+	}
+
+	// when initializing a new task, (not from history but from a tool command new_task) there is no need to remove the previouse task
+	// since the new task is a sub task of the previous one, and when it finishes it is removed from the stack and the caller is resumed
+	// in this way we can have a chain of tasks, each one being a sub task of the previous one until the main task is finished
 	public async initClineWithTask(task?: string, images?: string[]) {
-		await this.clearTask()
 		const {
 			apiConfiguration,
 			customModePrompts,
@@ -413,7 +471,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 		const modePrompt = customModePrompts?.[mode] as PromptComponent
 		const effectiveInstructions = [globalInstructions, modePrompt?.customInstructions].filter(Boolean).join("\n\n")
 
-		this.cline = new Cline(
+		const newCline = new Cline(
 			this,
 			apiConfiguration,
 			effectiveInstructions,
@@ -425,10 +483,11 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 			undefined,
 			experiments,
 		)
+		this.addClineToStack(newCline)
 	}
 
 	public async initClineWithHistoryItem(historyItem: HistoryItem) {
-		await this.clearTask()
+		await this.removeClineFromStack()
 
 		const {
 			apiConfiguration,
@@ -444,7 +503,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 		const modePrompt = customModePrompts?.[mode] as PromptComponent
 		const effectiveInstructions = [globalInstructions, modePrompt?.customInstructions].filter(Boolean).join("\n\n")
 
-		this.cline = new Cline(
+		const newCline = new Cline(
 			this,
 			apiConfiguration,
 			effectiveInstructions,
@@ -456,6 +515,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 			historyItem,
 			experiments,
 		)
+		this.addClineToStack(newCline)
 	}
 
 	public async postMessageToWebview(message: ExtensionMessage) {
@@ -810,11 +870,15 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 						await this.postStateToWebview()
 						break
 					case "askResponse":
-						this.cline?.handleWebviewAskResponse(message.askResponse!, message.text, message.images)
+						this.getCurrentCline()?.handleWebviewAskResponse(
+							message.askResponse!,
+							message.text,
+							message.images,
+						)
 						break
 					case "clearTask":
 						// newTask will start a new task with a given task text, while clear task resets the current session and allows for a new task to be started
-						await this.clearTask()
+						await this.removeClineFromStack()
 						await this.postStateToWebview()
 						break
 					case "didShowAnnouncement":
@@ -826,7 +890,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 						await this.postMessageToWebview({ type: "selectedImages", images })
 						break
 					case "exportCurrentTask":
-						const currentTaskId = this.cline?.taskId
+						const currentTaskId = this.getCurrentCline()?.taskId
 						if (currentTaskId) {
 							this.exportTaskWithId(currentTaskId)
 						}
@@ -892,7 +956,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 						const result = checkoutDiffPayloadSchema.safeParse(message.payload)
 
 						if (result.success) {
-							await this.cline?.checkpointDiff(result.data)
+							await this.getCurrentCline()?.checkpointDiff(result.data)
 						}
 
 						break
@@ -903,13 +967,13 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 							await this.cancelTask()
 
 							try {
-								await pWaitFor(() => this.cline?.isInitialized === true, { timeout: 3_000 })
+								await pWaitFor(() => this.getCurrentCline()?.isInitialized === true, { timeout: 3_000 })
 							} catch (error) {
 								vscode.window.showErrorMessage("Timed out when attempting to restore checkpoint.")
 							}
 
 							try {
-								await this.cline?.checkpointRestore(result.data)
+								await this.getCurrentCline()?.checkpointRestore(result.data)
 							} catch (error) {
 								vscode.window.showErrorMessage("Failed to restore checkpoint.")
 							}
@@ -1145,42 +1209,43 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 						)
 						if (
 							(answer === "Just this message" || answer === "This and all subsequent messages") &&
-							this.cline &&
+							this.getCurrentCline() &&
 							typeof message.value === "number" &&
 							message.value
 						) {
 							const timeCutoff = message.value - 1000 // 1 second buffer before the message to delete
-							const messageIndex = this.cline.clineMessages.findIndex(
-								(msg) => msg.ts && msg.ts >= timeCutoff,
-							)
-							const apiConversationHistoryIndex = this.cline.apiConversationHistory.findIndex(
+							const messageIndex = this.getCurrentCline()!.clineMessages.findIndex(
 								(msg) => msg.ts && msg.ts >= timeCutoff,
 							)
+							const apiConversationHistoryIndex =
+								this.getCurrentCline()?.apiConversationHistory.findIndex(
+									(msg) => msg.ts && msg.ts >= timeCutoff,
+								)
 
 							if (messageIndex !== -1) {
-								const { historyItem } = await this.getTaskWithId(this.cline.taskId)
+								const { historyItem } = await this.getTaskWithId(this.getCurrentCline()!.taskId)
 
 								if (answer === "Just this message") {
 									// Find the next user message first
-									const nextUserMessage = this.cline.clineMessages
-										.slice(messageIndex + 1)
+									const nextUserMessage = this.getCurrentCline()!
+										.clineMessages.slice(messageIndex + 1)
 										.find((msg) => msg.type === "say" && msg.say === "user_feedback")
 
 									// Handle UI messages
 									if (nextUserMessage) {
 										// Find absolute index of next user message
-										const nextUserMessageIndex = this.cline.clineMessages.findIndex(
+										const nextUserMessageIndex = this.getCurrentCline()!.clineMessages.findIndex(
 											(msg) => msg === nextUserMessage,
 										)
 										// Keep messages before current message and after next user message
-										await this.cline.overwriteClineMessages([
-											...this.cline.clineMessages.slice(0, messageIndex),
-											...this.cline.clineMessages.slice(nextUserMessageIndex),
+										await this.getCurrentCline()!.overwriteClineMessages([
+											...this.getCurrentCline()!.clineMessages.slice(0, messageIndex),
+											...this.getCurrentCline()!.clineMessages.slice(nextUserMessageIndex),
 										])
 									} else {
 										// If no next user message, keep only messages before current message
-										await this.cline.overwriteClineMessages(
-											this.cline.clineMessages.slice(0, messageIndex),
+										await this.getCurrentCline()!.overwriteClineMessages(
+											this.getCurrentCline()!.clineMessages.slice(0, messageIndex),
 										)
 									}
 
@@ -1188,30 +1253,36 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 									if (apiConversationHistoryIndex !== -1) {
 										if (nextUserMessage && nextUserMessage.ts) {
 											// Keep messages before current API message and after next user message
-											await this.cline.overwriteApiConversationHistory([
-												...this.cline.apiConversationHistory.slice(
+											await this.getCurrentCline()!.overwriteApiConversationHistory([
+												...this.getCurrentCline()!.apiConversationHistory.slice(
 													0,
 													apiConversationHistoryIndex,
 												),
-												...this.cline.apiConversationHistory.filter(
+												...this.getCurrentCline()!.apiConversationHistory.filter(
 													(msg) => msg.ts && msg.ts >= nextUserMessage.ts,
 												),
 											])
 										} else {
 											// If no next user message, keep only messages before current API message
-											await this.cline.overwriteApiConversationHistory(
-												this.cline.apiConversationHistory.slice(0, apiConversationHistoryIndex),
+											await this.getCurrentCline()!.overwriteApiConversationHistory(
+												this.getCurrentCline()!.apiConversationHistory.slice(
+													0,
+													apiConversationHistoryIndex,
+												),
 											)
 										}
 									}
 								} else if (answer === "This and all subsequent messages") {
 									// Delete this message and all that follow
-									await this.cline.overwriteClineMessages(
-										this.cline.clineMessages.slice(0, messageIndex),
+									await this.getCurrentCline()!.overwriteClineMessages(
+										this.getCurrentCline()!.clineMessages.slice(0, messageIndex),
 									)
 									if (apiConversationHistoryIndex !== -1) {
-										await this.cline.overwriteApiConversationHistory(
-											this.cline.apiConversationHistory.slice(0, apiConversationHistoryIndex),
+										await this.getCurrentCline()!.overwriteApiConversationHistory(
+											this.getCurrentCline()!.apiConversationHistory.slice(
+												0,
+												apiConversationHistoryIndex,
+											),
 										)
 									}
 								}
@@ -1481,8 +1552,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 						await this.updateGlobalState("experiments", updatedExperiments)
 
 						// Update diffStrategy in current Cline instance if it exists
-						if (message.values[EXPERIMENT_IDS.DIFF_STRATEGY] !== undefined && this.cline) {
-							await this.cline.updateDiffStrategy(
+						if (message.values[EXPERIMENT_IDS.DIFF_STRATEGY] !== undefined && this.getCurrentCline()) {
+							await this.getCurrentCline()!.updateDiffStrategy(
 								Experiments.isEnabled(updatedExperiments, EXPERIMENT_IDS.DIFF_STRATEGY),
 							)
 						}
@@ -1724,25 +1795,25 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 			this.updateGlobalState("requestyModelInfo", requestyModelInfo),
 			this.updateGlobalState("modelTemperature", modelTemperature),
 		])
-		if (this.cline) {
-			this.cline.api = buildApiHandler(apiConfiguration)
+		if (this.getCurrentCline()) {
+			this.getCurrentCline()!.api = buildApiHandler(apiConfiguration)
 		}
 	}
 
 	async cancelTask() {
-		if (this.cline) {
-			const { historyItem } = await this.getTaskWithId(this.cline.taskId)
-			this.cline.abortTask()
+		if (this.getCurrentCline()) {
+			const { historyItem } = await this.getTaskWithId(this.getCurrentCline()!.taskId)
+			this.getCurrentCline()!.abortTask()
 
 			await pWaitFor(
 				() =>
-					this.cline === undefined ||
-					this.cline.isStreaming === false ||
-					this.cline.didFinishAbortingStream ||
+					this.getCurrentCline()! === undefined ||
+					this.getCurrentCline()!.isStreaming === false ||
+					this.getCurrentCline()!.didFinishAbortingStream ||
 					// If only the first chunk is processed, then there's no
 					// need to wait for graceful abort (closes edits, browser,
 					// etc).
-					this.cline.isWaitingForFirstChunk,
+					this.getCurrentCline()!.isWaitingForFirstChunk,
 				{
 					timeout: 3_000,
 				},
@@ -1750,11 +1821,11 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 				console.error("Failed to abort task")
 			})
 
-			if (this.cline) {
+			if (this.getCurrentCline()) {
 				// 'abandoned' will prevent this Cline instance from affecting
 				// future Cline instances. This may happen if its hanging on a
 				// streaming request.
-				this.cline.abandoned = true
+				this.getCurrentCline()!.abandoned = true
 			}
 
 			// Clears task again, so we need to abortTask manually above.
@@ -1765,8 +1836,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 	async updateCustomInstructions(instructions?: string) {
 		// User may be clearing the field
 		await this.updateGlobalState("customInstructions", instructions || undefined)
-		if (this.cline) {
-			this.cline.customInstructions = instructions || undefined
+		if (this.getCurrentCline()) {
+			this.getCurrentCline()!.customInstructions = instructions || undefined
 		}
 		await this.postStateToWebview()
 	}
@@ -1980,8 +2051,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 		await this.updateGlobalState("apiProvider", openrouter)
 		await this.storeSecret("openRouterApiKey", apiKey)
 		await this.postStateToWebview()
-		if (this.cline) {
-			this.cline.api = buildApiHandler({ apiProvider: openrouter, openRouterApiKey: apiKey })
+		if (this.getCurrentCline()) {
+			this.getCurrentCline()!.api = buildApiHandler({ apiProvider: openrouter, openRouterApiKey: apiKey })
 		}
 		// await this.postMessageToWebview({ type: "action", action: "settingsButtonClicked" }) // bad ux if user is on welcome
 	}
@@ -2012,8 +2083,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 		await this.updateGlobalState("apiProvider", glama)
 		await this.storeSecret("glamaApiKey", apiKey)
 		await this.postStateToWebview()
-		if (this.cline) {
-			this.cline.api = buildApiHandler({
+		if (this.getCurrentCline()) {
+			this.getCurrentCline()!.api = buildApiHandler({
 				apiProvider: glama,
 				glamaApiKey: apiKey,
 			})
@@ -2295,7 +2366,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 	}
 
 	async showTaskWithId(id: string) {
-		if (id !== this.cline?.taskId) {
+		if (id !== this.getCurrentCline()?.taskId) {
 			// non-current task
 			const { historyItem } = await this.getTaskWithId(id)
 			await this.initClineWithHistoryItem(historyItem) // clears existing task
@@ -2309,8 +2380,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 	}
 
 	async deleteTaskWithId(id: string) {
-		if (id === this.cline?.taskId) {
-			await this.clearTask()
+		if (id === this.getCurrentCline()?.taskId) {
+			await this.removeClineWithIdFromStack(id)
 		}
 
 		const { taskDirPath, apiConversationHistoryFilePath, uiMessagesFilePath } = await this.getTaskWithId(id)
@@ -2434,10 +2505,10 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 			alwaysAllowMcp: alwaysAllowMcp ?? false,
 			alwaysAllowModeSwitch: alwaysAllowModeSwitch ?? false,
 			uriScheme: vscode.env.uriScheme,
-			currentTaskItem: this.cline?.taskId
-				? (taskHistory || []).find((item) => item.id === this.cline?.taskId)
+			currentTaskItem: this.getCurrentCline()?.taskId
+				? (taskHistory || []).find((item) => item.id === this.getCurrentCline()?.taskId)
 				: undefined,
-			clineMessages: this.cline?.clineMessages || [],
+			clineMessages: this.getCurrentCline()?.clineMessages || [],
 			taskHistory: (taskHistory || [])
 				.filter((item: HistoryItem) => item.ts && item.task)
 				.sort((a: HistoryItem, b: HistoryItem) => b.ts - a.ts),
@@ -2472,11 +2543,6 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 		}
 	}
 
-	async clearTask() {
-		this.cline?.abortTask()
-		this.cline = undefined // removes reference to it, so once promises end it will be garbage collected
-	}
-
 	// Caching mechanism to keep track of webview messages + API conversation history per provider instance
 
 	/*
@@ -2914,10 +2980,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 		}
 		await this.configManager.resetAllConfigs()
 		await this.customModesManager.resetCustomModes()
-		if (this.cline) {
-			this.cline.abortTask()
-			this.cline = undefined
-		}
+		await this.removeClineFromStack()
 		await this.postStateToWebview()
 		await this.postMessageToWebview({ type: "action", action: "chatButtonClicked" })
 	}
@@ -2935,7 +2998,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 	}
 
 	get messages() {
-		return this.cline?.clineMessages || []
+		return this.getCurrentCline()?.clineMessages || []
 	}
 
 	// Add public getter

+ 1 - 1
src/core/webview/__tests__/ClineProvider.test.ts

@@ -381,7 +381,7 @@ describe("ClineProvider", () => {
 		// @ts-ignore - accessing private property for testing
 		provider.cline = { abortTask: mockAbortTask }
 
-		await provider.clearTask()
+		await provider.removeClineFromStack()
 
 		expect(mockAbortTask).toHaveBeenCalled()
 		// @ts-ignore - accessing private property for testing

+ 1 - 1
src/exports/index.ts

@@ -15,7 +15,7 @@ export function createClineAPI(outputChannel: vscode.OutputChannel, sidebarProvi
 
 		startNewTask: async (task?: string, images?: string[]) => {
 			outputChannel.appendLine("Starting new task")
-			await sidebarProvider.clearTask()
+			await sidebarProvider.removeClineFromStack()
 			await sidebarProvider.postStateToWebview()
 			await sidebarProvider.postMessageToWebview({ type: "action", action: "chatButtonClicked" })
 			await sidebarProvider.postMessageToWebview({