mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
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.
This commit is contained in:
parent
1d46bd1bbc
commit
b733d5e205
8 changed files with 451 additions and 9 deletions
|
|
@ -17,6 +17,7 @@ export const historyItemSchema = z.object({
|
|||
size: z.number().optional(),
|
||||
workspace: z.string().optional(),
|
||||
mode: z.string().optional(),
|
||||
parentId: z.string().optional(),
|
||||
})
|
||||
|
||||
export type HistoryItem = z.infer<typeof historyItemSchema>
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ export type TaskMetadataOptions = {
|
|||
globalStoragePath: string
|
||||
workspace: string
|
||||
mode?: string
|
||||
parentId?: string
|
||||
}
|
||||
|
||||
export async function taskMetadata({
|
||||
|
|
@ -28,6 +29,7 @@ export async function taskMetadata({
|
|||
globalStoragePath,
|
||||
workspace,
|
||||
mode,
|
||||
parentId,
|
||||
}: TaskMetadataOptions) {
|
||||
const taskDir = await getTaskDirectoryPath(globalStoragePath, taskId)
|
||||
|
||||
|
|
@ -95,6 +97,7 @@ export async function taskMetadata({
|
|||
size: taskDirSize,
|
||||
workspace,
|
||||
mode,
|
||||
parentId,
|
||||
}
|
||||
|
||||
return { historyItem, tokenUsage }
|
||||
|
|
|
|||
|
|
@ -636,6 +636,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
|
|||
globalStoragePath: this.globalStoragePath,
|
||||
workspace: this.cwd,
|
||||
mode: this._taskMode || defaultModeSlug, // Use the task's own mode, not the current provider mode
|
||||
parentId: this.parentTask?.taskId,
|
||||
})
|
||||
|
||||
this.emit(RooCodeEventName.TaskTokenUsageUpdated, this.taskId, tokenUsage)
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import { useAppTranslation } from "@/i18n/TranslationContext"
|
|||
import { Tab, TabContent, TabHeader } from "../common/Tab"
|
||||
import { useTaskSearch } from "./useTaskSearch"
|
||||
import TaskItem from "./TaskItem"
|
||||
import { useTaskHierarchy } from "./useTaskHierarchy"
|
||||
|
||||
type HistoryViewProps = {
|
||||
onDone: () => void
|
||||
|
|
@ -44,6 +45,13 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
|
|||
const [isSelectionMode, setIsSelectionMode] = useState(false)
|
||||
const [selectedTaskIds, setSelectedTaskIds] = useState<string[]>([])
|
||||
const [showBatchDeleteDialog, setShowBatchDeleteDialog] = useState<boolean>(false)
|
||||
const [showHierarchical, setShowHierarchical] = useState(true)
|
||||
|
||||
// Use the hierarchical task hook
|
||||
const { flattenedTasks, expandedIds, toggleExpanded, expandAll, collapseAll } = useTaskHierarchy(tasks)
|
||||
|
||||
// Use either flat or hierarchical tasks based on toggle
|
||||
const displayTasks = showHierarchical ? flattenedTasks : tasks
|
||||
|
||||
// Toggle selection mode
|
||||
const toggleSelectionMode = () => {
|
||||
|
|
@ -65,7 +73,7 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
|
|||
// Toggle select all tasks
|
||||
const toggleSelectAll = (selectAll: boolean) => {
|
||||
if (selectAll) {
|
||||
setSelectedTaskIds(tasks.map((task) => task.id))
|
||||
setSelectedTaskIds(displayTasks.map((task) => task.id))
|
||||
} else {
|
||||
setSelectedTaskIds([])
|
||||
}
|
||||
|
|
@ -84,6 +92,29 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
|
|||
<div className="flex justify-between items-center">
|
||||
<h3 className="text-vscode-foreground m-0">{t("history:history")}</h3>
|
||||
<div className="flex gap-2">
|
||||
{showHierarchical && (
|
||||
<>
|
||||
<StandardTooltip content={t("history:expandAll")}>
|
||||
<Button variant="secondary" onClick={expandAll}>
|
||||
<span className="codicon codicon-expand-all" />
|
||||
</Button>
|
||||
</StandardTooltip>
|
||||
<StandardTooltip content={t("history:collapseAll")}>
|
||||
<Button variant="secondary" onClick={collapseAll}>
|
||||
<span className="codicon codicon-collapse-all" />
|
||||
</Button>
|
||||
</StandardTooltip>
|
||||
</>
|
||||
)}
|
||||
<StandardTooltip
|
||||
content={showHierarchical ? t("history:showFlat") : t("history:showHierarchical")}>
|
||||
<Button variant="secondary" onClick={() => setShowHierarchical(!showHierarchical)}>
|
||||
<span
|
||||
className={`codicon ${showHierarchical ? "codicon-list-flat" : "codicon-list-tree"} mr-1`}
|
||||
/>
|
||||
{showHierarchical ? t("history:flatView") : t("history:treeView")}
|
||||
</Button>
|
||||
</StandardTooltip>
|
||||
<StandardTooltip
|
||||
content={
|
||||
isSelectionMode
|
||||
|
|
@ -197,23 +228,23 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
|
|||
</div>
|
||||
|
||||
{/* Select all control in selection mode */}
|
||||
{isSelectionMode && tasks.length > 0 && (
|
||||
{isSelectionMode && displayTasks.length > 0 && (
|
||||
<div className="flex items-center py-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
checked={tasks.length > 0 && selectedTaskIds.length === tasks.length}
|
||||
checked={displayTasks.length > 0 && selectedTaskIds.length === displayTasks.length}
|
||||
onCheckedChange={(checked) => toggleSelectAll(checked === true)}
|
||||
variant="description"
|
||||
/>
|
||||
<span className="text-vscode-foreground">
|
||||
{selectedTaskIds.length === tasks.length
|
||||
{selectedTaskIds.length === displayTasks.length
|
||||
? t("history:deselectAll")
|
||||
: t("history:selectAll")}
|
||||
</span>
|
||||
<span className="ml-auto text-vscode-descriptionForeground text-xs">
|
||||
{t("history:selectedItems", {
|
||||
selected: selectedTaskIds.length,
|
||||
total: tasks.length,
|
||||
total: displayTasks.length,
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -225,7 +256,7 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
|
|||
<TabContent className="px-2 py-0">
|
||||
<Virtuoso
|
||||
className="flex-1 overflow-y-scroll"
|
||||
data={tasks}
|
||||
data={displayTasks}
|
||||
data-testid="virtuoso-container"
|
||||
initialTopMostItemIndex={0}
|
||||
components={{
|
||||
|
|
@ -244,6 +275,11 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
|
|||
onToggleSelection={toggleTaskSelection}
|
||||
onDelete={setDeleteTaskId}
|
||||
className="m-2"
|
||||
isHierarchical={showHierarchical}
|
||||
level={showHierarchical ? (item as any).level || 0 : 0}
|
||||
hasChildren={showHierarchical && (item as any).children?.length > 0}
|
||||
isExpanded={showHierarchical && expandedIds.has(item.id)}
|
||||
onToggleExpanded={showHierarchical ? () => toggleExpanded(item.id) : undefined}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
|
@ -253,7 +289,7 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
|
|||
{isSelectionMode && selectedTaskIds.length > 0 && (
|
||||
<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">
|
||||
<div className="text-vscode-foreground">
|
||||
{t("history:selectedItems", { selected: selectedTaskIds.length, total: tasks.length })}
|
||||
{t("history:selectedItems", { selected: selectedTaskIds.length, total: displayTasks.length })}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="secondary" onClick={() => setSelectedTaskIds([])}>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ import TaskItemFooter from "./TaskItemFooter"
|
|||
|
||||
interface DisplayHistoryItem extends HistoryItem {
|
||||
highlight?: string
|
||||
level?: number
|
||||
children?: DisplayHistoryItem[]
|
||||
}
|
||||
|
||||
interface TaskItemProps {
|
||||
|
|
@ -20,6 +22,11 @@ interface TaskItemProps {
|
|||
onToggleSelection?: (taskId: string, isSelected: boolean) => void
|
||||
onDelete?: (taskId: string) => void
|
||||
className?: string
|
||||
isHierarchical?: boolean
|
||||
level?: number
|
||||
hasChildren?: boolean
|
||||
isExpanded?: boolean
|
||||
onToggleExpanded?: () => void
|
||||
}
|
||||
|
||||
const TaskItem = ({
|
||||
|
|
@ -31,15 +38,27 @@ const TaskItem = ({
|
|||
onToggleSelection,
|
||||
onDelete,
|
||||
className,
|
||||
isHierarchical = false,
|
||||
level = 0,
|
||||
hasChildren = false,
|
||||
isExpanded = false,
|
||||
onToggleExpanded,
|
||||
}: TaskItemProps) => {
|
||||
const handleClick = () => {
|
||||
if (isSelectionMode && onToggleSelection) {
|
||||
onToggleSelection(item.id, !isSelected)
|
||||
} else {
|
||||
} else if (!isHierarchical || !hasChildren) {
|
||||
vscode.postMessage({ type: "showTaskWithId", text: item.id })
|
||||
}
|
||||
}
|
||||
|
||||
const handleExpandClick = (e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
if (onToggleExpanded) {
|
||||
onToggleExpanded()
|
||||
}
|
||||
}
|
||||
|
||||
const isCompact = variant === "compact"
|
||||
|
||||
return (
|
||||
|
|
@ -50,8 +69,28 @@ const TaskItem = ({
|
|||
"cursor-pointer group bg-vscode-editor-background rounded relative overflow-hidden border border-transparent hover:bg-vscode-list-hoverBackground transition-colors",
|
||||
className,
|
||||
)}
|
||||
style={isHierarchical ? { marginLeft: `${level * 24}px` } : undefined}
|
||||
onClick={handleClick}>
|
||||
<div className={(!isCompact && isSelectionMode ? "pl-3 pb-3" : "pl-4") + " flex gap-3 px-3 pt-3 pb-1"}>
|
||||
{/* Expand/collapse button for hierarchical view */}
|
||||
{isHierarchical && hasChildren && (
|
||||
<button
|
||||
className="flex items-center justify-center w-5 h-5 mt-1 hover:bg-vscode-list-hoverBackground rounded"
|
||||
onClick={handleExpandClick}
|
||||
aria-label={isExpanded ? "Collapse" : "Expand"}>
|
||||
<span
|
||||
className={cn(
|
||||
"codicon",
|
||||
isExpanded ? "codicon-chevron-down" : "codicon-chevron-right",
|
||||
"text-xs",
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Spacer for items without children in hierarchical view */}
|
||||
{isHierarchical && !hasChildren && <div className="w-5" />}
|
||||
|
||||
{/* Selection checkbox - only in full variant */}
|
||||
{!isCompact && isSelectionMode && (
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -0,0 +1,214 @@
|
|||
import { describe, it, expect } from "vitest"
|
||||
import { buildTaskHierarchy, flattenTaskHierarchy } from "../useTaskHierarchy"
|
||||
import type { HistoryItem } from "@roo-code/types"
|
||||
|
||||
describe("useTaskHierarchy", () => {
|
||||
describe("buildTaskHierarchy", () => {
|
||||
it("should build a hierarchical structure from flat tasks", () => {
|
||||
const tasks: HistoryItem[] = [
|
||||
{
|
||||
id: "task1",
|
||||
number: 1,
|
||||
ts: 1000,
|
||||
task: "Parent Task 1",
|
||||
tokensIn: 100,
|
||||
tokensOut: 200,
|
||||
totalCost: 0.01,
|
||||
},
|
||||
{
|
||||
id: "task2",
|
||||
number: 2,
|
||||
ts: 2000,
|
||||
task: "Child Task 1",
|
||||
tokensIn: 50,
|
||||
tokensOut: 100,
|
||||
totalCost: 0.005,
|
||||
parentId: "task1",
|
||||
},
|
||||
{
|
||||
id: "task3",
|
||||
number: 3,
|
||||
ts: 3000,
|
||||
task: "Parent Task 2",
|
||||
tokensIn: 150,
|
||||
tokensOut: 250,
|
||||
totalCost: 0.015,
|
||||
},
|
||||
{
|
||||
id: "task4",
|
||||
number: 4,
|
||||
ts: 4000,
|
||||
task: "Child Task 2",
|
||||
tokensIn: 75,
|
||||
tokensOut: 125,
|
||||
totalCost: 0.007,
|
||||
parentId: "task1",
|
||||
},
|
||||
]
|
||||
|
||||
const hierarchy = buildTaskHierarchy(tasks)
|
||||
|
||||
// Should have 2 root tasks
|
||||
expect(hierarchy).toHaveLength(2)
|
||||
|
||||
// First root task should be task3 (newer timestamp)
|
||||
expect(hierarchy[0].id).toBe("task3")
|
||||
expect(hierarchy[0].children).toHaveLength(0)
|
||||
expect(hierarchy[0].level).toBe(0)
|
||||
|
||||
// Second root task should be task1 (older timestamp)
|
||||
expect(hierarchy[1].id).toBe("task1")
|
||||
expect(hierarchy[1].children).toHaveLength(2)
|
||||
expect(hierarchy[1].level).toBe(0)
|
||||
|
||||
// Children of task1 should be sorted by timestamp (newest first)
|
||||
expect(hierarchy[1].children[0].id).toBe("task4")
|
||||
expect(hierarchy[1].children[0].level).toBe(1)
|
||||
expect(hierarchy[1].children[1].id).toBe("task2")
|
||||
expect(hierarchy[1].children[1].level).toBe(1)
|
||||
})
|
||||
|
||||
it("should handle tasks with no parent-child relationships", () => {
|
||||
const tasks: HistoryItem[] = [
|
||||
{
|
||||
id: "task1",
|
||||
number: 1,
|
||||
ts: 1000,
|
||||
task: "Task 1",
|
||||
tokensIn: 100,
|
||||
tokensOut: 200,
|
||||
totalCost: 0.01,
|
||||
},
|
||||
{
|
||||
id: "task2",
|
||||
number: 2,
|
||||
ts: 2000,
|
||||
task: "Task 2",
|
||||
tokensIn: 50,
|
||||
tokensOut: 100,
|
||||
totalCost: 0.005,
|
||||
},
|
||||
]
|
||||
|
||||
const hierarchy = buildTaskHierarchy(tasks)
|
||||
|
||||
expect(hierarchy).toHaveLength(2)
|
||||
expect(hierarchy[0].id).toBe("task2") // Newer first
|
||||
expect(hierarchy[1].id).toBe("task1")
|
||||
expect(hierarchy[0].children).toHaveLength(0)
|
||||
expect(hierarchy[1].children).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should handle nested hierarchies", () => {
|
||||
const tasks: HistoryItem[] = [
|
||||
{
|
||||
id: "task1",
|
||||
number: 1,
|
||||
ts: 1000,
|
||||
task: "Root Task",
|
||||
tokensIn: 100,
|
||||
tokensOut: 200,
|
||||
totalCost: 0.01,
|
||||
},
|
||||
{
|
||||
id: "task2",
|
||||
number: 2,
|
||||
ts: 2000,
|
||||
task: "Child Task",
|
||||
tokensIn: 50,
|
||||
tokensOut: 100,
|
||||
totalCost: 0.005,
|
||||
parentId: "task1",
|
||||
},
|
||||
{
|
||||
id: "task3",
|
||||
number: 3,
|
||||
ts: 3000,
|
||||
task: "Grandchild Task",
|
||||
tokensIn: 25,
|
||||
tokensOut: 50,
|
||||
totalCost: 0.002,
|
||||
parentId: "task2",
|
||||
},
|
||||
]
|
||||
|
||||
const hierarchy = buildTaskHierarchy(tasks)
|
||||
|
||||
expect(hierarchy).toHaveLength(1)
|
||||
expect(hierarchy[0].id).toBe("task1")
|
||||
expect(hierarchy[0].children).toHaveLength(1)
|
||||
expect(hierarchy[0].children[0].id).toBe("task2")
|
||||
expect(hierarchy[0].children[0].level).toBe(1)
|
||||
expect(hierarchy[0].children[0].children).toHaveLength(1)
|
||||
expect(hierarchy[0].children[0].children[0].id).toBe("task3")
|
||||
expect(hierarchy[0].children[0].children[0].level).toBe(2)
|
||||
})
|
||||
})
|
||||
|
||||
describe("flattenTaskHierarchy", () => {
|
||||
it("should flatten hierarchical tasks with all expanded", () => {
|
||||
const tasks: HistoryItem[] = [
|
||||
{
|
||||
id: "task1",
|
||||
number: 1,
|
||||
ts: 1000,
|
||||
task: "Parent Task",
|
||||
tokensIn: 100,
|
||||
tokensOut: 200,
|
||||
totalCost: 0.01,
|
||||
},
|
||||
{
|
||||
id: "task2",
|
||||
number: 2,
|
||||
ts: 2000,
|
||||
task: "Child Task",
|
||||
tokensIn: 50,
|
||||
tokensOut: 100,
|
||||
totalCost: 0.005,
|
||||
parentId: "task1",
|
||||
},
|
||||
]
|
||||
|
||||
const hierarchy = buildTaskHierarchy(tasks)
|
||||
const expandedIds = new Set(["task1"])
|
||||
const flattened = flattenTaskHierarchy(hierarchy, expandedIds)
|
||||
|
||||
expect(flattened).toHaveLength(2)
|
||||
expect(flattened[0].id).toBe("task1")
|
||||
expect(flattened[1].id).toBe("task2")
|
||||
})
|
||||
|
||||
it("should hide children when parent is collapsed", () => {
|
||||
const tasks: HistoryItem[] = [
|
||||
{
|
||||
id: "task1",
|
||||
number: 1,
|
||||
ts: 1000,
|
||||
task: "Parent Task",
|
||||
tokensIn: 100,
|
||||
tokensOut: 200,
|
||||
totalCost: 0.01,
|
||||
},
|
||||
{
|
||||
id: "task2",
|
||||
number: 2,
|
||||
ts: 2000,
|
||||
task: "Child Task",
|
||||
tokensIn: 50,
|
||||
tokensOut: 100,
|
||||
totalCost: 0.005,
|
||||
parentId: "task1",
|
||||
},
|
||||
]
|
||||
|
||||
const hierarchy = buildTaskHierarchy(tasks)
|
||||
// When expandedIds is empty (size === 0), all items are expanded by default
|
||||
// To test collapsed state, we need to pass a non-empty set that doesn't include task1
|
||||
const expandedIds = new Set<string>(["some-other-id"]) // Non-empty set without task1
|
||||
const flattened = flattenTaskHierarchy(hierarchy, expandedIds)
|
||||
|
||||
expect(flattened).toHaveLength(1)
|
||||
expect(flattened[0].id).toBe("task1")
|
||||
})
|
||||
})
|
||||
})
|
||||
142
webview-ui/src/components/history/useTaskHierarchy.ts
Normal file
142
webview-ui/src/components/history/useTaskHierarchy.ts
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
import { useMemo } from "react"
|
||||
import type { HistoryItem } from "@roo-code/types"
|
||||
|
||||
export interface HierarchicalHistoryItem extends HistoryItem {
|
||||
children: HierarchicalHistoryItem[]
|
||||
level: number
|
||||
isExpanded?: boolean
|
||||
highlight?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a hierarchical tree structure from flat task history
|
||||
*/
|
||||
export function buildTaskHierarchy(tasks: HistoryItem[]): HierarchicalHistoryItem[] {
|
||||
const taskMap = new Map<string, HierarchicalHistoryItem>()
|
||||
const rootTasks: HierarchicalHistoryItem[] = []
|
||||
|
||||
// First pass: Create hierarchical items for all tasks
|
||||
tasks.forEach((task) => {
|
||||
const hierarchicalTask: HierarchicalHistoryItem = {
|
||||
...task,
|
||||
children: [],
|
||||
level: 0,
|
||||
isExpanded: true, // Default to expanded
|
||||
}
|
||||
taskMap.set(task.id, hierarchicalTask)
|
||||
})
|
||||
|
||||
// Second pass: Build the tree structure
|
||||
tasks.forEach((task) => {
|
||||
const hierarchicalTask = taskMap.get(task.id)!
|
||||
|
||||
if (task.parentId && taskMap.has(task.parentId)) {
|
||||
// This is a child task
|
||||
const parent = taskMap.get(task.parentId)!
|
||||
parent.children.push(hierarchicalTask)
|
||||
hierarchicalTask.level = parent.level + 1
|
||||
} else {
|
||||
// This is a root task
|
||||
rootTasks.push(hierarchicalTask)
|
||||
}
|
||||
})
|
||||
|
||||
// Sort tasks by timestamp (newest first) at each level
|
||||
const sortByTimestamp = (a: HierarchicalHistoryItem, b: HierarchicalHistoryItem) => b.ts - a.ts
|
||||
|
||||
const sortRecursively = (items: HierarchicalHistoryItem[]) => {
|
||||
items.sort(sortByTimestamp)
|
||||
items.forEach((item) => {
|
||||
if (item.children.length > 0) {
|
||||
sortRecursively(item.children)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
sortRecursively(rootTasks)
|
||||
|
||||
return rootTasks
|
||||
}
|
||||
|
||||
/**
|
||||
* Flatten a hierarchical task structure for display
|
||||
*/
|
||||
export function flattenTaskHierarchy(
|
||||
tasks: HierarchicalHistoryItem[],
|
||||
expandedIds: Set<string> = new Set(),
|
||||
): HierarchicalHistoryItem[] {
|
||||
const result: HierarchicalHistoryItem[] = []
|
||||
|
||||
const traverse = (items: HierarchicalHistoryItem[]) => {
|
||||
items.forEach((item) => {
|
||||
result.push(item)
|
||||
|
||||
// Only include children if the parent is expanded
|
||||
// Default to expanded (true) unless explicitly collapsed (not in expandedIds when expandedIds is being used)
|
||||
const isExpanded = expandedIds.size === 0 ? true : expandedIds.has(item.id)
|
||||
if (item.children.length > 0 && isExpanded) {
|
||||
traverse(item.children)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
traverse(tasks)
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom hook to manage task hierarchy state
|
||||
*/
|
||||
export function useTaskHierarchy(tasks: HistoryItem[]) {
|
||||
const [expandedIds, setExpandedIds] = useState<Set<string>>(new Set())
|
||||
|
||||
const hierarchicalTasks = useMemo(() => {
|
||||
return buildTaskHierarchy(tasks)
|
||||
}, [tasks])
|
||||
|
||||
const flattenedTasks = useMemo(() => {
|
||||
return flattenTaskHierarchy(hierarchicalTasks, expandedIds)
|
||||
}, [hierarchicalTasks, expandedIds])
|
||||
|
||||
const toggleExpanded = (taskId: string) => {
|
||||
setExpandedIds((prev) => {
|
||||
const next = new Set(prev)
|
||||
if (next.has(taskId)) {
|
||||
next.delete(taskId)
|
||||
} else {
|
||||
next.add(taskId)
|
||||
}
|
||||
return next
|
||||
})
|
||||
}
|
||||
|
||||
const expandAll = () => {
|
||||
const allIds = new Set<string>()
|
||||
const collectIds = (items: HierarchicalHistoryItem[]) => {
|
||||
items.forEach((item) => {
|
||||
if (item.children.length > 0) {
|
||||
allIds.add(item.id)
|
||||
collectIds(item.children)
|
||||
}
|
||||
})
|
||||
}
|
||||
collectIds(hierarchicalTasks)
|
||||
setExpandedIds(allIds)
|
||||
}
|
||||
|
||||
const collapseAll = () => {
|
||||
setExpandedIds(new Set())
|
||||
}
|
||||
|
||||
return {
|
||||
hierarchicalTasks,
|
||||
flattenedTasks,
|
||||
expandedIds,
|
||||
toggleExpanded,
|
||||
expandAll,
|
||||
collapseAll,
|
||||
}
|
||||
}
|
||||
|
||||
// Import useState
|
||||
import { useState } from "react"
|
||||
|
|
@ -41,5 +41,11 @@
|
|||
"mostTokens": "Most Tokens",
|
||||
"mostRelevant": "Most Relevant"
|
||||
},
|
||||
"viewAllHistory": "View all tasks"
|
||||
"viewAllHistory": "View all tasks",
|
||||
"expandAll": "Expand All",
|
||||
"collapseAll": "Collapse All",
|
||||
"showFlat": "Show Flat View",
|
||||
"showHierarchical": "Show Hierarchical View",
|
||||
"flatView": "Flat View",
|
||||
"treeView": "Tree View"
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue