From 1bc2eefdf4eb951760feec373a444df8906d1e8f Mon Sep 17 00:00:00 2001 From: Roo Code Date: Sun, 3 Aug 2025 07:53:44 +0000 Subject: [PATCH] feat: persist parent-child task relationships across extension reloads - Add parentTaskId, rootTaskId, and taskHierarchy fields to HistoryItem schema - Store task IDs alongside object references in Task class - Update taskMetadata to save parent-child relationship data - Modify ClineProvider to restore task hierarchy when loading from history - Implement getTaskHierarchy() method to build hierarchy array Fixes #6624 --- packages/types/src/history.ts | 3 +++ src/core/task-persistence/taskMetadata.ts | 9 +++++++ src/core/task/Task.ts | 24 +++++++++++++++++ src/core/webview/ClineProvider.ts | 33 +++++++++++++++++++++-- 4 files changed, 67 insertions(+), 2 deletions(-) diff --git a/packages/types/src/history.ts b/packages/types/src/history.ts index ace134566e..cd99fb77c6 100644 --- a/packages/types/src/history.ts +++ b/packages/types/src/history.ts @@ -17,6 +17,9 @@ export const historyItemSchema = z.object({ size: z.number().optional(), workspace: z.string().optional(), mode: z.string().optional(), + parentTaskId: z.string().optional(), + rootTaskId: z.string().optional(), + taskHierarchy: z.array(z.string()).optional(), }) export type HistoryItem = z.infer diff --git a/src/core/task-persistence/taskMetadata.ts b/src/core/task-persistence/taskMetadata.ts index 7b93b5c14a..0afcd8eed5 100644 --- a/src/core/task-persistence/taskMetadata.ts +++ b/src/core/task-persistence/taskMetadata.ts @@ -19,6 +19,9 @@ export type TaskMetadataOptions = { globalStoragePath: string workspace: string mode?: string + parentTaskId?: string + rootTaskId?: string + taskHierarchy?: string[] } export async function taskMetadata({ @@ -28,6 +31,9 @@ export async function taskMetadata({ globalStoragePath, workspace, mode, + parentTaskId, + rootTaskId, + taskHierarchy, }: TaskMetadataOptions) { const taskDir = await getTaskDirectoryPath(globalStoragePath, taskId) @@ -95,6 +101,9 @@ export async function taskMetadata({ size: taskDirSize, workspace, mode, + parentTaskId, + rootTaskId, + taskHierarchy, } return { historyItem, tokenUsage } diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index 84797b815c..07155d1eea 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -129,6 +129,10 @@ export class Task extends EventEmitter implements TaskLike { readonly taskNumber: number readonly workspacePath: string + // Store task IDs for persistence + readonly rootTaskId: string | undefined = undefined + readonly parentTaskId: string | undefined = undefined + /** * The mode associated with this task. Persisted across sessions * to maintain user context when reopening tasks from history. @@ -307,6 +311,10 @@ export class Task extends EventEmitter implements TaskLike { this.parentTask = parentTask this.taskNumber = taskNumber + // Store task IDs for persistence + this.rootTaskId = rootTask?.taskId + this.parentTaskId = parentTask?.taskId + // Store the task's mode when it's created. // For history items, use the stored mode; for new tasks, we'll set it // after getting state. @@ -582,6 +590,9 @@ export class Task extends EventEmitter implements TaskLike { globalStoragePath: this.globalStoragePath, workspace: this.cwd, mode: this._taskMode || defaultModeSlug, // Use the task's own mode, not the current provider mode + parentTaskId: this.parentTaskId, + rootTaskId: this.rootTaskId, + taskHierarchy: this.getTaskHierarchy(), }) this.emit(RooCodeEventName.TaskTokenUsageUpdated, this.taskId, tokenUsage) @@ -2152,4 +2163,17 @@ export class Task extends EventEmitter implements TaskLike { public get cwd() { return this.workspacePath } + + // Get task hierarchy for persistence + public getTaskHierarchy(): string[] { + const hierarchy: string[] = [] + let currentTask: Task | undefined = this.parentTask + + while (currentTask) { + hierarchy.unshift(currentTask.taskId) + currentTask = currentTask.parentTask + } + + return hierarchy + } } diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index ed8f8a27d1..cac66f41e8 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -740,6 +740,18 @@ export class ClineProvider experiments, } = await this.getState() + // Restore parent and root tasks if their IDs are stored in the history item + let rootTask: Task | undefined = historyItem.rootTask + let parentTask: Task | undefined = historyItem.parentTask + + // If we don't have the actual task objects but have their IDs, try to find them in the stack + if (!rootTask && historyItem.rootTaskId) { + rootTask = this.clineStack.find((task) => task.taskId === historyItem.rootTaskId) + } + if (!parentTask && historyItem.parentTaskId) { + parentTask = this.clineStack.find((task) => task.taskId === historyItem.parentTaskId) + } + const task = new Task({ provider: this, apiConfiguration, @@ -749,8 +761,8 @@ export class ClineProvider consecutiveMistakeLimit: apiConfiguration.consecutiveMistakeLimit, historyItem, experiments, - rootTask: historyItem.rootTask, - parentTask: historyItem.parentTask, + rootTask, + parentTask, taskNumber: historyItem.number, onCreated: (instance) => this.emit(RooCodeEventName.TaskCreated, instance), }) @@ -1344,6 +1356,23 @@ export class ClineProvider if (id !== this.getCurrentCline()?.taskId) { // Non-current task. const { historyItem } = await this.getTaskWithId(id) + + // Check if this task has parent/child relationships that need to be restored + if (historyItem.taskHierarchy && historyItem.taskHierarchy.length > 0) { + // Restore the entire task hierarchy from root to this task + const taskHistory = this.getGlobalState("taskHistory") ?? [] + + // First, restore all parent tasks in the hierarchy + for (const taskId of historyItem.taskHierarchy) { + const parentHistoryItem = taskHistory.find((item: HistoryItem) => item.id === taskId) + if (parentHistoryItem && !this.clineStack.find((task) => task.taskId === taskId)) { + // This parent task is not in the stack, so restore it + await this.initClineWithHistoryItem(parentHistoryItem) + } + } + } + + // Now restore the requested task await this.initClineWithHistoryItem(historyItem) // Clears existing task. }