diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx
index dd9cbbe36a7..2d58eb82bc1 100644
--- a/webview-ui/src/components/chat/ChatView.tsx
+++ b/webview-ui/src/components/chat/ChatView.tsx
@@ -44,6 +44,7 @@ import { CheckpointWarning } from "./CheckpointWarning"
import { QueuedMessages } from "./QueuedMessages"
import { WorktreeSelector } from "./WorktreeSelector"
import FileChangesPanel from "./FileChangesPanel"
+import { TaskDashboard } from "./task-dashboard"
import DismissibleUpsell from "../common/DismissibleUpsell"
import { useCloudUpsell } from "@src/hooks/useCloudUpsell"
import { useScrollLifecycle } from "@src/hooks/useScrollLifecycle"
@@ -1615,6 +1616,8 @@ const ChatViewComponent: React.ForwardRefRenderFunction
+
+
{checkpointWarning && (
diff --git a/webview-ui/src/components/chat/task-dashboard/TaskDashboard.tsx b/webview-ui/src/components/chat/task-dashboard/TaskDashboard.tsx
new file mode 100644
index 00000000000..b06f21cbbee
--- /dev/null
+++ b/webview-ui/src/components/chat/task-dashboard/TaskDashboard.tsx
@@ -0,0 +1,213 @@
+import { memo, useState, useCallback, useMemo } from "react"
+import { ChevronDown, ChevronRight, GitBranch } from "lucide-react"
+
+import type { ModeConfig } from "@roo-code/types"
+
+import { getAllModes } from "@roo/modes"
+
+import { cn } from "@src/lib/utils"
+import { vscode } from "@src/utils/vscode"
+import { useExtensionState } from "@src/context/ExtensionStateContext"
+
+import type { TaskTreeNode } from "./useTaskTree"
+import { useTaskTree } from "./useTaskTree"
+
+/**
+ * Status badge colors for task states.
+ */
+const statusConfig: Record
= {
+ active: { label: "Active", className: "bg-vscode-charts-green text-white" },
+ delegated: { label: "Delegated", className: "bg-vscode-charts-blue text-white" },
+ completed: {
+ label: "Completed",
+ className: "bg-vscode-descriptionForeground/30 text-vscode-descriptionForeground",
+ },
+}
+
+interface TaskNodeRowProps {
+ node: TaskTreeNode
+ depth: number
+ currentTaskId?: string
+ modeMap: Map
+}
+
+/**
+ * A single row in the task tree, showing mode name, status badge,
+ * and active indicator. Supports click-to-navigate.
+ */
+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"
+ const status = item.status ?? "active"
+ const statusInfo = statusConfig[status] ?? statusConfig.active
+
+ const handleClick = useCallback(() => {
+ vscode.postMessage({ type: "showTaskWithId", text: item.id })
+ }, [item.id])
+
+ const handleKeyDown = useCallback(
+ (e: React.KeyboardEvent) => {
+ if (e.key === "Enter" || e.key === " ") {
+ e.preventDefault()
+ handleClick()
+ }
+ },
+ [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
+
+ return (
+
+
+ {/* Expand/collapse toggle for nodes with children */}
+ {hasChildren ? (
+
+ ) : (
+
+ )}
+
+ {/* Mode icon/indicator */}
+
+
+ {/* Mode name */}
+
+ {modeName}
+
+
+ {/* Status badge */}
+
+ {statusInfo.label}
+
+
+ {/* Task summary (truncated) */}
+
+ {taskSummary}
+
+
+
+ {/* Render children (collapsible) */}
+ {hasChildren && isNodeExpanded && (
+
+ {children.map((child) => (
+
+ ))}
+
+ )}
+
+ )
+})
+
+TaskNodeRow.displayName = "TaskNodeRow"
+
+/**
+ * The Task Coordination Dashboard component.
+ *
+ * Displays a collapsible tree view of the current delegation session,
+ * showing each task's mode, status, and delegation relationships.
+ * Only visible when the current task is part of a multi-task delegation hierarchy.
+ */
+const TaskDashboard = () => {
+ const { taskHistory, currentTaskItem, currentTaskId, customModes } = useExtensionState()
+ const { rootNode, hasDelegationHierarchy, taskCount } = useTaskTree(taskHistory, currentTaskItem)
+ const [isExpanded, setIsExpanded] = useState(true)
+
+ // Build a mode lookup map
+ const modeMap = useMemo(() => {
+ const allModes = getAllModes(customModes)
+ const map = new Map()
+ for (const mode of allModes) {
+ map.set(mode.slug, mode)
+ }
+ return map
+ }, [customModes])
+
+ const toggleExpanded = useCallback(() => {
+ setIsExpanded((prev) => !prev)
+ }, [])
+
+ // Don't render if there's no delegation hierarchy
+ if (!hasDelegationHierarchy || !rootNode) {
+ return null
+ }
+
+ return (
+
+ {/* Header */}
+
+
+ {/* Tree content */}
+ {isExpanded && (
+
+
+
+ )}
+
+ )
+}
+
+export default memo(TaskDashboard)
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
new file mode 100644
index 00000000000..6c55ba7b294
--- /dev/null
+++ b/webview-ui/src/components/chat/task-dashboard/__tests__/TaskDashboard.spec.tsx
@@ -0,0 +1,370 @@
+import { render, screen, fireEvent } from "@testing-library/react"
+import type { HistoryItem } from "@roo-code/types"
+import TaskDashboard from "../TaskDashboard"
+
+// Mock the vscode API
+const mockPostMessage = vi.fn()
+vi.mock("@src/utils/vscode", () => ({
+ vscode: { postMessage: (...args: any[]) => mockPostMessage(...args) },
+}))
+
+// Mock useTranslation
+vi.mock("react-i18next", () => ({
+ useTranslation: () => ({ t: (key: string) => key }),
+}))
+
+// Mock extension state
+let mockState: Record = {}
+vi.mock("@src/context/ExtensionStateContext", () => ({
+ useExtensionState: () => mockState,
+}))
+
+// Mock modes
+vi.mock("@roo/modes", () => ({
+ getAllModes: (customModes: any[]) => [
+ { slug: "orchestrator", name: "Orchestrator" },
+ { slug: "code", name: "Code" },
+ { slug: "architect", name: "Architect" },
+ { slug: "debug", name: "Debug" },
+ ...(customModes || []),
+ ],
+}))
+
+function makeItem(overrides: Partial & { id: string }): HistoryItem {
+ return {
+ ts: Date.now(),
+ task: "Test task",
+ tokensIn: 0,
+ tokensOut: 0,
+ totalCost: 0,
+ number: 1,
+ ...overrides,
+ }
+}
+
+describe("TaskDashboard", () => {
+ beforeEach(() => {
+ mockPostMessage.mockClear()
+ mockState = {
+ taskHistory: [],
+ currentTaskItem: undefined,
+ currentTaskId: undefined,
+ customModes: [],
+ }
+ })
+
+ it("does not render when there is no delegation hierarchy", () => {
+ const standalone = makeItem({ id: "standalone", task: "Simple task" })
+ mockState = {
+ taskHistory: [standalone],
+ currentTaskItem: standalone,
+ currentTaskId: "standalone",
+ customModes: [],
+ }
+
+ const { container } = render()
+ expect(container.innerHTML).toBe("")
+ })
+
+ it("renders the dashboard when delegation hierarchy exists", () => {
+ const parent = makeItem({
+ id: "parent-1",
+ task: "Orchestrator task",
+ mode: "orchestrator",
+ status: "delegated",
+ childIds: ["child-1"],
+ })
+ const child = makeItem({
+ id: "child-1",
+ task: "Code task",
+ mode: "code",
+ status: "active",
+ rootTaskId: "parent-1",
+ parentTaskId: "parent-1",
+ })
+ mockState = {
+ taskHistory: [parent, child],
+ currentTaskItem: child,
+ currentTaskId: "child-1",
+ customModes: [],
+ }
+
+ render()
+
+ expect(screen.getByTestId("task-dashboard")).toBeTruthy()
+ expect(screen.getByText("Task Delegation (2 tasks)")).toBeTruthy()
+ })
+
+ it("displays mode names for each task node", () => {
+ 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()
+
+ expect(screen.getByText("Orchestrator")).toBeTruthy()
+ expect(screen.getByText("Code")).toBeTruthy()
+ })
+
+ it("displays status badges", () => {
+ 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()
+
+ expect(screen.getByText("Delegated")).toBeTruthy()
+ expect(screen.getByText("Active")).toBeTruthy()
+ })
+
+ it("sends showTaskWithId message on task node click", () => {
+ 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()
+
+ const parentNode = screen.getByTestId("task-node-parent-1")
+ fireEvent.click(parentNode.querySelector("[role='button']")!)
+
+ expect(mockPostMessage).toHaveBeenCalledWith({
+ type: "showTaskWithId",
+ text: "parent-1",
+ })
+ })
+
+ it("collapses and expands the dashboard", () => {
+ 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()
+
+ // Should start expanded
+ expect(screen.getByTestId("task-dashboard-content")).toBeTruthy()
+
+ // Click toggle to collapse
+ fireEvent.click(screen.getByTestId("task-dashboard-toggle"))
+
+ // Content should be hidden
+ expect(screen.queryByTestId("task-dashboard-content")).toBeNull()
+
+ // Click again to expand
+ fireEvent.click(screen.getByTestId("task-dashboard-toggle"))
+
+ expect(screen.getByTestId("task-dashboard-content")).toBeTruthy()
+ })
+
+ it("highlights the currently active task", () => {
+ 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()
+
+ // The active task node should have the active selection class
+ const activeNode = screen.getByTestId("task-node-child-1")
+ 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
new file mode 100644
index 00000000000..eaa12aec309
--- /dev/null
+++ b/webview-ui/src/components/chat/task-dashboard/__tests__/useTaskTree.spec.ts
@@ -0,0 +1,336 @@
+import type { HistoryItem } from "@roo-code/types"
+import { buildTaskTree, countTreeNodes } from "../useTaskTree"
+
+function makeItem(overrides: Partial & { id: string }): HistoryItem {
+ return {
+ ts: Date.now(),
+ task: "Test task",
+ tokensIn: 0,
+ tokensOut: 0,
+ totalCost: 0,
+ number: 1,
+ ...overrides,
+ }
+}
+
+describe("buildTaskTree", () => {
+ it("returns null when currentTaskItem is undefined", () => {
+ 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", () => {
+ const item = makeItem({ id: "standalone" })
+ 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", () => {
+ const parent = makeItem({
+ id: "parent-1",
+ task: "Orchestrator task",
+ mode: "orchestrator",
+ status: "delegated",
+ childIds: ["child-1"],
+ })
+ const child = makeItem({
+ id: "child-1",
+ task: "Code task",
+ mode: "code",
+ status: "active",
+ rootTaskId: "parent-1",
+ parentTaskId: "parent-1",
+ })
+ const history = [parent, child]
+
+ const result = buildTaskTree(history, child)
+
+ expect(result.hasDelegationHierarchy).toBe(true)
+ expect(result.rootNode).not.toBeNull()
+ 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", () => {
+ const parent = makeItem({
+ id: "parent-1",
+ task: "Orchestrator task",
+ mode: "orchestrator",
+ status: "delegated",
+ childIds: ["child-1"],
+ })
+ const child = makeItem({
+ id: "child-1",
+ task: "Code task",
+ mode: "code",
+ status: "active",
+ rootTaskId: "parent-1",
+ parentTaskId: "parent-1",
+ })
+ const history = [parent, child]
+
+ // Current task is the root itself
+ const result = buildTaskTree(history, parent)
+
+ expect(result.hasDelegationHierarchy).toBe(true)
+ expect(result.rootNode!.item.id).toBe("parent-1")
+ expect(result.rootNode!.children).toHaveLength(1)
+ })
+
+ it("builds a deep tree (parent -> child -> grandchild)", () => {
+ 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.hasDelegationHierarchy).toBe(true)
+ expect(result.rootNode!.item.id).toBe("root")
+ expect(result.rootNode!.children).toHaveLength(1)
+ expect(result.rootNode!.children[0].item.id).toBe("mid")
+ expect(result.rootNode!.children[0].children).toHaveLength(1)
+ expect(result.rootNode!.children[0].children[0].item.id).toBe("leaf")
+ })
+
+ it("builds a tree with 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.hasDelegationHierarchy).toBe(true)
+ expect(result.rootNode!.children).toHaveLength(3)
+ })
+
+ it("handles circular references safely", () => {
+ const taskA = makeItem({
+ id: "a",
+ task: "Task A",
+ status: "delegated",
+ childIds: ["b"],
+ })
+ const taskB = makeItem({
+ id: "b",
+ task: "Task B",
+ status: "delegated",
+ rootTaskId: "a",
+ parentTaskId: "a",
+ childIds: ["a"], // circular reference
+ })
+ const history = [taskA, taskB]
+
+ // Should not throw or infinite loop
+ const result = buildTaskTree(history, taskB)
+
+ expect(result.hasDelegationHierarchy).toBe(true)
+ expect(result.rootNode!.item.id).toBe("a")
+ })
+
+ it("excludes tasks from other sessions", () => {
+ const root = makeItem({
+ id: "root",
+ task: "Root task",
+ childIds: ["child"],
+ })
+ const child = makeItem({
+ id: "child",
+ task: "Child task",
+ rootTaskId: "root",
+ parentTaskId: "root",
+ })
+ const otherRoot = makeItem({
+ id: "other-root",
+ task: "Other session",
+ childIds: ["other-child"],
+ })
+ const otherChild = makeItem({
+ id: "other-child",
+ task: "Other child",
+ rootTaskId: "other-root",
+ parentTaskId: "other-root",
+ })
+ const history = [root, child, otherRoot, otherChild]
+
+ const result = buildTaskTree(history, child)
+
+ expect(result.hasDelegationHierarchy).toBe(true)
+ expect(result.rootNode!.item.id).toBe("root")
+ expect(result.rootNode!.children).toHaveLength(1)
+ 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",
+ task: "Root task",
+ childIds: ["existing-child", "missing-child"],
+ })
+ const child = makeItem({
+ id: "existing-child",
+ task: "Existing child",
+ rootTaskId: "root",
+ parentTaskId: "root",
+ })
+ // "missing-child" is not in the history
+ const history = [root, child]
+
+ const result = buildTaskTree(history, child)
+
+ expect(result.hasDelegationHierarchy).toBe(true)
+ // Only the existing child should appear
+ expect(result.rootNode!.children).toHaveLength(1)
+ 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/index.ts b/webview-ui/src/components/chat/task-dashboard/index.ts
new file mode 100644
index 00000000000..d69211552b1
--- /dev/null
+++ b/webview-ui/src/components/chat/task-dashboard/index.ts
@@ -0,0 +1,3 @@
+export { default as TaskDashboard } from "./TaskDashboard"
+export { useTaskTree, buildTaskTree } from "./useTaskTree"
+export type { TaskTreeNode, TaskTreeResult } from "./useTaskTree"
diff --git a/webview-ui/src/components/chat/task-dashboard/useTaskTree.ts b/webview-ui/src/components/chat/task-dashboard/useTaskTree.ts
new file mode 100644
index 00000000000..41379d509f6
--- /dev/null
+++ b/webview-ui/src/components/chat/task-dashboard/useTaskTree.ts
@@ -0,0 +1,109 @@
+import { useMemo } from "react"
+import type { HistoryItem } from "@roo-code/types"
+
+/**
+ * A node in the task delegation tree.
+ */
+export interface TaskTreeNode {
+ /** The history item for this task */
+ item: HistoryItem
+ /** Child tasks that were delegated from this task */
+ children: TaskTreeNode[]
+}
+
+/**
+ * Result from the useTaskTree hook.
+ */
+export interface TaskTreeResult {
+ /** The root node of the task tree (null if no delegation hierarchy exists) */
+ 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
+}
+
+/**
+ * Given the full taskHistory and the current task item, build a tree
+ * of tasks belonging to the current delegation session.
+ *
+ * A "session" is identified by the rootTaskId: the top-level orchestrator
+ * task that started the delegation chain. All tasks sharing the same
+ * rootTaskId (or whose id IS the rootTaskId) belong to the same session.
+ */
+export function buildTaskTree(taskHistory: HistoryItem[], currentTaskItem?: HistoryItem): TaskTreeResult {
+ if (!currentTaskItem) {
+ return { rootNode: null, hasDelegationHierarchy: false, taskCount: 0 }
+ }
+
+ // Determine the root task ID for the current session.
+ // If the current task has a rootTaskId, use that. Otherwise,
+ // if the current task itself has children, it IS the root.
+ const rootId = currentTaskItem.rootTaskId ?? currentTaskItem.id
+
+ // Collect all tasks belonging to this session
+ const sessionTasks = taskHistory.filter((item) => item.id === rootId || item.rootTaskId === rootId)
+
+ // Need at least 2 tasks for a delegation hierarchy
+ if (sessionTasks.length < 2) {
+ return { rootNode: null, hasDelegationHierarchy: false, taskCount: 0 }
+ }
+
+ // Build lookup by id
+ const taskMap = new Map()
+ for (const task of sessionTasks) {
+ taskMap.set(task.id, task)
+ }
+
+ // Build tree nodes recursively
+ const buildNode = (item: HistoryItem, visited: Set): TaskTreeNode => {
+ // Prevent circular references
+ if (visited.has(item.id)) {
+ return { item, children: [] }
+ }
+ visited.add(item.id)
+
+ const children: TaskTreeNode[] = []
+ if (item.childIds) {
+ for (const childId of item.childIds) {
+ const childItem = taskMap.get(childId)
+ if (childItem) {
+ children.push(buildNode(childItem, visited))
+ }
+ }
+ }
+
+ return { item, children }
+ }
+
+ const rootItem = taskMap.get(rootId)
+ if (!rootItem) {
+ return { rootNode: null, hasDelegationHierarchy: false, taskCount: 0 }
+ }
+
+ const rootNode = buildNode(rootItem, new Set())
+ return { rootNode, hasDelegationHierarchy: true, taskCount: countTreeNodes(rootNode) }
+}
+
+/**
+ * Hook that builds a task delegation tree for the current session.
+ *
+ * @param taskHistory - Full task history from extension state
+ * @param currentTaskItem - The currently active task's history item
+ * @returns The delegation tree for the current session
+ */
+export function useTaskTree(taskHistory: HistoryItem[], currentTaskItem?: HistoryItem): TaskTreeResult {
+ return useMemo(() => buildTaskTree(taskHistory, currentTaskItem), [taskHistory, currentTaskItem])
+}