diff --git a/webview-ui/src/components/chat/task-dashboard/TaskDashboard.tsx b/webview-ui/src/components/chat/task-dashboard/TaskDashboard.tsx
index d245daa4fa..b06f21cbbe 100644
--- a/webview-ui/src/components/chat/task-dashboard/TaskDashboard.tsx
+++ b/webview-ui/src/components/chat/task-dashboard/TaskDashboard.tsx
@@ -37,6 +37,8 @@ interface TaskNodeRowProps {
*/
const TaskNodeRow = memo(({ node, depth, currentTaskId, modeMap }: TaskNodeRowProps) => {
const { item, children } = node
+ const hasChildren = children.length > 0
+ const [isNodeExpanded, setIsNodeExpanded] = useState(true)
const isCurrentTask = item.id === currentTaskId
const modeConfig = item.mode ? modeMap.get(item.mode) : undefined
const modeName = modeConfig?.name ?? item.mode ?? "Unknown"
@@ -57,6 +59,11 @@ const TaskNodeRow = memo(({ node, depth, currentTaskId, modeMap }: TaskNodeRowPr
[handleClick],
)
+ const toggleNodeExpanded = useCallback((e: React.MouseEvent) => {
+ e.stopPropagation()
+ setIsNodeExpanded((prev) => !prev)
+ }, [])
+
// Truncate task description for display
const taskSummary = item.task.length > 60 ? item.task.slice(0, 57) + "..." : item.task
@@ -64,7 +71,7 @@ const TaskNodeRow = memo(({ node, depth, currentTaskId, modeMap }: TaskNodeRowPr
+ {/* Expand/collapse toggle for nodes with children */}
+ {hasChildren ? (
+
+ ) : (
+
+ )}
+
{/* Mode icon/indicator */}
- {/* Render children */}
- {children.map((child) => (
-
- ))}
+ {/* Render children (collapsible) */}
+ {hasChildren && isNodeExpanded && (
+
+ {children.map((child) => (
+
+ ))}
+
+ )}
)
})
@@ -134,7 +158,7 @@ TaskNodeRow.displayName = "TaskNodeRow"
*/
const TaskDashboard = () => {
const { taskHistory, currentTaskItem, currentTaskId, customModes } = useExtensionState()
- const { rootNode, hasDelegationHierarchy } = useTaskTree(taskHistory, currentTaskItem)
+ const { rootNode, hasDelegationHierarchy, taskCount } = useTaskTree(taskHistory, currentTaskItem)
const [isExpanded, setIsExpanded] = useState(true)
// Build a mode lookup map
@@ -171,7 +195,9 @@ const TaskDashboard = () => {
data-testid="task-dashboard-toggle">
{isExpanded ? : }
- Task Delegation
+
+ Task Delegation ({taskCount} {taskCount === 1 ? "task" : "tasks"})
+
{/* Tree content */}
diff --git a/webview-ui/src/components/chat/task-dashboard/__tests__/TaskDashboard.spec.tsx b/webview-ui/src/components/chat/task-dashboard/__tests__/TaskDashboard.spec.tsx
index 117d9eca46..6c55ba7b29 100644
--- a/webview-ui/src/components/chat/task-dashboard/__tests__/TaskDashboard.spec.tsx
+++ b/webview-ui/src/components/chat/task-dashboard/__tests__/TaskDashboard.spec.tsx
@@ -92,7 +92,7 @@ describe("TaskDashboard", () => {
render()
expect(screen.getByTestId("task-dashboard")).toBeTruthy()
- expect(screen.getByText("Task Delegation")).toBeTruthy()
+ expect(screen.getByText("Task Delegation (2 tasks)")).toBeTruthy()
})
it("displays mode names for each task node", () => {
@@ -257,4 +257,114 @@ describe("TaskDashboard", () => {
const button = activeNode.querySelector("[role='button']")
expect(button?.className).toContain("activeSelection")
})
+
+ it("displays task count in the header", () => {
+ const parent = makeItem({
+ id: "parent-1",
+ task: "Root task",
+ mode: "orchestrator",
+ status: "delegated",
+ childIds: ["child-1", "child-2"],
+ })
+ const child1 = makeItem({
+ id: "child-1",
+ task: "Child task 1",
+ mode: "code",
+ status: "completed",
+ rootTaskId: "parent-1",
+ parentTaskId: "parent-1",
+ })
+ const child2 = makeItem({
+ id: "child-2",
+ task: "Child task 2",
+ mode: "debug",
+ status: "active",
+ rootTaskId: "parent-1",
+ parentTaskId: "parent-1",
+ })
+ mockState = {
+ taskHistory: [parent, child1, child2],
+ currentTaskItem: child2,
+ currentTaskId: "child-2",
+ customModes: [],
+ }
+
+ render()
+
+ expect(screen.getByText("Task Delegation (3 tasks)")).toBeTruthy()
+ })
+
+ it("shows expand/collapse toggle on nodes with children", () => {
+ const parent = makeItem({
+ id: "parent-1",
+ task: "Root task",
+ mode: "orchestrator",
+ status: "delegated",
+ childIds: ["child-1"],
+ })
+ const child = makeItem({
+ id: "child-1",
+ task: "Child task",
+ mode: "code",
+ status: "active",
+ rootTaskId: "parent-1",
+ parentTaskId: "parent-1",
+ })
+ mockState = {
+ taskHistory: [parent, child],
+ currentTaskItem: child,
+ currentTaskId: "child-1",
+ customModes: [],
+ }
+
+ render()
+
+ // Parent has children so it should have a toggle button
+ expect(screen.getByTestId("task-node-toggle-parent-1")).toBeTruthy()
+
+ // Child has no children so it should NOT have a toggle button
+ expect(screen.queryByTestId("task-node-toggle-child-1")).toBeNull()
+ })
+
+ it("collapses and expands tree node children", () => {
+ const parent = makeItem({
+ id: "parent-1",
+ task: "Root task",
+ mode: "orchestrator",
+ status: "delegated",
+ childIds: ["child-1"],
+ })
+ const child = makeItem({
+ id: "child-1",
+ task: "Child task",
+ mode: "code",
+ status: "active",
+ rootTaskId: "parent-1",
+ parentTaskId: "parent-1",
+ })
+ mockState = {
+ taskHistory: [parent, child],
+ currentTaskItem: child,
+ currentTaskId: "child-1",
+ customModes: [],
+ }
+
+ render()
+
+ // Children should be visible by default
+ expect(screen.getByTestId("task-node-children-parent-1")).toBeTruthy()
+ expect(screen.getByTestId("task-node-child-1")).toBeTruthy()
+
+ // Click the toggle to collapse
+ fireEvent.click(screen.getByTestId("task-node-toggle-parent-1"))
+
+ // Children container should be hidden
+ expect(screen.queryByTestId("task-node-children-parent-1")).toBeNull()
+
+ // Click again to expand
+ fireEvent.click(screen.getByTestId("task-node-toggle-parent-1"))
+
+ // Children should be visible again
+ expect(screen.getByTestId("task-node-children-parent-1")).toBeTruthy()
+ })
})
diff --git a/webview-ui/src/components/chat/task-dashboard/__tests__/useTaskTree.spec.ts b/webview-ui/src/components/chat/task-dashboard/__tests__/useTaskTree.spec.ts
index f0d47d071e..eaa12aec30 100644
--- a/webview-ui/src/components/chat/task-dashboard/__tests__/useTaskTree.spec.ts
+++ b/webview-ui/src/components/chat/task-dashboard/__tests__/useTaskTree.spec.ts
@@ -1,5 +1,5 @@
import type { HistoryItem } from "@roo-code/types"
-import { buildTaskTree } from "../useTaskTree"
+import { buildTaskTree, countTreeNodes } from "../useTaskTree"
function makeItem(overrides: Partial & { id: string }): HistoryItem {
return {
@@ -18,6 +18,7 @@ describe("buildTaskTree", () => {
const result = buildTaskTree([], undefined)
expect(result.rootNode).toBeNull()
expect(result.hasDelegationHierarchy).toBe(false)
+ expect(result.taskCount).toBe(0)
})
it("returns null when current task has no delegation hierarchy", () => {
@@ -25,6 +26,7 @@ describe("buildTaskTree", () => {
const result = buildTaskTree([item], item)
expect(result.rootNode).toBeNull()
expect(result.hasDelegationHierarchy).toBe(false)
+ expect(result.taskCount).toBe(0)
})
it("builds a simple parent-child tree", () => {
@@ -52,6 +54,7 @@ describe("buildTaskTree", () => {
expect(result.rootNode!.item.id).toBe("parent-1")
expect(result.rootNode!.children).toHaveLength(1)
expect(result.rootNode!.children[0].item.id).toBe("child-1")
+ expect(result.taskCount).toBe(2)
})
it("builds a tree when current task is the root", () => {
@@ -214,6 +217,75 @@ describe("buildTaskTree", () => {
expect(result.rootNode!.children[0].item.id).toBe("child")
})
+ it("returns correct taskCount for deep trees", () => {
+ const root = makeItem({
+ id: "root",
+ task: "Root task",
+ mode: "orchestrator",
+ status: "delegated",
+ childIds: ["mid"],
+ })
+ const mid = makeItem({
+ id: "mid",
+ task: "Middle task",
+ mode: "architect",
+ status: "delegated",
+ rootTaskId: "root",
+ parentTaskId: "root",
+ childIds: ["leaf"],
+ })
+ const leaf = makeItem({
+ id: "leaf",
+ task: "Leaf task",
+ mode: "code",
+ status: "active",
+ rootTaskId: "root",
+ parentTaskId: "mid",
+ })
+ const history = [root, mid, leaf]
+
+ const result = buildTaskTree(history, leaf)
+ expect(result.taskCount).toBe(3)
+ })
+
+ it("returns correct taskCount for multiple children", () => {
+ const root = makeItem({
+ id: "root",
+ task: "Root task",
+ mode: "orchestrator",
+ status: "delegated",
+ childIds: ["child-a", "child-b", "child-c"],
+ })
+ const childA = makeItem({
+ id: "child-a",
+ task: "Task A",
+ mode: "code",
+ status: "completed",
+ rootTaskId: "root",
+ parentTaskId: "root",
+ })
+ const childB = makeItem({
+ id: "child-b",
+ task: "Task B",
+ mode: "debug",
+ status: "completed",
+ rootTaskId: "root",
+ parentTaskId: "root",
+ })
+ const childC = makeItem({
+ id: "child-c",
+ task: "Task C",
+ mode: "code",
+ status: "active",
+ rootTaskId: "root",
+ parentTaskId: "root",
+ })
+ const history = [root, childA, childB, childC]
+
+ const result = buildTaskTree(history, childC)
+ expect(result.taskCount).toBe(4)
+ })
+
it("handles missing child items gracefully", () => {
const root = makeItem({
id: "root",
@@ -237,3 +309,28 @@ describe("buildTaskTree", () => {
expect(result.rootNode!.children[0].item.id).toBe("existing-child")
})
})
+
+describe("countTreeNodes", () => {
+ it("returns 0 for null", () => {
+ expect(countTreeNodes(null)).toBe(0)
+ })
+
+ it("returns 1 for a single node", () => {
+ const node = { item: makeItem({ id: "single" }), children: [] }
+ expect(countTreeNodes(node)).toBe(1)
+ })
+
+ it("counts all nodes in a tree", () => {
+ const node = {
+ item: makeItem({ id: "root" }),
+ children: [
+ {
+ item: makeItem({ id: "child-1" }),
+ children: [{ item: makeItem({ id: "grandchild" }), children: [] }],
+ },
+ { item: makeItem({ id: "child-2" }), children: [] },
+ ],
+ }
+ expect(countTreeNodes(node)).toBe(4)
+ })
+})
diff --git a/webview-ui/src/components/chat/task-dashboard/useTaskTree.ts b/webview-ui/src/components/chat/task-dashboard/useTaskTree.ts
index af9edd9469..41379d509f 100644
--- a/webview-ui/src/components/chat/task-dashboard/useTaskTree.ts
+++ b/webview-ui/src/components/chat/task-dashboard/useTaskTree.ts
@@ -19,6 +19,20 @@ export interface TaskTreeResult {
rootNode: TaskTreeNode | null
/** Whether the current task is part of a delegation hierarchy */
hasDelegationHierarchy: boolean
+ /** Total number of tasks in the delegation tree */
+ taskCount: number
+}
+
+/**
+ * Count the total number of nodes in a task tree.
+ */
+export function countTreeNodes(node: TaskTreeNode | null): number {
+ if (!node) return 0
+ let count = 1
+ for (const child of node.children) {
+ count += countTreeNodes(child)
+ }
+ return count
}
/**
@@ -31,7 +45,7 @@ export interface TaskTreeResult {
*/
export function buildTaskTree(taskHistory: HistoryItem[], currentTaskItem?: HistoryItem): TaskTreeResult {
if (!currentTaskItem) {
- return { rootNode: null, hasDelegationHierarchy: false }
+ return { rootNode: null, hasDelegationHierarchy: false, taskCount: 0 }
}
// Determine the root task ID for the current session.
@@ -44,7 +58,7 @@ export function buildTaskTree(taskHistory: HistoryItem[], currentTaskItem?: Hist
// Need at least 2 tasks for a delegation hierarchy
if (sessionTasks.length < 2) {
- return { rootNode: null, hasDelegationHierarchy: false }
+ return { rootNode: null, hasDelegationHierarchy: false, taskCount: 0 }
}
// Build lookup by id
@@ -76,11 +90,11 @@ export function buildTaskTree(taskHistory: HistoryItem[], currentTaskItem?: Hist
const rootItem = taskMap.get(rootId)
if (!rootItem) {
- return { rootNode: null, hasDelegationHierarchy: false }
+ return { rootNode: null, hasDelegationHierarchy: false, taskCount: 0 }
}
const rootNode = buildNode(rootItem, new Set())
- return { rootNode, hasDelegationHierarchy: true }
+ return { rootNode, hasDelegationHierarchy: true, taskCount: countTreeNodes(rootNode) }
}
/**