Skip to content

Commit 7db4bfe

Browse files
feat(history): render nested subtasks as recursive tree (#11299)
* feat(history): render nested subtasks as recursive tree * fix(lockfile): resolve missing ai-sdk provider entry * fix: address review feedback — dedupe countAll, increase SubtaskRow max-h - HistoryView: replace local countAll with imported countAllSubtasks from types.ts - SubtaskRow: increase nested children max-h from 500px to 2000px to match TaskGroupItem
1 parent 5d17f56 commit 7db4bfe

9 files changed

Lines changed: 687 additions & 77 deletions

File tree

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ const HistoryPreview = () => {
3838
group={group}
3939
variant="compact"
4040
onToggleExpand={() => toggleExpand(group.parent.id)}
41+
onToggleSubtaskExpand={toggleExpand}
4142
/>
4243
))}
4344
</>

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { useAppTranslation } from "@/i18n/TranslationContext"
2121
import { Tab, TabContent, TabHeader } from "../common/Tab"
2222
import { useTaskSearch } from "./useTaskSearch"
2323
import { useGroupedTasks } from "./useGroupedTasks"
24+
import { countAllSubtasks } from "./types"
2425
import TaskItem from "./TaskItem"
2526
import TaskGroupItem from "./TaskGroupItem"
2627

@@ -52,11 +53,11 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
5253
const [selectedTaskIds, setSelectedTaskIds] = useState<string[]>([])
5354
const [showBatchDeleteDialog, setShowBatchDeleteDialog] = useState<boolean>(false)
5455

55-
// Get subtask count for a task
56+
// Get subtask count for a task (recursive total)
5657
const getSubtaskCount = useMemo(() => {
5758
const countMap = new Map<string, number>()
5859
for (const group of groups) {
59-
countMap.set(group.parent.id, group.subtasks.length)
60+
countMap.set(group.parent.id, countAllSubtasks(group.subtasks))
6061
}
6162
return (taskId: string) => countMap.get(taskId) || 0
6263
}, [groups])
@@ -300,6 +301,7 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
300301
onToggleSelection={toggleTaskSelection}
301302
onDelete={handleDelete}
302303
onToggleExpand={() => toggleExpand(group.parent.id)}
304+
onToggleSubtaskExpand={toggleExpand}
303305
className="m-2"
304306
/>
305307
)}

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

Lines changed: 66 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,46 +2,87 @@ import { memo } from "react"
22
import { ArrowRight } from "lucide-react"
33
import { vscode } from "@/utils/vscode"
44
import { cn } from "@/lib/utils"
5-
import type { DisplayHistoryItem } from "./types"
5+
import type { SubtaskTreeNode } from "./types"
6+
import { countAllSubtasks } from "./types"
67
import { StandardTooltip } from "../ui"
8+
import SubtaskCollapsibleRow from "./SubtaskCollapsibleRow"
79

810
interface SubtaskRowProps {
9-
/** The subtask to display */
10-
item: DisplayHistoryItem
11+
/** The subtask tree node to display */
12+
node: SubtaskTreeNode
13+
/** Nesting depth (1 = direct child of parent group) */
14+
depth: number
15+
/** Callback when expand/collapse is toggled for a node */
16+
onToggleExpand: (taskId: string) => void
1117
/** Optional className for styling */
1218
className?: string
1319
}
1420

1521
/**
16-
* Displays an individual subtask row when the parent's subtask list is expanded.
17-
* Shows the task name and token/cost info in an indented format.
22+
* Displays a subtask row with recursive nesting support.
23+
* Leaf nodes render just the task row. Nodes with children show
24+
* a collapsible section that can be expanded to reveal nested subtasks.
1825
*/
19-
const SubtaskRow = ({ item, className }: SubtaskRowProps) => {
26+
const SubtaskRow = ({ node, depth, onToggleExpand, className }: SubtaskRowProps) => {
27+
const { item, children, isExpanded } = node
28+
const hasChildren = children.length > 0
29+
2030
const handleClick = () => {
2131
vscode.postMessage({ type: "showTaskWithId", text: item.id })
2232
}
2333

2434
return (
25-
<div
26-
data-testid={`subtask-row-${item.id}`}
27-
className={cn(
28-
"group flex items-center justify-between gap-2 pl-1 pr-4 py-1 ml-6 cursor-pointer",
29-
"text-vscode-foreground/60 hover:text-vscode-foreground transition-colors",
30-
className,
35+
<div data-testid={`subtask-row-${item.id}`} className={className}>
36+
{/* Task row with depth indentation */}
37+
<div
38+
className={cn(
39+
"group flex items-center justify-between gap-2 pr-4 py-1 cursor-pointer",
40+
"text-vscode-foreground/60 hover:text-vscode-foreground transition-colors",
41+
)}
42+
style={{ paddingLeft: `${depth * 16}px` }}
43+
onClick={handleClick}
44+
role="button"
45+
tabIndex={0}
46+
onKeyDown={(e) => {
47+
if (e.key === "Enter" || e.key === " ") {
48+
e.preventDefault()
49+
handleClick()
50+
}
51+
}}>
52+
<StandardTooltip content={item.task} delay={600}>
53+
<span className="text-sm line-clamp-1">{item.task}</span>
54+
</StandardTooltip>
55+
<ArrowRight className="size-3 opacity-0 group-hover:opacity-100 transition-opacity shrink-0" />
56+
</div>
57+
58+
{/* Nested subtask collapsible section */}
59+
{hasChildren && (
60+
<div style={{ paddingLeft: `${depth * 16}px` }}>
61+
<SubtaskCollapsibleRow
62+
count={countAllSubtasks(children)}
63+
isExpanded={isExpanded}
64+
onToggle={() => onToggleExpand(item.id)}
65+
/>
66+
</div>
67+
)}
68+
69+
{/* Expanded nested subtasks */}
70+
{hasChildren && (
71+
<div
72+
className={cn(
73+
"overflow-clip transition-all duration-300",
74+
isExpanded ? "max-h-[2000px]" : "max-h-0",
75+
)}>
76+
{children.map((child) => (
77+
<SubtaskRow
78+
key={child.item.id}
79+
node={child}
80+
depth={depth + 1}
81+
onToggleExpand={onToggleExpand}
82+
/>
83+
))}
84+
</div>
3185
)}
32-
onClick={handleClick}
33-
role="button"
34-
tabIndex={0}
35-
onKeyDown={(e) => {
36-
if (e.key === "Enter" || e.key === " ") {
37-
e.preventDefault()
38-
handleClick()
39-
}
40-
}}>
41-
<StandardTooltip content={item.task} delay={600}>
42-
<span className="text-sm line-clamp-1">{item.task}</span>
43-
</StandardTooltip>
44-
<ArrowRight className="size-3 opacity-0 group-hover:opacity-100 transition-opacity shrink-0" />
4586
</div>
4687
)
4788
}

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

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { memo } from "react"
22
import { cn } from "@/lib/utils"
33
import type { TaskGroup } from "./types"
4+
import { countAllSubtasks } from "./types"
45
import TaskItem from "./TaskItem"
56
import SubtaskCollapsibleRow from "./SubtaskCollapsibleRow"
67
import SubtaskRow from "./SubtaskRow"
@@ -20,15 +21,17 @@ interface TaskGroupItemProps {
2021
onToggleSelection?: (taskId: string, isSelected: boolean) => void
2122
/** Callback when delete is requested */
2223
onDelete?: (taskId: string) => void
23-
/** Callback when expand/collapse is toggled */
24+
/** Callback when the parent group expand/collapse is toggled */
2425
onToggleExpand: () => void
26+
/** Callback when a nested subtask node expand/collapse is toggled */
27+
onToggleSubtaskExpand: (taskId: string) => void
2528
/** Optional className for styling */
2629
className?: string
2730
}
2831

2932
/**
30-
* Renders a task group consisting of a parent task and its collapsible subtask list.
31-
* When expanded, shows individual subtask rows.
33+
* Renders a task group consisting of a parent task and its collapsible subtask tree.
34+
* When expanded, shows recursively nested subtask rows.
3235
*/
3336
const TaskGroupItem = ({
3437
group,
@@ -39,10 +42,12 @@ const TaskGroupItem = ({
3942
onToggleSelection,
4043
onDelete,
4144
onToggleExpand,
45+
onToggleSubtaskExpand,
4246
className,
4347
}: TaskGroupItemProps) => {
4448
const { parent, subtasks, isExpanded } = group
4549
const hasSubtasks = subtasks.length > 0
50+
const totalSubtaskCount = hasSubtasks ? countAllSubtasks(subtasks) : 0
4651

4752
return (
4853
<div
@@ -63,21 +68,21 @@ const TaskGroupItem = ({
6368
hasSubtasks={hasSubtasks}
6469
/>
6570

66-
{/* Subtask collapsible row */}
71+
{/* Subtask collapsible row — shows total recursive count */}
6772
{hasSubtasks && (
68-
<SubtaskCollapsibleRow count={subtasks.length} isExpanded={isExpanded} onToggle={onToggleExpand} />
73+
<SubtaskCollapsibleRow count={totalSubtaskCount} isExpanded={isExpanded} onToggle={onToggleExpand} />
6974
)}
7075

71-
{/* Expanded subtasks */}
76+
{/* Expanded subtask tree */}
7277
{hasSubtasks && (
7378
<div
7479
data-testid="subtask-list"
7580
className={cn(
7681
"overflow-clip transition-all duration-500",
77-
isExpanded ? "max-h-100 pb-2" : "max-h-0",
82+
isExpanded ? "max-h-[2000px] pb-2" : "max-h-0",
7883
)}>
79-
{subtasks.map((subtask) => (
80-
<SubtaskRow key={subtask.id} item={subtask} />
84+
{subtasks.map((node) => (
85+
<SubtaskRow key={node.item.id} node={node} depth={1} onToggleExpand={onToggleSubtaskExpand} />
8186
))}
8287
</div>
8388
)}

0 commit comments

Comments
 (0)