Skip to content

Commit b733d5e

Browse files
committed
feat: implement hierarchical task execution history with HTA-inspired visualization
- Add parentId field to HistoryItem type for parent-child relationships - Update Task class to track and persist parent task relationships - Create useTaskHierarchy hook with tree building and flattening utilities - Update HistoryView with tree/flat view toggle and expand/collapse controls - Enhance TaskItem with hierarchical display including indentation and chevrons - Add comprehensive tests for hierarchical data structure utilities - Add translation keys for new UI elements This implementation allows users to visualize task relationships in a tree structure, making it easier to understand complex orchestrated workflows and trace task lineage.
1 parent 1d46bd1 commit b733d5e

File tree

8 files changed

+451
-9
lines changed

8 files changed

+451
-9
lines changed

packages/types/src/history.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export const historyItemSchema = z.object({
1717
size: z.number().optional(),
1818
workspace: z.string().optional(),
1919
mode: z.string().optional(),
20+
parentId: z.string().optional(),
2021
})
2122

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

src/core/task-persistence/taskMetadata.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export type TaskMetadataOptions = {
1919
globalStoragePath: string
2020
workspace: string
2121
mode?: string
22+
parentId?: string
2223
}
2324

2425
export async function taskMetadata({
@@ -28,6 +29,7 @@ export async function taskMetadata({
2829
globalStoragePath,
2930
workspace,
3031
mode,
32+
parentId,
3133
}: TaskMetadataOptions) {
3234
const taskDir = await getTaskDirectoryPath(globalStoragePath, taskId)
3335

@@ -95,6 +97,7 @@ export async function taskMetadata({
9597
size: taskDirSize,
9698
workspace,
9799
mode,
100+
parentId,
98101
}
99102

100103
return { historyItem, tokenUsage }

src/core/task/Task.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -636,6 +636,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
636636
globalStoragePath: this.globalStoragePath,
637637
workspace: this.cwd,
638638
mode: this._taskMode || defaultModeSlug, // Use the task's own mode, not the current provider mode
639+
parentId: this.parentTask?.taskId,
639640
})
640641

641642
this.emit(RooCodeEventName.TaskTokenUsageUpdated, this.taskId, tokenUsage)

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

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { useAppTranslation } from "@/i18n/TranslationContext"
2020
import { Tab, TabContent, TabHeader } from "../common/Tab"
2121
import { useTaskSearch } from "./useTaskSearch"
2222
import TaskItem from "./TaskItem"
23+
import { useTaskHierarchy } from "./useTaskHierarchy"
2324

2425
type HistoryViewProps = {
2526
onDone: () => void
@@ -44,6 +45,13 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
4445
const [isSelectionMode, setIsSelectionMode] = useState(false)
4546
const [selectedTaskIds, setSelectedTaskIds] = useState<string[]>([])
4647
const [showBatchDeleteDialog, setShowBatchDeleteDialog] = useState<boolean>(false)
48+
const [showHierarchical, setShowHierarchical] = useState(true)
49+
50+
// Use the hierarchical task hook
51+
const { flattenedTasks, expandedIds, toggleExpanded, expandAll, collapseAll } = useTaskHierarchy(tasks)
52+
53+
// Use either flat or hierarchical tasks based on toggle
54+
const displayTasks = showHierarchical ? flattenedTasks : tasks
4755

4856
// Toggle selection mode
4957
const toggleSelectionMode = () => {
@@ -65,7 +73,7 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
6573
// Toggle select all tasks
6674
const toggleSelectAll = (selectAll: boolean) => {
6775
if (selectAll) {
68-
setSelectedTaskIds(tasks.map((task) => task.id))
76+
setSelectedTaskIds(displayTasks.map((task) => task.id))
6977
} else {
7078
setSelectedTaskIds([])
7179
}
@@ -84,6 +92,29 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
8492
<div className="flex justify-between items-center">
8593
<h3 className="text-vscode-foreground m-0">{t("history:history")}</h3>
8694
<div className="flex gap-2">
95+
{showHierarchical && (
96+
<>
97+
<StandardTooltip content={t("history:expandAll")}>
98+
<Button variant="secondary" onClick={expandAll}>
99+
<span className="codicon codicon-expand-all" />
100+
</Button>
101+
</StandardTooltip>
102+
<StandardTooltip content={t("history:collapseAll")}>
103+
<Button variant="secondary" onClick={collapseAll}>
104+
<span className="codicon codicon-collapse-all" />
105+
</Button>
106+
</StandardTooltip>
107+
</>
108+
)}
109+
<StandardTooltip
110+
content={showHierarchical ? t("history:showFlat") : t("history:showHierarchical")}>
111+
<Button variant="secondary" onClick={() => setShowHierarchical(!showHierarchical)}>
112+
<span
113+
className={`codicon ${showHierarchical ? "codicon-list-flat" : "codicon-list-tree"} mr-1`}
114+
/>
115+
{showHierarchical ? t("history:flatView") : t("history:treeView")}
116+
</Button>
117+
</StandardTooltip>
87118
<StandardTooltip
88119
content={
89120
isSelectionMode
@@ -197,23 +228,23 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
197228
</div>
198229

199230
{/* Select all control in selection mode */}
200-
{isSelectionMode && tasks.length > 0 && (
231+
{isSelectionMode && displayTasks.length > 0 && (
201232
<div className="flex items-center py-1">
202233
<div className="flex items-center gap-2">
203234
<Checkbox
204-
checked={tasks.length > 0 && selectedTaskIds.length === tasks.length}
235+
checked={displayTasks.length > 0 && selectedTaskIds.length === displayTasks.length}
205236
onCheckedChange={(checked) => toggleSelectAll(checked === true)}
206237
variant="description"
207238
/>
208239
<span className="text-vscode-foreground">
209-
{selectedTaskIds.length === tasks.length
240+
{selectedTaskIds.length === displayTasks.length
210241
? t("history:deselectAll")
211242
: t("history:selectAll")}
212243
</span>
213244
<span className="ml-auto text-vscode-descriptionForeground text-xs">
214245
{t("history:selectedItems", {
215246
selected: selectedTaskIds.length,
216-
total: tasks.length,
247+
total: displayTasks.length,
217248
})}
218249
</span>
219250
</div>
@@ -225,7 +256,7 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
225256
<TabContent className="px-2 py-0">
226257
<Virtuoso
227258
className="flex-1 overflow-y-scroll"
228-
data={tasks}
259+
data={displayTasks}
229260
data-testid="virtuoso-container"
230261
initialTopMostItemIndex={0}
231262
components={{
@@ -244,6 +275,11 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
244275
onToggleSelection={toggleTaskSelection}
245276
onDelete={setDeleteTaskId}
246277
className="m-2"
278+
isHierarchical={showHierarchical}
279+
level={showHierarchical ? (item as any).level || 0 : 0}
280+
hasChildren={showHierarchical && (item as any).children?.length > 0}
281+
isExpanded={showHierarchical && expandedIds.has(item.id)}
282+
onToggleExpanded={showHierarchical ? () => toggleExpanded(item.id) : undefined}
247283
/>
248284
)}
249285
/>
@@ -253,7 +289,7 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
253289
{isSelectionMode && selectedTaskIds.length > 0 && (
254290
<div className="fixed bottom-0 left-0 right-2 bg-vscode-editor-background border-t border-vscode-panel-border p-2 flex justify-between items-center">
255291
<div className="text-vscode-foreground">
256-
{t("history:selectedItems", { selected: selectedTaskIds.length, total: tasks.length })}
292+
{t("history:selectedItems", { selected: selectedTaskIds.length, total: displayTasks.length })}
257293
</div>
258294
<div className="flex gap-2">
259295
<Button variant="secondary" onClick={() => setSelectedTaskIds([])}>

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

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import TaskItemFooter from "./TaskItemFooter"
99

1010
interface DisplayHistoryItem extends HistoryItem {
1111
highlight?: string
12+
level?: number
13+
children?: DisplayHistoryItem[]
1214
}
1315

1416
interface TaskItemProps {
@@ -20,6 +22,11 @@ interface TaskItemProps {
2022
onToggleSelection?: (taskId: string, isSelected: boolean) => void
2123
onDelete?: (taskId: string) => void
2224
className?: string
25+
isHierarchical?: boolean
26+
level?: number
27+
hasChildren?: boolean
28+
isExpanded?: boolean
29+
onToggleExpanded?: () => void
2330
}
2431

2532
const TaskItem = ({
@@ -31,15 +38,27 @@ const TaskItem = ({
3138
onToggleSelection,
3239
onDelete,
3340
className,
41+
isHierarchical = false,
42+
level = 0,
43+
hasChildren = false,
44+
isExpanded = false,
45+
onToggleExpanded,
3446
}: TaskItemProps) => {
3547
const handleClick = () => {
3648
if (isSelectionMode && onToggleSelection) {
3749
onToggleSelection(item.id, !isSelected)
38-
} else {
50+
} else if (!isHierarchical || !hasChildren) {
3951
vscode.postMessage({ type: "showTaskWithId", text: item.id })
4052
}
4153
}
4254

55+
const handleExpandClick = (e: React.MouseEvent) => {
56+
e.stopPropagation()
57+
if (onToggleExpanded) {
58+
onToggleExpanded()
59+
}
60+
}
61+
4362
const isCompact = variant === "compact"
4463

4564
return (
@@ -50,8 +69,28 @@ const TaskItem = ({
5069
"cursor-pointer group bg-vscode-editor-background rounded relative overflow-hidden border border-transparent hover:bg-vscode-list-hoverBackground transition-colors",
5170
className,
5271
)}
72+
style={isHierarchical ? { marginLeft: `${level * 24}px` } : undefined}
5373
onClick={handleClick}>
5474
<div className={(!isCompact && isSelectionMode ? "pl-3 pb-3" : "pl-4") + " flex gap-3 px-3 pt-3 pb-1"}>
75+
{/* Expand/collapse button for hierarchical view */}
76+
{isHierarchical && hasChildren && (
77+
<button
78+
className="flex items-center justify-center w-5 h-5 mt-1 hover:bg-vscode-list-hoverBackground rounded"
79+
onClick={handleExpandClick}
80+
aria-label={isExpanded ? "Collapse" : "Expand"}>
81+
<span
82+
className={cn(
83+
"codicon",
84+
isExpanded ? "codicon-chevron-down" : "codicon-chevron-right",
85+
"text-xs",
86+
)}
87+
/>
88+
</button>
89+
)}
90+
91+
{/* Spacer for items without children in hierarchical view */}
92+
{isHierarchical && !hasChildren && <div className="w-5" />}
93+
5594
{/* Selection checkbox - only in full variant */}
5695
{!isCompact && isSelectionMode && (
5796
<div

0 commit comments

Comments
 (0)