diff --git a/src/core/Cline.ts b/src/core/Cline.ts index 9c2977a266..9eee82f227 100644 --- a/src/core/Cline.ts +++ b/src/core/Cline.ts @@ -75,6 +75,7 @@ type UserContent = Array< export class Cline { readonly taskId: string + readonly parentTaskId?: string api: ApiHandler private terminalManager: TerminalManager private urlContentFetcher: UrlContentFetcher @@ -129,12 +130,14 @@ export class Cline { images?: string[] | undefined, historyItem?: HistoryItem | undefined, experiments?: Record, + parentTaskId?: string, ) { if (!task && !images && !historyItem) { throw new Error("Either historyItem or task/images must be provided") } this.taskId = crypto.randomUUID() + this.parentTaskId = parentTaskId this.api = buildApiHandler(apiConfiguration) this.terminalManager = new TerminalManager() this.urlContentFetcher = new UrlContentFetcher(provider.context) @@ -2553,7 +2556,7 @@ export class Cline { const provider = this.providerRef.deref() if (provider) { await provider.handleModeSwitch(mode) - await provider.initClineWithTask(message) + await provider.initClineWithTask(message, undefined, this.taskId) pushToolResult( `Successfully created new task in ${targetMode.name} mode with message: ${message}`, ) @@ -2667,10 +2670,22 @@ export class Cline { await this.say("completion_result", result, undefined, false) } + // If this is a child task, return result to parent + if (this.parentTaskId) { + const provider = this.providerRef.deref() + if (provider) { + try { + await provider.handleTaskResult(this.parentTaskId, result) + } catch (error) { + console.error("Failed to return result to parent task:", error) + } + } + } + // we already sent completion_result says, an empty string asks relinquishes control over button and field const { response, text, images } = await this.ask("completion_result", "", false) if (response === "yesButtonClicked") { - pushToolResult("") // signals to recursive loop to stop (for now this never happens since yesButtonClicked will trigger a new task) + pushToolResult("") // signals to recursive loop to stop break } await this.say("user_feedback", text ?? "", images) diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 05faa13834..9092db42ae 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -18,7 +18,7 @@ import WorkspaceTracker from "../../integrations/workspace/WorkspaceTracker" import { McpHub } from "../../services/mcp/McpHub" import { ApiConfiguration, ApiProvider, ModelInfo } from "../../shared/api" import { findLast } from "../../shared/array" -import { ApiConfigMeta, ExtensionMessage } from "../../shared/ExtensionMessage" +import { ApiConfigMeta, ExtensionMessage, ClineMessage } from "../../shared/ExtensionMessage" import { HistoryItem } from "../../shared/HistoryItem" import { checkoutDiffPayloadSchema, checkoutRestorePayloadSchema, WebviewMessage } from "../../shared/WebviewMessage" import { Mode, CustomModePrompts, PromptComponent, defaultModeSlug } from "../../shared/modes" @@ -277,7 +277,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { return } - await visibleProvider.initClineWithTask(prompt) + await visibleProvider.initClineWithTask(prompt, undefined, undefined) } public static async handleTerminalAction( @@ -397,7 +397,80 @@ export class ClineProvider implements vscode.WebviewViewProvider { this.outputChannel.appendLine("Webview view resolved") } - public async initClineWithTask(task?: string, images?: string[]) { + /** + * Adds a child task's result as a text message to a parent task's messages and saves it + */ + private async addChildTaskResultToParent(taskId: string, result: string): Promise { + const taskDir = path.join(this.context.globalStorageUri.fsPath, "tasks", taskId) + const messagesPath = path.join(taskDir, GlobalFileNames.uiMessages) + + // Read existing messages + let messages: ClineMessage[] = [] + if (await fileExistsAtPath(messagesPath)) { + messages = JSON.parse(await fs.readFile(messagesPath, "utf8")) + } + + // Add result as text message from child task + messages.push({ + ts: Date.now(), + type: "say", + say: "text", + text: result, + }) + + // Save updated messages + await fs.writeFile(messagesPath, JSON.stringify(messages)) + } + + /** + * Handles returning a result to a parent task + */ + public async handleTaskResult(parentTaskId: string, result: string): Promise { + try { + // Get parent task data + const { historyItem, apiConversationHistory } = await this.getTaskWithId(parentTaskId) + + // Add text message to parent task's UI messages + await this.addChildTaskResultToParent(parentTaskId, result) + + // Add message to parent task's API conversation history + const message: Anthropic.MessageParam & { ts?: number } = { + role: "assistant", + content: [ + { + type: "text", + text: result, + }, + ], + ts: Date.now(), + } + apiConversationHistory.push(message) + + // Save updated API conversation history + const taskDir = path.join(this.context.globalStorageUri.fsPath, "tasks", parentTaskId) + const apiHistoryPath = path.join(taskDir, GlobalFileNames.apiConversationHistory) + await fs.writeFile(apiHistoryPath, JSON.stringify(apiConversationHistory)) + + // Switch back to parent task + await this.initClineWithHistoryItem(historyItem) + await this.postMessageToWebview({ + type: "action", + action: "chatButtonClicked", + }) + // Wait for chat view to load before clicking the button + await delay(300) + // Hit the primary button to continue the parent task + await this.postMessageToWebview({ + type: "invoke", + invoke: "primaryButtonClick", + }) + } catch (error) { + this.outputChannel.appendLine(`Failed to handle task result: ${error}`) + throw error + } + } + + public async initClineWithTask(task?: string, images?: string[], parentTaskId?: string) { await this.clearTask() const { apiConfiguration, @@ -424,6 +497,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { images, undefined, experiments, + parentTaskId, ) } @@ -774,7 +848,9 @@ export class ClineProvider implements vscode.WebviewViewProvider { // Could also do this in extension .ts //this.postMessageToWebview({ type: "text", text: `Extension: ${Date.now()}` }) // initializing new instance of Cline will make sure that any agentically running promises in old instance don't affect our new task. this essentially creates a fresh slate for the new task - await this.initClineWithTask(message.text, message.images) + // Pass the current task's ID as the parent ID if it exists + const parentTaskId = this.cline?.taskId + await this.initClineWithTask(message.text, message.images, parentTaskId) break case "apiConfiguration": if (message.apiConfiguration) { diff --git a/src/shared/HistoryItem.ts b/src/shared/HistoryItem.ts index ef242cb967..51688b4379 100644 --- a/src/shared/HistoryItem.ts +++ b/src/shared/HistoryItem.ts @@ -8,4 +8,5 @@ export type HistoryItem = { cacheReads?: number totalCost: number size?: number + parentTaskId?: string // ID of the parent task if this was created via new_task }