Skip to content

Commit b6d8d0d

Browse files
author
Eric Wheeler
committed
feat: implement hierarchical task history with parent-child relationships
This change adds support for tracking parent-child relationships between tasks in the task history, particularly for tasks created using the new_task tool. Key changes: - Add parent_task_id field to HistoryItem schema - Modify Task class to store parent task ID - Update taskMetadata to include parent task ID in history items - Enhance History UI components to display hierarchical relationships - Add visual indicators for tasks with children - Add green styling for completed tasks Fixes: #3688 Signed-off-by: Eric Wheeler <[email protected]>
1 parent 6e958df commit b6d8d0d

File tree

8 files changed

+482
-299
lines changed

8 files changed

+482
-299
lines changed

src/core/task-persistence/taskMetadata.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export type TaskMetadataOptions = {
1717
taskNumber: number
1818
globalStoragePath: string
1919
workspace: string
20+
parentTaskId?: string
2021
}
2122

2223
export async function taskMetadata({
@@ -25,6 +26,7 @@ export async function taskMetadata({
2526
taskNumber,
2627
globalStoragePath,
2728
workspace,
29+
parentTaskId,
2830
}: TaskMetadataOptions) {
2931
const taskDir = await getTaskDirectoryPath(globalStoragePath, taskId)
3032
const taskMessage = messages[0] // First message is always the task say.
@@ -57,6 +59,7 @@ export async function taskMetadata({
5759
totalCost: tokenUsage.totalCost,
5860
size: taskDirSize,
5961
workspace,
62+
parent_task_id: parentTaskId,
6063
}
6164

6265
return { historyItem, tokenUsage }

src/core/task/Task.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ export class Task extends EventEmitter<ClineEvents> {
117117

118118
readonly rootTask: Task | undefined = undefined
119119
readonly parentTask: Task | undefined = undefined
120+
readonly parentTaskId?: string
120121
readonly taskNumber: number
121122
readonly workspacePath: string
122123

@@ -237,6 +238,9 @@ export class Task extends EventEmitter<ClineEvents> {
237238

238239
this.rootTask = rootTask
239240
this.parentTask = parentTask
241+
if (parentTask) {
242+
this.parentTaskId = parentTask.taskId
243+
}
240244
this.taskNumber = taskNumber
241245

242246
if (historyItem) {
@@ -344,6 +348,7 @@ export class Task extends EventEmitter<ClineEvents> {
344348
taskNumber: this.taskNumber,
345349
globalStoragePath: this.globalStoragePath,
346350
workspace: this.cwd,
351+
parentTaskId: this.parentTaskId,
347352
})
348353

349354
this.emit("taskTokenUsageUpdated", this.taskId, tokenUsage)

src/exports/roo-code.d.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ type GlobalSettings = {
5454
totalCost: number
5555
size?: number | undefined
5656
workspace?: string | undefined
57+
parent_task_id?: string | undefined
5758
}[]
5859
| undefined
5960
autoApprovalEnabled?: boolean | undefined
@@ -767,6 +768,7 @@ type IpcMessage =
767768
totalCost: number
768769
size?: number | undefined
769770
workspace?: string | undefined
771+
parent_task_id?: string | undefined
770772
}[]
771773
| undefined
772774
autoApprovalEnabled?: boolean | undefined
@@ -1230,6 +1232,7 @@ type TaskCommand =
12301232
totalCost: number
12311233
size?: number | undefined
12321234
workspace?: string | undefined
1235+
parent_task_id?: string | undefined
12331236
}[]
12341237
| undefined
12351238
autoApprovalEnabled?: boolean | undefined

src/exports/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ type GlobalSettings = {
5454
totalCost: number
5555
size?: number | undefined
5656
workspace?: string | undefined
57+
parent_task_id?: string | undefined
5758
}[]
5859
| undefined
5960
autoApprovalEnabled?: boolean | undefined
@@ -781,6 +782,7 @@ type IpcMessage =
781782
totalCost: number
782783
size?: number | undefined
783784
workspace?: string | undefined
785+
parent_task_id?: string | undefined
784786
}[]
785787
| undefined
786788
autoApprovalEnabled?: boolean | undefined
@@ -1246,6 +1248,7 @@ type TaskCommand =
12461248
totalCost: number
12471249
size?: number | undefined
12481250
workspace?: string | undefined
1251+
parent_task_id?: string | undefined
12491252
}[]
12501253
| undefined
12511254
autoApprovalEnabled?: boolean | undefined

src/schemas/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ export const historyItemSchema = z.object({
150150
totalCost: z.number(),
151151
size: z.number().optional(),
152152
workspace: z.string().optional(),
153+
parent_task_id: z.string().optional(),
153154
})
154155

155156
export type HistoryItem = z.infer<typeof historyItemSchema>

webview-ui/src/components/history/HistoryPreview.tsx

Lines changed: 63 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -2,62 +2,84 @@ import { memo } from "react"
22

33
import { vscode } from "@/utils/vscode"
44
import { formatLargeNumber, formatDate } from "@/utils/format"
5+
import { cn } from "@/lib/utils" // Added for cn utility
6+
import { useExtensionState } from "@/context/ExtensionStateContext" // Added for completion status
7+
import { ClineMessage } from "@roo/shared/ExtensionMessage" // Added for ClineMessage type
58

69
import { CopyButton } from "./CopyButton"
7-
import { useTaskSearch } from "./useTaskSearch"
10+
import { useTaskSearch, HierarchicalHistoryItem } from "./useTaskSearch" // Updated import
811

9-
import { Coins } from "lucide-react"
12+
import { Coins, ChevronRight } from "lucide-react" // Added ChevronRight for children indicator
1013

1114
const HistoryPreview = () => {
1215
const { tasks, showAllWorkspaces } = useTaskSearch()
16+
const { clineMessages, currentTaskItem } = useExtensionState()
1317

1418
return (
1519
<>
1620
<div className="flex flex-col gap-3">
1721
{tasks.length !== 0 && (
1822
<>
19-
{tasks.slice(0, 3).map((item) => (
20-
<div
21-
key={item.id}
22-
className="bg-vscode-editor-background rounded relative overflow-hidden cursor-pointer border border-vscode-toolbar-hoverBackground/30 hover:border-vscode-toolbar-hoverBackground/60"
23-
onClick={() => vscode.postMessage({ type: "showTaskWithId", text: item.id })}>
24-
<div className="flex flex-col gap-2 p-3 pt-1">
25-
<div className="flex justify-between items-center">
26-
<span className="text-xs font-medium text-vscode-descriptionForeground uppercase">
27-
{formatDate(item.ts)}
28-
</span>
29-
<CopyButton itemTask={item.task} />
30-
</div>
31-
<div
32-
className="text-vscode-foreground overflow-hidden whitespace-pre-wrap"
33-
style={{
34-
display: "-webkit-box",
35-
WebkitLineClamp: 2,
36-
WebkitBoxOrient: "vertical",
37-
wordBreak: "break-word",
38-
overflowWrap: "anywhere",
39-
}}>
40-
{item.task}
41-
</div>
42-
<div className="flex flex-row gap-2 text-xs text-vscode-descriptionForeground">
43-
<span>{formatLargeNumber(item.tokensIn || 0)}</span>
44-
<span>{formatLargeNumber(item.tokensOut || 0)}</span>
45-
{!!item.totalCost && (
46-
<span>
47-
<Coins className="inline-block size-[1em]" />{" "}
48-
{"$" + item.totalCost?.toFixed(2)}
49-
</span>
23+
{tasks.slice(0, 3).map((item: HierarchicalHistoryItem) => {
24+
let isCompleted = false
25+
if (item.id === currentTaskItem?.id && clineMessages) {
26+
isCompleted = clineMessages.some(
27+
(msg: ClineMessage) =>
28+
(msg.type === "ask" && msg.ask === "completion_result") ||
29+
(msg.type === "say" && msg.say === "completion_result"),
30+
)
31+
}
32+
return (
33+
<div
34+
key={item.id}
35+
className={cn(
36+
"bg-vscode-editor-background rounded relative overflow-hidden cursor-pointer border border-vscode-toolbar-hoverBackground/30 hover:border-vscode-toolbar-hoverBackground/60",
37+
{ "bg-green-100/10 dark:bg-green-900/20": isCompleted }, // Adjusted green styling for preview
38+
)}
39+
onClick={() => vscode.postMessage({ type: "showTaskWithId", text: item.id })}>
40+
<div className="flex flex-col gap-2 p-3 pt-1">
41+
<div className="flex justify-between items-center">
42+
<div className="flex items-center">
43+
{item.children && item.children.length > 0 && (
44+
<ChevronRight className="inline-block size-3 mr-1 text-vscode-descriptionForeground" />
45+
)}
46+
<span className="text-xs font-medium text-vscode-descriptionForeground uppercase">
47+
{formatDate(item.ts)}
48+
</span>
49+
</div>
50+
<CopyButton itemTask={item.task} />
51+
</div>
52+
<div
53+
className="text-vscode-foreground overflow-hidden whitespace-pre-wrap"
54+
style={{
55+
display: "-webkit-box",
56+
WebkitLineClamp: 2,
57+
WebkitBoxOrient: "vertical",
58+
wordBreak: "break-word",
59+
overflowWrap: "anywhere",
60+
}}
61+
dangerouslySetInnerHTML={{ __html: item.task }} // Use dangerouslySetInnerHTML if task contains HTML
62+
/>
63+
<div className="flex flex-row gap-2 text-xs text-vscode-descriptionForeground">
64+
<span>{formatLargeNumber(item.tokensIn || 0)}</span>
65+
<span>{formatLargeNumber(item.tokensOut || 0)}</span>
66+
{!!item.totalCost && (
67+
<span>
68+
<Coins className="inline-block size-[1em]" />{" "}
69+
{"$" + item.totalCost?.toFixed(2)}
70+
</span>
71+
)}
72+
</div>
73+
{showAllWorkspaces && item.workspace && (
74+
<div className="flex flex-row gap-1 text-vscode-descriptionForeground text-xs mt-1">
75+
<span className="codicon codicon-folder scale-80" />
76+
<span>{item.workspace}</span>
77+
</div>
5078
)}
5179
</div>
52-
{showAllWorkspaces && item.workspace && (
53-
<div className="flex flex-row gap-1 text-vscode-descriptionForeground text-xs mt-1">
54-
<span className="codicon codicon-folder scale-80" />
55-
<span>{item.workspace}</span>
56-
</div>
57-
)}
5880
</div>
59-
</div>
60-
))}
81+
)
82+
})}
6183
</>
6284
)}
6385
</div>

0 commit comments

Comments
 (0)