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
This commit is contained in:
Roo Code 2025-08-03 07:53:44 +00:00
parent 82a007a211
commit 1bc2eefdf4
4 changed files with 67 additions and 2 deletions

View file

@ -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<typeof historyItemSchema>

View file

@ -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 }

View file

@ -129,6 +129,10 @@ export class Task extends EventEmitter<TaskEvents> 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<TaskEvents> 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<TaskEvents> 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<TaskEvents> 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
}
}

View file

@ -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.
}