Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
2 changes: 1 addition & 1 deletion webview-ui/src/components/common/VersionIndicator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const VersionIndicator: React.FC<VersionIndicatorProps> = ({ onClick, className
return (
<button
onClick={onClick}
className={`text-xs text-vscode-descriptionForeground hover:text-vscode-foreground transition-colors cursor-pointer px-2 py-1 rounded border border-vscode-panel-border hover:border-vscode-focusBorder ${className}`}
className={`text-xs text-vscode-descriptionForeground hover:text-vscode-foreground transition-colors cursor-pointer px-2 py-1 rounded border ${className}`}
aria-label={t("chat:versionIndicator.ariaLabel", { version: Package.version })}>
v{Package.version}
</button>
Expand Down
2 changes: 1 addition & 1 deletion webview-ui/src/components/history/DeleteButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export const DeleteButton = ({ itemId, onDelete }: DeleteButtonProps) => {
size="icon"
data-testid="delete-task-button"
onClick={handleDeleteClick}
className="group-hover:opacity-100 opacity-50 transition-opacity">
className="opacity-70">
<span className="codicon codicon-trash size-4 align-middle text-vscode-descriptionForeground" />
</Button>
</StandardTooltip>
Expand Down
6 changes: 3 additions & 3 deletions webview-ui/src/components/history/HistoryView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
</div>
</TabHeader>

<TabContent className="p-0">
<TabContent className="px-2 py-0">
<Virtuoso
className="flex-1 overflow-y-scroll"
data={tasks}
Expand All @@ -243,15 +243,15 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
isSelected={selectedTaskIds.includes(item.id)}
onToggleSelection={toggleTaskSelection}
onDelete={setDeleteTaskId}
className="m-2 mr-0"
className="m-2"
/>
)}
/>
</TabContent>

{/* Fixed action bar at bottom - only shown in selection mode with selected items */}
{isSelectionMode && selectedTaskIds.length > 0 && (
<div className="fixed bottom-0 left-0 right-0 bg-vscode-editor-background border-t border-vscode-panel-border p-2 flex justify-between items-center">
<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 })}
</div>
Expand Down
30 changes: 15 additions & 15 deletions webview-ui/src/components/history/TaskItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { vscode } from "@/utils/vscode"
import { cn } from "@/lib/utils"
import { Checkbox } from "@/components/ui/checkbox"

import TaskItemHeader from "./TaskItemHeader"
import TaskItemFooter from "./TaskItemFooter"

interface DisplayHistoryItem extends HistoryItem {
Expand Down Expand Up @@ -48,11 +47,11 @@ const TaskItem = ({
key={item.id}
data-testid={`task-item-${item.id}`}
className={cn(
"cursor-pointer group bg-vscode-editor-background rounded relative overflow-hidden hover:border-vscode-toolbar-hoverBackground/60",
"cursor-pointer group bg-vscode-editor-background rounded relative overflow-hidden border border-transparent hover:bg-vscode-list-hoverBackground transition-colors",
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The hover effect looks good! Could we verify that the contrast ratio between text and meets WCAG accessibility standards? This would ensure the UI remains accessible for users with visual impairments.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yup, thanks for the consideration.

className,
)}
onClick={handleClick}>
<div className="flex gap-2 p-3">
<div className={(!isCompact && isSelectionMode ? "pl-3 pb-3" : "pl-4") + " flex gap-3 px-3 pt-3 pb-1"}>
{/* Selection checkbox - only in full variant */}
{!isCompact && isSelectionMode && (
<div
Expand All @@ -69,24 +68,25 @@ const TaskItem = ({
)}

<div className="flex-1 min-w-0">
{/* Header with metadata */}
<TaskItemHeader item={item} isSelectionMode={isSelectionMode} onDelete={onDelete} />

{/* Task content */}
<div
className={cn("overflow-hidden whitespace-pre-wrap text-vscode-foreground text-ellipsis", {
"text-base line-clamp-3": !isCompact,
"line-clamp-2": isCompact,
})}
className={cn(
"overflow-hidden whitespace-pre-wrap text-vscode-foreground text-ellipsis line-clamp-2",
{
"text-base": !isCompact,
},
!isCompact && isSelectionMode ? "mb-1" : "",
)}
data-testid="task-content"
{...(item.highlight ? { dangerouslySetInnerHTML: { __html: item.highlight } } : {})}>
{item.highlight ? undefined : item.task}
</div>
<TaskItemFooter
item={item}
variant={variant}
isSelectionMode={isSelectionMode}
onDelete={onDelete}
/>

{/* Task Item Footer */}
<TaskItemFooter item={item} variant={variant} isSelectionMode={isSelectionMode} />

{/* Workspace info */}
{showWorkspace && item.workspace && (
<div className="flex flex-row gap-1 text-vscode-descriptionForeground text-xs mt-1">
<span className="codicon codicon-folder scale-80" />
Expand Down
52 changes: 17 additions & 35 deletions webview-ui/src/components/history/TaskItemFooter.tsx
Original file line number Diff line number Diff line change
@@ -1,59 +1,41 @@
import React from "react"
import type { HistoryItem } from "@roo-code/types"
import { Coins, FileIcon } from "lucide-react"
import prettyBytes from "pretty-bytes"
import { formatLargeNumber } from "@/utils/format"
import { formatTimeAgo } from "@/utils/format"
import { CopyButton } from "./CopyButton"
import { ExportButton } from "./ExportButton"
import { DeleteButton } from "./DeleteButton"
import { StandardTooltip } from "../ui/standard-tooltip"

export interface TaskItemFooterProps {
item: HistoryItem
variant: "compact" | "full"
isSelectionMode?: boolean
onDelete?: (taskId: string) => void
}

const TaskItemFooter: React.FC<TaskItemFooterProps> = ({ item, variant, isSelectionMode = false }) => {
const TaskItemFooter: React.FC<TaskItemFooterProps> = ({ item, variant, isSelectionMode = false, onDelete }) => {
return (
<div className="text-xs text-vscode-descriptionForeground flex justify-between items-center mt-1">
<div className="flex gap-2">
{!!(item.cacheReads || item.cacheWrites) && (
<span className="flex items-center" data-testid="cache-compact">
<i className="mr-1 codicon codicon-cloud-upload text-sm! text-vscode-descriptionForeground" />
<span className="inline-block mr-1">{formatLargeNumber(item.cacheWrites || 0)}</span>
<i className="mr-1 codicon codicon-cloud-download text-sm! text-vscode-descriptionForeground" />
<span>{formatLargeNumber(item.cacheReads || 0)}</span>
</span>
)}

{/* Full Tokens */}
{!!(item.tokensIn || item.tokensOut) && (
<span className="flex items-center gap-1">
<span data-testid="tokens-in-footer-compact">↑ {formatLargeNumber(item.tokensIn || 0)}</span>
<span data-testid="tokens-out-footer-compact">↓ {formatLargeNumber(item.tokensOut || 0)}</span>
</span>
)}

{/* Full Cost */}
<div className="text-xs text-vscode-descriptionForeground flex justify-between items-center">
<div className="flex gap-2 items-center text-vscode-descriptionForeground/60">
{/* Datetime with time-ago format */}
<StandardTooltip content={new Date(item.ts).toLocaleString()}>
<span className="first-letter:uppercase">{formatTimeAgo(item.ts)}</span>
</StandardTooltip>
<span>·</span>
{/* Cost */}
{!!item.totalCost && (
<span className="flex items-center">
<Coins className="inline-block size-[1em] mr-1" />
<span data-testid="cost-footer-compact">{"$" + item.totalCost.toFixed(2)}</span>
</span>
)}

{!!item.size && (
<span className="flex items-center">
<FileIcon className="inline-block size-[1em] mr-1" />
<span data-testid="size-footer-compact">{prettyBytes(item.size)}</span>
<span className="flex items-center" data-testid="cost-footer-compact">
{"$" + item.totalCost.toFixed(2)}
</span>
)}
</div>

{/* Action Buttons for non-compact view */}
{!isSelectionMode && (
<div className="flex flex-row gap-0 items-center opacity-50 hover:opacity-100">
<div className="flex flex-row gap-0 items-center text-vscode-descriptionForeground/60 hover:text-vscode-descriptionForeground">
<CopyButton itemTask={item.task} />
{variant === "full" && <ExportButton itemId={item.id} />}
{onDelete && <DeleteButton itemId={item.id} onDelete={onDelete} />}
</div>
)}
</div>
Expand Down
37 changes: 0 additions & 37 deletions webview-ui/src/components/history/TaskItemHeader.tsx

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,8 @@ describe("HistoryPreview", () => {
expect(screen.getByTestId("task-item-task-2")).toBeInTheDocument()
expect(screen.getByTestId("task-item-task-3")).toBeInTheDocument()
expect(screen.queryByTestId("task-item-task-4")).not.toBeInTheDocument()
expect(screen.queryByTestId("task-item-task-5")).not.toBeInTheDocument()
expect(screen.queryByTestId("task-item-task-6")).not.toBeInTheDocument()
})

it("renders only 1 task when there is only 1 task", () => {
Expand Down
30 changes: 8 additions & 22 deletions webview-ui/src/components/history/__tests__/TaskItem.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,47 +74,33 @@ describe("TaskItem", () => {
expect(screen.getByTestId("export")).toBeInTheDocument()
})

it("displays cache information when present", () => {
const mockTaskWithCache = {
...mockTask,
cacheReads: 10,
cacheWrites: 5,
}

it("displays time ago information", () => {
render(
<TaskItem
item={mockTaskWithCache}
item={mockTask}
variant="full"
isSelected={false}
onToggleSelection={vi.fn()}
isSelectionMode={false}
/>,
)

// Should display cache information in the footer
expect(screen.getByTestId("cache-compact")).toBeInTheDocument()
expect(screen.getByText("5")).toBeInTheDocument() // cache writes
expect(screen.getByText("10")).toBeInTheDocument() // cache reads
// Should display time ago format
expect(screen.getByText(/ago/)).toBeInTheDocument()
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Would it be helpful to add more specific test cases for edge cases? For example:

  • Very recent times ("less than a minute ago")
  • Times from different periods (hours, days, months ago)
  • Future timestamps (edge case handling)

This would ensure the time formatting works correctly across all scenarios.

Copy link
Collaborator

Choose a reason for hiding this comment

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

That would be testing the library, I don't think it's necessary.

})

it("does not display cache information when not present", () => {
const mockTaskWithoutCache = {
...mockTask,
cacheReads: 0,
cacheWrites: 0,
}

it("applies hover effect class", () => {
render(
<TaskItem
item={mockTaskWithoutCache}
item={mockTask}
variant="full"
isSelected={false}
onToggleSelection={vi.fn()}
isSelectionMode={false}
/>,
)

// Cache section should not be present
expect(screen.queryByTestId("cache-compact")).not.toBeInTheDocument()
const taskItem = screen.getByTestId("task-item-1")
expect(taskItem).toHaveClass("hover:bg-vscode-list-hoverBackground")
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,11 @@ const mockItem = {
}

describe("TaskItemFooter", () => {
it("renders token information", () => {
it("renders time ago information", () => {
render(<TaskItemFooter item={mockItem} variant="full" />)

// Check for token counts using testids since the text is split across elements
expect(screen.getByTestId("tokens-in-footer-compact")).toBeInTheDocument()
expect(screen.getByTestId("tokens-out-footer-compact")).toBeInTheDocument()
// Should show time ago format
expect(screen.getByText(/ago/)).toBeInTheDocument()
})

it("renders cost information", () => {
Expand All @@ -43,31 +42,38 @@ describe("TaskItemFooter", () => {
expect(screen.getByTestId("export")).toBeInTheDocument()
})

it("renders cache information when present", () => {
const mockItemWithCache = {
...mockItem,
cacheReads: 5,
cacheWrites: 3,
}
it("hides export button in compact variant", () => {
render(<TaskItemFooter item={mockItem} variant="compact" />)

render(<TaskItemFooter item={mockItemWithCache} variant="full" />)
// Should show copy button but not export button
expect(screen.getByTestId("copy-prompt-button")).toBeInTheDocument()
expect(screen.queryByTestId("export")).not.toBeInTheDocument()
})

it("hides action buttons in selection mode", () => {
render(<TaskItemFooter item={mockItem} variant="full" isSelectionMode={true} />)

// Should not show any action buttons
expect(screen.queryByTestId("copy-prompt-button")).not.toBeInTheDocument()
expect(screen.queryByTestId("export")).not.toBeInTheDocument()
expect(screen.queryByTestId("delete-task-button")).not.toBeInTheDocument()
})

it("shows delete button when not in selection mode and onDelete is provided", () => {
render(<TaskItemFooter item={mockItem} variant="full" isSelectionMode={false} onDelete={vi.fn()} />)

// Check for cache display using testid
expect(screen.getByTestId("cache-compact")).toBeInTheDocument()
expect(screen.getByText("3")).toBeInTheDocument() // cache writes
expect(screen.getByText("5")).toBeInTheDocument() // cache reads
expect(screen.getByTestId("delete-task-button")).toBeInTheDocument()
})

it("does not render cache information when not present", () => {
const mockItemWithoutCache = {
...mockItem,
cacheReads: 0,
cacheWrites: 0,
}
it("does not show delete button in selection mode", () => {
render(<TaskItemFooter item={mockItem} variant="full" isSelectionMode={true} onDelete={vi.fn()} />)

expect(screen.queryByTestId("delete-task-button")).not.toBeInTheDocument()
})

render(<TaskItemFooter item={mockItemWithoutCache} variant="full" />)
it("does not show delete button when onDelete is not provided", () => {
render(<TaskItemFooter item={mockItem} variant="full" isSelectionMode={false} />)

// Cache section should not be present
expect(screen.queryByTestId("cache-compact")).not.toBeInTheDocument()
expect(screen.queryByTestId("delete-task-button")).not.toBeInTheDocument()
})
})

This file was deleted.

Loading
Loading