Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/types/src/history.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>
3 changes: 3 additions & 0 deletions src/core/task-persistence/taskMetadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export type TaskMetadataOptions = {
globalStoragePath: string
workspace: string
mode?: string
parentId?: string
}

export async function taskMetadata({
Expand All @@ -28,6 +29,7 @@ export async function taskMetadata({
globalStoragePath,
workspace,
mode,
parentId,
}: TaskMetadataOptions) {
const taskDir = await getTaskDirectoryPath(globalStoragePath, taskId)

Expand Down Expand Up @@ -95,6 +97,7 @@ export async function taskMetadata({
size: taskDirSize,
workspace,
mode,
parentId,
}

return { historyItem, tokenUsage }
Expand Down
1 change: 1 addition & 0 deletions src/core/task/Task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
50 changes: 43 additions & 7 deletions webview-ui/src/components/history/HistoryView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Performance consideration: The flattenedTasks computation happens on every render when showHierarchical changes. Since useTaskHierarchy already uses useMemo internally, this might be acceptable, but have you considered the performance impact with large task lists?


// Use either flat or hierarchical tasks based on toggle
const displayTasks = showHierarchical ? flattenedTasks : tasks

// Toggle selection mode
const toggleSelectionMode = () => {
Expand All @@ -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([])
}
Expand All @@ -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
Expand Down Expand Up @@ -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>
Expand All @@ -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={{
Expand All @@ -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}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we avoid using any type casting here? Lines 279-282 use (item as any).level and similar assertions which bypass TypeScript's type safety. Consider properly typing displayTasks as HierarchicalHistoryItem[] when showHierarchical is true.

hasChildren={showHierarchical && (item as any).children?.length > 0}
isExpanded={showHierarchical && expandedIds.has(item.id)}
onToggleExpanded={showHierarchical ? () => toggleExpanded(item.id) : undefined}
/>
)}
/>
Expand All @@ -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([])}>
Expand Down
41 changes: 40 additions & 1 deletion webview-ui/src/components/history/TaskItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import TaskItemFooter from "./TaskItemFooter"

interface DisplayHistoryItem extends HistoryItem {
highlight?: string
level?: number
children?: DisplayHistoryItem[]
}

interface TaskItemProps {
Expand All @@ -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 = ({
Expand All @@ -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 (
Expand All @@ -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
Expand Down
Loading
Loading